Browse Source

Merge pull request #8371 from nocodb/nc-fix/sf-issues

fix: pre-post insert ops
pull/8377/head
Pranav C 7 months ago committed by GitHub
parent
commit
2197b8db1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 15
      packages/nc-gui/components/smartsheet/Cell.vue
  2. 19
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  3. 34
      packages/nc-gui/components/smartsheet/grid/Table.vue
  4. 16
      packages/nc-gui/components/virtual-cell/components/UnLinkedItems.vue
  5. 66
      packages/nc-gui/composables/useData.ts
  6. 4
      packages/nc-gui/composables/useExpandedFormStore.ts
  7. 6
      packages/nc-gui/composables/useLTARStore.ts
  8. 1
      packages/nc-gui/lib/types.ts
  9. 10
      packages/nc-gui/utils/dataUtils.ts
  10. 1
      packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts
  11. 108
      packages/nocodb/src/db/BaseModelSqlv2.ts
  12. 1
      packages/nocodb/src/db/sql-mgr/code/models/xc/ModelXcMetaSnowflake.ts
  13. 7
      packages/nocodb/src/helpers/getUniqueName.ts
  14. 15
      packages/nocodb/src/helpers/populateMeta.ts

15
packages/nc-gui/components/smartsheet/Cell.vue

