diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx index 1177f73978e..63407a653e7 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx @@ -1163,11 +1163,49 @@ export async function insertTableRows({ const batchedPromises = chunk(tasks, 10) for (const batchedPromise of batchedPromises) { const res = await Promise.allSettled(batchedPromise.map((batch) => batch())) - const hasFailedBatch = find(res, { status: 'rejected' }) - if (hasFailedBatch) break + const failedBatch = res.find((result) => result.status === 'rejected') + if (failedBatch?.status === 'rejected') { + if (insertError === undefined) insertError = failedBatch.reason + break + } onProgressUpdate(insertProgress * 100) } - return { error: insertError } + + if (insertError !== undefined) { + return { error: insertError } + } + + const sequenceColumns = (table.columns ?? []).filter( + (column) => + column.is_identity || + (typeof column.default_value === 'string' && column.default_value.includes('nextval(')) + ) + + if (sequenceColumns.length === 0) { + return { error: insertError } + } + + const updateSequenceSQL = sequenceColumns + .map((column) => + getUpdateIdentitySequenceSQL({ + schema: table.schema, + table: table.name, + column: column.name, + }) + ) + .join(';\n') + + try { + await executeSql({ + projectRef, + connectionString, + sql: updateSequenceSQL, + queryKey: ['sequences', 'update-batch'], + }) + return { error: insertError } + } catch (error) { + return { error } + } } const updateForeignKeys = async ({ diff --git a/e2e/studio/features/table-editor.spec.ts b/e2e/studio/features/table-editor.spec.ts index b3047dd9766..71c7af36492 100644 --- a/e2e/studio/features/table-editor.spec.ts +++ b/e2e/studio/features/table-editor.spec.ts @@ -1369,6 +1369,85 @@ testRunner('table editor', () => { .toBe('230') }) + test('pasted CSV text syncs custom owned sequences before the next insert', async ({ + page, + ref, + }) => { + const tableName = 'pw_table_paste_sequence_sync' + const sequenceName = 'pw_table_paste_import_owned_seq' + + await using _ = await withSetupCleanup( + async () => { + await query(`drop table if exists public.${tableName} cascade;`) + await query(`drop sequence if exists public.${sequenceName};`) + await query(`create sequence public.${sequenceName};`) + await query(`create table public.${tableName} ( + id bigint primary key default nextval('public.${sequenceName}'), + name text + );`) + await query(`alter sequence public.${sequenceName} owned by public.${tableName}.id;`) + }, + async () => { + await query(`drop table if exists public.${tableName} cascade;`) + await query(`drop sequence if exists public.${sequenceName};`) + } + ) + + const waitForTable = waitForTableToLoad(page, ref) + await page.goto(toUrl(`/project/${ref}/editor?schema=public`)) + await waitForTable + await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click() + await page.waitForURL(/\/editor\/\d+\?schema=public$/) + + const csvFilePath = path.join(import.meta.dirname, 'files', 'table-editor-import-sequence.csv') + const csvText = fs.readFileSync(csvFilePath, 'utf-8') + await page.getByRole('button', { name: 'Import data from CSV' }).click() + await page.getByRole('tab', { name: 'Paste text' }).click() + await page.getByRole('textbox').fill(csvText) + await expect(page.getByText('A total of 3 rows will be')).toBeVisible() + + const waitForCsvInsert = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=', { + method: 'POST', + }) + await page.getByRole('button', { name: 'Import data' }).click() + await waitForCsvInsert + await waitForGridDataToLoad(page, ref) + await expect(page.getByText('3 records')).toBeVisible() + + await expect + .poll(async () => { + const [{ state }] = await query<{ state: string }>(` + select format( + '%s|%s|%s', + (select coalesce(max(id), 0) from public.${tableName}), + last_value, + is_called + ) as state + from public.${sequenceName}; + `) + return state + }) + .toBe('229|229|t') + + await page.getByTestId('table-editor-insert-new-row').click() + await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click() + await page.getByTestId('name-input').fill('Dave') + const insertPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { + method: 'POST', + }) + await page.getByTestId('action-bar-save-row').click() + await insertPromise + + await expect + .poll(async () => { + const [{ id }] = await query<{ id: string }>( + `select id::text as id from public.${tableName} where name = 'Dave'` + ) + return id + }) + .toBe('230') + }) + test('row insert via side panel saves immediately', async ({ page, ref }) => { const tableName = 'pw_table_row_insert' const columnName = 'name'