diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx index ebe48d6929c..1177f73978e 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx @@ -1072,6 +1072,40 @@ export async function insertRowsViaSpreadsheet({ console.log( `Total time taken for importing spreadsheet: ${(t2.getTime() - t1.getTime()) / 1000} seconds` ) + if (insertError === undefined) { + const sequenceColumns = (table.columns ?? []).filter( + (column) => + column.is_identity || + (typeof column.default_value === 'string' && + column.default_value.includes('nextval(')) + ) + + if (sequenceColumns.length === 0) { + resolve({ error: insertError }) + return + } + + const updateSequenceSQL = sequenceColumns + .map((column) => + getUpdateIdentitySequenceSQL({ + schema: table.schema, + table: table.name, + column: column.name, + }) + ) + .join(';\n') + + executeSql({ + projectRef, + connectionString, + sql: updateSequenceSQL, + queryKey: ['sequences', 'update-batch'], + }) + .then(() => resolve({ error: insertError })) + .catch((error) => resolve({ error })) + return + } + resolve({ error: insertError }) }, }) diff --git a/e2e/studio/features/files/table-editor-import-sequence.csv b/e2e/studio/features/files/table-editor-import-sequence.csv new file mode 100644 index 00000000000..53a575b6a52 --- /dev/null +++ b/e2e/studio/features/files/table-editor-import-sequence.csv @@ -0,0 +1,4 @@ +id,name +227,Alice +228,Bob +229,Carol diff --git a/e2e/studio/features/table-editor.spec.ts b/e2e/studio/features/table-editor.spec.ts index 5d061129a48..b3047dd9766 100644 --- a/e2e/studio/features/table-editor.spec.ts +++ b/e2e/studio/features/table-editor.spec.ts @@ -1295,6 +1295,80 @@ testRunner('table editor', () => { await expect(page.getByRole('gridcell', { name: 'value 1' })).toBeVisible() }) + test('CSV import syncs custom owned sequences before the next insert', async ({ page, ref }) => { + const tableName = 'pw_table_csv_sequence_sync' + const sequenceName = 'pw_table_csv_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};`) + } + ) + + await page.goto(toUrl(`/project/${ref}/editor?schema=public`)) + await waitForTableToLoad(page, ref) + 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') + await page.getByRole('button', { name: 'Import data from CSV' }).click() + await page.getByRole('tab', { name: 'Upload CSV' }).click() + await page.setInputFiles('input[type="file"]', csvFilePath) + 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' diff --git a/packages/pg-meta/src/sql/studio/table-editor/identity.ts b/packages/pg-meta/src/sql/studio/table-editor/identity.ts index cf5c6601825..b3daffbec1e 100644 --- a/packages/pg-meta/src/sql/studio/table-editor/identity.ts +++ b/packages/pg-meta/src/sql/studio/table-editor/identity.ts @@ -9,7 +9,15 @@ export const getUpdateIdentitySequenceSQL = ({ table: string column: string }): SafeSqlFragment => { - return safeSql`SELECT setval(${literal(`${ident(schema)}.${ident(`${table}_${column}_seq`)}`)}::regclass, (SELECT COALESCE(MAX(${ident(column)}), 1) FROM ${ident(schema)}.${ident(table)}))` + return safeSql`WITH sequence_reference AS ( + SELECT pg_get_serial_sequence(${literal(`${schema}.${table}`)}, ${literal(column)}) AS sequence_name +) +SELECT setval( + sequence_reference.sequence_name, + COALESCE((SELECT MAX(${ident(column)}) FROM ${ident(schema)}.${ident(table)}), 1) +) +FROM sequence_reference +WHERE sequence_reference.sequence_name IS NOT NULL` } export const getDuplicateIdentitySequenceSQL = ({