From 1f6de96eceaaa8cf7c4a5306670475d09ad5945c Mon Sep 17 00:00:00 2001 From: Jaan Altosaar Date: Wed, 30 Nov 2022 12:11:25 +0700 Subject: [PATCH 001/134] Update --bug-report.yaml Bug: incorrect description of project info. --- .github/ISSUE_TEMPLATE/--bug-report.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/--bug-report.yaml b/.github/ISSUE_TEMPLATE/--bug-report.yaml index 111122ab37..e36d11df60 100644 --- a/.github/ISSUE_TEMPLATE/--bug-report.yaml +++ b/.github/ISSUE_TEMPLATE/--bug-report.yaml @@ -35,7 +35,7 @@ body: - type: textarea attributes: label: Project Details - description: Where to find it ? (See [YouTube video](https://www.youtube.com/watch?v=AUSNN-RCwhE) or [Docs](https://docs.nocodb.com/FAQs#how-to-check-my-project-info-)) + description: Click on top left icon and click `Copy Project Info`. (See [YouTube video](https://www.youtube.com/watch?v=AUSNN-RCwhE) or [Docs](https://docs.nocodb.com/FAQs#how-to-check-my-project-info-)) placeholder: | or provide the following info ``` @@ -58,4 +58,4 @@ body: placeholder: | > Drag & drop relevant image or videos validations: - required: false \ No newline at end of file + required: false From 5ea918b96e361194bf0ac922e25863f87bb18a42 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Fri, 2 Dec 2022 20:20:22 +0800 Subject: [PATCH 002/134] fix(nocodb): handle comparing a date with empty string in pg --- .../lib/sql/formulav2/formulaQueryBuilderv2.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts index 0c382ea3b4..925ab10066 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts @@ -55,8 +55,11 @@ export default async function formulaQueryBuilderv2( jsep.plugins.register(jsepCurlyHook); const tree = jsep(_tree); + const columnIdToUidt = {}; + // todo: improve - implement a common solution for filter, sort, formula, etc for (const col of await model.getColumns()) { + columnIdToUidt[col.id] = col.uidt; if (col.id in aliasToColumn) continue; switch (col.uidt) { case UITypes.Formula: @@ -659,6 +662,20 @@ export default async function formulaQueryBuilderv2( const right = fn(pt.right, null, pt.operator).toQuery(); let sql = `${left} ${pt.operator} ${right}${colAlias}`; + // comparing a date with empty string would throw + // `ERROR: zero-length delimited identifier` in Postgres + if ( + knex.clientType() === 'pg' && + columnIdToUidt[pt.left.name] === UITypes.Date && + pt.right.value === '' + ) { + if (pt.operator === '=') { + sql = `${left} IS NULL ${colAlias}`; + } else if (pt.operator === '!=') { + sql = `${left} IS NOT NULL ${colAlias}`; + } + } + // handle NULL values when calling CONCAT for sqlite3 if (pt.left.fnName === 'CONCAT' && knex.clientType() === 'sqlite3') { sql = `COALESCE(${left}, '') ${pt.operator} COALESCE(${right},'')${colAlias}`; From f8b5d2f3fc4c18fdb55e9aadcbc11ad223c7ad20 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sat, 3 Dec 2022 12:31:13 +0800 Subject: [PATCH 003/134] feat(nocodb): add validateDateWithUnknownFormat --- .../lib/sql/helpers/formulaFnHelper.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/formulaFnHelper.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/formulaFnHelper.ts index f8057a2473..e3268d7406 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/formulaFnHelper.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/formulaFnHelper.ts @@ -1,3 +1,7 @@ +import dayjs, { extend } from 'dayjs'; +import customParseFormat from 'dayjs/plugin/customParseFormat.js'; +extend(customParseFormat); + export function getWeekdayByText(v: string) { return { monday: 0, @@ -21,3 +25,28 @@ export function getWeekdayByIndex(idx: number): string { 6: 'sunday', }[idx || 0]; } + +export function validateDateWithUnknownFormat(v: string) { + 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', + ]; + 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; +} From d138c79709c90934ac0dfe9d4b73814737eb3482 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sat, 3 Dec 2022 12:31:37 +0800 Subject: [PATCH 004/134] fix(nocodb): handle comparing a date in IF in pg --- .../sql/formulav2/formulaQueryBuilderv2.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts index 925ab10066..2478f5a251 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts @@ -8,6 +8,7 @@ import { XKnex } from '../../../index'; import LinkToAnotherRecordColumn from '../../../../../models/LinkToAnotherRecordColumn'; import LookupColumn from '../../../../../models/LookupColumn'; import { jsepCurlyHook, UITypes } from 'nocodb-sdk'; +import { validateDateWithUnknownFormat } from '../helpers/formulaFnHelper'; // todo: switch function based on database @@ -666,12 +667,20 @@ export default async function formulaQueryBuilderv2( // `ERROR: zero-length delimited identifier` in Postgres if ( knex.clientType() === 'pg' && - columnIdToUidt[pt.left.name] === UITypes.Date && - pt.right.value === '' + columnIdToUidt[pt.left.name] === UITypes.Date ) { - if (pt.operator === '=') { - sql = `${left} IS NULL ${colAlias}`; - } else if (pt.operator === '!=') { + // The correct way to compare with Date should be using + // `IS_AFTER`, `IS_BEFORE`, or `IS_SAME` + // This is to prevent empty data returned to UI due to incorrect SQL + if (pt.right.value === '') { + if (pt.operator === '=') { + sql = `${left} IS NULL ${colAlias}`; + } else { + sql = `${left} IS NOT NULL ${colAlias}`; + } + } else if (!validateDateWithUnknownFormat(pt.right.value)) { + // left tree value is date but right tree value is not date + // return true if left tree value is not null, else false sql = `${left} IS NOT NULL ${colAlias}`; } } From 560cf9fc62eacc9ee1048549f182346f2f125d15 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sat, 3 Dec 2022 14:20:52 +0800 Subject: [PATCH 005/134] feat(nc-gui): add time formats --- packages/nc-gui/utils/dateTimeUtils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/nc-gui/utils/dateTimeUtils.ts b/packages/nc-gui/utils/dateTimeUtils.ts index 8736221840..6f58db18c5 100644 --- a/packages/nc-gui/utils/dateTimeUtils.ts +++ b/packages/nc-gui/utils/dateTimeUtils.ts @@ -5,17 +5,19 @@ export const timeAgo = (date: any) => { } export const dateFormats = [ + 'YYYY-MM-DD', + 'YYYY/MM/DD', '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 const timeFormats = ['HH:mm', 'HH:mm:ss', 'HH:mm:ss.SSS'] + export const handleTZ = (val: any) => { if (!val) { return @@ -60,7 +62,7 @@ export function getDateFormat(v: string) { export function getDateTimeFormat(v: string) { 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}` if (dayjs(v, dateTimeFormat, true).isValid() as any) { return dateTimeFormat From 0295f4ca7163a93972173efdaed447439a7e02e2 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sat, 3 Dec 2022 14:21:04 +0800 Subject: [PATCH 006/134] feat(nc-gui): add LazySmartsheetColumnDateTimeOptions for DateTime --- packages/nc-gui/components/smartsheet/column/EditOrAdd.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue b/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue index f7a63fe1de..cfe70bae07 100644 --- a/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue +++ b/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue @@ -177,6 +177,7 @@ useEventListener('keydown', (e: KeyboardEvent) => { + Date: Sat, 3 Dec 2022 14:21:41 +0800 Subject: [PATCH 007/134] feat(nc-gui): add DateTimeOptions --- .../smartsheet/column/DateTimeOptions.vue | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue diff --git a/packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue b/packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue new file mode 100644 index 0000000000..6b0f8e25e6 --- /dev/null +++ b/packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue @@ -0,0 +1,46 @@ + + + From 55fd0fee74ce814cb0a4ea42a741182433b57f15 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sat, 3 Dec 2022 14:22:09 +0800 Subject: [PATCH 008/134] feat(nc-gui): format DateTime data --- .../nc-gui/components/cell/DateTimePicker.vue | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/nc-gui/components/cell/DateTimePicker.vue b/packages/nc-gui/components/cell/DateTimePicker.vue index 8819d23ce7..ffd88acc0c 100644 --- a/packages/nc-gui/components/cell/DateTimePicker.vue +++ b/packages/nc-gui/components/cell/DateTimePicker.vue @@ -2,10 +2,13 @@ import dayjs from 'dayjs' import { ActiveCellInj, + ColumnInj, ReadonlyInj, + dateFormats, inject, isDrawerOrModalExist, ref, + timeFormats, useProject, useSelectedCellKeyupListener, watch, @@ -28,9 +31,15 @@ const active = inject(ActiveCellInj, ref(false)) const editable = inject(EditModeInj, ref(false)) +const columnMeta = inject(ColumnInj, null)! + let isDateInvalid = $ref(false) -const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ' +const dateTimeFormat = $computed(() => { + const dateFormat = columnMeta?.value?.meta?.date_format ?? dateFormats[0] + const timeFormat = columnMeta?.value?.meta?.time_format ?? timeFormats[0] + return `${dateFormat} ${timeFormat}${!isMysql ? 'Z' : ''}` +}) let localState = $computed({ get() { @@ -52,7 +61,7 @@ let localState = $computed({ } if (val.isValid()) { - emit('update:modelValue', val?.format(dateFormat)) + emit('update:modelValue', val?.format(dateTimeFormat)) } }, }) @@ -163,7 +172,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => { :show-time="true" :bordered="false" class="!w-full !px-0 !border-none" - format="YYYY-MM-DD HH:mm" + :format="dateTimeFormat" :placeholder="isDateInvalid ? 'Invalid date' : ''" :allow-clear="!readOnly && !localState && !isPk" :input-read-only="true" From 164f0c9f8eb82024cd95cfabfe24ea648c3c9273 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sat, 3 Dec 2022 16:27:53 +0800 Subject: [PATCH 009/134] feat(playwrights): add DateTimeCell.ts --- .../Dashboard/common/Cell/DateTimeCell.ts | 36 +++++++++++++++++++ .../pages/Dashboard/common/Cell/index.ts | 3 ++ 2 files changed, 39 insertions(+) create mode 100644 tests/playwright/pages/Dashboard/common/Cell/DateTimeCell.ts diff --git a/tests/playwright/pages/Dashboard/common/Cell/DateTimeCell.ts b/tests/playwright/pages/Dashboard/common/Cell/DateTimeCell.ts new file mode 100644 index 0000000000..9d876726cb --- /dev/null +++ b/tests/playwright/pages/Dashboard/common/Cell/DateTimeCell.ts @@ -0,0 +1,36 @@ +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.cell.dblclick({ + index, + columnHeader, + }); + } + + async selectDateTime({ + // date in format `YYYY-MM-DD` + // time in format 'HH:mm' + dateTime, + }: { + dateTime: string; + }) { + await this.rootPage.locator(`td[title="${dateTime}"]`).click(); + } + + async close() { + await this.rootPage.keyboard.press('Escape'); + } +} diff --git a/tests/playwright/pages/Dashboard/common/Cell/index.ts b/tests/playwright/pages/Dashboard/common/Cell/index.ts index 22052a2ca6..92fdf7a2f2 100644 --- a/tests/playwright/pages/Dashboard/common/Cell/index.ts +++ b/tests/playwright/pages/Dashboard/common/Cell/index.ts @@ -7,6 +7,7 @@ import { SharedFormPage } from '../../../SharedForm'; import { CheckboxCellPageObject } from './CheckboxCell'; import { RatingCellPageObject } from './RatingCell'; import { DateCellPageObject } from './DateCell'; +import { DateTimeCellPageObject } from './DateTimeCell'; export class CellPageObject extends BasePage { readonly parent: GridPage | SharedFormPage; @@ -15,6 +16,7 @@ export class CellPageObject extends BasePage { readonly checkbox: CheckboxCellPageObject; readonly rating: RatingCellPageObject; readonly date: DateCellPageObject; + readonly dateTime: DateTimeCellPageObject; constructor(parent: GridPage | SharedFormPage) { super(parent.rootPage); @@ -24,6 +26,7 @@ export class CellPageObject extends BasePage { this.checkbox = new CheckboxCellPageObject(this); this.rating = new RatingCellPageObject(this); this.date = new DateCellPageObject(this); + this.dateTime = new DateTimeCellPageObject(this); } get({ index, columnHeader }: { index?: number; columnHeader: string }): Locator { From beb9c3bbf2e73ed211667e59ca91d9c8a0e07115 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Sat, 3 Dec 2022 20:07:01 +0800 Subject: [PATCH 010/134] refactor(nc-gui): add class to select and remove unnecessary div --- .../smartsheet/column/DateTimeOptions.vue | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue b/packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue index 6b0f8e25e6..152b504fd4 100644 --- a/packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue +++ b/packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue @@ -22,24 +22,16 @@ if (!vModel.value.meta?.time_format) { diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index 0bcddde8d4..4dc08c0990 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -172,7 +172,7 @@ const getContainerScrollForElement = ( return scroll } -const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyValue, isCellSelected, selectedCell } = +const { isCellSelected, activeCell, handleMouseDown, handleMouseOver, handleCellClick, clearSelectedRange, copyValue } = useMultiSelect( meta, fields, @@ -201,9 +201,10 @@ const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyVa const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey const altOrOptionKey = e.altKey if (e.key === ' ') { - if (selectedCell.row !== null && !editEnabled) { + if (activeCell.row != null && !editEnabled) { e.preventDefault() - const row = data.value[selectedCell.row] + clearSelectedRange() + const row = data.value[activeCell.row] expandForm(row) return true } @@ -227,29 +228,34 @@ const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyVa switch (e.key) { case 'ArrowUp': e.preventDefault() - selectedCell.row = 0 - selectedCell.col = selectedCell.col ?? 0 + clearSelectedRange() + $e('c:shortcut', { key: 'CTRL + ArrowUp' }) + activeCell.row = 0 + activeCell.col = activeCell.col ?? 0 scrollToCell?.() editEnabled = false return true case 'ArrowDown': e.preventDefault() - selectedCell.row = data.value.length - 1 - selectedCell.col = selectedCell.col ?? 0 + clearSelectedRange() + activeCell.row = data.value.length - 1 + activeCell.col = activeCell.col ?? 0 scrollToCell?.() editEnabled = false return true case 'ArrowRight': e.preventDefault() - selectedCell.row = selectedCell.row ?? 0 - selectedCell.col = fields.value?.length - 1 + clearSelectedRange() + activeCell.row = activeCell.row ?? 0 + activeCell.col = fields.value?.length - 1 scrollToCell?.() editEnabled = false return true case 'ArrowLeft': e.preventDefault() - selectedCell.row = selectedCell.row ?? 0 - selectedCell.col = 0 + clearSelectedRange() + activeCell.row = activeCell.row ?? 0 + activeCell.col = 0 scrollToCell?.() editEnabled = false return true @@ -279,7 +285,7 @@ const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyVa }, async (ctx: { row: number; col?: number; updatedColumnTitle?: string }) => { const rowObj = data.value[ctx.row] - const columnObj = ctx.col !== null && ctx.col !== undefined ? fields.value[ctx.col] : null + const columnObj = ctx.col !== undefined ? fields.value[ctx.col] : null if (!ctx.updatedColumnTitle && isVirtualCol(columnObj)) { return @@ -291,10 +297,10 @@ const { selectCell, startSelectRange, endSelectRange, clearSelectedRange, copyVa ) function scrollToCell(row?: number | null, col?: number | null) { - row = row ?? selectedCell.row - col = col ?? selectedCell.col + row = row ?? activeCell.row + col = col ?? activeCell.col - if (row !== undefined && col !== undefined && row !== null && col !== null) { + if (row !== null && col !== null) { // get active cell const rows = tbodyEl.value?.querySelectorAll('tr') const cols = rows?.[row].querySelectorAll('td') @@ -455,13 +461,14 @@ useEventListener(document, 'keyup', async (e: KeyboardEvent) => { /** On clicking outside of table reset active cell */ const smartTable = ref(null) + onClickOutside(smartTable, (e) => { // do nothing if context menu was open if (contextMenu.value) return - clearSelectedRange() - if (selectedCell.col === null) return - const activeCol = fields.value[selectedCell.col] + if (activeCell.row === null || activeCell.col === null) return + + const activeCol = fields.value[activeCell.col] if (editEnabled && (isVirtualCol(activeCol) || activeCol.uidt === UITypes.JSON)) return @@ -482,25 +489,29 @@ onClickOutside(smartTable, (e) => { return } - selectedCell.row = null - selectedCell.col = null + clearSelectedRange() + activeCell.row = null + activeCell.col = null }) const onNavigate = (dir: NavigateDir) => { - if (selectedCell.row === null || selectedCell.col === null) return + if (activeCell.row === null || activeCell.col === null) return + editEnabled = false + clearSelectedRange() + switch (dir) { case NavigateDir.NEXT: - if (selectedCell.row < data.value.length - 1) { - selectedCell.row++ + if (activeCell.row < data.value.length - 1) { + activeCell.row++ } else { addEmptyRow() - selectedCell.row++ + activeCell.row++ } break case NavigateDir.PREV: - if (selectedCell.row > 0) { - selectedCell.row-- + if (activeCell.row > 0) { + activeCell.row-- } break } @@ -782,10 +793,10 @@ const closeAddColumnDropdown = () => { :data-key="rowIndex + columnObj.id" :data-col="columnObj.id" :data-title="columnObj.title" - @click="selectCell(rowIndex, colIndex)" + @mousedown="handleMouseDown($event, rowIndex, colIndex)" + @mouseover="handleMouseOver(rowIndex, colIndex)" + @click="handleCellClick($event, rowIndex, colIndex)" @dblclick="makeEditable(row, columnObj)" - @mousedown="startSelectRange($event, rowIndex, colIndex)" - @mouseover="endSelectRange(rowIndex, colIndex)" @contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })" >
@@ -793,7 +804,7 @@ const closeAddColumnDropdown = () => { v-if="isVirtualCol(columnObj)" v-model="row.row[columnObj.title]" :column="columnObj" - :active="selectedCell.col === colIndex && selectedCell.row === rowIndex" + :active="activeCell.col === colIndex && activeCell.row === rowIndex" :row="row" @navigate="onNavigate" /> @@ -803,10 +814,10 @@ const closeAddColumnDropdown = () => { v-model="row.row[columnObj.title]" :column="columnObj" :edit-enabled=" - !!hasEditPermission && !!editEnabled && selectedCell.col === colIndex && selectedCell.row === rowIndex + !!hasEditPermission && !!editEnabled && activeCell.col === colIndex && activeCell.row === rowIndex " :row-index="rowIndex" - :active="selectedCell.col === colIndex && selectedCell.row === rowIndex" + :active="activeCell.col === colIndex && activeCell.row === rowIndex" @update:edit-enabled="editEnabled = $event" @save="updateOrSaveRow(row, columnObj.title, state)" @navigate="onNavigate" @@ -872,7 +883,7 @@ const closeAddColumnDropdown = () => {
- +
{{ $t('general.copy') }} diff --git a/packages/nc-gui/composables/useMultiSelect/cellRange.ts b/packages/nc-gui/composables/useMultiSelect/cellRange.ts index c059cbfe12..7d0ccc0e41 100644 --- a/packages/nc-gui/composables/useMultiSelect/cellRange.ts +++ b/packages/nc-gui/composables/useMultiSelect/cellRange.ts @@ -1,6 +1,6 @@ export interface Cell { - row: number | null - col: number | null + row: number + col: number } export class CellRange { @@ -12,14 +12,22 @@ export class CellRange { this._end = end ?? this._start } - get start() { + isEmpty() { + return this._start == null || this._end == null + } + + isSingleCell() { + return !this.isEmpty() && this._start?.col === this._end?.col && this._start?.row === this._end?.row + } + + get start(): Cell { return { row: Math.min(this._start?.row ?? NaN, this._end?.row ?? NaN), col: Math.min(this._start?.col ?? NaN, this._end?.col ?? NaN), } } - get end() { + get end(): Cell { return { row: Math.max(this._start?.row ?? NaN, this._end?.row ?? NaN), col: Math.max(this._start?.col ?? NaN, this._end?.col ?? NaN), @@ -27,19 +35,11 @@ export class CellRange { } startRange(value: Cell) { - if (value == null) { - return - } - this._start = value this._end = value } endRange(value: Cell) { - if (value == null) { - return - } - this._end = value } @@ -47,8 +47,4 @@ export class CellRange { this._start = null this._end = null } - - isEmpty() { - return this._start == null || this._end == null - } } diff --git a/packages/nc-gui/composables/useMultiSelect/index.ts b/packages/nc-gui/composables/useMultiSelect/index.ts index a4c1024df5..104b7888b9 100644 --- a/packages/nc-gui/composables/useMultiSelect/index.ts +++ b/packages/nc-gui/composables/useMultiSelect/index.ts @@ -4,7 +4,7 @@ import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' import type { Cell } from './cellRange' import { CellRange } from './cellRange' import convertCellData from './convertCellData' -import type { Row } from '~/lib' +import type { Nullable, Row } from '~/lib' import { copyTable, extractPkFromRow, @@ -22,11 +22,13 @@ import { useProject, } from '#imports' +const MAIN_MOUSE_PRESSED = 0 + /** * Utility to help with multi-selecting rows/cells in the smartsheet */ export function useMultiSelect( - _meta: MaybeRef, + _meta: MaybeRef, fields: MaybeRef, data: MaybeRef, _editEnabled: MaybeRef, @@ -51,15 +53,26 @@ export function useMultiSelect( const editEnabled = ref(_editEnabled) - const selectedCell = reactive({ row: null, col: null }) - const selectedRange = reactive(new CellRange()) let isMouseDown = $ref(false) + const selectedRange = reactive(new CellRange()) + + const activeCell = reactive>({ row: null, col: null }) + const columnLength = $computed(() => unref(fields)?.length) + function makeActive(row: number, col: number) { + if (activeCell.row === row && activeCell.col === col) { + return + } + + activeCell.row = row + activeCell.col = col + } + async function copyValue(ctx?: Cell) { try { - if (!selectedRange.isEmpty()) { + if (selectedRange.start !== null && selectedRange.end !== null && !selectedRange.isSingleCell()) { const cprows = unref(data).slice(selectedRange.start.row, selectedRange.end.row + 1) // slice the selected rows for copy const cpcols = unref(fields).slice(selectedRange.start.col, selectedRange.end.col + 1) // slice the selected cols for copy @@ -68,8 +81,8 @@ export function useMultiSelect( } else { // if copy was called with context (right click position) - copy value from context // else if there is just one selected cell, copy it's value - const cpRow = ctx?.row ?? selectedCell?.row - const cpCol = ctx?.col ?? selectedCell?.col + const cpRow = ctx?.row ?? activeCell.row + const cpCol = ctx?.col ?? activeCell.col if (cpRow != null && cpCol != null) { const rowObj = unref(data)[cpRow] @@ -93,29 +106,19 @@ export function useMultiSelect( } } - function selectCell(row: number, col: number) { - selectedRange.clear() - if (selectedCell.row === row && selectedCell.col === col) return - editEnabled.value = false - selectedCell.row = row - selectedCell.col = col - } - - function endSelectRange(row: number, col: number) { + function handleMouseOver(row: number, col: number) { if (!isMouseDown) { return } - selectedCell.row = null - selectedCell.col = null selectedRange.endRange({ row, col }) } function isCellSelected(row: number, col: number) { - if (selectedCell?.row === row && selectedCell?.col === col) { + if (activeCell.col === col && activeCell.row === row) { return true } - if (selectedRange.isEmpty()) { + if (selectedRange.start === null || selectedRange.end === null) { return false } @@ -127,46 +130,51 @@ export function useMultiSelect( ) } - function startSelectRange(event: MouseEvent, row: number, col: number) { + function handleMouseDown(event: MouseEvent, row: number, col: number) { // if there was a right click on selected range, don't restart the selection - const leftClickButton = 0 - if (event?.button !== leftClickButton && isCellSelected(row, col)) { + if (event?.button !== MAIN_MOUSE_PRESSED && isCellSelected(row, col)) { return } - if (unref(editEnabled)) { - event.preventDefault() - return - } + editEnabled.value = false + isMouseDown = true + selectedRange.startRange({ row, col }) + } + const handleCellClick = (event: MouseEvent, row: number, col: number) => { isMouseDown = true - selectedRange.clear() + editEnabled.value = false selectedRange.startRange({ row, col }) + selectedRange.endRange({ row, col }) + makeActive(row, col) + isMouseDown = false } - useEventListener(document, 'mouseup', (e) => { - // if the editEnabled is false prevent the mouseup event for not select text + const handleMouseUp = (event: MouseEvent) => { + // timeout is needed, because we want to set cell as active AFTER all the child's click handler's called + // this is needed e.g. for date field edit, where two clicks had to be done - one to select cell, and another one to open date dropdown + setTimeout(() => { + makeActive(selectedRange.start.row, selectedRange.start.col) + }, 0) + + // if the editEnabled is false, prevent selecting text on mouseUp if (!unref(editEnabled)) { - e.preventDefault() + event.preventDefault() } isMouseDown = false - }) + } - const onKeyDown = async (e: KeyboardEvent) => { + const handleKeyDown = async (e: KeyboardEvent) => { // invoke the keyEventHandler if provided and return if it returns true if (await keyEventHandler?.(e)) { return true } - if (!selectedRange.isEmpty()) { - // In case the user press tabs or arrows keys - selectedCell.row = selectedRange.start.row - selectedCell.col = selectedRange.start.col + if (activeCell.row === null || activeCell.col === null) { + return } - if (selectedCell.row === null || selectedCell.col === null) return - /** on tab key press navigate through cells */ switch (e.key) { case 'Tab': @@ -174,21 +182,21 @@ export function useMultiSelect( selectedRange.clear() if (e.shiftKey) { - if (selectedCell.col > 0) { - selectedCell.col-- + if (activeCell.col > 0) { + activeCell.col-- editEnabled.value = false - } else if (selectedCell.row > 0) { - selectedCell.row-- - selectedCell.col = unref(columnLength) - 1 + } else if (activeCell.row > 0) { + activeCell.row-- + activeCell.col = unref(columnLength) - 1 editEnabled.value = false } } else { - if (selectedCell.col < unref(columnLength) - 1) { - selectedCell.col++ + if (activeCell.col < unref(columnLength) - 1) { + activeCell.col++ editEnabled.value = false - } else if (selectedCell.row < unref(data).length - 1) { - selectedCell.row++ - selectedCell.col = 0 + } else if (activeCell.row < unref(data).length - 1) { + activeCell.row++ + activeCell.col = 0 editEnabled.value = false } } @@ -198,63 +206,68 @@ export function useMultiSelect( case 'Enter': e.preventDefault() selectedRange.clear() - makeEditable(unref(data)[selectedCell.row], unref(fields)[selectedCell.col]) + + makeEditable(unref(data)[activeCell.row], unref(fields)[activeCell.col]) break /** on delete key press clear cell */ case 'Delete': e.preventDefault() selectedRange.clear() - await clearCell(selectedCell as { row: number; col: number }) + + await clearCell(activeCell as { row: number; col: number }) break /** on arrow key press navigate through cells */ case 'ArrowRight': e.preventDefault() selectedRange.clear() - if (selectedCell.col < unref(columnLength) - 1) { - selectedCell.col++ + + if (activeCell.col < unref(columnLength) - 1) { + activeCell.col++ scrollToActiveCell?.() editEnabled.value = false } break case 'ArrowLeft': - selectedRange.clear() e.preventDefault() - if (selectedCell.col > 0) { - selectedCell.col-- + selectedRange.clear() + + if (activeCell.col > 0) { + activeCell.col-- scrollToActiveCell?.() editEnabled.value = false } break case 'ArrowUp': - selectedRange.clear() e.preventDefault() - if (selectedCell.row > 0) { - selectedCell.row-- + selectedRange.clear() + + if (activeCell.row > 0) { + activeCell.row-- scrollToActiveCell?.() editEnabled.value = false } break case 'ArrowDown': - selectedRange.clear() e.preventDefault() - if (selectedCell.row < unref(data).length - 1) { - selectedCell.row++ + selectedRange.clear() + + if (activeCell.row < unref(data).length - 1) { + activeCell.row++ scrollToActiveCell?.() editEnabled.value = false } break default: { - const rowObj = unref(data)[selectedCell.row] - - const columnObj = unref(fields)[selectedCell.col] + const rowObj = unref(data)[activeCell.row] + const columnObj = unref(fields)[activeCell.col] if ((!unref(editEnabled) || !isTypableInputColumn(columnObj)) && (isMac() ? e.metaKey : e.ctrlKey)) { switch (e.keyCode) { // copy - ctrl/cmd +c case 67: // set clipboard context only if single cell selected - if (rowObj.row[columnObj.title!]) { + if (selectedRange.isSingleCell() && rowObj.row[columnObj.title!]) { clipboardContext = { value: rowObj.row[columnObj.title!], uidt: columnObj.uidt as UITypes, @@ -264,6 +277,7 @@ export function useMultiSelect( } await copyValue() break + // paste - ctrl/cmd + v case 86: try { // handle belongs to column @@ -297,7 +311,7 @@ export function useMultiSelect( (relatedTableMeta as any)!.columns!, ) - return await syncCellData?.({ ...selectedCell, updatedColumnTitle: foreignKeyColumn.title }) + return await syncCellData?.({ ...activeCell, updatedColumnTitle: foreignKeyColumn.title }) } // if it's a virtual column excluding belongs to cell type skip paste @@ -315,9 +329,9 @@ export function useMultiSelect( isMysql.value, ) e.preventDefault() - syncCellData?.(selectedCell) + syncCellData?.(activeCell) } else { - clearCell(selectedCell as { row: number; col: number }, true) + clearCell(activeCell as { row: number; col: number }, true) makeEditable(rowObj, columnObj) } } catch (error: any) { @@ -346,15 +360,18 @@ export function useMultiSelect( } } - useEventListener(document, 'keydown', onKeyDown) + const clearSelectedRange = selectedRange.clear.bind(selectedRange) + + useEventListener(document, 'keydown', handleKeyDown) + useEventListener(document, 'mouseup', handleMouseUp) return { - selectCell, - startSelectRange, - endSelectRange, - clearSelectedRange: selectedRange.clear.bind(selectedRange), + handleMouseDown, + handleMouseOver, + clearSelectedRange, copyValue, isCellSelected, - selectedCell, + activeCell, + handleCellClick, } } diff --git a/packages/nc-gui/lib/types.ts b/packages/nc-gui/lib/types.ts index 9807981404..49ce8be69d 100644 --- a/packages/nc-gui/lib/types.ts +++ b/packages/nc-gui/lib/types.ts @@ -100,3 +100,5 @@ export interface SharedView { export type importFileList = (UploadFile & { data: string | ArrayBuffer })[] export type streamImportFileList = UploadFile[] + +export type Nullable = { [K in keyof T]: T[K] | null } diff --git a/tests/playwright/pages/Dashboard/Grid/index.ts b/tests/playwright/pages/Dashboard/Grid/index.ts index 66a85bee3b..39da3232a3 100644 --- a/tests/playwright/pages/Dashboard/Grid/index.ts +++ b/tests/playwright/pages/Dashboard/Grid/index.ts @@ -1,7 +1,7 @@ import { expect, Locator } from '@playwright/test'; import { DashboardPage } from '..'; import BasePage from '../../Base'; -import { CellPageObject } from '../common/Cell'; +import { CellPageObject, CellProps } from '../common/Cell'; import { ColumnPageObject } from './Column'; import { ToolbarPage } from '../common/Toolbar'; import { ProjectMenuObject } from '../common/ProjectMenu'; @@ -286,4 +286,34 @@ export class GridPage extends BasePage { param.role === 'creator' || param.role === 'editor' ? 1 : 0 ); } + + async selectRange({ start, end }: { start: CellProps; end: CellProps }) { + const startCell = await this.cell.get({ index: start.index, columnHeader: start.columnHeader }); + const endCell = await this.cell.get({ index: end.index, columnHeader: end.columnHeader }); + const page = await this.dashboard.get().page(); + await startCell.hover(); + await page.mouse.down(); + await endCell.hover(); + await page.mouse.up(); + } + + async selectedCount() { + return this.get().locator('.cell.active').count(); + } + + async copyWithKeyboard() { + await this.get().press((await this.isMacOs()) ? 'Meta+C' : 'Control+C'); + await this.verifyToast({ message: 'Copied to clipboard' }); + + return this.getClipboardText(); + } + + async copyWithMouse({ index, columnHeader }: CellProps) { + await this.cell.get({ index, columnHeader }).click({ button: 'right' }); + await this.get().page().getByTestId('context-menu-item-copy').click(); + + await this.verifyToast({ message: 'Copied to clipboard' }); + + return this.getClipboardText(); + } } diff --git a/tests/playwright/pages/Dashboard/common/Cell/index.ts b/tests/playwright/pages/Dashboard/common/Cell/index.ts index 4e1ddb538e..0e1bc8e04b 100644 --- a/tests/playwright/pages/Dashboard/common/Cell/index.ts +++ b/tests/playwright/pages/Dashboard/common/Cell/index.ts @@ -8,6 +8,11 @@ import { CheckboxCellPageObject } from './CheckboxCell'; import { RatingCellPageObject } from './RatingCell'; import { DateCellPageObject } from './DateCell'; +export interface CellProps { + index?: number; + columnHeader: string; +} + export class CellPageObject extends BasePage { readonly parent: GridPage | SharedFormPage; readonly selectOption: SelectOptionCellPageObject; @@ -26,7 +31,7 @@ export class CellPageObject extends BasePage { this.date = new DateCellPageObject(this); } - get({ index, columnHeader }: { index?: number; columnHeader: string }): Locator { + get({ index, columnHeader }: CellProps): Locator { if (this.parent instanceof SharedFormPage) { return this.parent.get().locator(`[data-testid="nc-form-input-cell-${columnHeader}"]`); } else { @@ -34,19 +39,16 @@ export class CellPageObject extends BasePage { } } - async click( - { index, columnHeader }: { index: number; columnHeader: string }, - ...options: Parameters - ) { + async click({ index, columnHeader }: CellProps, ...options: Parameters) { await this.get({ index, columnHeader }).click(...options); await (await this.get({ index, columnHeader }).elementHandle()).waitForElementState('stable'); } - async dblclick({ index, columnHeader }: { index?: number; columnHeader: string }) { + async dblclick({ index, columnHeader }: CellProps) { return await this.get({ index, columnHeader }).dblclick(); } - async fillText({ index, columnHeader, text }: { index?: number; columnHeader: string; text: string }) { + async fillText({ index, columnHeader, text }: CellProps & { text: string }) { await this.dblclick({ index, columnHeader, @@ -67,7 +69,7 @@ export class CellPageObject extends BasePage { } } - async inCellExpand({ index, columnHeader }: { index: number; columnHeader: string }) { + async inCellExpand({ index, columnHeader }: CellProps) { await this.get({ index, columnHeader }).hover(); await this.waitForResponse({ uiAction: this.get({ index, columnHeader }).locator('.nc-action-icon >> nth=0').click(), @@ -76,20 +78,20 @@ export class CellPageObject extends BasePage { }); } - async inCellAdd({ index, columnHeader }: { index: number; columnHeader: string }) { + async inCellAdd({ index, columnHeader }: CellProps) { await this.get({ index, columnHeader }).hover(); await this.get({ index, columnHeader }).locator('.nc-action-icon.nc-plus').click(); } - async verifyCellActiveSelected({ index, columnHeader }: { index: number; columnHeader: string }) { + async verifyCellActiveSelected({ index, columnHeader }: CellProps) { await expect(this.get({ index, columnHeader })).toHaveClass(/active/); } - async verifyCellEditable({ index, columnHeader }: { index: number; columnHeader: string }) { + async verifyCellEditable({ index, columnHeader }: CellProps) { await this.get({ index, columnHeader }).isEditable(); } - async verify({ index, columnHeader, value }: { index: number; columnHeader: string; value: string | string[] }) { + async verify({ index, columnHeader, value }: CellProps & { value: string | string[] }) { const _verify = async text => { await expect .poll(async () => { @@ -115,9 +117,7 @@ export class CellPageObject extends BasePage { index, columnHeader, expectedSrcValue, - }: { - index: number; - columnHeader: string; + }: CellProps & { expectedSrcValue: string; }) { const _verify = async expectedQrCodeImgSrc => { @@ -147,9 +147,7 @@ export class CellPageObject extends BasePage { columnHeader, count, value, - }: { - index: number; - columnHeader: string; + }: CellProps & { count?: number; value: string[]; }) { @@ -166,7 +164,7 @@ export class CellPageObject extends BasePage { } } - async unlinkVirtualCell({ index, columnHeader }: { index: number; columnHeader: string }) { + async unlinkVirtualCell({ index, columnHeader }: CellProps) { const cell = this.get({ index, columnHeader }); await cell.click(); await cell.locator('.nc-icon.unlink-icon').click(); @@ -200,10 +198,7 @@ export class CellPageObject extends BasePage { ); } - async copyToClipboard( - { index, columnHeader }: { index: number; columnHeader: string }, - ...clickOptions: Parameters - ) { + async copyToClipboard({ index, columnHeader }: CellProps, ...clickOptions: Parameters) { await this.get({ index, columnHeader }).click(...clickOptions); await (await this.get({ index, columnHeader }).elementHandle()).waitForElementState('stable'); diff --git a/tests/playwright/tests/cellSelection.spec.ts b/tests/playwright/tests/cellSelection.spec.ts new file mode 100644 index 0000000000..c58f4c3cd0 --- /dev/null +++ b/tests/playwright/tests/cellSelection.spec.ts @@ -0,0 +1,111 @@ +import { expect, test } from '@playwright/test'; +import { DashboardPage } from '../pages/Dashboard'; +import { GridPage } from '../pages/Dashboard/Grid'; +import setup from '../setup'; + +test.describe('Verify cell selection', () => { + let dashboard: DashboardPage, grid: GridPage; + let context: any; + + test.beforeEach(async ({ page }) => { + context = await setup({ page }); + dashboard = new DashboardPage(page, context.project); + grid = dashboard.grid; + }); + + test('#1 when range is selected, it has correct number of selected cells', async () => { + await dashboard.treeView.openTable({ title: 'Country' }); + await grid.selectRange({ + start: { index: 0, columnHeader: 'Country' }, + end: { index: 2, columnHeader: 'City List' }, + }); + + expect(await grid.selectedCount()).toBe(9); + }); + + test('#2 when copied with clipboard, it copies correct text', async () => { + await dashboard.treeView.openTable({ title: 'Country' }); + await grid.selectRange({ + start: { index: 0, columnHeader: 'Country' }, + end: { index: 1, columnHeader: 'LastUpdate' }, + }); + + expect(await grid.copyWithKeyboard()).toBe( + 'Afghanistan \t 2006-02-15 04:44:00\n' + ' Algeria \t 2006-02-15 04:44:00\n' + ); + }); + + test('#3 when copied with mouse, it copies correct text', async () => { + await dashboard.treeView.openTable({ title: 'Country' }); + await grid.selectRange({ + start: { index: 0, columnHeader: 'Country' }, + end: { index: 1, columnHeader: 'LastUpdate' }, + }); + + expect(await grid.copyWithMouse({ index: 0, columnHeader: 'Country' })).toBe( + 'Afghanistan \t 2006-02-15 04:44:00\n' + ' Algeria \t 2006-02-15 04:44:00\n' + ); + }); + + // FIXME: this is edge case, better be moved to integration tests + test('#4 when cell inside selection range is clicked, it clears previous selection', async () => { + await dashboard.treeView.openTable({ title: 'Country' }); + await grid.selectRange({ + start: { index: 0, columnHeader: 'Country' }, + end: { index: 2, columnHeader: 'City List' }, + }); + + expect(await grid.selectedCount()).toBe(9); + + await grid.cell.get({ index: 0, columnHeader: 'Country' }).click(); + + expect(await grid.selectedCount()).toBe(1); + expect(await grid.cell.verifyCellActiveSelected({ index: 0, columnHeader: 'Country' })); + }); + + // FIXME: this is edge case, better be moved to integration tests + test('#5 when cell outside selection range is clicked, it clears previous selection', async () => { + await dashboard.treeView.openTable({ title: 'Country' }); + await grid.selectRange({ + start: { index: 0, columnHeader: 'Country' }, + end: { index: 2, columnHeader: 'City List' }, + }); + + expect(await grid.selectedCount()).toBe(9); + + await grid.cell.get({ index: 5, columnHeader: 'Country' }).click(); + + expect(await grid.selectedCount()).toBe(1); + expect(await grid.cell.verifyCellActiveSelected({ index: 5, columnHeader: 'Country' })); + }); + + // FIXME: this is edge case, better be moved to integration tests + test('#6 when selection ends on locked field, it still works as expected', async () => { + await dashboard.treeView.openTable({ title: 'Country' }); + await dashboard.grid.toolbar.fields.toggleShowSystemFields(); + await grid.selectRange({ + start: { index: 2, columnHeader: 'City List' }, + end: { index: 0, columnHeader: 'CountryId' }, + }); + + expect(await grid.selectedCount()).toBe(12); + + await grid.cell.get({ index: 1, columnHeader: 'Country' }).click(); + + expect(await grid.selectedCount()).toBe(1); + expect(await grid.cell.verifyCellActiveSelected({ index: 1, columnHeader: 'Country' })); + }); + + // FIXME: this is edge case, better be moved to integration tests + test('#7 when navigated with keyboard, only active cell is affected', async ({ page }) => { + await dashboard.treeView.openTable({ title: 'Country' }); + await grid.selectRange({ + start: { index: 0, columnHeader: 'Country' }, + end: { index: 2, columnHeader: 'City List' }, + }); + + await page.keyboard.press('ArrowRight'); + expect(await grid.selectedCount()).toBe(1); + expect(await grid.cell.verifyCellActiveSelected({ index: 0, columnHeader: 'LastUpdate' })); + }); +}); From f869c8fdc6a32bc54e654d34a3de93edbb197cc0 Mon Sep 17 00:00:00 2001 From: Ekaterina Balakina Date: Tue, 20 Dec 2022 17:41:04 +0100 Subject: [PATCH 067/134] fix tests --- tests/playwright/tests/cellSelection.spec.ts | 26 +++++++++----------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/tests/playwright/tests/cellSelection.spec.ts b/tests/playwright/tests/cellSelection.spec.ts index c58f4c3cd0..9ef20b7fb9 100644 --- a/tests/playwright/tests/cellSelection.spec.ts +++ b/tests/playwright/tests/cellSelection.spec.ts @@ -14,36 +14,34 @@ test.describe('Verify cell selection', () => { }); test('#1 when range is selected, it has correct number of selected cells', async () => { - await dashboard.treeView.openTable({ title: 'Country' }); + await dashboard.treeView.openTable({ title: 'Customer' }); await grid.selectRange({ - start: { index: 0, columnHeader: 'Country' }, - end: { index: 2, columnHeader: 'City List' }, + start: { index: 0, columnHeader: 'FirstName' }, + end: { index: 2, columnHeader: 'Email' }, }); expect(await grid.selectedCount()).toBe(9); }); test('#2 when copied with clipboard, it copies correct text', async () => { - await dashboard.treeView.openTable({ title: 'Country' }); + await dashboard.treeView.openTable({ title: 'Customer' }); await grid.selectRange({ - start: { index: 0, columnHeader: 'Country' }, - end: { index: 1, columnHeader: 'LastUpdate' }, + start: { index: 0, columnHeader: 'FirstName' }, + end: { index: 1, columnHeader: 'LastName' }, }); - expect(await grid.copyWithKeyboard()).toBe( - 'Afghanistan \t 2006-02-15 04:44:00\n' + ' Algeria \t 2006-02-15 04:44:00\n' - ); + expect(await grid.copyWithKeyboard()).toBe('MARY \t SMITH\n' + ' PATRICIA \t JOHNSON\n'); }); test('#3 when copied with mouse, it copies correct text', async () => { - await dashboard.treeView.openTable({ title: 'Country' }); + await dashboard.treeView.openTable({ title: 'Customer' }); await grid.selectRange({ - start: { index: 0, columnHeader: 'Country' }, - end: { index: 1, columnHeader: 'LastUpdate' }, + start: { index: 0, columnHeader: 'FirstName' }, + end: { index: 1, columnHeader: 'LastName' }, }); - expect(await grid.copyWithMouse({ index: 0, columnHeader: 'Country' })).toBe( - 'Afghanistan \t 2006-02-15 04:44:00\n' + ' Algeria \t 2006-02-15 04:44:00\n' + expect(await grid.copyWithMouse({ index: 0, columnHeader: 'FirstName' })).toBe( + 'MARY \t SMITH\n' + ' PATRICIA \t JOHNSON\n' ); }); From fc9a4caae33594ac0a066fb738723285e75b4a72 Mon Sep 17 00:00:00 2001 From: Ekaterina Balakina Date: Wed, 21 Dec 2022 09:38:45 +0100 Subject: [PATCH 068/134] review fixes --- packages/nc-gui/components/smartsheet/Grid.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index 4dc08c0990..304daa4b8e 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -229,7 +229,6 @@ const { isCellSelected, activeCell, handleMouseDown, handleMouseOver, handleCell case 'ArrowUp': e.preventDefault() clearSelectedRange() - $e('c:shortcut', { key: 'CTRL + ArrowUp' }) activeCell.row = 0 activeCell.col = activeCell.col ?? 0 scrollToCell?.() From 5c19bd3c9a2cdd7c1d776ac40682122beb9b622d Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Wed, 21 Dec 2022 18:46:17 +0800 Subject: [PATCH 069/134] fix(nc-gui): check undefined or null instead for boolean case --- packages/nc-gui/utils/dateTimeUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nc-gui/utils/dateTimeUtils.ts b/packages/nc-gui/utils/dateTimeUtils.ts index 8736221840..674b063854 100644 --- a/packages/nc-gui/utils/dateTimeUtils.ts +++ b/packages/nc-gui/utils/dateTimeUtils.ts @@ -17,7 +17,7 @@ export const dateFormats = [ ] export const handleTZ = (val: any) => { - if (!val) { + if (val === undefined || val === null) { return } if (typeof val !== 'string') { From eea2c6e5d0c30dd89b626b9e994704749d9d591a Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Wed, 21 Dec 2022 18:46:39 +0800 Subject: [PATCH 070/134] fix(nc-gui): missing result for PG --- .../lib/sql/formulav2/formulaQueryBuilderv2.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts index ecfd94e6ac..2637810013 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts @@ -721,6 +721,18 @@ export default async function formulaQueryBuilderv2( : pt.right.value === '' : 0 }) ${colAlias}`; + } else if (knex.clientType() === 'pg') { + sql = `COALESCE(${left} ${pt.operator} ${right}, ${ + pt.operator === '=' + ? pt.left.type === 'Literal' + ? pt.left.value === '' + : pt.right.value === '' + : pt.operator === '!=' + ? pt.left.type !== 'Literal' + ? pt.left.value === '' + : pt.right.value === '' + : false + }) ${colAlias}`; } const query = knex.raw(sql); From aa91fb2cca2d42982ceaad39af75a73bb47a926e Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Wed, 21 Dec 2022 18:56:54 +0800 Subject: [PATCH 071/134] fix(nocodb): missing result for sqlite3 --- .../lib/sql/formulav2/formulaQueryBuilderv2.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts index 2637810013..91240d6e92 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts @@ -721,7 +721,10 @@ export default async function formulaQueryBuilderv2( : pt.right.value === '' : 0 }) ${colAlias}`; - } else if (knex.clientType() === 'pg') { + } else if ( + knex.clientType() === 'pg' || + knex.clientType() === 'sqlite3' + ) { sql = `COALESCE(${left} ${pt.operator} ${right}, ${ pt.operator === '=' ? pt.left.type === 'Literal' From f584211eaa7d206805d47b869a689958327b891e Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Wed, 21 Dec 2022 21:28:25 +0800 Subject: [PATCH 072/134] fix(nocodb): add case when for AND and OR in MSSQL fn --- .../lib/sql/functionMappings/mssql.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mssql.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mssql.ts index 1358a87411..903478af5f 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mssql.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mssql.ts @@ -123,6 +123,30 @@ const mssql = { )} % 7 + 7) % 7 ${colAlias}` ); }, + AND: (args: MapFnArgs) => { + return args.knex.raw( + `CASE WHEN ${args.knex + .raw( + `${args.pt.arguments + .map((ar) => args.fn(ar, '', 'AND').toQuery()) + .join(' AND ')}` + ) + .wrap('(', ')') + .toQuery()} THEN 1 ELSE 0 END ${args.colAlias}` + ); + }, + OR: (args: MapFnArgs) => { + return args.knex.raw( + `CASE WHEN ${args.knex + .raw( + `${args.pt.arguments + .map((ar) => args.fn(ar, '', 'OR').toQuery()) + .join(' OR ')}` + ) + .wrap('(', ')') + .toQuery()} THEN 1 ELSE 0 END ${args.colAlias}` + ); + }, }; export default mssql; From 4cef10222db474d5b3a07f597a691f4afce61ea5 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Wed, 21 Dec 2022 21:29:07 +0800 Subject: [PATCH 073/134] fix(nocodb): empty result for MSSQL --- .../sql/formulav2/formulaQueryBuilderv2.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts index 91240d6e92..8b091d2fa8 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts @@ -736,8 +736,27 @@ export default async function formulaQueryBuilderv2( : pt.right.value === '' : false }) ${colAlias}`; - } + } else if (knex.clientType() === 'mssql') { + if (pt.operator === '=') { + if (pt.left.type === 'Literal' && pt.left.value === '') { + sql = `${right} IS NULL OR ${right} = ''`; + } else if (pt.right.type === 'Literal' && pt.right.value === '') { + sql = `${left} IS NULL OR ${left} = ''`; + } + } else if (pt.operator === '!=') { + if (pt.left.type === 'Literal' && pt.left.value === '') { + sql = `${right} IS NOT NULL AND ${right} != ''`; + } else if (pt.right.type === 'Literal' && pt.right.value === '') { + sql = `${left} IS NOT NULL AND ${left} != ''`; + } + } + if (prevBinaryOp !== 'AND' && prevBinaryOp !== 'OR') { + sql = `CASE WHEN ${sql} THEN 1 ELSE 0 END ${colAlias}`; + } else { + sql = `${sql} ${colAlias}`; + } + } const query = knex.raw(sql); if (prevBinaryOp && pt.operator !== prevBinaryOp) { query.wrap('(', ')'); From addb773fa25de1f921672f3f63d1f3760d295848 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Wed, 21 Dec 2022 21:39:01 +0800 Subject: [PATCH 074/134] refactor(nocodb): rename extractRawQueryAndExec to execAndParse --- .../sql-data-mapper/lib/sql/BaseModelSqlv2.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts index f6771498a6..6554e721a0 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts @@ -100,7 +100,7 @@ class BaseModelSqlv2 { qb.where(_wherePk(this.model.primaryKeys, id)); - let data = (await this.extractRawQueryAndExec(qb))?.[0]; + let data = (await this.execAndParse(qb))?.[0]; if (data) { const proto = await this.getProto(); @@ -252,7 +252,7 @@ class BaseModelSqlv2 { if (!ignoreViewFilterAndSort) applyPaginate(qb, rest); const proto = await this.getProto(); - let data = await this.extractRawQueryAndExec(qb); + let data = await this.execAndParse(qb); return data?.map((d) => { d.__proto__ = proto; @@ -423,7 +423,7 @@ class BaseModelSqlv2 { .as('list') ); - let children = await this.extractRawQueryAndExec(childQb, childTable); + let children = await this.execAndParse(childQb, childTable); const proto = await ( await Model.getBaseModelSQL({ id: childTable.id, @@ -550,7 +550,7 @@ class BaseModelSqlv2 { await childModel.selectObject({ qb }); - let children = await this.extractRawQueryAndExec(qb, childTable); + let children = await this.execAndParse(qb, childTable); const proto = await ( await Model.getBaseModelSQL({ @@ -667,7 +667,7 @@ class BaseModelSqlv2 { !this.isSqlite ); - let children = await this.extractRawQueryAndExec(finalQb, childTable); + let children = await this.execAndParse(finalQb, childTable); if (this.isMySQL) { children = children[0]; } @@ -735,7 +735,7 @@ class BaseModelSqlv2 { qb.limit(+rest?.limit || 25); qb.offset(+rest?.offset || 0); - let children = await this.extractRawQueryAndExec(qb, childTable); + let children = await this.execAndParse(qb, childTable); const proto = await ( await Model.getBaseModelSQL({ id: rtnId, dbDriver: this.dbDriver }) ).getProto(); @@ -1075,7 +1075,7 @@ class BaseModelSqlv2 { applyPaginate(qb, rest); const proto = await childModel.getProto(); - let data = await this.extractRawQueryAndExec(qb, childTable); + let data = await this.execAndParse(qb, childTable); return data.map((c) => { c.__proto__ = proto; @@ -1193,7 +1193,7 @@ class BaseModelSqlv2 { applyPaginate(qb, rest); const proto = await parentModel.getProto(); - let data = await this.extractRawQueryAndExec(qb, childTable); + let data = await this.execAndParse(qb, childTable); return data.map((c) => { c.__proto__ = proto; @@ -1525,7 +1525,7 @@ class BaseModelSqlv2 { query.returning( `${this.model.primaryKey.column_name} as ${this.model.primaryKey.title}` ); - response = await this.extractRawQueryAndExec(query); + response = await this.execAndParse(query); } const ai = this.model.columns.find((c) => c.ai); @@ -1535,7 +1535,7 @@ class BaseModelSqlv2 { // handle if autogenerated primary key is used if (ag) { - if (!response) await this.extractRawQueryAndExec(query); + if (!response) await this.execAndParse(query); response = await this.readByPk(data[ag.title]); } else if ( !response || @@ -1545,7 +1545,7 @@ class BaseModelSqlv2 { if (response?.length) { id = response[0]; } else { - const res = await this.extractRawQueryAndExec(query); + const res = await this.execAndParse(query); id = res?.id ?? res[0]?.insertId; } @@ -1650,7 +1650,7 @@ class BaseModelSqlv2 { .update(updateObj) .where(await this._wherePk(id)); - await this.extractRawQueryAndExec(query); + await this.execAndParse(query); const response = await this.readByPk(id); await this.afterUpdate(response, trx, cookie); @@ -2755,7 +2755,7 @@ class BaseModelSqlv2 { return await qb; } - private async extractRawQueryAndExec( + private async execAndParse( qb: Knex.QueryBuilder, childTable?: Model ) { From 9105cd8cd910ed703f98642f20dbe6b1e4b26c0f Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Wed, 21 Dec 2022 21:51:08 +0800 Subject: [PATCH 075/134] refactor(nocodb): move the logic to switch --- .../lib/sql/helpers/convertUnits.ts | 202 ++++++++---------- 1 file changed, 92 insertions(+), 110 deletions(-) diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/convertUnits.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/convertUnits.ts index 22daefe71c..961a9d26d3 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/convertUnits.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/convertUnits.ts @@ -5,149 +5,131 @@ export function convertUnits( switch (unit) { case 'milliseconds': case 'ms': { - if (type === 'mssql') { - return 'millisecond'; + 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; } - - if (type === 'mysql') { - // MySQL doesn't support millisecond - // hence change from MICROSECOND to millisecond manually - return 'MICROSECOND'; - } - - if (type === 'pg' || type === 'sqlite') { - return 'milliseconds'; - } - - return unit; } case 'seconds': case 's': { - if (type === 'mssql' || type === 'pg') { - return 'second'; - } - - if (type === 'mysql') { - return 'SECOND'; + switch (type) { + case 'mssql': + case 'pg': + return 'second'; + case 'mysql': + return 'SECOND'; + case 'sqlite': + return 'seconds'; + default: + return unit; } - - if (type === 'sqlite') { - return 'seconds'; - } - - return unit; } case 'minutes': case 'm': { - if (type === 'mssql' || type === 'pg') { - return 'minute'; - } - - if (type === 'mysql') { - return 'MINUTE'; + switch (type) { + case 'mssql': + case 'pg': + return 'minute'; + case 'mysql': + return 'MINUTE'; + case 'sqlite': + return 'minutes'; + default: + return unit; } - - if (type === 'sqlite') { - return 'minutes'; - } - - return unit; } case 'hours': case 'h': { - if (type === 'mssql' || type === 'pg') { - return 'hour'; - } - - if (type === 'mysql') { - return 'HOUR'; - } - - if (type === 'sqlite') { - return 'hours'; + switch (type) { + case 'mssql': + case 'pg': + return 'hour'; + case 'mysql': + return 'HOUR'; + case 'sqlite': + return 'hours'; + default: + return unit; } - - return unit; } case 'days': case 'd': { - if (type === 'mssql' || type === 'pg') { - return 'day'; - } - - if (type === 'mysql') { - return 'DAY'; - } - - if (type === 'sqlite') { - return 'days'; + switch (type) { + case 'mssql': + case 'pg': + return 'day'; + case 'mysql': + return 'DAY'; + case 'sqlite': + return 'days'; + default: + return unit; } - - return unit; } case 'weeks': case 'w': { - if (type === 'mssql' || type === 'pg') { - return 'week'; - } - - if (type === 'mysql') { - return 'WEEK'; + switch (type) { + case 'mssql': + case 'pg': + return 'week'; + case 'mysql': + return 'WEEK'; + case 'sqlite': + return 'weeks'; + default: + return unit; } - - if (type === 'sqlite') { - return 'weeks'; - } - - return unit; } case 'months': case 'M': { - if (type === 'mssql' || type === 'pg') { - return 'month'; + switch (type) { + case 'mssql': + case 'pg': + return 'month'; + case 'mysql': + return 'MONTH'; + case 'sqlite': + return 'months'; + default: + return unit; } - - if (type === 'mysql') { - return 'MONTH'; - } - - if (type === 'sqlite') { - return 'months'; - } - - return unit; } case 'quarters': case 'Q': { - if (type === 'mssql' || type === 'pg') { - return 'quarter'; + switch (type) { + case 'mssql': + case 'pg': + return 'quarter'; + case 'mysql': + return 'QUARTER'; + case 'sqlite': + return 'quarters'; + default: + return unit; } - - if (type === 'mysql') { - return 'QUARTER'; - } - - if (type === 'sqlite') { - return 'quarters'; - } - - return unit; } case 'years': case 'y': { - if (type === 'mssql' || type === 'pg') { - return 'year'; + switch (type) { + case 'mssql': + case 'pg': + return 'year'; + case 'mysql': + return 'YEAR'; + case 'sqlite': + return 'years'; + default: + return unit; } - - if (type === 'mysql') { - return 'YEAR'; - } - - if (type === 'sqlite') { - return 'years'; - } - - return unit; } default: return unit; From f51458b73b26cc96f3c288a9d58d816183cd54eb Mon Sep 17 00:00:00 2001 From: Vijay Rathore Date: Wed, 21 Dec 2022 20:09:35 +0530 Subject: [PATCH 076/134] Correct parameter in delete token --- packages/nocodb/src/lib/meta/api/apiTokenApis.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nocodb/src/lib/meta/api/apiTokenApis.ts b/packages/nocodb/src/lib/meta/api/apiTokenApis.ts index 11b9b3627c..dea013346d 100644 --- a/packages/nocodb/src/lib/meta/api/apiTokenApis.ts +++ b/packages/nocodb/src/lib/meta/api/apiTokenApis.ts @@ -14,7 +14,7 @@ export async function apiTokenCreate(req: Request, res: Response) { res.json(await ApiToken.insert({ ...req.body, fk_user_id: req['user'].id })); } export async function apiTokenDelete(req: Request, res: Response) { - const apiToken = await ApiToken.getByToken(req.params.apiTokenId); + const apiToken = await ApiToken.getByToken(req.params.token); if ( !req['user'].roles.includes(OrgUserRoles.SUPER_ADMIN) && apiToken.fk_user_id !== req['user'].id From badf05c8f2ed05a2ba76fea2e1a7e0698fccfb57 Mon Sep 17 00:00:00 2001 From: mertmit Date: Fri, 9 Dec 2022 14:26:47 +0300 Subject: [PATCH 077/134] feat: SnowflakeClient alfa Signed-off-by: mertmit --- .../nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts | 1981 +++++++++++++ .../nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts | 5 + packages/nocodb-sdk/src/lib/sqlUi/index.ts | 1 + .../lib/db/sql-client/lib/SqlClientFactory.ts | 5 + .../lib/snowflake/SnowflakeClient.ts | 2443 +++++++++++++++++ .../lib/snowflake/snowflake.queries.ts | 5 + .../sql-data-mapper/lib/sql/BaseModelSqlv2.ts | 10 +- .../db/sql-data-mapper/lib/sql/CustomKnex.ts | 5 +- .../code/models/xc/ModelXcMetaFactory.ts | 3 + .../code/models/xc/ModelXcMetaSnowflake.ts | 975 +++++++ 10 files changed, 5429 insertions(+), 4 deletions(-) create mode 100644 packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts create mode 100644 packages/nocodb/src/lib/db/sql-client/lib/snowflake/SnowflakeClient.ts create mode 100644 packages/nocodb/src/lib/db/sql-client/lib/snowflake/snowflake.queries.ts create mode 100644 packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaSnowflake.ts diff --git a/packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts b/packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts new file mode 100644 index 0000000000..ce3fa80b9a --- /dev/null +++ b/packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts @@ -0,0 +1,1981 @@ +import UITypes from '../UITypes'; +import { IDType } from './index'; + +const dbTypes = [ + 'int', + 'integer', + 'bigint', + 'bigserial', + 'char', + 'int2', + 'int4', + 'int8', + 'int4range', + 'int8range', + 'serial', + 'serial2', + 'serial8', + 'character', + 'bit', + 'bool', + 'boolean', + 'date', + 'double precision', + 'event_trigger', + 'fdw_handler', + 'float4', + 'float8', + 'uuid', + 'smallint', + 'smallserial', + 'character varying', + 'text', + 'real', + 'time', + 'time without time zone', + 'timestamp', + 'timestamp without time zone', + 'timestamptz', + 'timestamp with time zone', + 'timetz', + 'time with time zone', + 'daterange', + 'json', + 'jsonb', + 'gtsvector', + 'index_am_handler', + 'anyenum', + 'anynonarray', + 'anyrange', + 'box', + 'bpchar', + 'bytea', + 'cid', + 'cidr', + 'circle', + 'cstring', + 'inet', + 'internal', + 'interval', + 'language_handler', + 'line', + 'lsec', + 'macaddr', + 'money', + 'name', + 'numeric', + 'numrange', + 'oid', + 'opaque', + 'path', + 'pg_ddl_command', + 'pg_lsn', + 'pg_node_tree', + 'point', + 'polygon', + 'record', + 'refcursor', + 'regclass', + 'regconfig', + 'regdictionary', + 'regnamespace', + 'regoper', + 'regoperator', + 'regproc', + 'regpreocedure', + 'regrole', + 'regtype', + 'reltime', + 'smgr', + 'tid', + 'tinterval', + 'trigger', + 'tsm_handler', + 'tsquery', + 'tsrange', + 'tstzrange', + 'tsvector', + 'txid_snapshot', + 'unknown', + 'void', + 'xid', + 'xml', +]; + +export class SnowflakeUi { + static getNewTableColumns() { + return [ + { + column_name: 'id', + title: 'Id', + dt: 'int4', + dtx: 'integer', + ct: 'int(11)', + nrqd: false, + rqd: true, + ck: false, + pk: true, + un: false, + ai: true, + cdf: null, + clen: null, + np: 11, + ns: 0, + dtxp: '11', + dtxs: '', + altered: 1, + uidt: 'ID', + uip: '', + uicn: '', + }, + { + column_name: 'title', + title: 'Title', + dt: 'character varying', + dtx: 'specificType', + ct: 'varchar(45)', + nrqd: true, + rqd: false, + ck: false, + pk: false, + un: false, + ai: false, + cdf: null, + clen: 45, + np: null, + ns: null, + dtxp: '45', + dtxs: '', + altered: 1, + uidt: 'SingleLineText', + uip: '', + uicn: '', + }, + { + column_name: 'created_at', + title: 'CreatedAt', + dt: 'timestamp', + dtx: 'specificType', + ct: 'varchar(45)', + nrqd: true, + rqd: false, + ck: false, + pk: false, + un: false, + ai: false, + cdf: 'now()', + clen: 45, + np: null, + ns: null, + dtxp: '', + dtxs: '', + altered: 1, + uidt: UITypes.DateTime, + uip: '', + uicn: '', + }, + { + column_name: 'updated_at', + title: 'UpdatedAt', + dt: 'timestamp', + dtx: 'specificType', + ct: 'varchar(45)', + nrqd: true, + rqd: false, + ck: false, + pk: false, + un: false, + ai: false, + au: true, + cdf: 'now()', + clen: 45, + np: null, + ns: null, + dtxp: '', + dtxs: '', + altered: 1, + uidt: UITypes.DateTime, + uip: '', + uicn: '', + }, + ]; + } + + static getNewColumn(suffix) { + return { + column_name: 'title' + suffix, + dt: 'character varying', + dtx: 'specificType', + ct: 'varchar(45)', + nrqd: true, + rqd: false, + ck: false, + pk: false, + un: false, + ai: false, + cdf: null, + clen: 45, + np: null, + ns: null, + dtxp: '45', + dtxs: '', + altered: 1, + uidt: 'SingleLineText', + uip: '', + uicn: '', + }; + } + + // static getDefaultLengthForDatatype(type) { + // switch (type) { + // case "int": + // return 11; + // break; + // case "tinyint": + // return 1; + // break; + // case "smallint": + // return 5; + // break; + // + // case "mediumint": + // return 9; + // break; + // case "bigint": + // return 20; + // break; + // case "bit": + // return 64; + // break; + // case "boolean": + // return ''; + // break; + // case "float": + // return 12; + // break; + // case "decimal": + // return 10; + // break; + // case "double": + // return 22; + // break; + // case "serial": + // return 20; + // break; + // case "date": + // return ''; + // break; + // case "datetime": + // case "timestamp": + // return 6; + // break; + // case "time": + // return ''; + // break; + // case "year": + // return ''; + // break; + // case "char": + // return 255; + // break; + // case "varchar": + // return 45; + // break; + // case "nchar": + // return 255; + // break; + // case "text": + // return ''; + // break; + // case "tinytext": + // return ''; + // break; + // case "mediumtext": + // return ''; + // break; + // case "longtext": + // return '' + // break; + // case "binary": + // return 255; + // break; + // case "varbinary": + // return 65500; + // break; + // case "blob": + // return ''; + // break; + // case "tinyblob": + // return ''; + // break; + // case "mediumblob": + // return ''; + // break; + // case "longblob": + // return ''; + // break; + // case "enum": + // return '\'a\',\'b\''; + // break; + // case "set": + // return '\'a\',\'b\''; + // break; + // case "geometry": + // return ''; + // case "point": + // return ''; + // case "linestring": + // return ''; + // case "polygon": + // return ''; + // case "multipoint": + // return ''; + // case "multilinestring": + // return ''; + // case "multipolygon": + // return ''; + // case "json": + // return '' + // break; + // + // } + // + // } + + static getDefaultLengthForDatatype(type): any { + switch (type) { + case 'int': + return ''; + + case 'tinyint': + return ''; + + case 'smallint': + return ''; + + case 'mediumint': + return ''; + + case 'bigint': + return ''; + + case 'bit': + return ''; + + case 'boolean': + return ''; + + case 'float': + return ''; + + case 'decimal': + return ''; + + case 'double': + return ''; + + case 'serial': + return ''; + + case 'date': + return ''; + + case 'datetime': + case 'timestamp': + return ''; + + case 'time': + return ''; + + case 'year': + return ''; + + case 'char': + return ''; + + case 'varchar': + return ''; + + case 'nchar': + return ''; + + case 'text': + return ''; + + case 'tinytext': + return ''; + + case 'mediumtext': + return ''; + + case 'longtext': + return ''; + + case 'binary': + return ''; + + case 'varbinary': + return ''; + + case 'blob': + return ''; + + case 'tinyblob': + return ''; + + case 'mediumblob': + return ''; + + case 'longblob': + return ''; + + case 'enum': + return ''; + + case 'set': + return ''; + + case 'geometry': + return ''; + case 'point': + return ''; + case 'linestring': + return ''; + case 'polygon': + return ''; + case 'multipoint': + return ''; + case 'multilinestring': + return ''; + case 'multipolygon': + return ''; + case 'json': + return ''; + } + } + + static getDefaultLengthIsDisabled(type): any { + switch (type) { + case 'anyenum': + case 'anynonarray': + case 'anyrange': + case 'bigint': + case 'bigserial': + case 'bit': + case 'bool': + case 'box': + case 'bpchar': + case 'bytea': + case 'char': + case 'character': + case 'cid': + case 'cidr': + case 'circle': + case 'cstring': + case 'date': + case 'daterange': + case 'double precision': + case 'event_trigger': + case 'fdw_handler': + case 'float4': + case 'float8': + case 'gtsvector': + case 'index_am_handler': + case 'inet': + case 'int': + case 'int2': + case 'int4': + case 'int8': + case 'int4range': + case 'int8range': + case 'integer': + case 'internal': + case 'interval': + case 'jsonb': + case 'language_handler': + case 'line': + case 'lsec': + case 'macaddr': + case 'money': + case 'name': + case 'numeric': + case 'numrange': + case 'oid': + case 'opaque': + case 'path': + case 'pg_ddl_command': + case 'pg_lsn': + case 'pg_node_tree': + case 'real': + case 'record': + case 'refcursor': + case 'regclass': + case 'regconfig': + case 'regdictionary': + case 'regnamespace': + case 'regoper': + case 'regoperator': + case 'regproc': + case 'regpreocedure': + case 'regrole': + case 'regtype': + case 'reltime': + case 'serial': + case 'serial2': + case 'serial8': + case 'smallint': + case 'smallserial': + case 'smgr': + case 'text': + case 'tid': + case 'time': + case 'time without time zone': + case 'timestamp': + case 'timestamp without time zone': + case 'timestamptz': + case 'timestamp with time zone': + case 'timetz': + case 'time with time zone': + case 'tinterval': + case 'trigger': + case 'tsm_handler': + case 'tsquery': + case 'tsrange': + case 'tstzrange': + case 'tsvector': + case 'txid_snapshot': + case 'unknown': + case 'void': + case 'xid': + case 'xml': + case 'character varying': + case 'tinyint': + case 'mediumint': + case 'float': + case 'decimal': + case 'double': + case 'boolean': + case 'datetime': + case 'uuid': + case 'year': + case 'varchar': + case 'nchar': + case 'tinytext': + case 'mediumtext': + case 'longtext': + case 'binary': + case 'varbinary': + case 'blob': + case 'tinyblob': + case 'mediumblob': + case 'longblob': + case 'enum': + case 'set': + case 'geometry': + case 'point': + case 'linestring': + case 'polygon': + case 'multipoint': + case 'multilinestring': + case 'multipolygon': + case 'json': + return true; + } + } + + static getDefaultValueForDatatype(type): any { + switch (type) { + case 'anyenum': + return 'eg: '; + + case 'anynonarray': + return 'eg: '; + + case 'anyrange': + return 'eg: '; + + case 'bigint': + return 'eg: '; + + case 'bigserial': + return 'eg: '; + + case 'bit': + return 'eg: '; + + case 'bool': + return 'eg: '; + + case 'box': + return 'eg: '; + + case 'bpchar': + return 'eg: '; + + case 'bytea': + return 'eg: '; + + case 'char': + return 'eg: '; + + case 'character': + return "eg: 'sample'"; + + case 'cid': + return 'eg: '; + + case 'cidr': + return 'eg: '; + + case 'circle': + return 'eg: '; + + case 'cstring': + return 'eg: '; + + case 'date': + return "eg: '2020-09-09'"; + + case 'daterange': + return 'eg: '; + + case 'double precision': + return 'eg: 1.2'; + + case 'event_trigger': + return 'eg: '; + + case 'fdw_handler': + return 'eg: '; + + case 'float4': + return 'eg: 1.2'; + + case 'float8': + return 'eg: 1.2'; + + case 'gtsvector': + return 'eg: '; + + case 'index_am_handler': + return 'eg: '; + + case 'inet': + return 'eg: '; + + case 'int': + return 'eg: '; + + case 'int2': + return 'eg: '; + + case 'int4': + return 'eg: '; + + case 'int8': + return 'eg: '; + + case 'int4range': + return 'eg: '; + + case 'int8range': + return 'eg: '; + + case 'integer': + return 'eg: '; + + case 'internal': + return 'eg: '; + + case 'interval': + return 'eg: '; + + case 'json': + return 'eg: '; + + case 'jsonb': + return 'eg: '; + + case 'language_handler': + return 'eg: '; + + case 'line': + return 'eg: '; + + case 'lsec': + return 'eg: '; + + case 'macaddr': + return 'eg: '; + + case 'money': + return 'eg: '; + + case 'name': + return 'eg: '; + + case 'numeric': + return 'eg: '; + + case 'numrange': + return 'eg: '; + + case 'oid': + return 'eg: '; + + case 'opaque': + return 'eg: '; + + case 'path': + return 'eg: '; + + case 'pg_ddl_command': + return 'eg: '; + + case 'pg_lsn': + return 'eg: '; + + case 'pg_node_tree': + return 'eg: '; + + case 'point': + return 'eg: '; + + case 'polygon': + return 'eg: '; + + case 'real': + return 'eg: 1.2'; + + case 'record': + return 'eg: '; + + case 'refcursor': + return 'eg: '; + + case 'regclass': + return 'eg: '; + + case 'regconfig': + return 'eg: '; + + case 'regdictionary': + return 'eg: '; + + case 'regnamespace': + return 'eg: '; + + case 'regoper': + return 'eg: '; + + case 'regoperator': + return 'eg: '; + + case 'regproc': + return 'eg: '; + + case 'regpreocedure': + return 'eg: '; + + case 'regrole': + return 'eg: '; + + case 'regtype': + return 'eg: '; + + case 'reltime': + return 'eg: '; + + case 'serial': + return 'eg: '; + + case 'serial2': + return 'eg: '; + + case 'serial8': + return 'eg: '; + + case 'smallint': + return 'eg: '; + + case 'smallserial': + return 'eg: '; + + case 'smgr': + return 'eg: '; + + case 'text': + return "eg: 'sample text'"; + + case 'tid': + return 'eg: '; + + case 'time': + return "eg: now()\n\n'04:05:06.789'"; + + case 'time without time zone': + return "eg: now()\n\n'04:05:06.789'"; + + case 'timestamp': + return "eg: now()\n\n'2016-06-22 19:10:25-07'"; + + case 'timestamp without time zone': + return "eg: now()\n\n'2016-06-22 19:10:25-07'"; + + case 'timestamptz': + return "eg: timezone('America/New_York','2016-06-01 00:00')\n\nnow()\n\n'2016-06-22 19:10:25-07'"; + + case 'timestamp with time zone': + return "eg: now()\n\n'2016-06-22 19:10:25-07'"; + + case 'timetz': + return 'eg: now()'; + + case 'time with time zone': + return 'eg: now()'; + + case 'tinterval': + return 'eg: '; + + case 'trigger': + return 'eg: '; + + case 'tsm_handler': + return 'eg: '; + + case 'tsquery': + return 'eg: '; + + case 'tsrange': + return 'eg: '; + + case 'tstzrange': + return 'eg: '; + + case 'tsvector': + return 'eg: '; + + case 'txid_snapshot': + return 'eg: '; + + case 'unknown': + return 'eg: '; + + case 'void': + return 'eg: '; + + case 'xid': + return 'eg: '; + + case 'xml': + return 'eg: '; + + case 'character varying': + return "eg: 'sample text'"; + + case 'tinyint': + return 'eg: '; + + case 'mediumint': + return 'eg: '; + + case 'float': + return 'eg: '; + + case 'decimal': + return 'eg: '; + + case 'double': + return 'eg: 1.2'; + + case 'boolean': + return 'eg: true\n\nfalse'; + + case 'datetime': + return 'eg: '; + + case 'uuid': + return 'eg: '; + + case 'year': + return 'eg: '; + + case 'varchar': + return 'eg: '; + + case 'nchar': + return 'eg: '; + + case 'tinytext': + return 'eg: '; + + case 'mediumtext': + return 'eg: '; + + case 'longtext': + return 'eg: '; + + case 'binary': + return 'eg: '; + + case 'varbinary': + return 'eg: '; + + case 'blob': + return 'eg: '; + + case 'tinyblob': + return 'eg: '; + + case 'mediumblob': + return 'eg: '; + + case 'longblob': + return 'eg: '; + + case 'enum': + return 'eg: '; + + case 'set': + return 'eg: '; + + case 'geometry': + return 'eg: '; + + case 'linestring': + return 'eg: '; + + case 'multipoint': + return 'eg: '; + + case 'multilinestring': + return 'eg: '; + + case 'multipolygon': + return 'eg: '; + } + } + + static getDefaultScaleForDatatype(type): any { + switch (type) { + case 'int': + return ' '; + + case 'tinyint': + return ' '; + + case 'smallint': + return ' '; + + case 'mediumint': + return ' '; + + case 'bigint': + return ' '; + + case 'bit': + return ' '; + + case 'boolean': + return ' '; + + case 'float': + return '2'; + + case 'decimal': + return '2'; + + case 'double': + return '2'; + + case 'serial': + return ' '; + + case 'date': + case 'datetime': + case 'timestamp': + return ' '; + + case 'time': + return ' '; + + case 'year': + return ' '; + + case 'char': + return ' '; + + case 'varchar': + return ' '; + + case 'nchar': + return ' '; + + case 'text': + return ' '; + + case 'tinytext': + return ' '; + + case 'mediumtext': + return ' '; + + case 'longtext': + return ' '; + + case 'binary': + return ' '; + + case 'varbinary': + return ' '; + + case 'blob': + return ' '; + + case 'tinyblob': + return ' '; + + case 'mediumblob': + return ' '; + + case 'longblob': + return ' '; + + case 'enum': + return ' '; + + case 'set': + return ' '; + + case 'geometry': + return ' '; + case 'point': + return ' '; + case 'linestring': + return ' '; + case 'polygon': + return ' '; + case 'multipoint': + return ' '; + case 'multilinestring': + return ' '; + case 'multipolygon': + return ' '; + case 'json': + return ' '; + } + } + + static colPropAIDisabled(col, columns) { + // console.log(col); + if ( + col.dt === 'int4' || + col.dt === 'integer' || + col.dt === 'bigint' || + col.dt === 'smallint' + ) { + for (let i = 0; i < columns.length; ++i) { + if (columns[i].cn !== col.cn && columns[i].ai) { + return true; + } + } + return false; + } else { + return true; + } + } + + static colPropUNDisabled(_col) { + // console.log(col); + return true; + // if (col.dt === 'int' || + // col.dt === 'tinyint' || + // col.dt === 'smallint' || + // col.dt === 'mediumint' || + // col.dt === 'bigint') { + // return false; + // } else { + // return true; + // } + } + + static onCheckboxChangeAI(col) { + console.log(col); + if ( + col.dt === 'int' || + col.dt === 'bigint' || + col.dt === 'smallint' || + col.dt === 'tinyint' + ) { + col.altered = col.altered || 2; + } + + // if (!col.ai) { + // col.dtx = 'specificType' + // } else { + // col.dtx = '' + // } + } + + static onCheckboxChangeAU(col) { + console.log(col); + // if (1) { + col.altered = col.altered || 2; + // } + if (col.au) { + col.cdf = 'now()'; + } + + // if (!col.ai) { + // col.dtx = 'specificType' + // } else { + // col.dtx = '' + // } + } + + static showScale(_columnObj) { + return false; + } + + static removeUnsigned(columns) { + for (let i = 0; i < columns.length; ++i) { + if ( + columns[i].altered === 1 && + !( + columns[i].dt === 'int' || + columns[i].dt === 'bigint' || + columns[i].dt === 'tinyint' || + columns[i].dt === 'smallint' || + columns[i].dt === 'mediumint' + ) + ) { + columns[i].un = false; + console.log('>> resetting unsigned value', columns[i].cn); + } + console.log(columns[i].cn); + } + } + + static columnEditable(colObj) { + return colObj.tn !== '_evolutions' || colObj.tn !== 'nc_evolutions'; + } + + static extractFunctionName(query) { + const reg = + /^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*FUNCTION\s+(?:[\w\d_]+\.)?([\w_\d]+)/i; + const match = query.match(reg); + return match && match[1]; + } + + static extractProcedureName(query) { + const reg = + /^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*PROCEDURE\s+(?:[\w\d_]+\.)?([\w_\d]+)/i; + const match = query.match(reg); + return match && match[1]; + } + + static handleRawOutput(result, headers) { + if (['DELETE', 'INSERT', 'UPDATE'].includes(result.command.toUpperCase())) { + headers.push({ text: 'Row count', value: 'rowCount', sortable: false }); + result = [ + { + rowCount: result.rowCount, + }, + ]; + } else { + result = result.rows; + if (Array.isArray(result) && result[0]) { + const keys = Object.keys(result[0]); + // set headers before settings result + for (let i = 0; i < keys.length; i++) { + const text = keys[i]; + headers.push({ text, value: text, sortable: false }); + } + } + } + return result; + } + + static splitQueries(query) { + /*** + * we are splitting based on semicolon + * there are mechanism to escape semicolon within single/double quotes(string) + */ + return query.match(/\b("[^"]*;[^"]*"|'[^']*;[^']*'|[^;])*;/g); + } + + /** + * if sql statement is SELECT - it limits to a number + * @param args + * @returns {string|*} + */ + sanitiseQuery(args) { + let q = args.query.trim().split(';'); + + if (q[0].startsWith('Select')) { + q = q[0] + ` LIMIT 0,${args.limit ? args.limit : 100};`; + } else if (q[0].startsWith('select')) { + q = q[0] + ` LIMIT 0,${args.limit ? args.limit : 100};`; + } else if (q[0].startsWith('SELECT')) { + q = q[0] + ` LIMIT 0,${args.limit ? args.limit : 100};`; + } else { + return args.query; + } + + return q; + } + + static getColumnsFromJson(json, tn) { + const columns = []; + + try { + if (typeof json === 'object' && !Array.isArray(json)) { + const keys = Object.keys(json); + for (let i = 0; i < keys.length; ++i) { + const column = { + dp: null, + tn, + column_name: keys[i], + cno: keys[i], + np: 10, + ns: 0, + clen: null, + cop: 1, + pk: false, + nrqd: false, + rqd: false, + un: false, + ct: 'int(11) unsigned', + ai: false, + unique: false, + cdf: null, + cc: '', + csn: null, + dtx: 'specificType', + dtxp: null, + dtxs: 0, + altered: 1, + }; + + switch (typeof json[keys[i]]) { + case 'number': + if (Number.isInteger(json[keys[i]])) { + if (SnowflakeUi.isValidTimestamp(keys[i], json[keys[i]])) { + Object.assign(column, { + dt: 'timestamp', + }); + } else { + Object.assign(column, { + dt: 'int', + np: 10, + ns: 0, + }); + } + } else { + Object.assign(column, { + dt: 'float4', + np: null, + ns: null, + dtxp: null, + dtxs: null, + }); + } + break; + case 'string': + if (SnowflakeUi.isValidDate(json[keys[i]])) { + Object.assign(column, { + dt: 'date', + }); + } else if (json[keys[i]].length <= 255) { + Object.assign(column, { + dt: 'character varying', + np: null, + ns: 0, + dtxp: null, + }); + } else { + Object.assign(column, { + dt: 'text', + }); + } + break; + case 'boolean': + Object.assign(column, { + dt: 'boolean', + np: 3, + ns: 0, + }); + break; + case 'object': + Object.assign(column, { + dt: 'json', + np: 3, + ns: 0, + }); + break; + default: + break; + } + columns.push(column); + } + } + } catch (e) { + console.log('Error in getColumnsFromJson', e); + } + + return columns; + } + + static isValidTimestamp(key, value) { + if (typeof value !== 'number') { + return false; + } + return new Date(value).getTime() > 0 && /(?:_|(?=A))[aA]t$/.test(key); + } + + static isValidDate(value) { + return new Date(value).getTime() > 0; + } + + static colPropAuDisabled(col) { + if (col.altered !== 1) { + return true; + } + + switch (col.dt) { + case 'time': + case 'time without time zone': + case 'timestamp': + case 'timestamp without time zone': + case 'timestamptz': + case 'timestamp with time zone': + case 'timetz': + case 'time with time zone': + return false; + default: + return true; + } + } + + static getAbstractType(col): any { + switch ((col.dt || col.dt).toLowerCase()) { + case 'anyenum': + return 'enum'; + case 'anynonarray': + case 'anyrange': + return 'string'; + + case 'bit': + return 'integer'; + case 'bigint': + case 'bigserial': + return 'string'; + + case 'bool': + return 'boolean'; + + case 'box': + case 'bpchar': + case 'bytea': + case 'char': + case 'character': + return 'string'; + + case 'cid': + case 'cidr': + case 'circle': + case 'cstring': + return 'string'; + + case 'date': + return 'date'; + case 'daterange': + return 'string'; + case 'double precision': + return 'string'; + + case 'event_trigger': + case 'fdw_handler': + return 'string'; + + case 'float4': + case 'float8': + return 'float'; + + case 'gtsvector': + case 'index_am_handler': + case 'inet': + return 'string'; + + case 'int': + case 'int2': + case 'int4': + case 'int8': + case 'integer': + return 'integer'; + case 'int4range': + case 'int8range': + case 'internal': + case 'interval': + return 'string'; + case 'jsonb': + return 'string'; + + case 'language_handler': + case 'line': + case 'lsec': + case 'macaddr': + case 'money': + case 'name': + case 'numeric': + case 'numrange': + case 'oid': + case 'opaque': + case 'path': + case 'pg_ddl_command': + case 'pg_lsn': + case 'pg_node_tree': + case 'point': + case 'polygon': + return 'string'; + case 'real': + return 'float'; + case 'record': + case 'refcursor': + case 'regclass': + case 'regconfig': + case 'regdictionary': + case 'regnamespace': + case 'regoper': + case 'regoperator': + case 'regproc': + case 'regpreocedure': + case 'regrole': + case 'regtype': + case 'reltime': + return 'string'; + case 'serial': + case 'serial2': + case 'serial8': + case 'smallint': + case 'smallserial': + return 'integer'; + case 'smgr': + return 'string'; + case 'text': + return 'text'; + case 'tid': + return 'string'; + case 'time': + case 'time without time zone': + return 'time'; + case 'timestamp': + case 'timestamp without time zone': + case 'timestamptz': + case 'timestamp with time zone': + return 'datetime'; + case 'timetz': + case 'time with time zone': + return 'time'; + + case 'tinterval': + case 'trigger': + case 'tsm_handler': + case 'tsquery': + case 'tsrange': + case 'tstzrange': + case 'tsvector': + case 'txid_snapshot': + case 'unknown': + case 'void': + case 'xid': + case 'character varying': + case 'xml': + return 'string'; + + case 'tinyint': + case 'mediumint': + return 'integer'; + + case 'float': + case 'decimal': + case 'double': + return 'float'; + case 'boolean': + return 'boolean'; + case 'datetime': + return 'datetime'; + + case 'uuid': + case 'year': + case 'varchar': + case 'nchar': + return 'string'; + case 'tinytext': + case 'mediumtext': + case 'longtext': + return 'text'; + case 'binary': + case 'varbinary': + return 'string'; + case 'blob': + case 'tinyblob': + case 'mediumblob': + case 'longblob': + return 'blob'; + case 'enum': + return 'enum'; + case 'set': + return 'set'; + case 'geometry': + case 'linestring': + case 'multipoint': + case 'multilinestring': + case 'multipolygon': + return 'string'; + case 'json': + return 'json'; + } + } + + static getUIType(col): any { + switch (this.getAbstractType(col)) { + case 'integer': + return 'Number'; + case 'boolean': + return 'Checkbox'; + case 'float': + return 'Decimal'; + case 'date': + return 'Date'; + case 'datetime': + return 'CreateTime'; + case 'time': + return 'Time'; + case 'year': + return 'Year'; + case 'string': + return 'SingleLineText'; + case 'text': + return 'LongText'; + case 'blob': + return 'Attachment'; + case 'enum': + return 'SingleSelect'; + case 'set': + return 'MultiSelect'; + case 'json': + return 'LongText'; + } + } + + static getDataTypeForUiType(col: { uidt: UITypes }, idType?: IDType) { + const colProp: any = {}; + switch (col.uidt) { + case 'ID': + { + const isAutoIncId = idType === 'AI'; + const isAutoGenId = idType === 'AG'; + colProp.dt = isAutoGenId ? 'character varying' : 'int4'; + colProp.pk = true; + colProp.un = isAutoIncId; + colProp.ai = isAutoIncId; + colProp.rqd = true; + colProp.meta = isAutoGenId ? { ag: 'nc' } : undefined; + } + break; + case 'ForeignKey': + colProp.dt = 'character varying'; + break; + case 'SingleLineText': + colProp.dt = 'character varying'; + break; + case 'LongText': + colProp.dt = 'text'; + break; + case 'Attachment': + colProp.dt = 'text'; + break; + case 'Checkbox': + colProp.dt = 'bool'; + break; + case 'MultiSelect': + colProp.dt = 'text'; + break; + case 'SingleSelect': + colProp.dt = 'text'; + break; + case 'Collaborator': + colProp.dt = 'character varying'; + break; + case 'Date': + colProp.dt = 'date'; + + break; + case 'Year': + colProp.dt = 'int'; + break; + case 'Time': + colProp.dt = 'time'; + break; + case 'PhoneNumber': + colProp.dt = 'character varying'; + colProp.validate = { + func: ['isMobilePhone'], + args: [''], + msg: ['Validation failed : isMobilePhone'], + }; + break; + case 'Email': + colProp.dt = 'character varying'; + colProp.validate = { + func: ['isEmail'], + args: [''], + msg: ['Validation failed : isEmail'], + }; + break; + case 'URL': + colProp.dt = 'character varying'; + colProp.validate = { + func: ['isURL'], + args: [''], + msg: ['Validation failed : isURL'], + }; + break; + case 'Number': + colProp.dt = 'bigint'; + break; + case 'Decimal': + colProp.dt = 'decimal'; + break; + case 'Currency': + colProp.dt = 'decimal'; + colProp.validate = { + func: ['isCurrency'], + args: [''], + msg: ['Validation failed : isCurrency'], + }; + break; + case 'Percent': + colProp.dt = 'double precision'; + break; + case 'Duration': + colProp.dt = 'decimal'; + break; + case 'Rating': + colProp.dt = 'smallint'; + break; + case 'Formula': + colProp.dt = 'character varying'; + break; + case 'Rollup': + colProp.dt = 'character varying'; + break; + case 'Count': + colProp.dt = 'int8'; + break; + case 'Lookup': + colProp.dt = 'character varying'; + break; + case 'DateTime': + colProp.dt = 'timestamp'; + break; + case 'CreateTime': + colProp.dt = 'timestamp'; + break; + case 'LastModifiedTime': + colProp.dt = 'timestamp'; + break; + case 'AutoNumber': + colProp.dt = 'int'; + break; + case 'Barcode': + colProp.dt = 'character varying'; + break; + case 'Button': + colProp.dt = 'character varying'; + break; + case 'JSON': + colProp.dt = 'json'; + break; + default: + colProp.dt = 'character varying'; + break; + } + return colProp; + } + + static getDataTypeListForUiType(col: { uidt: UITypes }, idType: IDType) { + switch (col.uidt) { + case 'ID': + if (idType === 'AG') { + return ['char', 'character', 'character varying']; + } else if (idType === 'AI') { + return [ + 'int', + 'integer', + 'bigint', + 'bigserial', + 'int2', + 'int4', + 'int8', + 'serial', + 'serial2', + 'serial8', + 'smallint', + 'smallserial', + ]; + } else { + return dbTypes; + } + case 'ForeignKey': + return dbTypes; + + case 'SingleLineText': + case 'LongText': + case 'Collaborator': + return ['char', 'character', 'character varying', 'text']; + + case 'Attachment': + return ['json', 'char', 'character', 'character varying', 'text']; + + case 'JSON': + return ['json', 'jsonb', 'text']; + case 'Checkbox': + return [ + 'bit', + 'bool', + 'int2', + 'int4', + 'int8', + 'boolean', + 'smallint', + 'int', + 'integer', + 'bigint', + 'char', + 'int4range', + 'int8range', + ]; + + case 'MultiSelect': + return ['text']; + + case 'SingleSelect': + return ['text']; + + case 'Year': + return ['int']; + + case 'Time': + return [ + 'time', + 'time without time zone', + 'timestamp', + 'timestamp without time zone', + 'timestamptz', + 'timestamp with time zone', + 'timetz', + 'time with time zone', + ]; + + case 'PhoneNumber': + case 'Email': + return ['character varying']; + + case 'URL': + return ['character varying', 'text']; + + case 'Number': + return [ + 'int', + 'integer', + 'bigint', + 'int2', + 'int4', + 'int8', + 'double precision', + 'float4', + 'float8', + 'smallint', + 'numeric', + ]; + + case 'Decimal': + return ['double precision', 'float4', 'float8', 'numeric']; + + case 'Currency': + return [ + 'int', + 'integer', + 'bigint', + 'int2', + 'int4', + 'int8', + 'double precision', + 'money', + 'float4', + 'float8', + 'numeric', + ]; + + case 'Percent': + return [ + 'int', + 'integer', + 'bigint', + 'int2', + 'int4', + 'int8', + 'double precision', + 'float4', + 'float8', + 'smallint', + 'numeric', + ]; + + case 'Duration': + return [ + 'int', + 'integer', + 'bigint', + 'int2', + 'int4', + 'int8', + 'double precision', + 'float4', + 'float8', + 'smallint', + 'smallserial', + 'numeric', + ]; + + case 'Rating': + return [ + 'int', + 'integer', + 'bigint', + 'bigserial', + 'int2', + 'int4', + 'int8', + 'serial', + 'serial2', + 'serial8', + 'double precision', + 'float4', + 'float8', + 'smallint', + 'smallserial', + 'numeric', + ]; + + case 'Formula': + return ['text', 'character varying']; + + case 'Rollup': + return ['character varying']; + + case 'Count': + return [ + 'int', + 'integer', + 'bigint', + 'bigserial', + 'int2', + 'int4', + 'int8', + 'serial', + 'serial2', + 'serial8', + 'smallint', + 'smallserial', + ]; + + case 'Lookup': + return ['character varying']; + + case 'Date': + return [ + 'date', + 'timestamp', + 'timestamp without time zone', + 'timestamptz', + 'timestamp with time zone', + ]; + + case 'DateTime': + case 'CreateTime': + case 'LastModifiedTime': + return [ + 'timestamp', + 'timestamp without time zone', + 'timestamptz', + 'timestamp with time zone', + ]; + + case 'AutoNumber': + return [ + 'int', + 'integer', + 'bigint', + 'bigserial', + 'int2', + 'int4', + 'int8', + 'serial', + 'serial2', + 'serial8', + 'smallint', + 'smallserial', + ]; + + case 'Barcode': + return ['character varying']; + + case 'Geometry': + return [ + 'polygon', + 'point', + 'circle', + 'box', + 'line', + 'lseg', + 'path', + 'circle', + ]; + + case 'Button': + default: + return dbTypes; + } + } + + static getUnsupportedFnList() { + return []; + } +} + +// module.exports = SnowflakeUiHelp; +/** + * @copyright Copyright (c) 2021, Xgene Cloud Ltd + * + * @author Naveen MR + * @author Pranav C Balan + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ diff --git a/packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts b/packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts index c48ae6c94c..6e79066cad 100644 --- a/packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts +++ b/packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts @@ -5,6 +5,7 @@ import { MysqlUi } from './MysqlUi'; import { OracleUi } from './OracleUi'; import { PgUi } from './PgUi'; import { SqliteUi } from './SqliteUi'; +import { SnowflakeUi } from './SnowflakeUi'; // import {YugabyteUi} from "./YugabyteUi"; // import {TidbUi} from "./TidbUi"; @@ -42,6 +43,10 @@ export class SqlUiFactory { return PgUi; } + if (connectionConfig.client === 'snowflake') { + return SnowflakeUi; + } + throw new Error('Database not supported'); } } diff --git a/packages/nocodb-sdk/src/lib/sqlUi/index.ts b/packages/nocodb-sdk/src/lib/sqlUi/index.ts index 5d3c4aecaa..bdc9600add 100644 --- a/packages/nocodb-sdk/src/lib/sqlUi/index.ts +++ b/packages/nocodb-sdk/src/lib/sqlUi/index.ts @@ -5,4 +5,5 @@ export * from './PgUi'; export * from './MssqlUi'; export * from './OracleUi'; export * from './SqliteUi'; +export * from './SnowflakeUi'; export * from './SqlUiFactory'; diff --git a/packages/nocodb/src/lib/db/sql-client/lib/SqlClientFactory.ts b/packages/nocodb/src/lib/db/sql-client/lib/SqlClientFactory.ts index cbc5d22a6c..a3a6af266e 100644 --- a/packages/nocodb/src/lib/db/sql-client/lib/SqlClientFactory.ts +++ b/packages/nocodb/src/lib/db/sql-client/lib/SqlClientFactory.ts @@ -6,6 +6,8 @@ import PgClient from './pg/PgClient'; import YugabyteClient from './pg/YugabyteClient'; import TidbClient from './mysql/TidbClient'; import VitessClient from './mysql/VitessClient'; +import SfClient from './snowflake/SnowflakeClient'; +import { SnowflakeClient } from 'knex-snowflake'; class SqlClientFactory { static create(connectionConfig) { @@ -31,6 +33,9 @@ class SqlClientFactory { if (connectionConfig.meta.dbtype === 'yugabyte') return new YugabyteClient(connectionConfig); return new PgClient(connectionConfig); + } else if (connectionConfig.client === 'snowflake') { + connectionConfig.client = SnowflakeClient; + return new SfClient(connectionConfig); } throw new Error('Database not supported'); diff --git a/packages/nocodb/src/lib/db/sql-client/lib/snowflake/SnowflakeClient.ts b/packages/nocodb/src/lib/db/sql-client/lib/snowflake/SnowflakeClient.ts new file mode 100644 index 0000000000..b47c50f62c --- /dev/null +++ b/packages/nocodb/src/lib/db/sql-client/lib/snowflake/SnowflakeClient.ts @@ -0,0 +1,2443 @@ +import { nanoid } from 'nanoid'; + +import _ from 'lodash'; +import KnexClient from '../KnexClient'; +import Debug from '../../../util/Debug'; +import Result from '../../../util/Result'; +import queries from './snowflake.queries'; +import lodash from 'lodash'; +const log = new Debug('SnowflakeClient'); + +const rowsToLower = (arr) => { + for(const a of arr) { + for(const [k, v] of Object.entries(a)) { + delete a[k]; + a[k.toLowerCase()] = v; + } + } + return arr; +} + +class SnowflakeClient extends KnexClient { + private queries: any; + private _version: any; + constructor(connectionConfig) { + super(connectionConfig); + // this.sqlClient = null; + this.queries = queries; + this._version = {}; + } + + /** + * + * + * @param {Object} args + * @returns {Object} result + * @returns {Number} code + * @returns {String} message + */ + async schemaCreateWithCredentials(args) { + const func = this.schemaCreateWithCredentials.name; + const result = new Result(); + log.api(`${func}:args:`, args); + + try { + if (!args.schema) { + args.schema = `nc${nanoid(8)}`; + } + + // const connectionParamsWithoutDb = JSON.parse( + // JSON.stringify(this.connectionConfig) + // ); + // + // delete connectionParamsWithoutDb.connection.database; + // + // const tempSqlClient = knex(connectionParamsWithoutDb); + + const data = await this.sqlClient.raw('create schema ?', [args.schema]); + + // postgres=# create database mydb; + // postgres=# create user myuser with encrypted password 'mypass'; + // postgres=# grant all privileges on database mydb to myuser; + + log.debug('Create database if not exists', data); + + // create new knex client + // this.sqlClient = knex(this.connectionConfig); + // tempSqlClient.destroy(); + result.object = args; + } catch (e) { + // log.ppe(e); + result.code = -1; + result.message = e.message; + result.object = e; + } + return result; + } + + /** + * + * @param {Object} - args + * @param args.sequence_name + * @returns {Promise<{upStatement, downStatement}>} + */ + async sequenceDelete(args: any = {}) { + const _func = this.sequenceDelete.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + + try { + const query = `${this.querySeparator()}DROP SEQUENCE ${ + args.sequence_name + }`; + await this.sqlClient.raw(query); + result.data.object = { + upStatement: [{ sql: query }], + downStatement: [ + { + sql: `${this.querySeparator()}CREATE SEQUENCE ${ + args.sequence_name + }`, + }, + ], + }; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @returns {Object[]} - sequences + * @property {String} - sequences[].sequence_name + * @property {String} - sequences[].type + * @property {String} - sequences[].definer + * @property {String} - sequences[].modified + * @property {String} - sequences[].created + * @property {String} - sequences[].security_type + * @property {String} - sequences[].comment + * @property {String} - sequences[].character_set_client + * @property {String} - sequences[].collation_connection + * @property {String} - sequences[].database collation + */ + async sequenceList(args: any = {}) { + const _func = this.sequenceList.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + args.databaseName = this.connectionConfig.connection.database; + const { rows } = await this.raw(`select * + from INFORMATION_SCHEMA.sequences;`); + + result.data.list = rowsToLower(rows).map((seq) => { + return { + ...seq, + original_sequence_name: seq.sequence_name, + }; + }); + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {String} - args.sequence_name + * @param {String} - args.start_value + * @param {String} - args.min_value + * @param {String} - args.max_value + * @param {String} - args.increment_by + * @returns {Object} - result + */ + async sequenceCreate(args: any = {}) { + const func = this.sequenceCreate.name; + const result = new Result(); + log.api(`${func}:args:`, args); + try { + const query = + this.querySeparator() + `CREATE SEQUENCE ${args.sequence_name}`; + await this.sqlClient.raw(query); + result.data.object = { + upStatement: [{ sql: query }], + downStatement: [ + { + sql: this.querySeparator() + `DROP SEQUENCE ${args.sequence_name}`, + }, + ], + }; + } catch (e) { + log.ppe(e, func); + throw e; + } + + log.api(`${func}: result`, result); + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {String} - args.sequence_name + * @param {String} - args.start_value + * @param {String} - args.min_value + * @param {String} - args.max_value + * @param {String} - args.increment_by + * @returns {Object} - result + */ + async sequenceUpdate(args: any = {}) { + const func = this.sequenceUpdate.name; + const result = new Result(); + log.api(`${func}:args:`, args); + try { + const upQuery = + this.querySeparator() + + `ALTER SEQUENCE ${args.original_sequence_name} RENAME TO ${args.sequence_name};`; + const downQuery = + this.querySeparator() + + `ALTER SEQUENCE ${args.sequence_name} RENAME TO ${args.original_sequence_name};`; + + await this.sqlClient.raw(upQuery); + result.data.object = { + upStatement: [{ sql: upQuery }], + downStatement: [{ sql: downQuery }], + }; + } catch (e) { + log.ppe(e, func); + throw e; + } + + log.api(`${func}: result`, result); + return result; + } + + /** + * @param {Object} args + * @returns {Object} result + * @returns {Number} code + * @returns {String} message + */ + async testConnection(args: any = {}) { + const func = this.testConnection.name; + const result = new Result(); + log.api(`${func}:args:`, args); + + try { + await this.sqlClient.raw('SELECT 1+1 as data'); + } catch (e) { + // log.ppe(e); + result.code = -1; + result.message = e.message; + result.object = e; + } finally { + if (result.code) { + this.emitE(`TestConnection result: ${result.message}`); + } else { + this.emit(`TestConnection result: ${result.code}`); + } + } + return result; + } + + getKnexDataTypes() { + const result = new Result(); + + result.data.list = [ + 'NUMBER', + 'DECIMAL', + 'NUMERIC', + 'INT', + 'INTEGER', + 'BIGINT', + 'SMALLINT', + 'TINYINT', + 'BYTEINT', + 'FLOAT', + 'FLOAT4', + 'FLOAT8', + 'DOUBLE', + 'DOUBLE PRECISION', + 'REAL', + 'VARCHAR', + 'CHAR', + 'CHARACTER', + 'STRING', + 'TEXT', + 'BINARY', + 'VARBINARY', + 'BOOLEAN', + 'DATE', + 'DATETIME', + 'TIME', + 'TIMESTAMP', + 'TIMESTAMP_LTZ', + 'TIMESTAMP_NTZ', + 'TIMESTAMP_TZ', + 'VARIANT', + 'OBJECT', + 'ARRAY', + 'GEOGRAPHY', + ]; + + return result; + } + + /** + * + * + * @param {Object} args + * @returns {Object} result + * @returns {Number} code + * @returns {String} message + * @returns {Object} object - {version, primary, major, minor} + */ + async version(args: any = {}) { + const _func = this.version.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + + try { + result.data.object = {}; + const data = await this.sqlClient.raw('SELECT CURRENT_VERSION() as "server_version"'); + log.debug(data.rows[0]); + result.data.object.version = data.rows[0].server_version; + const versions = data.rows[0].server_version.split('.'); + + if (versions.length && (versions.length === 3 || versions.length === 2)) { + result.data.object.primary = versions[0]; + result.data.object.major = versions[1]; + result.data.object.minor = + versions.length > 2 ? versions[2] : versions[1]; + result.data.object.key = versions[0] + versions[1]; + } else { + result.code = -1; + result.message = `Invalid version : ${data.rows[0].server_version}`; + } + } catch (e) { + log.ppe(e); + result.code = -1; + result.message = e.message; + } finally { + log.api(`${_func} :result: %o`, result); + } + return result; + } + + /** + * + * @param {Object} args + * @param {String} args.database + * @returns {Result} + */ + async createDatabaseIfNotExists(args: any = {}) { + const _func = this.createDatabaseIfNotExists.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + let tempSqlClient; + + try { + let rows = []; + try { + log.debug('checking if db exists'); + rows = ( + await this.sqlClient.raw( + `SELECT DATABASE_NAME as database FROM information_schema.DATABASES WHERE DATABASE_NAME = '${args.database}'` + ) + ).rows; + } catch (e) { + log.debug('db does not exist'); + } + if (rows.length === 0) { + log.debug('creating database:', args); + await this.sqlClient.raw(`CREATE DATABASE ??`, [args.database]); + await this.sqlClient.raw(`USE ??`, [args.database]); + } + + await this.sqlClient.raw(`CREATE SCHEMA IF NOT EXISTS ??`,[this.schema]); + + } catch (e) { + log.ppe(e, _func); + throw e; + } finally { + if (tempSqlClient) { + await tempSqlClient.destroy(); + } + } + + log.api(`${_func}: result`, result); + return result; + } + + async dropDatabase(args) { + const _func = this.dropDatabase.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + + try { + await this.sqlClient.raw(`DROP DATABASE ${args.database};`); + } catch (e) { + log.ppe(e, _func); + // throw e; + } + + log.api(`${_func}: result`, result); + return result; + } + + /** + * + * @param args {tn} + * @returns + */ + async createTableIfNotExists(args) { + const _func = this.createTableIfNotExists.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + + try { + /** ************** START : create _evolution table if not exists *************** */ + const exists = await this.sqlClient.raw( + `SELECT table_schema,table_name as "tn", table_catalog + FROM information_schema.tables + where table_schema=? and table_name = ?`, + [this.schema, args.tn] + ); + + if (exists.rows.length === 0) { + const data = await this.sqlClient.schema.createTable( + args.tn, + function (table) { + table.increments(); + table.string('title').notNullable(); + table.string('titleDown').nullable(); + table.string('description').nullable(); + table.integer('batch').nullable(); + table.string('checksum').nullable(); + table.integer('status').nullable(); + table.dateTime('created'); + table.timestamps(); + } + ); + log.debug('Table created:', `${args.tn}`, data); + } else { + log.debug(`${args.tn} tables exists`); + } + /** ************** END : create _evolution table if not exists *************** */ + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + async hasTable(args: any = {}) { + const _func = this.hasTable.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + + try { + const { rows } = await this.sqlClient.raw( + `SELECT table_schema,table_name as "tn", table_catalog FROM information_schema.tables where table_schema = ? and table_name = ? and table_catalog = ?`, + [this.schema, args.tn, this.connectionConfig.connection.database] + ); + result.data.value = rows.length > 0; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + async hasDatabase(args: any = {}) { + const _func = this.hasDatabase.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + + try { + const { rows } = await this.sqlClient.raw( + `SELECT DATABASE_NAME as database FROM information_schema.DATABASES WHERE DATABASE_NAME = ?`, + [args.databaseName] + ); + result.data.value = rows.length > 0; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * + * @param {Object} - args - for future reasons + * @returns {Object[]} - databases + * @property {String} - databases[].database_name + */ + async databaseList(args: any = {}) { + const _func = this.databaseList.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + + try { + const { rows } = await this.sqlClient.raw( + `SELECT DATABASE_NAME as database_name FROM information_schema.DATABASES;` + ); + result.data.list = rows; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * + * @param {Object} - args - for future reasons + * @returns {Object[]} - tables + * @property {String} - tables[].tn + */ + async tableList(args: any = {}) { + const _func = this.tableList.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + const { rows } = await this.raw( + `SELECT table_schema as "ts", table_name as "tn", table_type as "table_type" + FROM information_schema.tables + where table_schema = ? + ORDER BY table_schema, table_name`, + [this.schema] + ); + + result.data.list = rows.filter( + ({ table_type }) => table_type.toLowerCase() === 'base table' + ); + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + async schemaList(args: any = {}) { + const _func = this.schemaList.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + + try { + const { rows } = await this + .raw(`SELECT SCHEMA_NAME as "schema_name" FROM information_schema.SCHEMATA order by schema_name;`); + + result.data.list = rows; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {Object} - args.tn - + * @returns {Object[]} - columns + * @property {String} - columns[].tn + * @property {String} - columns[].cn + * @property {String} - columns[].dt + * @property {String} - columns[].dtx + * @property {String} - columns[].np + * @property {String} - columns[].ns - + * @property {String} - columns[].clen - + * @property {String} - columns[].dp - + * @property {String} - columns[].cop - + * @property {String} - columns[].pk - + * @property {String} - columns[].nrqd - + * @property {String} - columns[].not_nullable - + * @property {String} - columns[].ct - + * @property {String} - columns[].un - + * @property {String} - columns[].ai - + * @property {String} - columns[].unique - + * @property {String} - columns[].cdf - + * @property {String} - columns[].cc - + * @property {String} - columns[].csn - + */ + + async columnList(args: any = {}) { + const _func = this.columnList.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + args.databaseName = this.connectionConfig.connection.database; + await this.sqlClient.raw(`SHOW PRIMARY KEYS IN SCHEMA ${this.connectionConfig.connection.database}.${this.schema};`); + await this.sqlClient.raw(`SHOW UNIQUE KEYS IN SCHEMA ${this.connectionConfig.connection.database}.${this.schema};`); + + const lastQueries = await this.sqlClient.raw(` + select * from table(information_schema.query_history()) + WHERE query_text like 'SHOW%' + ORDER BY start_time DESC + LIMIT 200` + ); + + let pk_query_id, uq_query_id; + for (const r of lastQueries.rows) { + if (r.QUERY_TEXT === `SHOW PRIMARY KEYS IN SCHEMA ${this.connectionConfig.connection.database}.${this.schema};`) { + pk_query_id = r.QUERY_ID; + } else if (r.QUERY_TEXT === `SHOW UNIQUE KEYS IN SCHEMA ${this.connectionConfig.connection.database}.${this.schema};`) { + uq_query_id = r.QUERY_ID; + } + if (pk_query_id && uq_query_id) { + break; + } + } + + const response = await this.sqlClient.raw( + `SELECT + cl.table_name as "tn", + column_name as "cn", + data_type as "dt", + is_identity as "au", + tc.constraint_type as "ck", + character_maximum_length as "clen", + numeric_precision as "np", + numeric_scale as "ns", + ordinal_position as "cop", + is_nullable as "nrqd", + column_default as "cdf", + identity_generation as "generation_expression", + character_octet_length as "character_octet_length", + character_set_name as "csn", + "PK"."key_sequence" as "pk_ordinal_position", + "PK"."constraint_name" as "pk_constraint_name", + udt_name + FROM information_schema.COLUMNS cl + LEFT JOIN (select * from table(result_scan('${pk_query_id}')) UNION select * from table(result_scan('${uq_query_id}'))) pk + LEFT JOIN information_schema.table_constraints tc ON tc.constraint_name = "PK"."constraint_name" + ON "PK"."schema_name" = cl.table_schema and "PK"."table_name" = cl.table_name and pk."column_name" = cl.column_name + WHERE cl.table_catalog = ? and cl.table_schema = ? and cl.table_name = ?;`, + [this.connectionConfig.connection.database, this.schema, args.tn] + ); + + const columns = []; + + for (let i = 0; i < response.rows.length; ++i) { + const column: any = {}; + + column.tn = response.rows[i].tn; + column.cn = response.rows[i].cn; + column.cno = response.rows[i].cn; + column.dt = response.rows[i].dt; + column.np = response.rows[i].np; + column.ns = response.rows[i].ns; + column.clen = response.rows[i].clen; + column.dp = response.rows[i].dp; + column.cop = response.rows[i].cop; + + // todo : there are lot of types in pg - handle them + //column.dtx = this.getKnexDataType(column.dt); + column.dtx = response.rows[i].dt; + column.pk = response.rows[i].pk_constraint_name !== null; + + column.nrqd = response.rows[i].nrqd !== 'NO'; + column.not_nullable = !column.nrqd; + column.rqd = !column.nrqd; + + // todo: there is no type of unsigned in postgres + response.rows[i].ct = response.rows[i].dt || ''; + column.un = response.rows[i].ct.indexOf('unsigned') !== -1; + + column.ai = false; + if (response.rows[i].cdf) { + column.ai = response.rows[i].cdf.indexOf('nextval') !== -1; + } + + // todo : need to find if column is unique or not + // column['unique'] = response.rows[i]['cst'].indexOf('UNIQUE') === -1 ? false : true; + + column.cdf = response.rows[i].cdf; + // ? response.rows[i].cdf.split("::")[0].replace(/'/g, "") + // : response.rows[i].cdf; + + // todo : need to find column comment + column.cc = response.rows[i].cc; + + column.csn = response.rows[i].csn; + column.dtxp = + response.rows[i].clen || response.rows[i].np || response.rows[i].dp; + column.dtxs = response.rows[i].ns; + column.au = response.rows[i].au; + column.data_type_custom = response.rows[i].udt_name; + if (column.dt === 'USER-DEFINED') { + column.dtxp = response.rows[i].enum_values; + } + + columns.push(column); + } + + result.data.list = columns; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {Object} - args.tn - + * @returns {Object[]} - indexes + * @property {String} - indexes[].table - + * @property {String} - indexes[].cn - + * @property {String} - indexes[].key_name - + * @property {String} - indexes[].non_unique - + * @property {String} - indexes[].seq_in_index - + * @property {String} - indexes[].collation - + * @property {String} - indexes[].cardinality - + * @property {String} - indexes[].sub_part - + * @property {String} - indexes[].packed - + * @property {String} - indexes[].null - + * @property {String} - indexes[].index_type - + * @property {String} - indexes[].comment - + * @property {String} - indexes[].index_comment - + * @property {String} - indexes[].cstn - + * @property {String} - indexes[].cst - c = check constraint, f = foreign key constraint, p = primary key constraint, u = unique constraint, t = constraint trigger, x = exclusion constraint + */ + async indexList(args: any = {}) { + const _func = this.indexList.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + // TODO indexList + result.data.list = []; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {Object} - args.tn - + * @returns {Object[]} - indexes + * @property {String} - indexes[].cstn - + * @property {String} - indexes[].cn - + * @property {String} - indexes[].op - + * @property {String} - indexes[].puc - + * @property {String} - indexes[].cst - + */ + async constraintList(args: any = {}) { + const _func = this.constraintList.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + await this.sqlClient.raw(`SHOW PRIMARY KEYS IN SCHEMA ${this.connectionConfig.connection.database}.${this.schema};`); + await this.sqlClient.raw(`SHOW UNIQUE KEYS IN SCHEMA ${this.connectionConfig.connection.database}.${this.schema};`); + + const lastQueries = await this.sqlClient.raw(` + select * from table(information_schema.query_history()) + WHERE query_text like 'SHOW%' + ORDER BY start_time DESC + LIMIT 200` + ); + + let pk_query_id, uq_query_id; + for (const r of lastQueries.rows) { + if (r.QUERY_TEXT === `SHOW PRIMARY KEYS IN SCHEMA ${this.connectionConfig.connection.database}.${this.schema};`) { + pk_query_id = r.QUERY_ID; + } else if (r.QUERY_TEXT === `SHOW UNIQUE KEYS IN SCHEMA ${this.connectionConfig.connection.database}.${this.schema};`) { + uq_query_id = r.QUERY_ID; + } + if (pk_query_id && uq_query_id) { + break; + } + } + + const { rows } = await this.sqlClient.raw( + `SELECT + constraint_name as "cstn", + "PK"."column_name" as "cn", + constraint_type as "cst" + FROM information_schema.table_constraints tc + LEFT JOIN (select * from table(result_scan('${pk_query_id}')) UNION select * from table(result_scan('${uq_query_id}'))) pk + ON "PK"."constraint_name" = tc.constraint_name + WHERE tc.table_catalog = ? and tc.table_schema = ? and tc.table_name = ?;`, + [this.connectionConfig.connection.database, this.schema, args.tn]); + + result.data.list = rows; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {Object} - args.tn - + * @returns {Object[]} - relations + * @property {String} - relations[].tn + * @property {String} - relations[].cstn - + * @property {String} - relations[].tn - + * @property {String} - relations[].cn - + * @property {String} - relations[].rtn - + * @property {String} - relations[].rcn - + * @property {String} - relations[].puc - + * @property {String} - relations[].ur - + * @property {String} - relations[].dr - + * @property {String} - relations[].mo - + */ + async relationList(args: any = {}) { + const _func = this.relationList.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + result.data.list = []; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {Object} - args.tn - + * @returns {Object[]} - relations + * @property {String} - relations[].tn + * @property {String} - relations[].cstn - + * @property {String} - relations[].tn - + * @property {String} - relations[].cn - + * @property {String} - relations[].rtn - + * @property {String} - relations[].rcn - + * @property {String} - relations[].puc - + * @property {String} - relations[].ur - + * @property {String} - relations[].dr - + * @property {String} - relations[].mo - + */ + async relationListAll(args: any = {}) { + const _func = this.relationList.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + result.data.list = []; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {Object} - args.tn - + * @returns {Object[]} - triggers + * @property {String} - triggers[].trigger + * @property {String} - triggers[].event + * @property {String} - triggers[].table + * @property {String} - triggers[].statement + * @property {String} - triggers[].timing + * @property {String} - triggers[].created + * @property {String} - triggers[].sql_mode + * @property {String} - triggers[].definer + * @property {String} - triggers[].character_set_client + * @property {String} - triggers[].collation_connection + * @property {String} - triggers[].database collation + */ + async triggerList(args: any = {}) { + const _func = this.triggerList.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + result.data.list = []; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @returns {Object[]} - functions + * @property {String} - functions[].function_name + * @property {String} - functions[].type + * @property {String} - functions[].definer + * @property {String} - functions[].modified + * @property {String} - functions[].created + * @property {String} - functions[].security_type + * @property {String} - functions[].comment + * @property {String} - functions[].character_set_client + * @property {String} - functions[].collation_connection + * @property {String} - functions[].database collation + */ + async functionList(args: any = {}) { + const _func = this.functionList.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + result.data.list = []; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * @todo Remove the function - pg doesn't support procedure + * + * @param {Object} - args - For future reasons + * @returns {Object[]} - procedures + * @property {String} - procedures[].procedure_name + * @property {String} - procedures[].type + * @property {String} - procedures[].definer + * @property {String} - procedures[].modified + * @property {String} - procedures[].created + * @property {String} - procedures[].security_type + * @property {String} - procedures[].comment + * @property {String} - procedures[].definer + * @property {String} - procedures[].character_set_client + * @property {String} - procedures[].collation_connection + * @property {String} - procedures[].database collation + */ + async procedureList(args: any = {}) { + const _func = this.procedureList.name; + const result = new Result(); + result.data.list = []; + log.api(`${_func}:args:`, args); + + // todo: update query - https://dataedo.com/kb/query/postgresql/list-stored-procedures + try { + result.data.list = []; + } catch (e) { + // todo: enable log + // log.ppe(e, _func); + // throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @returns {Object[]} - views + * @property {String} - views[].sql_mode + * @property {String} - views[].create_function + * @property {String} - views[].database collation + * @property {String} - views[].collation_connection + * @property {String} - views[].character_set_client + */ + async viewList(args: any = {}) { + const _func = this.viewList.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + const { rows } = await this.sqlClient.raw( + `select * from INFORMATION_SCHEMA.views WHERE table_schema = ?;`, + [this.schema] + ); + + for (let i = 0; i < rows.length; ++i) { + rows[i].view_name = rows[i].TABLE_NAME; + } + + result.data.list = rows; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {Object} - args.function_name - + * @returns {Object[]} - functions + * @property {String} - create_function + * @property {String} - function_declaration + */ + async functionRead(args: any = {}) { + const _func = this.functionRead.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + result.data.list = []; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * @todo Remove the function - pg doesn't support procedure + * + * @param {Object} - args - Input arguments + * @param {Object} - args.procedure_name - + * @returns {Object[]} - functions + * @property {String} - sql_mode + * @property {String} - create_function + * @property {String} - database collation + * @property {String} - collation_connection + * @property {String} - character_set_client + */ + async procedureRead(args: any = {}) { + const _func = this.procedureRead.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + result.data.list = []; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {Object} - args.view_name - + * @returns {Object[]} - views + * @property {String} - views[].tn + */ + async viewRead(args: any = {}) { + const _func = this.viewRead.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + args.databaseName = this.connectionConfig.connection.database; + + const { rows } = await this.sqlClient.raw( + `select * from INFORMATION_SCHEMA.views WHERE table_name='${args.view_name}' and table_schema = ${this.schema};` + ); + + for (let i = 0; i < rows.length; ++i) { + rows[i].view_name = rows[i].TABLE_NAME; + // rows[i].view_definition = rows[i].view_definition; + } + + result.data.list = rows; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + async triggerRead(args: any = {}) { + const _func = this.triggerRead.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + result.data.list = []; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + async schemaCreate(args: any = {}) { + const _func = this.schemaCreate.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + + try { + await this.sqlClient.raw(`create schema ${args.database_name}`); + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + async schemaDelete(args: any = {}) { + const _func = this.schemaDelete.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + + try { + await this.sqlClient.raw(`drop schema ${args.database_name}`); + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + async triggerDelete(args: any = {}) { + const _func = this.triggerDelete.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + throw new Error('Function not supported for Snowflake yet'); + const upQuery = this.genQuery(`DROP TRIGGER IF EXISTS ?? ON ??`, [ + args.trigger_name, + args.tn, + ]); + await this.sqlClient.raw(upQuery); + result.data.object = { + upStatement: [{ sql: this.querySeparator() + upQuery }], + downStatement: [ + { + sql: + this.querySeparator() + + this.genQuery( + `CREATE TRIGGER ?? \n${args.timing} ${args.event}\nON ?? FOR EACH ROW\n${args.statement}`, + [args.trigger_name, args.tn] + ), + }, + ], + }; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {String} - args.function_name + * @param {String} - args.function_declaration + * @param {String} - args.create_function + * @returns {Object[]} - result rows + */ + async functionDelete(args: any = {}) { + const _func = this.functionDelete.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + const upQuery = + this.querySeparator() + + `DROP FUNCTION IF EXISTS ${args.function_declaration}`; + const downQuery = this.querySeparator() + args.create_function; + try { + throw new Error('Function not supported for Snowflake yet'); + await this.sqlClient.raw(upQuery); + result.data.object = { + upStatement: [{ sql: upQuery }], + downStatement: [{ sql: downQuery }], + }; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + log.api(`${_func}: result`, result); + + return result; + } + + // @todo Remove the function - pg doesn't support procedure + async procedureDelete(args: any = {}) { + const _func = this.procedureDelete.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + throw new Error('Function not supported for Snowflake yet'); + await this.sqlClient.raw( + `DROP PROCEDURE IF EXISTS ${args.procedure_name}` + ); + } catch (e) { + log.ppe(e, _func); + throw e; + } + log.api(`${_func}: result`, result); + + return result; + } + + /** + * + * @param {Object} args + * @param {String} func : function name + * @returns {Result} + * @returns {Object} - Result.data + * @returns {String} - Result.data.value - sql query + */ + async _getQuery(args) { + try { + if (_.isEmpty(this._version)) { + const result = await this.version(); + this._version = result.data.object; + log.debug( + `Version was empty for ${args.func}: population version for database as`, + this._version + ); + } + + // log.debug(this._version, args); + + if (this._version.key in this.queries[args.func]) { + return this.queries[args.func][this._version.key].sql; + } + return this.queries[args.func].default.sql; + } catch (error) { + log.ppe(error, this._getQuery.name); + throw error; + } + } + + /** + * + * @param {Object} - args - Input arguments + * @param {String} - args.function_name + * @param {String} - args.create_function + * @returns {Object[]} - result rows + */ + async functionCreate(args: any = {}) { + const func = this.functionCreate.name; + const result = new Result(); + + log.api(`${func}:args:`, args); + + try { + throw new Error('Function not supported for Snowflake yet'); + const upQuery = this.querySeparator() + args.create_function; + + await this.sqlClient.raw(upQuery); + + const functionCreated = await this.functionRead({ + function_name: args.function_name, + }); + + const downQuery = + this.querySeparator() + + `DROP FUNCTION IF EXISTS ${functionCreated.data.list[0].function_declaration}`; + + result.data.object = { + upStatement: [{ sql: upQuery }], + downStatement: [{ sql: downQuery }], + }; + } catch (e) { + log.ppe(e, func); + throw e; + } + + log.api(`${func}: result`, result); + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {String} - args.tn + * @param {String} - args.function_name + * @param {String} - args.event + * @param {String} - args.timing + * @returns {Object[]} - result rows + */ + async functionUpdate(args: any = {}) { + const func = this.functionUpdate.name; + const result = new Result(); + log.api(`${func}:args:`, args); + try { + throw new Error('Function not supported for Snowflake yet'); + const upQuery = this.querySeparator() + args.create_function; + let downQuery = this.querySeparator() + args.oldCreateFunction; + + await this.sqlClient.raw( + `DROP FUNCTION IF EXISTS ${args.function_declaration};` + ); + await this.sqlClient.raw(upQuery); + + const functionCreated = await this.functionRead({ + function_name: args.function_name, + }); + + downQuery = + `DROP FUNCTION IF EXISTS ${functionCreated.data.list[0].function_declaration};` + + downQuery; + + result.data.object = { + upStatement: [{ sql: upQuery }], + downStatement: [{ sql: downQuery }], + }; + } catch (e) { + log.ppe(e, func); + throw e; + } + + log.api(`${func}: result`, result); + return result; + } + + /** + * @todo Remove the function - pg doesn't support procedure + * + * @param {Object} - args - Input arguments + * @param {String} - args.tn + * @param {String} - args.procedure_name + * @param {String} - args.event + * @param {String} - args.timing + * @returns {Object[]} - result rows + * + */ + async procedureCreate(args: any = {}) { + const func = this.procedureCreate.name; + const result = new Result(); + log.api(`${func}:args:`, args); + try { + throw new Error('Function not supported for Snowflake yet'); + const upQuery = + this.querySeparator() + + `CREATE TRIGGER \`${args.procedure_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`; + await this.sqlClient.raw(upQuery); + const downQuery = + this.querySeparator() + + `DROP PROCEDURE IF EXISTS ${args.procedure_name}`; + result.data.object = { + upStatement: [{ sql: upQuery }], + downStatement: [{ sql: downQuery }], + }; + } catch (e) { + log.ppe(e, func); + throw e; + } + + log.api(`${func}: result`, result); + return result; + } + + /** + * @todo Remove the function - pg doesn't support procedure + * + * @param {Object} - args - Input arguments + * @param {String} - args.tn + * @param {String} - args.procedure_name + * @param {String} - args.event + * @param {String} - args.timing + * @returns {Object[]} - result rows + */ + async procedureUpdate(args: any = {}) { + const func = this.procedureUpdate.name; + const result = new Result(); + log.api(`${func}:args:`, args); + try { + throw new Error('Function not supported for Snowflake yet'); + const query = + this.querySeparator() + `DROP TRIGGER ${args.procedure_name}`; + const upQuery = + this.querySeparator() + + `CREATE TRIGGER \`${args.procedure_name}\` \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`; + + await this.sqlClient.raw(query); + await this.sqlClient.raw(upQuery); + + result.data.object = { + upStatement: [{ sql: upQuery }], + downStatement: [{ sql: ';' }], + }; + } catch (e) { + log.ppe(e, func); + throw e; + } + + log.api(`${func}: result`, result); + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {String} - args.tn + * @param {String} - args.trigger_name + * @param {String} - args.event + * @param {String} - args.timing + * @returns {Object[]} - result rows + */ + async triggerCreate(args: any = {}) { + const func = this.triggerCreate.name; + const result = new Result(); + log.api(`${func}:args:`, args); + try { + throw new Error('Function not supported for Snowflake yet'); + const upQuery = + this.querySeparator() + + `CREATE TRIGGER ${args.trigger_name} \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`; + await this.sqlClient.raw(upQuery); + result.data.object = { + upStatement: [{ sql: upQuery }], + downStatement: [ + { sql: this.querySeparator() + `DROP TRIGGER ${args.trigger_name}` }, + ], + }; + } catch (e) { + log.ppe(e, func); + throw e; + } + + log.api(`${func}: result`, result); + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {String} - args.tn + * @param {String} - args.trigger_name + * @param {String} - args.event + * @param {String} - args.timing + * @param {String} - args.oldStatement + * @returns {Object[]} - result rows + */ + async triggerUpdate(args: any = {}) { + const func = this.triggerUpdate.name; + const result = new Result(); + log.api(`${func}:args:`, args); + try { + throw new Error('Function not supported for Snowflake yet'); + await this.sqlClient.raw( + `DROP TRIGGER ${args.trigger_name} ON ${args.tn}` + ); + await this.sqlClient.raw( + `CREATE TRIGGER ${args.trigger_name} \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}` + ); + + result.data.object = { + upStatement: + this.querySeparator() + + `DROP TRIGGER ${args.trigger_name} ON ${ + args.tn + };${this.querySeparator()}CREATE TRIGGER ${args.trigger_name} \n${ + args.timing + } ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.statement}`, + downStatement: + this.querySeparator() + + `CREATE TRIGGER ${args.trigger_name} \n${args.timing} ${args.event}\nON "${args.tn}" FOR EACH ROW\n${args.oldStatement}`, + }; + } catch (e) { + log.ppe(e, func); + throw e; + } + + log.api(`${func}: result`, result); + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {String} - args.view_name + * @param {String} - args.view_definition + * @returns {Object} - up and down statements + */ + async viewCreate(args: any = {}) { + const func = this.viewCreate.name; + const result = new Result(); + log.api(`${func}:args:`, args); + try { + const query = args.view_definition; + + await this.sqlClient.raw(query); + result.data.object = { + upStatement: [{ sql: this.querySeparator() + query }], + downStatement: [ + { sql: this.querySeparator() + `DROP VIEW "${args.view_name}"` }, + ], + }; + } catch (e) { + log.ppe(e, func); + throw e; + } + + log.api(`${func}: result`, result); + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {String} - args.view_name + * @param {String} - args.view_definition + * @param {String} - args.oldViewDefination + * @returns {Object} - up and down statements + */ + async viewUpdate(args: any = {}) { + const func = this.viewUpdate.name; + const result = new Result(); + log.api(`${func}:args:`, args); + try { + const query = `CREATE OR REPLACE VIEW "${args.view_name}" AS \n${args.view_definition}`; + + await this.sqlClient.raw(query); + result.data.object = { + upStatement: this.querySeparator() + query, + downStatement: + this.querySeparator() + + `CREATE VIEW "${args.view_name}" AS \n${args.oldViewDefination}`, + }; + } catch (e) { + log.ppe(e, func); + throw e; + } + + log.api(`${func}: result`, result); + return result; + } + + /** + * + * @param {Object} - args - Input arguments + * @param {String} - args.view_name + * @param {String} - args.view_definition + * @param {String} - args.oldViewDefination + * @returns {Object} - up and down statements + */ + async viewDelete(args: any = {}) { + const func = this.viewDelete.name; + const result = new Result(); + log.api(`${func}:args:`, args); + // `DROP TRIGGER ${args.view_name}` + try { + const query = `DROP VIEW ${args.view_name}`; + + await this.sqlClient.raw(query); + + result.data.object = { + upStatement: [{ sql: this.querySeparator() + query }], + downStatement: [ + { + sql: + this.querySeparator() + + `CREATE VIEW "${args.view_name}" AS \n${args.oldViewDefination}`, + }, + ], + }; + } catch (e) { + log.ppe(e, func); + throw e; + } + + log.api(`${func}: result`, result); + return result; + } + + /** + * + * @param {Object} - args + * @param {String} - args.tn + * @param {Object[]} - args.columns + * @param {String} - args.columns[].tn + * @param {String} - args.columns[].cn + * @param {String} - args.columns[].dt + * @param {String} - args.columns[].np + * @param {String} - args.columns[].ns - + * @param {String} - args.columns[].clen - + * @param {String} - args.columns[].dp - + * @param {String} - args.columns[].cop - + * @param {String} - args.columns[].pk - + * @param {String} - args.columns[].nrqd - + * @param {String} - args.columns[].not_nullable - + * @param {String} - args.columns[].ct - + * @param {String} - args.columns[].un - + * @param {String} - args.columns[].ai - + * @param {String} - args.columns[].unique - + * @param {String} - args.columns[].cdf - + * @param {String} - args.columns[].cc - + * @param {String} - args.columns[].csn - + * @param {String} - args.columns[].dtx + * - value will be 'specificType' for all cols except ai + * - for ai it will be integer, bigInteger + * - tiny, small and medium Int auto increement is not supported + * @param {String} - args.columns[].dtxp - to use in UI + * @param {String} - args.columns[].dtxs - to use in UI + * @returns {Promise<{upStatement, downStatement}>} + */ + async tableCreate(args) { + const _func = this.tableCreate.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + + try { + args.table = args.tn; + args.sqlClient = this.sqlClient; + + /**************** create table ****************/ + const upQuery = this.querySeparator() + this.createTable(args.tn, args); + await this.sqlClient.raw(upQuery); + + const downStatement = + this.querySeparator() + + this.sqlClient.schema.dropTable(args.table).toString(); + + this.emit(`Success : ${upQuery}`); + + const triggerStatements = await this.afterTableCreate(args); + + /**************** return files *************** */ + result.data.object = { + upStatement: [{ sql: upQuery }, ...triggerStatements.upStatement], + downStatement: [ + ...triggerStatements.downStatement, + { sql: downStatement }, + ], + }; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + return result; + } + + async afterTableCreate(args) { + const result = { upStatement: [], downStatement: [] }; + let upQuery = ''; + let downQuery = ''; + + // TODO handle + for (let i = 0; i < 0; i++) { + const column = args.columns[i]; + if (column.au) { + const triggerFnName = `xc_au_${args.tn}_${column.cn}`; + const triggerName = `xc_trigger_${args.tn}_${column.cn}`; + + const triggerFnQuery = this.genQuery( + `CREATE OR REPLACE FUNCTION ??() + RETURNS TRIGGER AS $$ + BEGIN + NEW.?? = NOW(); + RETURN NEW; + END; + $$ LANGUAGE plpgsql;`, + [triggerFnName, column.cn] + ); + + upQuery += + this.querySeparator() + + triggerFnQuery + + this.querySeparator() + + this.genQuery( + `CREATE TRIGGER ?? + BEFORE UPDATE ON ?? + FOR EACH ROW + EXECUTE PROCEDURE ??();`, + [triggerName, args.tn, triggerFnName] + ); + + downQuery += + this.querySeparator() + + this.genQuery(`DROP TRIGGER IF EXISTS ?? ON ??;`, [ + triggerName, + args.tn, + ]) + + this.querySeparator() + + this.genQuery(`DROP FUNCTION IF EXISTS ??()`, [triggerFnName]); + } + } + await this.sqlClient.raw(upQuery); + result.upStatement[0] = { sql: upQuery }; + result.downStatement[0] = { sql: downQuery }; + + return result; + } + + async afterTableUpdate(args) { + const result = { upStatement: [], downStatement: [] }; + let upQuery = ''; + let downQuery = ''; + + // TODO handle + for (let i = 0; i < 0; i++) { + const column = args.columns[i]; + if (column.au && column.altered === 1) { + const triggerFnName = `xc_au_${args.tn}_${column.cn}`; + const triggerName = `xc_trigger_${args.tn}_${column.cn}`; + + const triggerFnQuery = this.genQuery( + `CREATE OR REPLACE FUNCTION ??() + RETURNS TRIGGER AS $$ + BEGIN + NEW.?? = NOW(); + RETURN NEW; + END; + $$ LANGUAGE plpgsql;`, + [triggerFnName, column.cn] + ); + + upQuery += + this.querySeparator() + + triggerFnQuery + + this.querySeparator() + + this.genQuery( + `CREATE TRIGGER ?? + BEFORE UPDATE ON ?? + FOR EACH ROW + EXECUTE PROCEDURE ??();`, + [triggerName, args.tn, triggerFnName] + ); + + downQuery += + this.querySeparator() + + this.genQuery(`DROP TRIGGER IF EXISTS ?? ON ??;`, [ + triggerName, + args.tn, + ]) + + this.querySeparator() + + this.genQuery(`DROP FUNCTION IF EXISTS ??()`, [triggerFnName]); + } + } + await this.sqlClient.raw(upQuery); + result.upStatement[0] = { sql: upQuery }; + result.downStatement[0] = { sql: downQuery }; + + return result; + } + + /** + * + * @param {Object} - args + * @param {String} - args.table + * @param {String} - args.table + * @param {Object[]} - args.columns + * @param {String} - args.columns[].tn + * @param {String} - args.columns[].cn + * @param {String} - args.columns[].dt + * @param {String} - args.columns[].np + * @param {String} - args.columns[].ns - + * @param {String} - args.columns[].clen - + * @param {String} - args.columns[].dp - + * @param {String} - args.columns[].cop - + * @param {String} - args.columns[].pk - + * @param {String} - args.columns[].nrqd - + * @param {String} - args.columns[].not_nullable - + * @param {String} - args.columns[].ct - + * @param {String} - args.columns[].un - + * @param {String} - args.columns[].ai - + * @param {String} - args.columns[].unique - + * @param {String} - args.columns[].cdf - + * @param {String} - args.columns[].cc - + * @param {String} - args.columns[].csn - + * @param {Number} - args.columns[].altered - 1,2,4 = addition,edited,deleted + * @returns {Promise<{upStatement, downStatement}>} + */ + async tableUpdate(args) { + const _func = this.tableUpdate.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + + try { + args.table = args.tn; + const originalColumns = args.originalColumns; + args.connectionConfig = this._connectionConfig; + args.sqlClient = this.sqlClient; + + let upQuery = ''; + let downQuery = ''; + + for (let i = 0; i < args.columns.length; ++i) { + const oldColumn = lodash.find(originalColumns, { + cn: args.columns[i].cno, + }); + + if (args.columns[i].altered & 4) { + // col remove + upQuery += this.alterTableRemoveColumn( + args.table, + args.columns[i], + oldColumn, + upQuery + ); + downQuery += this.alterTableAddColumn( + args.table, + oldColumn, + args.columns[i], + downQuery + ); + } else if (args.columns[i].altered & 2 || args.columns[i].altered & 8) { + // col edit + upQuery += this.alterTableChangeColumn( + args.table, + args.columns[i], + oldColumn, + upQuery + ); + downQuery += this.alterTableChangeColumn( + args.table, + oldColumn, + args.columns[i], + downQuery + ); + } else if (args.columns[i].altered & 1) { + // col addition + upQuery += this.alterTableAddColumn( + args.table, + args.columns[i], + oldColumn, + upQuery + ); + downQuery += this.alterTableRemoveColumn( + args.table, + args.columns[i], + oldColumn, + downQuery + ); + } + } + + upQuery += + (upQuery ? ';' : '') + + this.alterTablePK( + args.table, + args.columns, + args.originalColumns, + upQuery + ); + downQuery += + (downQuery ? ';' : '') + + this.alterTablePK( + args.table, + args.originalColumns, + args.columns, + downQuery + ); + + if (upQuery) { + //upQuery = `ALTER TABLE "${args.columns[0].tn}" ${upQuery};`; + //downQuery = `ALTER TABLE "${args.columns[0].tn}" ${downQuery};`; + } + + await this.sqlClient.raw(upQuery); + + // console.log(upQuery); + + const afterUpdate = await this.afterTableUpdate(args); + + result.data.object = { + upStatement: [ + { sql: this.querySeparator() + upQuery }, + ...afterUpdate.upStatement, + ], + downStatement: [ + ...afterUpdate.downStatement, + { sql: this.querySeparator() + downQuery }, + ], + }; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + return result; + } + + /** + * + * @param {Object} - args + * @param args.tn + * @returns {Promise<{upStatement, downStatement}>} + */ + async tableDelete(args) { + const _func = this.tableDelete.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + // const { columns } = args; + args.sqlClient = this.sqlClient; + + /** ************** create up & down statements *************** */ + const upStatement = + this.querySeparator() + + this.sqlClient.schema.dropTable(args.tn).toString(); + let downQuery = this.createTable(args.tn, args); + + /** + + columnList + relationList + indexesList + createAggregatedIndexes + filterOutPkAndFk + + downQuery + create table - via columnList - we are doing this + + create fks - via relationList + + create indexes - slightly tricky + + */ + + let relationsList: any = await this.relationList(args); + + relationsList = relationsList.data.list; + + for (const relation of relationsList) { + downQuery += + this.querySeparator() + + (await this.sqlClient.schema + .table(relation.tn, function (table) { + table = table + .foreign(relation.cn, null) + .references(relation.rcn) + .on(relation.rtn); + + if (relation.ur) { + table = table.onUpdate(relation.ur); + } + if (relation.dr) { + table = table.onDelete(relation.dr); + } + }) + .toQuery()); + } + + let indexList: any = await this.indexList(args); + + indexList = indexList.data.list.filter( + ({ cst }) => cst !== 'p' && cst !== 'f' + ); + + const indexesMap: { [key: string]: any } = {}; + + for (const { key_name, non_unique, cn } of indexList) { + if (!(key_name in indexesMap)) { + indexesMap[key_name] = { + tn: args.tn, + indexName: key_name, + non_unique, + columns: [], + }; + } + indexesMap[key_name].columns.push(cn); + } + + for (const { non_unique, tn, columns, indexName } of Object.values( + indexesMap + )) { + downQuery += + this.querySeparator() + + this.sqlClient.schema + .table(tn, function (table) { + if (non_unique) { + table.index(columns, indexName); + } else { + table.unique(columns, indexName); + } + }) + .toQuery(); + } + + this.emit(`Success : ${upStatement}`); + + /** ************** drop tn *************** */ + await this.sqlClient.schema.dropTable(args.tn); + + /** ************** return files *************** */ + result.data.object = { + upStatement: [{ sql: upStatement }], + downStatement: [{ sql: this.querySeparator() + downQuery }], + }; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + return result; + } + + /** + * + * @param args + * @param args.tn + * @returns {Object} Result + * @returns {String} result.data + */ + async tableCreateStatement(args) { + const _func = this.tableCreateStatement.name; + let result = new Result(); + log.api(`${_func}:args:`, args); + try { + result = await this.columnList(args); + const upQuery = this.createTable(args.tn, { + tn: args.tn, + columns: result.data.list, + }); + result.data = upQuery; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + return result; + } + + /** + * + * @param args + * @param args.tn + * @returns {Object} Result + * @returns {String} result.data + */ + async tableInsertStatement(args) { + const _func = this.tableCreateStatement.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + result.data = `INSERT INTO \`${args.tn}\` (`; + let values = ' VALUES ('; + const response = await this.columnList(args); + if (response.data && response.data.list) { + for (let i = 0; i < response.data.list.length; ++i) { + if (!i) { + result.data += `\n"${response.data.list[i].cn}"\n\t`; + values += `\n<${response.data.list[i].cn}>\n\t`; + } else { + result.data += `, \`"${response.data.list[i].cn}"\`\n\t`; + values += `, <${response.data.list[i].cn}>\n\t`; + } + } + } + + result.data += `)`; + values += `);`; + result.data += values; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + return result; + } + + /** + * + * @param args + * @param args.tn + * @returns {Object} Result + * @returns {String} result.data + */ + async tableUpdateStatement(args) { + const _func = this.tableUpdateStatement.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + + try { + result.data = `UPDATE "${args.tn}" \nSET\n`; + const response = await this.columnList(args); + if (response.data && response.data.list) { + for (let i = 0; i < response.data.list.length; ++i) { + if (!i) { + result.data += `"${response.data.list[i].cn}" = <\`${response.data.list[i].cn}\`>\n\t`; + } else { + result.data += `,"${response.data.list[i].cn}" = <\`${response.data.list[i].cn}\`>\n\t`; + } + } + } + + result.data += ';'; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + return result; + } + + /** + * + * @param args + * @param args.tn + * @returns {Object} Result + * @returns {String} result.data + */ + async tableDeleteStatement(args) { + const _func = this.tableDeleteStatement.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + result.data = `DELETE FROM "${args.tn}" where ;`; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + return result; + } + + /** + * + * @param args + * @param args.tn + * @returns {Object} Result + * @returns {String} result.data + */ + async tableTruncateStatement(args) { + const _func = this.tableTruncateStatement.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + try { + result.data = `TRUNCATE TABLE "${args.tn}";`; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + return result; + } + + /** + * + * @param args + * @param args.tn + * @returns {Object} Result + * @returns {String} result.data + */ + async tableSelectStatement(args) { + const _func = this.tableSelectStatement.name; + const result = new Result(); + log.api(`${_func}:args:`, args); + + try { + result.data = `SELECT `; + const response = await this.columnList(args); + if (response.data && response.data.list) { + for (let i = 0; i < response.data.list.length; ++i) { + if (!i) { + result.data += `"${response.data.list[i].cn}"\n\t`; + } else { + result.data += `, "${response.data.list[i].cn}"\n\t`; + } + } + } + + result.data += ` FROM "${args.tn}";`; + } catch (e) { + log.ppe(e, _func); + throw e; + } + + return result; + } + + alterTablePK(t, n, o, _existingQuery, createTable = false) { + const numOfPksInOriginal = []; + const numOfPksInNew = []; + let pksChanged = 0; + + for (let i = 0; i < n.length; ++i) { + if (n[i].pk) { + if (n[i].altered !== 4) numOfPksInNew.push(n[i].cn); + } + } + + for (let i = 0; i < o.length; ++i) { + if (o[i].pk) { + numOfPksInOriginal.push(o[i].cn); + } + } + + if (numOfPksInNew.length === numOfPksInOriginal.length) { + for (let i = 0; i < numOfPksInNew.length; ++i) { + if (numOfPksInOriginal[i] !== numOfPksInNew[i]) { + pksChanged = 1; + break; + } + } + } else { + pksChanged = numOfPksInNew.length - numOfPksInOriginal.length; + } + + let query = ''; + if (!numOfPksInNew.length && !numOfPksInOriginal.length) { + // do nothing + } else if (pksChanged) { + query += numOfPksInOriginal.length + ? this.genQuery(`alter TABLE ?? drop constraint IF EXISTS ??;`, [ + t, + `${t}_pkey`, + ]) + : ''; + if (numOfPksInNew.length) { + if (createTable) { + query += this.genQuery(`, PRIMARY KEY(??)`, [numOfPksInNew]); + } else { + query += this.genQuery( + `alter TABLE ?? add constraint ?? PRIMARY KEY(??);`, + [t, `${t}_pkey`, numOfPksInNew] + ); + } + } + } + + return query; + } + + alterTableRemoveColumn(t, n, _o, existingQuery) { + const shouldSanitize = true; + let query = existingQuery ? ',' : ''; + query += this.genQuery( + `ALTER TABLE ?? DROP COLUMN ??`, + [t, n.cn], + shouldSanitize + ); + return query; + } + + createTableColumn(t, n, o, existingQuery) { + return this.alterTableColumn(t, n, o, existingQuery, 0); + } + + alterTableAddColumn(t, n, o, existingQuery) { + return this.alterTableColumn(t, n, o, existingQuery, 1); + } + + alterTableChangeColumn(t, n, o, existingQuery) { + return this.alterTableColumn(t, n, o, existingQuery, 2); + } + + createTable(table, args) { + let query = ''; + + for (let i = 0; i < args.columns.length; ++i) { + query += this.createTableColumn(table, args.columns[i], null, query); + } + + query += this.alterTablePK(table, args.columns, [], query, true); + + query = this.genQuery(`CREATE TABLE ?? (${query});`, [args.tn]); + + return query; + } + + alterTableColumn(t, n, o, existingQuery, change = 2) { + let query = ''; + + const defaultValue = getDefaultValue(n); + const shouldSanitize = true; + + if (change === 0) { + query = existingQuery ? ',' : ''; + if (n.ai) { + if (n.dt === 'int8' || n.dt.indexOf('bigint') > -1) { + query += this.genQuery(` ?? bigserial`, [n.cn], shouldSanitize); + } else if (n.dt === 'int2' || n.dt.indexOf('smallint') > -1) { + query += this.genQuery(` ?? smallserial`, [n.cn], shouldSanitize); + } else { + query += this.genQuery(` ?? serial`, [n.cn], shouldSanitize); + } + } else { + query += this.genQuery(` ?? ${n.dt}`, [n.cn], shouldSanitize); + query += n.rqd ? ' NOT NULL' : ' NULL'; + query += defaultValue ? ` DEFAULT ${defaultValue}` : ''; + } + } else if (change === 1) { + query += this.genQuery(` ADD ?? ${n.dt}`, [n.cn], shouldSanitize); + query += n.rqd ? ' NOT NULL' : ' NULL'; + query += defaultValue ? ` DEFAULT ${defaultValue}` : ''; + query = this.genQuery(`ALTER TABLE ?? ${query};`, [t], shouldSanitize); + } else { + if (n.cn !== o.cn) { + query += this.genQuery( + `\nALTER TABLE ?? RENAME COLUMN ?? TO ?? ;\n`, + [t, o.cn, n.cn], + shouldSanitize + ); + } + + if (n.dt !== o.dt) { + query += this.genQuery( + `\nALTER TABLE ?? ALTER COLUMN ?? TYPE ${n.dt} USING ??::${n.dt};\n`, + [t, n.cn, n.cn], + shouldSanitize + ); + } + + if (n.rqd !== o.rqd) { + query += this.genQuery( + `\nALTER TABLE ?? ALTER COLUMN ?? `, + [t, n.cn], + shouldSanitize + ); + query += n.rqd ? ` SET NOT NULL;\n` : ` DROP NOT NULL;\n`; + } + + if (n.cdf !== o.cdf) { + query += this.genQuery( + `\nALTER TABLE ?? ALTER COLUMN ?? `, + [t, n.cn], + shouldSanitize + ); + query += n.cdf ? ` SET DEFAULT ${n.cdf};\n` : ` DROP DEFAULT;\n`; + } + } + return query; + } + + get schema() { + return ( + (this.connectionConfig && + this.connectionConfig.connection.schema) + ); + } + + /** + * + * @param {Object} args + * @returns {Object} result + * @returns {Number} code + * @returns {String} message + */ + async totalRecords(args: any = {}) { + const func = this.totalRecords.name; + const result = new Result(); + log.api(`${func}:args:`, args); + + try { + const data = await this.sqlClient.raw( + `SELECT SUM(record_count) as "TotalRecords" FROM + (SELECT t.table_schema || '.' || t.table_name as "table_name",t.row_count as record_count + FROM information_schema.tables t + WHERE t.table_type = 'BASE TABLE and table_schema = ?' + )`, + [this.schema] + ); + result.data = data.rows[0]; + } catch (e) { + result.code = -1; + result.message = e.message; + result.object = e; + } finally { + log.api(`${func} :result: ${result}`); + } + return result; + } +} + +function getDefaultValue(n) { + if (n.cdf === undefined || n.cdf === null) return n.cdf; + switch (n.dt) { + case 'serial': + case 'bigserial': + case 'smallserial': + return ''; + break; + case 'boolean': + case 'bool': + case 'tinyint': + case 'int': + case 'samllint': + case 'bigint': + case 'integer': + case 'mediumint': + case 'int2': + case 'int4': + case 'int8': + case 'long': + case 'number': + case 'float': + case 'double': + case 'decimal': + case 'numeric': + case 'real': + case 'double precision': + case 'money': + case 'smallmoney': + case 'dec': + return n.cdf; + break; + + case 'datetime': + case 'timestamp': + case 'date': + case 'time': + if ( + n.cdf.indexOf('CURRENT_TIMESTAMP') > -1 || + /\(([\d\w'", ]*)\)$/.test(n.cdf) + ) { + return n.cdf; + } + // return JSON.stringify(n.cdf); + break; + default: + // return JSON.stringify(n.cdf); + break; + } + return n.cdf; +} + +export default SnowflakeClient; diff --git a/packages/nocodb/src/lib/db/sql-client/lib/snowflake/snowflake.queries.ts b/packages/nocodb/src/lib/db/sql-client/lib/snowflake/snowflake.queries.ts new file mode 100644 index 0000000000..0cf8d6a46e --- /dev/null +++ b/packages/nocodb/src/lib/db/sql-client/lib/snowflake/snowflake.queries.ts @@ -0,0 +1,5 @@ +// Snowflake queries + +const sfQueries = {}; + +export default sfQueries; diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts index 25d3461b84..12265b76c0 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts @@ -320,7 +320,7 @@ class BaseModelSqlv2 { as: 'count', }).first(); const res = (await this.dbDriver.raw(unsanitize(qb.toQuery()))) as any; - return (this.isPg ? res.rows[0] : res[0][0] ?? res[0]).count; + return ((this.isPg || this.isSnowflake) ? res.rows[0] : res[0][0] ?? res[0]).count; } // todo: add support for sortArrJson and filterArrJson @@ -1696,6 +1696,10 @@ class BaseModelSqlv2 { return this.clientType === 'mysql2' || this.clientType === 'mysql'; } + get isSnowflake() { + return this.clientType === 'snowflake'; + } + get clientType() { return this.dbDriver.clientType(); } @@ -2764,13 +2768,13 @@ class BaseModelSqlv2 { childTable?: Model ) { let query = qb.toQuery(); - if (!this.isPg && !this.isMssql) { + if (!this.isPg && !this.isMssql && !this.isSnowflake) { query = unsanitize(qb.toQuery()); } else { query = sanitize(query); } return this.convertAttachmentType( - this.isPg + this.isPg || this.isSnowflake ? (await this.dbDriver.raw(query))?.rows : query.slice(0, 6) === 'select' && !this.isMssql ? await this.dbDriver.from( diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/CustomKnex.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/CustomKnex.ts index b114d11526..426e0860c6 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/CustomKnex.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/CustomKnex.ts @@ -1,4 +1,5 @@ import { Knex, knex } from 'knex'; +import { SnowflakeClient } from 'knex-snowflake'; const types = require('pg').types; // override parsing date column to Date() @@ -993,6 +994,8 @@ function CustomKnex(arg: string | Knex.Config | any): CustomKnex { arg.useNullAsDefault = true; } + if (arg?.client === 'snowflake') arg.client = SnowflakeClient; + const kn: any = knex(arg); const knexRaw = kn.raw; @@ -1019,7 +1022,7 @@ function CustomKnex(arg: string | Knex.Config | any): CustomKnex { value: () => { return typeof arg === 'string' ? arg.match(/^(\w+):/) ?? [1] - : arg.client; + : (arg.client?.name === 'SnowflakeClient') ? 'snowflake' : arg.client; }, }, searchPath: { diff --git a/packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaFactory.ts b/packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaFactory.ts index 62c4cfd28c..996a74beed 100644 --- a/packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaFactory.ts +++ b/packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaFactory.ts @@ -4,6 +4,7 @@ import ModelXcMetaMysql from './ModelXcMetaMysql'; import ModelXcMetaOracle from './ModelXcMetaOracle'; import ModelXcMetaPg from './ModelXcMetaPg'; import ModelXcMetaSqlite from './ModelXcMetaSqlite'; +import ModelXcMetaSnowflake from './ModelXcMetaSnowflake'; class ModelXcMetaFactory { public static create(connectionConfig, args): BaseModelXcMeta { @@ -20,6 +21,8 @@ class ModelXcMetaFactory { return new ModelXcMetaPg(args); } else if (connectionConfig.client === 'oracledb') { return new ModelXcMetaOracle(args); + } else if (connectionConfig.client === 'snowflake') { + return new ModelXcMetaSnowflake(args); } throw new Error('Database not supported'); diff --git a/packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaSnowflake.ts b/packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaSnowflake.ts new file mode 100644 index 0000000000..07ef5edd72 --- /dev/null +++ b/packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaSnowflake.ts @@ -0,0 +1,975 @@ +import BaseModelXcMeta from './BaseModelXcMeta'; + +class ModelXcMetaSnowflake extends BaseModelXcMeta { + /** + * @param dir + * @param filename + * @param ctx + * @param ctx.tn + * @param ctx.columns + * @param ctx.relations + */ + constructor({ dir, filename, ctx }) { + super({ dir, filename, ctx }); + } + + /** + * Prepare variables used in code template + */ + prepare() { + const data: any = {}; + + /* run of simple variable */ + data.tn = this.ctx.tn; + data.dbType = this.ctx.dbType; + + /* for complex code provide a func and args - do derivation within the func cbk */ + data.columns = { + func: this._renderXcColumns.bind(this), + args: { + tn: this.ctx.tn, + columns: this.ctx.columns, + relations: this.ctx.relations, + }, + }; + + /* for complex code provide a func and args - do derivation within the func cbk */ + data.hasMany = { + func: this._renderXcHasMany.bind(this), + args: { + tn: this.ctx.tn, + columns: this.ctx.columns, + hasMany: this.ctx.hasMany, + }, + }; + + /* for complex code provide a func and args - do derivation within the func cbk */ + data.belongsTo = { + func: this._renderXcBelongsTo.bind(this), + args: { + tn: this.ctx.tn, + columns: this.ctx.columns, + belongsTo: this.ctx.belongsTo, + }, + }; + + return data; + } + + _renderXcHasMany(args) { + return JSON.stringify(args.hasMany); + } + + _renderXcBelongsTo(args) { + return JSON.stringify(args.belongsTo); + } + + /** + * + * @param args + * @param args.columns + * @param args.relations + * @returns {string} + * @private + */ + _renderXcColumns(args) { + let str = '[\r\n'; + + for (let i = 0; i < args.columns.length; ++i) { + str += `{\r\n`; + str += `cn: '${args.columns[i].cn}',\r\n`; + str += `type: '${this._getAbstractType(args.columns[i])}',\r\n`; + str += `dt: '${args.columns[i].dt}',\r\n`; + if (args.columns[i].rqd) str += `rqd: ${args.columns[i].rqd},\r\n`; + + if (args.columns[i].cdf) { + str += `default: "${args.columns[i].cdf}",\r\n`; + str += `columnDefault: "${args.columns[i].cdf}",\r\n`; + } + + if (args.columns[i].un) str += `un: ${args.columns[i].un},\r\n`; + + if (args.columns[i].pk) str += `pk: ${args.columns[i].pk},\r\n`; + + if (args.columns[i].ai) str += `ai: ${args.columns[i].ai},\r\n`; + + if (args.columns[i].dtxp) str += `dtxp: "${args.columns[i].dtxp}",\r\n`; + + if (args.columns[i].dtxs) str += `dtxs: ${args.columns[i].dtxs},\r\n`; + + str += `validate: { + func: [], + args: [], + msg: [] + },`; + str += `},\r\n`; + } + + str += ']\r\n'; + + return str; + } + + _getAbstractType(column) { + let str = ''; + switch (column.dt) { + case 'int': + str = 'integer'; + break; + case 'integer': + str = 'integer'; + break; + case 'bigint': + str = 'bigInteger'; + break; + case 'bigserial': + str = 'bigserial'; + break; + case 'char': + str = 'string'; + break; + case 'int2': + str = 'integer'; + break; + case 'int4': + str = 'integer'; + break; + case 'int8': + str = 'integer'; + break; + case 'int4range': + str = 'int4range'; + break; + case 'int8range': + str = 'int8range'; + break; + case 'serial': + str = 'serial'; + break; + case 'serial2': + str = 'serial2'; + break; + case 'serial8': + str = 'serial8'; + break; + case 'character': + str = 'string'; + break; + case 'bit': + str = 'bit'; + break; + case 'bool': + str = 'boolean'; + break; + case 'boolean': + str = 'boolean'; + break; + case 'date': + str = 'date'; + break; + case 'double precision': + str = 'double'; + break; + case 'event_trigger': + str = 'event_trigger'; + break; + case 'fdw_handler': + str = 'fdw_handler'; + break; + case 'float4': + str = 'float'; + break; + case 'float8': + str = 'float'; + break; + case 'uuid': + str = 'uuid'; + break; + case 'smallint': + str = 'integer'; + break; + case 'smallserial': + str = 'smallserial'; + break; + case 'character varying': + str = 'string'; + break; + case 'text': + str = 'text'; + break; + case 'real': + str = 'float'; + break; + case 'time': + str = 'time'; + break; + case 'time without time zone': + str = 'time'; + break; + case 'timestamp': + str = 'timestamp'; + break; + case 'timestamp without time zone': + str = 'timestamp'; + break; + case 'timestamptz': + str = 'timestampt'; + break; + case 'timestamp with time zone': + str = 'timestamp'; + break; + case 'timetz': + str = 'time'; + break; + case 'time with time zone': + str = 'time'; + break; + case 'daterange': + str = 'daterange'; + break; + case 'json': + str = 'json'; + break; + case 'jsonb': + str = 'jsonb'; + break; + case 'gtsvector': + str = 'gtsvector'; + break; + case 'index_am_handler': + str = 'index_am_handler'; + break; + case 'anyenum': + str = 'enum'; + break; + case 'anynonarray': + str = 'anynonarray'; + break; + case 'anyrange': + str = 'anyrange'; + break; + case 'box': + str = 'box'; + break; + case 'bpchar': + str = 'bpchar'; + break; + case 'bytea': + str = 'bytea'; + break; + case 'cid': + str = 'cid'; + break; + case 'cidr': + str = 'cidr'; + break; + case 'circle': + str = 'circle'; + break; + case 'cstring': + str = 'cstring'; + break; + case 'inet': + str = 'inet'; + break; + case 'internal': + str = 'internal'; + break; + case 'interval': + str = 'interval'; + break; + case 'language_handler': + str = 'language_handler'; + break; + case 'line': + str = 'line'; + break; + case 'lsec': + str = 'lsec'; + break; + case 'macaddr': + str = 'macaddr'; + break; + case 'money': + str = 'float'; + break; + case 'name': + str = 'name'; + break; + case 'numeric': + str = 'numeric'; + break; + case 'numrange': + str = 'numrange'; + break; + case 'oid': + str = 'oid'; + break; + case 'opaque': + str = 'opaque'; + break; + case 'path': + str = 'path'; + break; + case 'pg_ddl_command': + str = 'pg_ddl_command'; + break; + case 'pg_lsn': + str = 'pg_lsn'; + break; + case 'pg_node_tree': + str = 'pg_node_tree'; + break; + case 'point': + str = 'point'; + break; + case 'polygon': + str = 'polygon'; + break; + case 'record': + str = 'record'; + break; + case 'refcursor': + str = 'refcursor'; + break; + case 'regclass': + str = 'regclass'; + break; + case 'regconfig': + str = 'regconfig'; + break; + case 'regdictionary': + str = 'regdictionary'; + break; + case 'regnamespace': + str = 'regnamespace'; + break; + case 'regoper': + str = 'regoper'; + break; + case 'regoperator': + str = 'regoperator'; + break; + case 'regproc': + str = 'regproc'; + break; + case 'regpreocedure': + str = 'regpreocedure'; + break; + case 'regrole': + str = 'regrole'; + break; + case 'regtype': + str = 'regtype'; + break; + case 'reltime': + str = 'reltime'; + break; + case 'smgr': + str = 'smgr'; + break; + case 'tid': + str = 'tid'; + break; + case 'tinterval': + str = 'tinterval'; + break; + case 'trigger': + str = 'trigger'; + break; + case 'tsm_handler': + str = 'tsm_handler'; + break; + case 'tsquery': + str = 'tsquery'; + break; + case 'tsrange': + str = 'tsrange'; + break; + case 'tstzrange': + str = 'tstzrange'; + break; + case 'tsvector': + str = 'tsvector'; + break; + case 'txid_snapshot': + str = 'txid_snapshot'; + break; + case 'unknown': + str = 'unknown'; + break; + case 'void': + str = 'void'; + break; + case 'xid': + str = 'xid'; + break; + case 'xml': + str = 'xml'; + break; + default: + str += `"${column.dt}"`; + break; + } + return str; + } + + getUIDataType(col): any { + switch (this.getAbstractType(col)) { + case 'integer': + return 'Number'; + case 'boolean': + return 'Checkbox'; + case 'float': + return 'Decimal'; + case 'date': + return 'Date'; + case 'datetime': + return 'DateTime'; + case 'time': + return 'Time'; + case 'year': + return 'Year'; + case 'string': + return 'SingleLineText'; + case 'text': + return 'LongText'; + case 'enum': + return 'SingleSelect'; + case 'set': + return 'MultiSelect'; + case 'json': + return 'JSON'; + case 'blob': + return 'LongText'; + case 'geometry': + return 'Geometry'; + default: + return 'SpecificDBType'; + } + } + + getAbstractType(col): any { + const dt = col.dt.toLowerCase(); + switch (dt) { + case 'anyenum': + return 'enum'; + case 'anynonarray': + case 'anyrange': + return dt; + + case 'bit': + return 'integer'; + case 'bigint': + case 'bigserial': + return 'integer'; + + case 'bool': + return 'boolean'; + + case 'bpchar': + case 'bytea': + return dt; + case 'char': + case 'character': + case 'character varying': + return 'string'; + + case 'cid': + case 'cidr': + case 'cstring': + return dt; + + case 'date': + return 'date'; + case 'daterange': + return 'string'; + case 'double precision': + return 'string'; + + case 'event_trigger': + case 'fdw_handler': + return dt; + + case 'float4': + case 'float8': + return 'float'; + + case 'gtsvector': + case 'index_am_handler': + case 'inet': + return dt; + + case 'int': + case 'int2': + case 'int4': + case 'int8': + case 'integer': + return 'integer'; + case 'int4range': + case 'int8range': + case 'internal': + case 'interval': + return 'string'; + case 'json': + case 'jsonb': + return 'json'; + + case 'language_handler': + case 'lsec': + case 'macaddr': + case 'money': + case 'name': + case 'numeric': + case 'numrange': + case 'oid': + case 'opaque': + case 'path': + case 'pg_ddl_command': + case 'pg_lsn': + case 'pg_node_tree': + return dt; + case 'real': + return 'float'; + case 'record': + case 'refcursor': + case 'regclass': + case 'regconfig': + case 'regdictionary': + case 'regnamespace': + case 'regoper': + case 'regoperator': + case 'regproc': + case 'regpreocedure': + case 'regrole': + case 'regtype': + case 'reltime': + return dt; + case 'serial': + case 'serial2': + case 'serial8': + case 'smallint': + case 'smallserial': + return 'integer'; + case 'smgr': + return dt; + case 'text': + return 'text'; + case 'tid': + return dt; + case 'time': + case 'time without time zone': + return 'time'; + case 'timestamp': + case 'timestamp without time zone': + case 'timestamptz': + case 'timestamp with time zone': + return 'datetime'; + case 'timetz': + case 'time with time zone': + return 'time'; + + case 'tinterval': + case 'trigger': + case 'tsm_handler': + case 'tsquery': + case 'tsrange': + case 'tstzrange': + case 'tsvector': + case 'txid_snapshot': + case 'unknown': + case 'void': + case 'xid': + case 'xml': + return dt; + + case 'tinyint': + case 'mediumint': + return 'integer'; + + case 'float': + case 'decimal': + case 'double': + return 'float'; + case 'boolean': + return 'boolean'; + case 'datetime': + return 'datetime'; + + case 'uuid': + case 'year': + case 'varchar': + case 'nchar': + return 'string'; + + case 'tinytext': + case 'mediumtext': + case 'longtext': + return 'text'; + + case 'binary': + case 'varbinary': + return 'text'; + + case 'blob': + case 'tinyblob': + case 'mediumblob': + case 'longblob': + return 'blob'; + case 'enum': + return 'enum'; + case 'set': + return 'set'; + + case 'line': + case 'point': + case 'polygon': + case 'circle': + case 'box': + case 'geometry': + case 'linestring': + case 'multipoint': + case 'multilinestring': + case 'multipolygon': + return 'geometry'; + } + } + + _sequelizeGetType(column) { + let str = ''; + switch (column.dt) { + case 'int': + str += `DataTypes.INTEGER(${column.dtxp})`; + if (column.un) str += `.UNSIGNED`; + break; + case 'tinyint': + str += `DataTypes.INTEGER(${column.dtxp})`; + if (column.un) str += `.UNSIGNED`; + + break; + case 'smallint': + str += `DataTypes.INTEGER(${column.dtxp})`; + if (column.un) str += `.UNSIGNED`; + + break; + case 'mediumint': + str += `DataTypes.INTEGER(${column.dtxp})`; + if (column.un) str += `.UNSIGNED`; + + break; + case 'bigint': + str += `DataTypes.BIGINT`; + if (column.un) str += `.UNSIGNED`; + + break; + case 'float': + str += `DataTypes.FLOAT`; + break; + case 'decimal': + str += `DataTypes.DECIMAL`; + break; + case 'double': + str += `"DOUBLE(${column.dtxp},${column.ns})"`; + break; + case 'real': + str += `DataTypes.FLOAT`; + break; + case 'bit': + str += `DataTypes.BOOLEAN`; + break; + case 'boolean': + str += `DataTypes.STRING(45)`; + break; + case 'serial': + str += `DataTypes.BIGINT`; + break; + case 'date': + str += `DataTypes.DATEONLY`; + break; + case 'datetime': + str += `DataTypes.DATE`; + break; + case 'timestamp': + str += `DataTypes.DATE`; + break; + case 'time': + str += `DataTypes.TIME`; + break; + case 'year': + str += `"YEAR"`; + break; + case 'char': + str += `DataTypes.CHAR(${column.dtxp})`; + break; + case 'varchar': + str += `DataTypes.STRING(${column.dtxp})`; + break; + case 'nchar': + str += `DataTypes.CHAR(${column.dtxp})`; + break; + case 'text': + str += `DataTypes.TEXT`; + break; + case 'tinytext': + str += `DataTypes.TEXT`; + break; + case 'mediumtext': + str += `DataTypes.TEXT`; + break; + case 'longtext': + str += `DataTypes.TEXT`; + break; + case 'binary': + str += `"BINARY(${column.dtxp})"`; + break; + case 'varbinary': + str += `"VARBINARY(${column.dtxp})"`; + break; + case 'blob': + str += `"BLOB"`; + break; + case 'tinyblob': + str += `"TINYBLOB"`; + break; + case 'mediumblob': + str += `"MEDIUMBLOB"`; + break; + case 'longblob': + str += `"LONGBLOB"`; + break; + case 'enum': + str += `DataTypes.ENUM(${column.dtxp})`; + break; + case 'set': + str += `"SET(${column.dtxp})"`; + break; + case 'geometry': + str += `DataTypes.GEOMETRY`; + break; + case 'point': + str += `"POINT"`; + break; + case 'linestring': + str += `"LINESTRING"`; + break; + case 'polygon': + str += `"POLYGON"`; + break; + case 'multipoint': + str += `"MULTIPOINT"`; + break; + case 'multilinestring': + str += `"MULTILINESTRING"`; + break; + case 'multipolygon': + str += `"MULTIPOLYGON"`; + break; + case 'json': + str += `DataTypes.JSON`; + break; + default: + str += `"${column.dt}"`; + break; + } + return str; + } + + _sequelizeGetDefault(column) { + let str = ''; + switch (column.dt) { + case 'int': + str += `'${column.cdf}'`; + break; + case 'tinyint': + str += `'${column.cdf}'`; + break; + case 'smallint': + str += `'${column.cdf}'`; + break; + case 'mediumint': + str += `'${column.cdf}'`; + break; + case 'bigint': + str += `'${column.cdf}'`; + break; + case 'float': + str += `'${column.cdf}'`; + break; + case 'decimal': + str += `'${column.cdf}'`; + break; + case 'double': + str += `'${column.cdf}'`; + break; + case 'real': + str += `'${column.cdf}'`; + break; + case 'bit': + str += column.cdf ? column.cdf.split('b')[1] : column.cdf; + break; + case 'boolean': + str += column.cdf; + break; + case 'serial': + str += column.cdf; + break; + case 'date': + str += `sequelize.literal('${column.cdf_sequelize}')`; + break; + case 'datetime': + str += `sequelize.literal('${column.cdf_sequelize}')`; + break; + case 'timestamp': + str += `sequelize.literal('${column.cdf_sequelize}')`; + break; + case 'time': + str += `'${column.cdf}'`; + break; + case 'year': + str += `'${column.cdf}'`; + break; + case 'char': + str += `'${column.cdf}'`; + break; + case 'varchar': + str += `'${column.cdf}'`; + break; + case 'nchar': + str += `'${column.cdf}'`; + break; + case 'text': + str += column.cdf; + break; + case 'tinytext': + str += column.cdf; + break; + case 'mediumtext': + str += column.cdf; + break; + case 'longtext': + str += column.cdf; + break; + case 'binary': + str += column.cdf; + break; + case 'varbinary': + str += column.cdf; + break; + case 'blob': + str += column.cdf; + break; + case 'tinyblob': + str += column.cdf; + break; + case 'mediumblob': + str += column.cdf; + break; + case 'longblob': + str += column.cdf; + break; + case 'enum': + str += `'${column.cdf}'`; + break; + case 'set': + str += `'${column.cdf}'`; + break; + case 'geometry': + str += `'${column.cdf}'`; + break; + case 'point': + str += `'${column.cdf}'`; + break; + case 'linestring': + str += `'${column.cdf}'`; + break; + case 'polygon': + str += `'${column.cdf}'`; + break; + case 'multipoint': + str += `'${column.cdf}'`; + break; + case 'multilinestring': + str += `'${column.cdf}'`; + break; + case 'multipolygon': + str += `'${column.cdf}'`; + break; + case 'json': + str += column.cdf; + break; + } + return str; + } + + /* getXcColumnsObject(args) { + const columnsArr = []; + + for (const column of args.columns) { + const columnObj = { + validate: { + func: [], + args: [], + msg: [] + }, + cn: column.cn, + _cn: column._cn || column.cn, + type: this._getAbstractType(column), + dt: column.dt, + uidt: column.uidt || this._getUIDataType(column), + uip: column.uip, + uicn: column.uicn, + ...column + }; + + if (column.rqd) { + columnObj.rqd = column.rqd; + } + + if (column.cdf) { + columnObj.default = column.cdf; + columnObj.columnDefault = column.cdf; + } + + if (column.un) { + columnObj.un = column.un; + } + + if (column.pk) { + columnObj.pk = column.pk; + } + + if (column.ai) { + columnObj.ai = column.ai; + } + + if (column.dtxp) { + columnObj.dtxp = column.dtxp; + } + + if (column.dtxs) { + columnObj.dtxs = column.dtxs; + } + + columnsArr.push(columnObj); + } + + this.mapDefaultPrimaryValue(columnsArr); + return columnsArr; + }*/ + + /* getObject() { + return { + tn: this.ctx.tn, + _tn: this.ctx._tn, + columns: this.getXcColumnsObject(this.ctx), + pks: [], + hasMany: this.ctx.hasMany, + belongsTo: this.ctx.belongsTo, + dbType: this.ctx.dbType, + type: this.ctx.type, + } + + }*/ +} + +export default ModelXcMetaSnowflake; From b800f838a7e0ca0ce203fea4b35724544bfc1057 Mon Sep 17 00:00:00 2001 From: mertmit Date: Sun, 9 Oct 2022 20:25:44 +0300 Subject: [PATCH 078/134] feat: snowflake front-end Signed-off-by: mertmit --- packages/nc-gui/lib/enums.ts | 1 + .../pages/index/index/create-external.vue | 39 +++++++++++++++++++ packages/nc-gui/utils/projectCreateUtils.ts | 29 ++++++++++++-- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/packages/nc-gui/lib/enums.ts b/packages/nc-gui/lib/enums.ts index 776aadbcee..607ae6c0c8 100644 --- a/packages/nc-gui/lib/enums.ts +++ b/packages/nc-gui/lib/enums.ts @@ -20,6 +20,7 @@ export enum ClientType { PG = 'pg', SQLITE = 'sqlite3', VITESS = 'vitess', + SNOWFLAKE = 'snowflake', } export enum Language { diff --git a/packages/nc-gui/pages/index/index/create-external.vue b/packages/nc-gui/pages/index/index/create-external.vue index 58ae4ea4c1..af522124f0 100644 --- a/packages/nc-gui/pages/index/index/create-external.vue +++ b/packages/nc-gui/pages/index/index/create-external.vue @@ -79,6 +79,8 @@ const validators = computed(() => { ? { 'dataSource.connection.connection.filename': [fieldRequiredValidator()], } + : formState.dataSource.client === ClientType.SNOWFLAKE + ? {} : { 'dataSource.connection.host': [fieldRequiredValidator()], 'dataSource.connection.port': [fieldRequiredValidator()], @@ -385,6 +387,43 @@ onMounted(async () => { + +