Fix table creation transaction (#43909)

Fixes #42089
Supersedes #43352 and
https://github.com/supabase/postgres-meta/pull/1049

## Problem

When creating a table in the Dashboard, if a column-specific error
occurs (e.g., invalid enum value or broken foreign key), the Dashboard
displays an error toast, but the side panel remains open. In the
background, the table is actually created (partially), leading to a
_42P07: relation already exists_ error if the user tries to click _Save_
again.

## Solution

We actually had nested transactions which is not supported by Postgres.

- [x] Allow generating SQL without transactions from `@supabase/pg-meta`
- [x] Fix the table creation
- [x] Fix some accessibility issues
- [x] Add tests

## How to test

1. Click _New table_ in the Table editor
2. Add a column with an _int8_ type and set its default value to
`bazinga`
3. Click _Save_

You should see an error.
4. Fix the default value by setting it to `10`
5. Click _Save_

Results:
- You should see a success message about the table creation
- The table should have the column with the correct default value
This commit is contained in:
Gildas Garcia authored and GitHub committed 2026-03-19 08:06:54 +01:00
1 parent 8499d30e98
commit df7e98bae6
7 files changed
+103 -36

No files matched your search

+1 -1
View File
@@ -258,7 +258,7 @@ test.describe('Database', () => {
// create a new table
await page.getByRole('button', { name: 'New table' }).click()
await page.getByLabel('Name').fill(databaseTableNameNew)
await page.getByLabel('Name', { exact: true }).fill(databaseTableNameNew)
const createTableWait = createApiResponseWaiter(
page,
'pg-meta',
+53 -9
View File
@@ -90,17 +90,16 @@ testRunner('table editor', () => {
.getByRole('button')
.nth(2)
.click()
const schemaPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-definition-')
await page.getByRole('menuitem', { name: 'Copy table schema' }).click()
await schemaPromise // wait for endpoint to generate schema
await page.waitForTimeout(500)
const copiedSchemaResult = await page.evaluate(() => navigator.clipboard.readText())
expect(copiedSchemaResult).toBe(`create table public.pw_table_actions (
await expect(async () => {
const copiedSchemaResult = await page.evaluate(() => navigator.clipboard.readText())
expect(copiedSchemaResult).toBe(`create table public.pw_table_actions (
id bigint generated by default as identity not null,
created_at timestamp with time zone null default now(),
pw_column text null,
constraint pw_table_actions_pkey primary key (id)
) TABLESPACE pg_default;`)
}).toPass({ timeout: 2000 })
// duplicates table
await page
@@ -213,7 +212,7 @@ testRunner('table editor', () => {
await page.getByText('Is Nullable').click()
await page.getByTestId('created_at-extra-options').click()
await page.getByRole('button', { name: 'Add column' }).click()
await page.getByRole('textbox', { name: 'column_name' }).fill(columnNameEnum)
await page.getByLabel('Column name').nth(2).fill(columnNameEnum)
await page.getByRole('combobox').filter({ hasText: 'Choose a column type...' }).click()
await page.getByPlaceholder('Search types...').fill(enum_name)
// wait for response, then click
@@ -302,7 +301,7 @@ testRunner('table editor', () => {
.click()
await page.getByRole('menuitem', { name: 'Edit table' }).click()
await page.getByTestId('table-name-input').fill(tableNameUpdated)
await page.getByRole('textbox', { name: 'pw_column' }).fill(columnNameUpdated)
await page.getByLabel('Column name').nth(2).fill(columnNameUpdated)
const updateTablePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=column-update', {
method: 'POST',
})
@@ -814,7 +813,7 @@ testRunner('table editor', () => {
// Add boolean column
await page.getByRole('button', { name: 'Add column' }).click()
await page.getByRole('textbox', { name: 'column_name' }).fill(boolColName)
await page.getByLabel('Column name').nth(3).fill(boolColName)
await page.getByText('Choose a column type...').click()
await page.getByPlaceholder('Search types...').fill('bool')
await page.getByRole('option', { name: 'bool' }).first().click()
@@ -928,7 +927,7 @@ testRunner('table editor', () => {
// Add nullable boolean column
await page.getByRole('button', { name: 'Add column' }).click()
await page.getByRole('textbox', { name: 'column_name' }).fill(boolColName)
await page.getByLabel('Column name').nth(3).fill(boolColName)
await page.getByText('Choose a column type...').click()
await page.getByPlaceholder('Search types...').fill('bool')
await page.getByRole('option', { name: 'bool' }).first().click()
@@ -1214,4 +1213,49 @@ testRunner('table editor', () => {
).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'drag drop value 1' })).toBeVisible()
})
test('create a table in a single transaction', async ({ page, ref }) => {
const tableName = 'pw_table_create_transaction'
await using _ = await withSetupCleanup(
async () => {
// Nothing
},
async () => {
await dropTable(tableName)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByRole('button', { name: 'New table' }).click()
await page.getByLabel('Name', { exact: true }).fill(tableName)
await page.getByRole('button', { name: 'Add column' }).click()
await page.getByLabel('Column name').nth(2).fill('pw_column')
await page.getByRole('combobox').filter({ hasText: 'Choose a column type...' }).click()
await page.getByRole('option').filter({ hasText: 'int8' }).click()
await page.getByLabel('Column default value').nth(2).fill('invalid')
await page.getByRole('button', { name: 'Save' }).click()
await expect(page.getByText('invalid input syntax')).toBeVisible()
await page.getByLabel('Column default value').nth(2).fill('10')
await page.getByRole('button', { name: 'Save' }).click()
await expect(page.getByText(`Table ${tableName} is good to go!`)).toBeVisible()
await expect(page.getByRole('button', { name: `View ${tableName}`, exact: true })).toBeVisible()
// copies table schema to clipboard when copy schema option is clicked
await page
.getByRole('button', { name: `View ${tableName}`, exact: true })
.getByRole('button')
.nth(2)
.click()
await page.getByRole('menuitem', { name: 'Copy table schema' }).click()
await expect(async () => {
const copiedSchemaResult = await page.evaluate(() => navigator.clipboard.readText())
expect(copiedSchemaResult).toBe(`create table public.${tableName} (
id bigint generated by default as identity not null,
created_at timestamp with time zone not null default now(),
pw_column bigint null default '10'::bigint,
constraint ${tableName}_pkey primary key (id)
) TABLESPACE pg_default;`)
}).toPass({ timeout: 2000 })
})
})