fix: csv seq sync (#45076)

## TL;DR
fixes csv imports with explicit id values leaving auto generated primary
keys out of sync
by resolving the primary key sequence correctly after import

## ex:

| Before | After |
| --- | --- |
| <img width="378" height="201" alt="Before: stale sequence after CSV
import"
src="https://github.com/user-attachments/assets/34c827f5-c69b-4c05-aa67-9fb6fd65a040"
/> | <img width="435" height="196" alt="After: sequence synced after CSV
import"
src="https://github.com/user-attachments/assets/bb84f286-1b3e-44a5-965b-5f2faa3f7622"
/> |

## ref:
- closes https://github.com/supabase/supabase/issues/45073


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
* Fixed identity sequence synchronization during CSV imports. After
spreadsheet data is inserted, the system now properly updates sequence
values, ensuring subsequent rows receive correct identifiers.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Vaibhav
2026-04-21 19:29:20 +05:30
committed by GitHub
parent 7e0c060e6e
commit 2e3ddc3a52
4 changed files with 121 additions and 1 deletions
@@ -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 })
},
})
@@ -0,0 +1,4 @@
id,name
227,Alice
228,Bob
229,Carol
1 id name
2 227 Alice
3 228 Bob
4 229 Carol
+74
View File
@@ -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'
@@ -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 = ({