@ -112,6 +112,17 @@ const syncValue = useDebounceFn(
{ maxWait: 2000 }, { maxWait: 2000 },
) )
let saveTimer: number
const updateWhenEditCompleted = () => {
if (editEnabled.value) {
if (saveTimer) clearTimeout(saveTimer)
saveTimer = window.setTimeout(updateWhenEditCompleted, 500)
} else {
emit('save')
}
}
const vModel = computed({ const vModel = computed({
get: () => { get: () => {
return props.modelValue return props.modelValue
@ -122,7 +133,9 @@ const vModel = computed({
} else if (val !== props.modelValue) { } else if (val !== props.modelValue) {
currentRow.value.rowMeta.changed = true currentRow.value.rowMeta.changed = true
emit('update:modelValue', val) emit('update:modelValue', val)
if (isAutoSaved(column.value)) { if (column.value.pk) {
updateWhenEditCompleted()
} else if (isAutoSaved(column.value)) {
syncValue() syncValue()
} else if (!isManualSaved(column.value)) { } else if (!isManualSaved(column.value)) {
emit('save') emit('save')

19
packages/nc-gui/components/smartsheet/expanded-form/index.vue

@ -51,6 +51,7 @@ interface Props {
lastRow?: boolean lastRow?: boolean
closeAfterSave?: boolean closeAfterSave?: boolean
newRecordHeader?: string newRecordHeader?: string
skipReload?: boolean
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@ -102,7 +103,7 @@ const expandedFormScrollWrapper = ref()
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook()) const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
const reloadViewDataTrigger = inject(ReloadViewDataHookInj) const reloadViewDataTrigger = inject(ReloadViewDataHookInj, createEventHook())
const { addOrEditStackRow } = useKanbanViewStoreOrThrow() const { addOrEditStackRow } = useKanbanViewStoreOrThrow()
@ -137,6 +138,8 @@ provide(MetaInj, meta)
const isLoading = ref(true) const isLoading = ref(true)
const isSaving = ref(false)
const { const {
commentsDrawer, commentsDrawer,
changedColumns, changedColumns,
@ -207,26 +210,31 @@ const onDuplicateRow = () => {
} }
const save = async () => { const save = async () => {
isSaving.value = true
let kanbanClbk let kanbanClbk
if (activeView.value?.type === ViewTypes.KANBAN) { if (activeView.value?.type === ViewTypes.KANBAN) {
kanbanClbk = (row: any, isNewRow: boolean) => { kanbanClbk = (row: any, isNewRow: boolean) => {
addOrEditStackRow(row, isNewRow) addOrEditStackRow(row, isNewRow)
} }
} }
if (isNew.value) { if (isNew.value) {
await _save(rowState.value, undefined, { await _save(rowState.value, undefined, {
kanbanClbk, kanbanClbk,
}) })
reloadTrigger?.trigger()
reloadViewDataTrigger?.trigger()
} else { } else {
await _save(undefined, undefined, { await _save(undefined, undefined, {
kanbanClbk, kanbanClbk,
}) })
_loadRow() _loadRow()
}
if (!props.skipReload) {
reloadTrigger?.trigger() reloadTrigger?.trigger()
reloadViewDataTrigger?.trigger() reloadViewDataTrigger?.trigger()
} }
isUnsavedFormExist.value = false isUnsavedFormExist.value = false
if (props.closeAfterSave) { if (props.closeAfterSave) {
@ -234,6 +242,8 @@ const save = async () => {
} }
emits('createdRecord', _row.value.row) emits('createdRecord', _row.value.row)
isSaving.value = false
} }
const isPreventChangeModalOpen = ref(false) const isPreventChangeModalOpen = ref(false)
@ -871,6 +881,7 @@ export default {
<NcButton <NcButton
v-e="['c:row-expand:save']" v-e="['c:row-expand:save']"
:disabled="changedColumns.size === 0 && !isUnsavedFormExist" :disabled="changedColumns.size === 0 && !isUnsavedFormExist"
:loading="isSaving"
class="nc-expand-form-save-btn !xs:(text-base)" class="nc-expand-form-save-btn !xs:(text-base)"
data-testid="nc-expanded-form-save" data-testid="nc-expanded-form-save"
type="primary" type="primary"
@ -917,7 +928,7 @@ export default {
<div class="flex flex-row justify-end gap-x-2 mt-5"> <div class="flex flex-row justify-end gap-x-2 mt-5">
<NcButton type="secondary" @click="discardPreventModal">{{ $t('labels.discard') }}</NcButton> <NcButton type="secondary" @click="discardPreventModal">{{ $t('labels.discard') }}</NcButton>
<NcButton key="submit" type="primary" label="Rename Table" loading-label="Renaming Table" @click="saveChanges"> <NcButton key="submit" type="primary" :loading="isSaving" @click="saveChanges">
{{ $t('tooltip.saveChanges') }} {{ $t('tooltip.saveChanges') }}
</NcButton> </NcButton>
</div> </div>

34
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -1064,13 +1064,27 @@ async function resetAndChangePage(row: number, col: number, pageChange?: number)
scrollToCell?.() scrollToCell?.()
} }
const saveOrUpdateRecords = async (args: { metaValue?: TableType; viewMetaValue?: ViewType; data?: any } = {}) => { const temporaryNewRowStore = ref<Row[]>([])
const saveOrUpdateRecords = async (
args: { metaValue?: TableType; viewMetaValue?: ViewType; data?: any; keepNewRecords?: boolean } = {},
) => {
for (const currentRow of args.data || dataRef.value) { for (const currentRow of args.data || dataRef.value) {
if (currentRow.rowMeta.fromExpandedForm) continue
/** if new record save row and save the LTAR cells */ /** if new record save row and save the LTAR cells */
if (currentRow.rowMeta.new) { if (currentRow.rowMeta.new) {
const savedRow = await updateOrSaveRow?.(currentRow, '', {}, args) const beforeSave = clone(currentRow)
await syncLTARRefs?.(currentRow, savedRow, args) const savedRow = await updateOrSaveRow?.(currentRow, '', currentRow.rowMeta.ltarState || {}, args)
if (savedRow) {
currentRow.rowMeta.changed = false currentRow.rowMeta.changed = false
} else {
if (args.keepNewRecords) {
if (beforeSave.rowMeta.new && Object.keys(beforeSave.row).length) {
temporaryNewRowStore.value.push(beforeSave)
}
}
}
continue continue
} }
@ -1277,7 +1291,10 @@ const showFillHandle = computed(
isFormula(fields.value[activeCell.col]) || isFormula(fields.value[activeCell.col]) ||
isCreatedOrLastModifiedTimeCol(fields.value[activeCell.col]) || isCreatedOrLastModifiedTimeCol(fields.value[activeCell.col]) ||
isCreatedOrLastModifiedByCol(fields.value[activeCell.col]) isCreatedOrLastModifiedByCol(fields.value[activeCell.col])
), ) &&
!isViewDataLoading.value &&
!isPaginationLoading.value &&
dataRef.value.length,
) )
watch( watch(
@ -1329,10 +1346,17 @@ async function reloadViewDataHandler(params: void | { shouldShowLoading?: boolea
predictedNextColumn.value = predictedNextColumn.value.filter((c) => !fieldsAvailable?.includes(c.title)) predictedNextColumn.value = predictedNextColumn.value.filter((c) => !fieldsAvailable?.includes(c.title))
} }
// save any unsaved data before reload // save any unsaved data before reload
await saveOrUpdateRecords() await saveOrUpdateRecords({
keepNewRecords: true,
})
await loadData?.({ ...(params?.offset !== undefined ? { offset: params.offset } : {}) }) await loadData?.({ ...(params?.offset !== undefined ? { offset: params.offset } : {}) })
if (temporaryNewRowStore.value.length) {
dataRef.value.push(...temporaryNewRowStore.value)
temporaryNewRowStore.value = []
}
calculateSlices() calculateSlices()
isViewDataLoading.value = false isViewDataLoading.value = false

16
packages/nc-gui/components/virtual-cell/components/UnLinkedItems.vue

@ -65,6 +65,10 @@ const isForm = inject(IsFormInj, ref(false))
const saveRow = inject(SaveRowInj, () => {}) const saveRow = inject(SaveRowInj, () => {})
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
const reloadViewDataTrigger = inject(ReloadViewDataHookInj, createEventHook())
const linkRow = async (row: Record<string, any>, id: number) => { const linkRow = async (row: Record<string, any>, id: number) => {
if (isNew.value) { if (isNew.value) {
addLTARRef(row, injectedColumn?.value as ColumnType) addLTARRef(row, injectedColumn?.value as ColumnType)
@ -213,6 +217,15 @@ const addNewRecord = () => {
} }
const onCreatedRecord = (record: any) => { const onCreatedRecord = (record: any) => {
addLTARRef(record, injectedColumn?.value as ColumnType)
reloadTrigger?.trigger({
shouldShowLoading: false,
})
reloadViewDataTrigger?.trigger({
shouldShowLoading: false,
})
const msgVNode = h( const msgVNode = h(
'div', 'div',
{ {
@ -240,6 +253,8 @@ const onCreatedRecord = (record: any) => {
) )
message.success(msgVNode) message.success(msgVNode)
vModel.value = false
} }
const linkedShortcuts = (e: KeyboardEvent) => { const linkedShortcuts = (e: KeyboardEvent) => {
@ -458,6 +473,7 @@ const onFilterChange = () => {
:row-id="extractPkFromRow(expandedFormRow, relatedTableMeta.columns as ColumnType[])" :row-id="extractPkFromRow(expandedFormRow, relatedTableMeta.columns as ColumnType[])"
:state="newRowState" :state="newRowState"
use-meta-fields use-meta-fields
:skip-reload="true"
@created-record="onCreatedRecord" @created-record="onCreatedRecord"
/> />
</Suspense> </Suspense>

66
packages/nc-gui/composables/useData.ts

@ -477,8 +477,8 @@ export function useData(args: {
try { try {
await $api.dbTableRow.nestedAdd( await $api.dbTableRow.nestedAdd(
NOCO, NOCO,
base.value.title as string, base.value.id as string,
metaValue?.title as string, metaValue?.id as string,
encodeURIComponent(rowId), encodeURIComponent(rowId),
type as RelationTypes, type as RelationTypes,
column.title as string, column.title as string,
@ -630,23 +630,25 @@ export function useData(args: {
async function deleteSelectedRows() { async function deleteSelectedRows() {
let row = formattedData.value.length let row = formattedData.value.length
let removedRowsData: Record<string, any>[] = [] const removedRowsData: Record<string, any>[] = []
let compositePrimaryKey = '' let compositePrimaryKey = ''
while (row--) { while (row--) {
const { row: rowObj, rowMeta } = formattedData.value[row] as Record<string, any> const { row: rowData, rowMeta } = formattedData.value[row] as Record<string, any>
if (!rowMeta.selected) { if (!rowMeta.selected) {
continue continue
} }
if (!rowMeta.new) { if (!rowMeta.new) {
const extractedPk = extractPk(meta?.value?.columns as ColumnType[]) const extractedPk = extractPk(meta?.value?.columns as ColumnType[])
const compositePkValue = extractPkFromRow(rowObj, meta?.value?.columns as ColumnType[]) const compositePkValue = extractPkFromRow(rowData, meta?.value?.columns as ColumnType[])
const pkData = rowPkData(rowData, meta?.value?.columns as ColumnType[])
if (extractedPk && compositePkValue) { if (extractedPk && compositePkValue) {
if (!compositePrimaryKey) compositePrimaryKey = extractedPk if (!compositePrimaryKey) compositePrimaryKey = extractedPk
removedRowsData.push({ removedRowsData.push({
[compositePrimaryKey]: compositePkValue as string, [compositePrimaryKey]: compositePkValue as string,
pkData,
row: clone(formattedData.value[row]) as Row, row: clone(formattedData.value[row]) as Row,
rowIndex: row as number, rowIndex: row as number,
}) })
@ -670,20 +672,7 @@ export function useData(args: {
rowObj.row = clone(fullRecord) rowObj.row = clone(fullRecord)
} }
const removedRowIds: Record<string, any>[] = await bulkDeleteRows( await bulkDeleteRows(removedRowsData.map((row) => row.pkData))
removedRowsData.map((row) => ({ [compositePrimaryKey]: row[compositePrimaryKey] as string })),
)
if (Array.isArray(removedRowIds)) {
const removedRowsDataSet = new Set(removedRowIds.map((row) => row[compositePrimaryKey]))
removedRowsData = removedRowsData.filter((row) => removedRowsDataSet.has(row[compositePrimaryKey] as string))
const rowIndexesSet = new Set(removedRowsData.map((row) => row.rowIndex))
formattedData.value = formattedData.value.filter((_, index) => rowIndexesSet.has(index))
} else {
removedRowsData = []
}
} catch (e: any) { } catch (e: any) {
return message.error(`${t('msg.error.deleteRowFailed')}: ${await extractSdkResponseErrorMsg(e)}`) return message.error(`${t('msg.error.deleteRowFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
} }
@ -692,10 +681,8 @@ export function useData(args: {
addUndo({ addUndo({
redo: { redo: {
fn: async function redo(this: UndoRedoAction, removedRowsData: Record<string, any>[], compositePrimaryKey: string) { fn: async function redo(this: UndoRedoAction, removedRowsData: Record<string, any>[]) {
const removedRowIds = await bulkDeleteRows( const removedRowIds = await bulkDeleteRows(removedRowsData.map((row) => row.pkData))
removedRowsData.map((row) => ({ [compositePrimaryKey]: row[compositePrimaryKey] as string })),
)
if (Array.isArray(removedRowIds)) { if (Array.isArray(removedRowIds)) {
for (const { row } of removedRowsData) { for (const { row } of removedRowsData) {
@ -708,7 +695,7 @@ export function useData(args: {
await callbacks?.syncPagination?.() await callbacks?.syncPagination?.()
}, },
args: [removedRowsData, compositePrimaryKey], args: [removedRowsData],
}, },
undo: { undo: {
fn: async function undo( fn: async function undo(
@ -764,22 +751,24 @@ export function useData(args: {
// plus one because we want to include the end row // plus one because we want to include the end row
let row = start + 1 let row = start + 1
let removedRowsData: Record<string, any>[] = [] const removedRowsData: Record<string, any>[] = []
let compositePrimaryKey = '' let compositePrimaryKey = ''
while (row--) { while (row--) {
try { try {
const { row: rowObj, rowMeta } = formattedData.value[row] as Record<string, any> const { row: rowData, rowMeta } = formattedData.value[row] as Record<string, any>
if (!rowMeta.new) { if (!rowMeta.new) {
const extractedPk = extractPk(meta?.value?.columns as ColumnType[]) const extractedPk = extractPk(meta?.value?.columns as ColumnType[])
const compositePkValue = extractPkFromRow(rowObj, meta?.value?.columns as ColumnType[]) const compositePkValue = extractPkFromRow(rowData, meta?.value?.columns as ColumnType[])
const pkData = rowPkData(rowData, meta?.value?.columns as ColumnType[])
if (extractedPk && compositePkValue) { if (extractedPk && compositePkValue) {
if (!compositePrimaryKey) compositePrimaryKey = extractedPk if (!compositePrimaryKey) compositePrimaryKey = extractedPk
removedRowsData.push({ removedRowsData.push({
[compositePrimaryKey]: compositePkValue as string, [compositePrimaryKey]: compositePkValue as string,
pkData,
row: clone(formattedData.value[row]) as Row, row: clone(formattedData.value[row]) as Row,
rowIndex: row as number, rowIndex: row as number,
}) })
@ -808,20 +797,7 @@ export function useData(args: {
rowObj.row = clone(fullRecord) rowObj.row = clone(fullRecord)
} }
const removedRowIds: Record<string, any>[] = await bulkDeleteRows( await bulkDeleteRows(removedRowsData.map((row) => row.pkData))
removedRowsData.map((row) => ({ [compositePrimaryKey]: row[compositePrimaryKey] as string })),
)
if (Array.isArray(removedRowIds)) {
const removedRowsDataSet = new Set(removedRowIds.map((row) => row[compositePrimaryKey]))
removedRowsData = removedRowsData.filter((row) => removedRowsDataSet.has(row[compositePrimaryKey] as string))
const rowIndexesSet = new Set(removedRowsData.map((row) => row.rowIndex))
formattedData.value = formattedData.value.filter((_, index) => rowIndexesSet.has(index))
} else {
removedRowsData = []
}
} catch (e: any) { } catch (e: any) {
return message.error(`${t('msg.error.deleteRowFailed')}: ${await extractSdkResponseErrorMsg(e)}`) return message.error(`${t('msg.error.deleteRowFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
} }
@ -830,10 +806,8 @@ export function useData(args: {
addUndo({ addUndo({
redo: { redo: {
fn: async function redo(this: UndoRedoAction, removedRowsData: Record<string, any>[], compositePrimaryKey: string) { fn: async function redo(this: UndoRedoAction, removedRowsData: Record<string, any>[]) {
const removedRowIds = await bulkDeleteRows( const removedRowIds = await bulkDeleteRows(removedRowsData.map((row) => row.pkData))
removedRowsData.map((row) => ({ [compositePrimaryKey]: row[compositePrimaryKey] as string })),
)
if (Array.isArray(removedRowIds)) { if (Array.isArray(removedRowIds)) {
for (const { row } of removedRowsData) { for (const { row } of removedRowsData) {
@ -846,7 +820,7 @@ export function useData(args: {
await callbacks?.syncPagination?.() await callbacks?.syncPagination?.()
}, },
args: [removedRowsData, compositePrimaryKey], args: [removedRowsData],
}, },
undo: { undo: {
fn: async function undo( fn: async function undo(

4
packages/nc-gui/composables/useExpandedFormStore.ts

@ -56,6 +56,8 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
: ({ row: {}, oldRow: {}, rowMeta: {} } as Row), : ({ row: {}, oldRow: {}, rowMeta: {} } as Row),
) )
row.value.rowMeta.fromExpandedForm = true
const rowStore = useProvideSmartsheetRowStore(row) const rowStore = useProvideSmartsheetRowStore(row)
const activeView = inject(ActiveViewInj, ref()) const activeView = inject(ActiveViewInj, ref())
@ -304,6 +306,8 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
} }
const loadRow = async (rowId?: string, onlyVirtual = false) => { const loadRow = async (rowId?: string, onlyVirtual = false) => {
if (row.value.rowMeta.new) return
if (isPublic.value || !meta.value?.id) return if (isPublic.value || !meta.value?.id) return
let record = await $api.dbTableRow.read( let record = await $api.dbTableRow.read(
NOCO, NOCO,

6
packages/nc-gui/composables/useLTARStore.ts

@ -269,7 +269,11 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
// Mark out exact same objects in activeState[column.value.title] as Linked // Mark out exact same objects in activeState[column.value.title] as Linked
// compare all keys and values // compare all keys and values
childrenExcludedList.value.list.forEach((row: any, index: number) => { childrenExcludedList.value.list.forEach((row: any, index: number) => {
const found = activeState[column.value.title].find((a: any) => { const found = (
[RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes(colOptions.value.type)
? [activeState[column.value.title]]
: activeState[column.value.title]
).find((a: any) => {
let isSame = true let isSame = true
for (const key in a) { for (const key in a) {

1
packages/nc-gui/lib/types.ts

@ -65,6 +65,7 @@ interface Row {
changed?: boolean changed?: boolean
saving?: boolean saving?: boolean
ltarState?: Record<string, Record<string, any> | Record<string, any>[] | null> ltarState?: Record<string, Record<string, any> | Record<string, any>[] | null>
fromExpandedForm?: boolean
// use in datetime picker component // use in datetime picker component
isUpdatedFromCopyNPaste?: Record<string, boolean> isUpdatedFromCopyNPaste?: Record<string, boolean>
// Used in Calendar view // Used in Calendar view

10
packages/nc-gui/utils/dataUtils.ts

@ -5,10 +5,12 @@ import type { Row } from '~/lib'
export const extractPkFromRow = (row: Record<string, any>, columns: ColumnType[]) => { export const extractPkFromRow = (row: Record<string, any>, columns: ColumnType[]) => {
if (!row || !columns) return null if (!row || !columns) return null
return columns
.filter((c) => c.pk) const pkColumns = columns.filter((c) => c.pk)
.map((c) => row?.[c.title as string])
.join('___') if (pkColumns.every((c) => row?.[c.title as string] === null && row?.[c.title as string] === undefined)) return null
return pkColumns.map((c) => row?.[c.title as string]).join('___')
} }
export const rowPkData = (row: Record<string, any>, columns: ColumnType[]) => { export const rowPkData = (row: Record<string, any>, columns: ColumnType[]) => {

1
packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts

@ -643,6 +643,7 @@ export class SnowflakeUi {
case 'STRING': case 'STRING':
return 'string'; return 'string';
case 'TEXT': case 'TEXT':
if (col.dtxp < 1024) return 'string';
return 'text'; return 'text';
case 'BINARY': case 'BINARY':
case 'VARBINARY': case 'VARBINARY':

108
packages/nocodb/src/db/BaseModelSqlv2.ts

@ -1897,7 +1897,7 @@ class BaseModelSqlv2 {
// .where(childTable.primaryKey.cn, cid) // .where(childTable.primaryKey.cn, cid)
.where(_wherePk(childTable.primaryKeys, cid)) .where(_wherePk(childTable.primaryKeys, cid))
.whereNotNull(cn), .whereNotNull(cn),
).orWhereNull(rcn); );
}); });
if (+rest?.shuffle) { if (+rest?.shuffle) {
@ -2107,7 +2107,7 @@ class BaseModelSqlv2 {
] = async function (args): Promise<any> { ] = async function (args): Promise<any> {
(listLoader as any).args = args; (listLoader as any).args = args;
return listLoader.load( return listLoader.load(
getCompositePk(self.model.primaryKeys, this), getCompositePkValue(self.model.primaryKeys, this),
); );
}; };
} else if (colOptions.type === 'mm') { } else if (colOptions.type === 'mm') {
@ -2148,7 +2148,7 @@ class BaseModelSqlv2 {
] = async function (args): Promise<any> { ] = async function (args): Promise<any> {
(listLoader as any).args = args; (listLoader as any).args = args;
return await listLoader.load( return await listLoader.load(
getCompositePk(self.model.primaryKeys, this), getCompositePkValue(self.model.primaryKeys, this),
); );
}; };
} else if (colOptions.type === 'bt') { } else if (colOptions.type === 'bt') {
@ -2370,7 +2370,7 @@ class BaseModelSqlv2 {
] = async function (args): Promise<any> { ] = async function (args): Promise<any> {
(listLoader as any).args = args; (listLoader as any).args = args;
return listLoader.load( return listLoader.load(
getCompositePk(self.model.primaryKeys, this), getCompositePkValue(self.model.primaryKeys, this),
); );
}; };
} }
@ -3124,7 +3124,7 @@ class BaseModelSqlv2 {
await this.prepareNocoData(insertObj, true, cookie); await this.prepareNocoData(insertObj, true, cookie);
await Promise.all(preInsertOps.map((f) => f(this.dbDriver))); await this.runOps(preInsertOps.map((f) => f()));
let response; let response;
const query = this.dbDriver(this.tnPath).insert(insertObj); const query = this.dbDriver(this.tnPath).insert(insertObj);
@ -3206,7 +3206,7 @@ class BaseModelSqlv2 {
} }
rowId = this.extractCompositePK({ ai, ag, rowId, insertObj }); rowId = this.extractCompositePK({ ai, ag, rowId, insertObj });
await Promise.all(postInsertOps.map((f) => f(rowId))); await this.runOps(postInsertOps.map((f) => f(rowId)));
if (rowId !== null && rowId !== undefined) { if (rowId !== null && rowId !== undefined) {
response = await this.readRecord({ response = await this.readRecord({
@ -3254,7 +3254,20 @@ class BaseModelSqlv2 {
} }
} }
rowId = pkObj; rowId = pkObj;
} else if (
!ai &&
!ag &&
(force || this.model.primaryKeys?.length > 1 || this.isSnowflake)
) {
// handle if primary key is not ai or ag
const pkObj = {};
for (const pk of this.model.primaryKeys) {
const key = pk.title;
pkObj[key] = insertObj[pk.column_name] ?? null;
}
rowId = pkObj;
} }
return rowId; return rowId;
} }
@ -3267,8 +3280,8 @@ class BaseModelSqlv2 {
data: Record<string, any>; data: Record<string, any>;
insertObj: Record<string, any>; insertObj: Record<string, any>;
}) { }) {
const postInsertOps: ((rowId: any, trx?: any) => Promise<void>)[] = []; const postInsertOps: ((rowId: any) => Promise<string>)[] = [];
const preInsertOps: ((trx?: any) => Promise<void>)[] = []; const preInsertOps: (() => Promise<string>)[] = [];
for (const col of nestedCols) { for (const col of nestedCols) {
if (col.title in data) { if (col.title in data) {
const colOptions = await col.getColOptions<LinkToAnotherRecordColumn>(); const colOptions = await col.getColOptions<LinkToAnotherRecordColumn>();
@ -3302,15 +3315,16 @@ class BaseModelSqlv2 {
if (isBt) { if (isBt) {
// todo: unlink the ref record // todo: unlink the ref record
preInsertOps.push(async (trx: any = this.dbDriver) => { preInsertOps.push(async () => {
await trx(this.getTnPath(childModel.table_name)) return this.dbDriver(this.getTnPath(childModel.table_name))
.update({ .update({
[childCol.column_name]: null, [childCol.column_name]: null,
}) })
.where( .where(
childCol.column_name, childCol.column_name,
nestedData[childModel.primaryKey.title], nestedData[childModel.primaryKey.title],
); )
.toQuery();
}); });
if (typeof nestedData !== 'object') continue; if (typeof nestedData !== 'object') continue;
@ -3318,15 +3332,16 @@ class BaseModelSqlv2 {
const parentCol = await colOptions.getParentColumn(); const parentCol = await colOptions.getParentColumn();
insertObj[childCol.column_name] = nestedData?.[parentCol.title]; insertObj[childCol.column_name] = nestedData?.[parentCol.title];
} else { } else {
postInsertOps.push(async (rowId, trx: any = this.dbDriver) => { postInsertOps.push(async (rowId) => {
await trx(this.getTnPath(childModel.table_name)) return this.dbDriver(this.getTnPath(childModel.table_name))
.update({ .update({
[childCol.column_name]: rowId, [childCol.column_name]: rowId,
}) })
.where( .where(
childModel.primaryKey.column_name, childModel.primaryKey.column_name,
nestedData[childModel.primaryKey.title], nestedData[childModel.primaryKey.title],
); )
.toQuery();
}); });
} }
} }
@ -3338,32 +3353,22 @@ class BaseModelSqlv2 {
const childModel = await childCol.getModel(); const childModel = await childCol.getModel();
await childModel.getColumns(); await childModel.getColumns();
postInsertOps.push( postInsertOps.push(async (rowId) => {
async ( return this.dbDriver(this.getTnPath(childModel.table_name))
rowId,
// todo: use transaction type
trx: any = this.dbDriver,
) => {
await trx(this.getTnPath(childModel.table_name))
.update({ .update({
[childCol.column_name]: rowId, [childCol.column_name]: rowId,
}) })
.whereIn( .whereIn(
childModel.primaryKey.column_name, childModel.primaryKey.column_name,
nestedData?.map((r) => r[childModel.primaryKey.title]), nestedData?.map((r) => r[childModel.primaryKey.title]),
); )
}, .toQuery();
); });
} }
break; break;
case RelationTypes.MANY_TO_MANY: { case RelationTypes.MANY_TO_MANY: {
if (!Array.isArray(nestedData)) continue; if (!Array.isArray(nestedData)) continue;
postInsertOps.push( postInsertOps.push(async (rowId) => {
async (
rowId,
// todo: use transaction type
trx: any = this.dbDriver,
) => {
const parentModel = await colOptions const parentModel = await colOptions
.getParentColumn() .getParentColumn()
.then((c) => c.getModel()); .then((c) => c.getModel());
@ -3376,9 +3381,10 @@ class BaseModelSqlv2 {
[parentMMCol.column_name]: r[parentModel.primaryKey.title], [parentMMCol.column_name]: r[parentModel.primaryKey.title],
[childMMCol.column_name]: rowId, [childMMCol.column_name]: rowId,
})); }));
await trx(this.getTnPath(mmModel.table_name)).insert(rows); return this.dbDriver(this.getTnPath(mmModel.table_name))
}, .insert(rows)
); .toQuery();
});
} }
} }
} }
@ -3410,8 +3416,8 @@ class BaseModelSqlv2 {
try { try {
// TODO: ag column handling for raw bulk insert // TODO: ag column handling for raw bulk insert
const insertDatas = raw ? datas : []; const insertDatas = raw ? datas : [];
let postInsertOps: ((rowId: any, trx?: any) => Promise<void>)[] = []; let postInsertOps: ((rowId: any) => Promise<string>)[] = [];
let preInsertOps: ((trx?: any) => Promise<void>)[] = []; let preInsertOps: (() => Promise<string>)[] = [];
let aiPkCol: Column; let aiPkCol: Column;
let agPkCol: Column; let agPkCol: Column;
@ -3596,7 +3602,10 @@ class BaseModelSqlv2 {
} }
} }
await Promise.all(preInsertOps.map((f) => f(trx))); await this.runOps(
preInsertOps.map((f) => f()),
trx,
);
let responses; let responses;
@ -3661,7 +3670,10 @@ class BaseModelSqlv2 {
}); });
} }
await Promise.all(postInsertOps.map((f) => f(rowId, trx))); await this.runOps(
postInsertOps.map((f) => f(rowId)),
trx,
);
} }
await trx.commit(); await trx.commit();
@ -3728,7 +3740,10 @@ class BaseModelSqlv2 {
const pkAndData: { pk: any; data: any }[] = []; const pkAndData: { pk: any; data: any }[] = [];
const readChunkSize = 100; const readChunkSize = 100;
for (const [i, d] of updateDatas.entries()) { for (const [i, d] of updateDatas.entries()) {
const pkValues = this._extractPksValues(d); const pkValues = getCompositePkValue(
this.model.primaryKeys,
this._extractPksValues(d),
);
if (!pkValues) { if (!pkValues) {
// throw or skip if no pk provided // throw or skip if no pk provided
if (throwExceptionIfNotExist) { if (throwExceptionIfNotExist) {
@ -3954,7 +3969,10 @@ class BaseModelSqlv2 {
const pkAndData: { pk: any; data: any }[] = []; const pkAndData: { pk: any; data: any }[] = [];
const readChunkSize = 100; const readChunkSize = 100;
for (const [i, d] of deleteIds.entries()) { for (const [i, d] of deleteIds.entries()) {
const pkValues = this._extractPksValues(d); const pkValues = getCompositePkValue(
this.model.primaryKeys,
this._extractPksValues(d),
);
if (!pkValues) { if (!pkValues) {
// throw or skip if no pk provided // throw or skip if no pk provided
if (throwExceptionIfNotExist) { if (throwExceptionIfNotExist) {
@ -5248,6 +5266,13 @@ class BaseModelSqlv2 {
return data; return data;
} }
async runOps(ops: Promise<string>[], trx = this.dbDriver) {
const queries = await Promise.all(ops);
for (const query of queries) {
await trx.raw(query);
}
}
protected async substituteColumnIdsWithColumnTitles( protected async substituteColumnIdsWithColumnTitles(
data: Record<string, any>[], data: Record<string, any>[],
dependencyColumns?: Column[], dependencyColumns?: Column[],
@ -6863,8 +6888,9 @@ export function _wherePk(primaryKeys: Column[], id: unknown | unknown[]) {
return where; return where;
} }
function getCompositePk(primaryKeys: Column[], row) { export function getCompositePkValue(primaryKeys: Column[], row) {
return primaryKeys.map((c) => row[c.title]).join('___'); if (typeof row !== 'object') return row;
return primaryKeys.map((c) => row[c.title] ?? row[c.column_name]).join('___');
} }
export function haveFormulaColumn(columns: Column[]) { export function haveFormulaColumn(columns: Column[]) {

1
packages/nocodb/src/db/sql-mgr/code/models/xc/ModelXcMetaSnowflake.ts

@ -500,6 +500,7 @@ class ModelXcMetaSnowflake extends BaseModelXcMeta {
case 'smgr': case 'smgr':
return dt; return dt;
case 'text': case 'text':
if (col.dtxp < 1024) return 'string';
return 'text'; return 'text';
case 'tid': case 'tid':
return dt; return dt;

7
packages/nocodb/src/helpers/getUniqueName.ts

@ -1,6 +1,9 @@
import type Column from '~/models/Column'; import type Column from '~/models/Column';
export function getUniqueColumnName(columns: Column[], initialName = 'field') { export function getUniqueColumnName(
columns: Partial<Column>[],
initialName = 'field',
) {
let c = 0; let c = 0;
while ( while (
@ -13,7 +16,7 @@ export function getUniqueColumnName(columns: Column[], initialName = 'field') {
} }
export function getUniqueColumnAliasName( export function getUniqueColumnAliasName(
columns: Column[], columns: Partial<Column>[],
initialName = 'field', initialName = 'field',
) { ) {
let c = 0; let c = 0;

15
packages/nocodb/src/helpers/populateMeta.ts

@ -2,7 +2,7 @@ import { ModelTypes, UITypes, ViewTypes } from 'nocodb-sdk';
import { isVirtualCol, RelationTypes } from 'nocodb-sdk'; import { isVirtualCol, RelationTypes } from 'nocodb-sdk';
import { pluralize, singularize } from 'inflection'; import { pluralize, singularize } from 'inflection';
import { isLinksOrLTAR } from 'nocodb-sdk'; import { isLinksOrLTAR } from 'nocodb-sdk';
import { getUniqueColumnAliasName } from './getUniqueName'; import { getUniqueColumnAliasName, getUniqueColumnName } from './getUniqueName';
import type { RollupColumn } from '~/models'; import type { RollupColumn } from '~/models';
import type LinkToAnotherRecordColumn from '~/models/LinkToAnotherRecordColumn'; import type LinkToAnotherRecordColumn from '~/models/LinkToAnotherRecordColumn';
import type Source from '~/models/Source'; import type Source from '~/models/Source';
@ -93,7 +93,11 @@ export async function extractAndGenerateManyToManyRelations(
if ( if (
belongsToCols?.length === 2 && belongsToCols?.length === 2 &&
normalColumns.length < 5 && normalColumns.length < 5 &&
assocModel.primaryKeys.length === 2 assocModel.primaryKeys.length === 2 &&
// check if both belongsToCol target primary keys
assocModel.primaryKeys.every((pk) =>
belongsToCols.some((c) => c.colOptions?.fk_child_column_id === pk.id),
)
) { ) {
const modelA = await belongsToCols[0].colOptions.getRelatedTable(); const modelA = await belongsToCols[0].colOptions.getRelatedTable();
const modelB = await belongsToCols[1].colOptions.getRelatedTable(); const modelB = await belongsToCols[1].colOptions.getRelatedTable();
@ -383,8 +387,11 @@ export async function populateMeta(
base_id: base.id, base_id: base.id,
db_alias: source.id, db_alias: source.id,
fk_model_id: models2[table.tn].id, fk_model_id: models2[table.tn].id,
cn: column.cn, cn: getUniqueColumnName(models2[table.tn].columns, column.cn),
title: column.title, title: getUniqueColumnAliasName(
models2[table.tn].columns,
column.title,
),
uidt: column.uidt, uidt: column.uidt,
type: column.hm ? 'hm' : column.mm ? 'mm' : 'bt', type: column.hm ? 'hm' : column.mm ? 'mm' : 'bt',
// column_id, // column_id,

Loading…
Cancel
Save