mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Studio SQL export emits valid PostgreSQL literals (#44025)
## Summary - fix the Studio SQL row formatter to emit raw boolean and numeric literals instead of quoted strings - serialize text array members as SQL string literals inside `ARRAY[...]` - escape fallback string formats outside `text`/`varchar`, and add regression coverage for that path Closes #44024 ## Test plan - [x] Ran a direct `tsx` smoke against `formatTableRowsToSQL()` for the `storage.buckets` case and confirmed it now emits `true`, `false`, `10485760`, and `ARRAY['image/*']` - [x] Ran focused formatter smokes for JSON escaping, text arrays, and fallback string formats like `citext` - [x] `pnpm --filter studio test -- TableEntity.utils.test.ts` Note: the targeted Vitest run is still blocked in this environment before the test executes (`localStorage.getItem is not a function`). --------- Co-authored-by: Alaister Young <alaister@users.noreply.github.com>
This commit is contained in:
1 parent
527c342837
commit
b93733c811
3 files changed
+59
-7
No files matched your search
@@ -24,7 +24,7 @@ describe('TableEntity.utils: formatTableRowsToSQL', () => {
|
||||
]
|
||||
|
||||
const result = formatTableRowsToSQL(table, rows)
|
||||
const expected = `INSERT INTO "public"."people" ("id", "name") VALUES ('1', 'Person 1'), ('2', 'Person 2'), ('3', 'Person 3');`
|
||||
const expected = `INSERT INTO "public"."people" ("id", "name") VALUES (1, 'Person 1'), (2, 'Person 2'), (3, 'Person 3');`
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('TableEntity.utils: formatTableRowsToSQL', () => {
|
||||
]
|
||||
|
||||
const result = formatTableRowsToSQL(table, rows)
|
||||
const expected = `INSERT INTO "public"."people" ("id", "name") VALUES ('1', 'Person 1'), ('2', null), ('3', 'Person 3');`
|
||||
const expected = `INSERT INTO "public"."people" ("id", "name") VALUES (1, 'Person 1'), (2, null), (3, 'Person 3');`
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
|
||||
@@ -84,7 +84,55 @@ describe('TableEntity.utils: formatTableRowsToSQL', () => {
|
||||
},
|
||||
]
|
||||
const result = formatTableRowsToSQL(table, rows)
|
||||
const expected = `INSERT INTO "public"."demo" ("id", "name", "tags", "metadata") VALUES ('2', 'Person 1', ARRAY["tag-a","tag-c"], '{"version": 1}'), ('3', 'ONeil', ARRAY["tag-a"], '{"version": 1, "name": "O''Neil"}');`
|
||||
const expected = `INSERT INTO "public"."demo" ("id", "name", "tags", "metadata") VALUES (2, 'Person 1', ARRAY['tag-a','tag-c'], '{"version": 1}'), (3, 'ONeil', ARRAY['tag-a'], '{"version": 1, "name": "O''Neil"}');`
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
|
||||
it('should emit valid Postgres literals for booleans, numbers and text arrays', () => {
|
||||
const table: SupaTable = {
|
||||
id: 1,
|
||||
type: ENTITY_TYPE.TABLE,
|
||||
columns: [
|
||||
{ name: 'id', dataType: 'text', format: 'text', position: 0 },
|
||||
{ name: 'public', dataType: 'bool', format: 'bool', position: 1 },
|
||||
{ name: 'avif_autodetection', dataType: 'bool', format: 'bool', position: 2 },
|
||||
{ name: 'file_size_limit', dataType: 'int8', format: 'int8', position: 3 },
|
||||
{ name: 'allowed_mime_types', dataType: 'ARRAY', format: '_text', position: 4 },
|
||||
],
|
||||
name: 'buckets',
|
||||
schema: 'storage',
|
||||
comment: undefined,
|
||||
estimateRowCount: 1,
|
||||
}
|
||||
const rows = [
|
||||
{
|
||||
id: 'emails',
|
||||
public: true,
|
||||
avif_autodetection: false,
|
||||
file_size_limit: 10485760,
|
||||
allowed_mime_types: ['image/*', "image/o'neil"],
|
||||
},
|
||||
]
|
||||
|
||||
const result = formatTableRowsToSQL(table, rows)
|
||||
const expected = `INSERT INTO "storage"."buckets" ("id", "public", "avif_autodetection", "file_size_limit", "allowed_mime_types") VALUES ('emails', true, false, 10485760, ARRAY['image/*','image/o''neil']);`
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
|
||||
it('should escape fallback string formats outside text and varchar', () => {
|
||||
const table: SupaTable = {
|
||||
id: 1,
|
||||
type: ENTITY_TYPE.TABLE,
|
||||
columns: [{ name: 'email', dataType: 'USER-DEFINED', format: 'citext', position: 0 }],
|
||||
name: 'users',
|
||||
schema: 'public',
|
||||
comment: undefined,
|
||||
estimateRowCount: 1,
|
||||
}
|
||||
const rows = [{ email: "o'neil@example.com" }]
|
||||
|
||||
const result = formatTableRowsToSQL(table, rows)
|
||||
const expected = `INSERT INTO "public"."users" ("email") VALUES ('o''neil@example.com');`
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
|
||||
@@ -124,7 +172,7 @@ describe('TableEntity.utils: formatTableRowsToSQL', () => {
|
||||
]
|
||||
|
||||
const result = formatTableRowsToSQL(table, rows)
|
||||
const expected = `INSERT INTO "public"."people" ("id", "name") VALUES ('1', 'Person 1'), ('2', 'Person 2');`
|
||||
const expected = `INSERT INTO "public"."people" ("id", "name") VALUES (1, 'Person 1'), (2, 'Person 2');`
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
})
|
||||
@@ -56,6 +56,10 @@ export const formatTableRowsToSQL = (table: SupaTable, rows: any[]) => {
|
||||
stringFormats.includes(format)
|
||||
) {
|
||||
return `'${val.replaceAll("'", "''")}'`
|
||||
} else if (typeof val === 'number' || typeof val === 'boolean') {
|
||||
return `${val}`
|
||||
} else if (typeof val === 'string') {
|
||||
return `'${val.replaceAll("'", "''")}'`
|
||||
} else {
|
||||
return `'${val}'`
|
||||
}
|
||||
@@ -83,7 +87,7 @@ const generateRandomTag = (): `$${string}$` => {
|
||||
/**
|
||||
* Wrap a string in dollar-quote tags, ensuring the tag does not appear in the string
|
||||
*
|
||||
* @throws Error if unable to generate a unique tag after multiple attempts
|
||||
* @throws Error if unable to generate a unique dollar-quote tag after multiple attempts
|
||||
*/
|
||||
const safeDollarQuote = (str: string): string => {
|
||||
let tag = generateRandomTag()
|
||||
@@ -108,7 +112,7 @@ const formatArrayForSql = (arr: unknown[]): string => {
|
||||
if (Array.isArray(item)) {
|
||||
result += formatArrayForSql(item)
|
||||
} else if (typeof item === 'string') {
|
||||
result += `"${item.replace(/"/g, '""')}"`
|
||||
result += `'${item.replaceAll("'", "''")}'`
|
||||
} else if (!!item && typeof item === 'object') {
|
||||
result += `${safeDollarQuote(JSON.stringify(item))}::json`
|
||||
} else {
|
||||
|
||||
@@ -665,7 +665,7 @@ testRunner('table editor', () => {
|
||||
const downloadSqlPath = await downloadSql.path()
|
||||
const sqlContent = fs.readFileSync(downloadSqlPath, 'utf-8')
|
||||
expect(sqlContent).toBe(
|
||||
`INSERT INTO "public"."${tableNameDataActions}" ("id", "created_at", "pw_column") VALUES ('4', '2025-01-01 12:00:00+00', 'value 4 to export'), ('5', '2025-01-01 12:00:00+00', 'value 5 to export'), ('6', '2025-01-01 12:00:00+00', 'value 6 to export');`
|
||||
`INSERT INTO "public"."${tableNameDataActions}" ("id", "created_at", "pw_column") VALUES (4, '2025-01-01 12:00:00+00', 'value 4 to export'), (5, '2025-01-01 12:00:00+00', 'value 5 to export'), (6, '2025-01-01 12:00:00+00', 'value 6 to export');`
|
||||
)
|
||||
await page.waitForTimeout(1000) // wait for event processing to complete
|
||||
fs.unlinkSync(downloadSqlPath)
|
||||
|
||||
Reference in new issue
Block a user