mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 08:56:46 -04:00
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:
+34
@@ -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
|
||||
|
@@ -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 = ({
|
||||
|
||||
Reference in New Issue
Block a user