mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 08:56:46 -04:00
fix: enum quotes (#45023)
## TL;DR
fixes enum create/update failures when names contain quotes
(also added a smol e2e)
## Ex:
<table>
<tr>
<td><strong>Before</strong></td>
<td><strong>After</strong></td>
</tr>
<tr>
<td>
<img width="424" height="178" alt="Before"
src="https://github.com/user-attachments/assets/d1815f4e-3879-4f8d-8d24-40d2c1f5563d"
/>
</td>
<td>
<img width="233" height="75" alt="After fix"
src="https://github.com/user-attachments/assets/f3f9b53c-b234-4e18-9b2d-db97ca4713d5"
/>
</td>
</tr>
</table>
## ref:
- closes https://github.com/supabase/supabase/issues/45022
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Fixed enumerated type description handling to preserve special
characters (quotes and apostrophes) without unintended escaping.
* **Tests**
* Extended enumerated types test coverage to include creation, updates,
and deletion of types with special characters in names and descriptions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
+1
-1
@@ -114,7 +114,7 @@ const CreateEnumeratedTypeSidePanel = ({
|
||||
connectionString: project.connectionString,
|
||||
schema,
|
||||
name: data.name,
|
||||
description: data.description?.replaceAll("'", "''"),
|
||||
description: data.description,
|
||||
values: data.values.filter((x) => x.value.length > 0).map((x) => x.value.trim()),
|
||||
})
|
||||
}
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ const EditEnumeratedTypeSidePanel = ({
|
||||
isNew: x.isNew,
|
||||
})),
|
||||
...(data.description !== selectedEnumeratedType.comment
|
||||
? { description: data.description?.replaceAll("'", "''") }
|
||||
? { description: data.description }
|
||||
: {}),
|
||||
}
|
||||
|
||||
|
||||
@@ -1051,12 +1051,31 @@ test.describe('Database Enumerated Types', () => {
|
||||
const databaseEnumValue1Name = 'pw_database_value1'
|
||||
const databaseEnumValue2Name = 'pw_database_value2'
|
||||
const databaseEnumValue3Name = 'pw_database_value3'
|
||||
const quotedEnumName = 'pw_database_enum_"quoted"'
|
||||
const updatedQuotedEnumName = 'pw_database_enum_"updated"'
|
||||
const quotedEnumValue1Name = 'pw_database_value_"double"'
|
||||
const quotedEnumValue2Name = `pw_database_value's_apostrophe`
|
||||
const quotedEnumValue3Name = `pw_database_value_"combo"'s`
|
||||
const quotedEnumTypes = [quotedEnumName, updatedQuotedEnumName].map(
|
||||
(name) => `public."${name.replaceAll('"', '""')}"`
|
||||
)
|
||||
|
||||
await using _ = await withSetupCleanup(
|
||||
async () => {
|
||||
for (const typeName of quotedEnumTypes) {
|
||||
await query(`drop type if exists ${typeName};`)
|
||||
}
|
||||
},
|
||||
async () => {
|
||||
for (const typeName of quotedEnumTypes) {
|
||||
await query(`drop type if exists ${typeName};`)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const wait = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=schemas')
|
||||
await page.goto(toUrl(`/project/${env.PROJECT_REF}/database/types?schema=public`))
|
||||
|
||||
// Wait for database roles list to be populated
|
||||
await wait
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// if enum exists, delete it.
|
||||
if ((await page.getByRole('cell', { name: databaseEnumName, exact: true }).count()) > 0) {
|
||||
@@ -1105,6 +1124,43 @@ test.describe('Database Enumerated Types', () => {
|
||||
await expect(page.getByText(`Successfully deleted type "${databaseEnumName}"`)).toBeVisible({
|
||||
timeout: 50000,
|
||||
})
|
||||
|
||||
await page.getByRole('button', { name: 'Create type' }).click()
|
||||
await page.getByRole('textbox', { name: 'Name' }).fill(quotedEnumName)
|
||||
await page.locator('input[name="values.0.value"]').fill(quotedEnumValue1Name)
|
||||
await page.getByRole('button', { name: 'Add value' }).click()
|
||||
await page.locator('input[name="values.1.value"]').fill(quotedEnumValue2Name)
|
||||
const quotedEnumCreateWait = createApiResponseWaiter(page, 'pg-meta', ref, 'types')
|
||||
await page.getByRole('button', { name: 'Create type' }).click()
|
||||
|
||||
await quotedEnumCreateWait
|
||||
const quotedEnumRow = page.getByRole('row', { name: `${quotedEnumName}` })
|
||||
await expect(quotedEnumRow).toContainText(quotedEnumName)
|
||||
await expect(quotedEnumRow).toContainText(
|
||||
`${quotedEnumValue1Name}, ${quotedEnumValue2Name}`
|
||||
)
|
||||
|
||||
await quotedEnumRow.getByRole('button').click()
|
||||
await page.getByRole('menuitem', { name: 'Update type' }).click()
|
||||
await page.getByRole('textbox', { name: 'Name' }).fill(updatedQuotedEnumName)
|
||||
await page.getByRole('button', { name: 'Add value' }).click()
|
||||
await page.locator('input[name="values.2.updatedValue"]').fill(quotedEnumValue3Name)
|
||||
await page.getByRole('button', { name: 'Update type' }).click()
|
||||
const updatedQuotedEnumRow = page.getByRole('row', { name: `${updatedQuotedEnumName}` })
|
||||
await expect(updatedQuotedEnumRow).toContainText(updatedQuotedEnumName)
|
||||
await expect(updatedQuotedEnumRow).toContainText(
|
||||
`${quotedEnumValue1Name}, ${quotedEnumValue2Name}, ${quotedEnumValue3Name}`
|
||||
)
|
||||
|
||||
await updatedQuotedEnumRow.getByRole('button').click()
|
||||
await page.getByRole('menuitem', { name: 'Delete type' }).click()
|
||||
await page.getByRole('heading', { name: 'Confirm to delete enumerated' }).click()
|
||||
await page.getByRole('button', { name: 'Confirm delete' }).click()
|
||||
await expect(
|
||||
page.getByText(`Successfully deleted type "${updatedQuotedEnumName}"`)
|
||||
).toBeVisible({
|
||||
timeout: 50000,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ident, literal } from '../../../pg-format'
|
||||
import { wrapWithTransaction } from '../../../query'
|
||||
|
||||
export const getCreateEnumeratedTypeSQL = ({
|
||||
@@ -11,17 +12,15 @@ export const getCreateEnumeratedTypeSQL = ({
|
||||
values: string[]
|
||||
description?: string
|
||||
}) => {
|
||||
const createSql = `create type "${schema}"."${name}" as enum (${values
|
||||
.map((x) => `'${x}'`)
|
||||
.join(', ')});`
|
||||
const typeSql = `${ident(schema)}.${ident(name)}`
|
||||
const createSql = `create type ${typeSql} as enum (${values.map(literal).join(', ')});`
|
||||
const commentSql =
|
||||
description !== undefined ? `comment on type "${schema}"."${name}" is '${description}';` : ''
|
||||
const sql = wrapWithTransaction(`${createSql} ${commentSql}`)
|
||||
return sql
|
||||
description !== undefined ? `comment on type ${typeSql} is ${literal(description)};` : ''
|
||||
return wrapWithTransaction(`${createSql} ${commentSql}`)
|
||||
}
|
||||
|
||||
export const getDeleteEnumeratedTypeSQL = ({ schema, name }: { schema: string; name: string }) => {
|
||||
return `drop type if exists ${schema}."${name}"`
|
||||
return `drop type if exists ${ident(schema)}.${ident(name)}`
|
||||
}
|
||||
|
||||
export const getUpdateEnumeratedTypeSQL = ({
|
||||
@@ -36,9 +35,14 @@ export const getUpdateEnumeratedTypeSQL = ({
|
||||
values?: { original: string; updated: string; isNew: boolean }[]
|
||||
}) => {
|
||||
const statements: string[] = []
|
||||
const typeSql = `${ident(schema)}.${ident(name.updated)}`
|
||||
|
||||
if (name.original !== name.updated) {
|
||||
statements.push(`alter type "${schema}"."${name.original}" rename to "${name.updated}";`)
|
||||
statements.push(
|
||||
`alter type ${ident(schema)}.${ident(name.original)} rename to ${ident(name.updated)};`
|
||||
)
|
||||
}
|
||||
|
||||
if (values.length > 0) {
|
||||
values.forEach((x, idx) => {
|
||||
if (x.isNew) {
|
||||
@@ -46,26 +50,23 @@ export const getUpdateEnumeratedTypeSQL = ({
|
||||
// Consider if any new enums were added before any existing enums
|
||||
const firstExistingEnumValue = values.find((x) => !x.isNew)
|
||||
statements.push(
|
||||
`alter type "${schema}"."${name.updated}" add value '${x.updated}' before '${firstExistingEnumValue?.original}';`
|
||||
`alter type ${typeSql} add value ${literal(x.updated)} before ${literal(firstExistingEnumValue?.original)};`
|
||||
)
|
||||
} else {
|
||||
statements.push(
|
||||
`alter type "${schema}"."${name.updated}" add value '${x.updated}' after '${
|
||||
values[idx - 1].updated
|
||||
}';`
|
||||
`alter type ${typeSql} add value ${literal(x.updated)} after ${literal(values[idx - 1].updated)};`
|
||||
)
|
||||
}
|
||||
} else if (x.original !== x.updated) {
|
||||
statements.push(
|
||||
`alter type "${schema}"."${name.updated}" rename value '${x.original}' to '${x.updated}';`
|
||||
`alter type ${typeSql} rename value ${literal(x.original)} to ${literal(x.updated)};`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (description !== undefined) {
|
||||
statements.push(`comment on type "${schema}"."${name.updated}" is '${description}';`)
|
||||
statements.push(`comment on type ${typeSql} is ${literal(description)};`)
|
||||
}
|
||||
|
||||
const sql = wrapWithTransaction(statements.join(' '))
|
||||
return sql
|
||||
return wrapWithTransaction(statements.join(' '))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user