fix(db): migrations failing on a fresh supabase instance (#645)

* fix(db): add extension function wrappers (avoid shadowing gen_random_uuid)

Signed-off-by: filip <filip.harald@gmail.com>

* test: added test for extension function wrappers (uuid_generate_v4 and gen_random_bytes) in public schema, covering existence, delegation, correctness, properties, and idempotency.

Signed-off-by: filip <filip.harald@gmail.com>

---------

Signed-off-by: filip <filip.harald@gmail.com>
This commit is contained in:
Filip
2026-06-08 09:41:36 +02:00
committed by GitHub
parent 23863371dd
commit 3d3f242d11
2 changed files with 220 additions and 0 deletions
@@ -0,0 +1,26 @@
CREATE SCHEMA IF NOT EXISTS extensions;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA extensions;
CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA extensions;
CREATE OR REPLACE FUNCTION public.uuid_generate_v4()
RETURNS uuid
LANGUAGE sql
VOLATILE
PARALLEL SAFE
SET search_path = ''
AS $$
SELECT extensions.uuid_generate_v4();
$$;
CREATE OR REPLACE FUNCTION public.gen_random_bytes(size integer)
RETURNS bytea
LANGUAGE sql
VOLATILE
PARALLEL SAFE
SET search_path = ''
AS $$
SELECT extensions.gen_random_bytes(size);
$$;
NOTIFY pgrst, 'reload schema';
+194
View File
@@ -0,0 +1,194 @@
import { describe, expect, it } from 'vitest'
import { getPool } from '@/tests/pg/setup'
/**
* Covers 20230101000000_extension_function_wrappers:
*
* - Wrapper existence: public.uuid_generate_v4() and
* public.gen_random_bytes() are present and resolve correctly.
*
* - Delegation: public.uuid_generate_v4() → extensions.uuid_generate_v4()
* and public.gen_random_bytes() → extensions.gen_random_bytes().
*
* - Correctness: uuid_generate_v4() produces valid non-null UUIDs with
* distinct values across calls. gen_random_bytes(n) returns a bytea of
* exactly n bytes; gen_random_bytes(0) returns empty bytea.
*
* - Function properties: VOLATILE, PARALLEL SAFE, SET search_path = '',
* LANGUAGE sql — consistent with the rest of the codebase.
*/
describe('extension function wrappers', () => {
it('uuid_generate_v4() exists in public schema and returns a valid UUID', async () => {
const pool = getPool()
const r = await pool.query<{ uuid: string }>(
`SELECT public.uuid_generate_v4() AS uuid`,
)
const uuid = r.rows[0]!.uuid
expect(uuid).toBeTruthy()
// UUID v4 format: xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx
expect(uuid).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
)
})
it('uuid_generate_v4() returns distinct values across calls (no caching)', async () => {
const pool = getPool()
const r = await pool.query<{ uuid: string }>(
`SELECT public.uuid_generate_v4() AS uuid
UNION ALL
SELECT public.uuid_generate_v4()`,
)
expect(r.rows).toHaveLength(2)
expect(r.rows[0]!.uuid).not.toBe(r.rows[1]!.uuid)
})
it('uuid_generate_v4() can be used as a column DEFAULT expression', async () => {
const pool = getPool()
// Create a temp table using the wrapper as default, insert a row, check
// the UUID was generated and is valid.
await pool.query(`
CREATE TEMP TABLE _test_uuid_default (
id uuid PRIMARY KEY DEFAULT public.uuid_generate_v4(),
label text
)
`)
await pool.query(`INSERT INTO _test_uuid_default (label) VALUES ('test')`)
const r = await pool.query<{ id: string }>(`SELECT id FROM _test_uuid_default`)
expect(r.rows[0]!.id).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
)
await pool.query(`DROP TABLE _test_uuid_default`)
})
it('gen_random_bytes() exists in public schema', async () => {
const pool = getPool()
const r = await pool.query<{ ok: boolean }>(
`SELECT pg_get_functiondef('public.gen_random_bytes(integer)'::regprocedure) IS NOT NULL AS ok`,
)
expect(r.rows[0]!.ok).toBe(true)
})
it('gen_random_bytes(n) returns bytea of exactly n bytes', async () => {
const pool = getPool()
for (const n of [1, 16, 32, 64]) {
const r = await pool.query<{ len: number }>(
`SELECT octet_length(public.gen_random_bytes($1)) AS len`,
[n],
)
expect(r.rows[0]!.len).toBe(n)
}
})
it('gen_random_bytes(0) returns empty bytea', async () => {
const pool = getPool()
const r = await pool.query<{ len: number }>(
`SELECT octet_length(public.gen_random_bytes(0)) AS len`,
)
expect(r.rows[0]!.len).toBe(0)
})
it('gen_random_bytes returns distinct values across calls', async () => {
const pool = getPool()
const r = await pool.query<{ a: Buffer; b: Buffer }>(
`SELECT public.gen_random_bytes(16) AS a, public.gen_random_bytes(16) AS b`,
)
expect(r.rows[0]!.a).not.toEqual(r.rows[0]!.b)
})
it('uuid_generate_v4() is VOLATILE and PARALLEL SAFE', async () => {
const pool = getPool()
const r = await pool.query<{
provolatile: string
proparallel: string
}>(
`SELECT provolatile, proparallel
FROM pg_proc
WHERE oid = 'public.uuid_generate_v4()'::regprocedure`,
)
expect(r.rows[0]!.provolatile).toBe('v')
expect(r.rows[0]!.proparallel).toBe('s')
})
it('gen_random_bytes(integer) is VOLATILE and PARALLEL SAFE', async () => {
const pool = getPool()
const r = await pool.query<{
provolatile: string
proparallel: string
}>(
`SELECT provolatile, proparallel
FROM pg_proc
WHERE oid = 'public.gen_random_bytes(integer)'::regprocedure`,
)
expect(r.rows[0]!.provolatile).toBe('v')
expect(r.rows[0]!.proparallel).toBe('s')
})
it('both wrappers delegate to extensions schema functions', async () => {
const pool = getPool()
// Verify the extensions schema exists and has the source functions.
const extFns = await pool.query<{ name: string; schema: string }>(
`SELECT p.proname AS name, n.nspname AS schema
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'extensions'
AND p.proname IN ('uuid_generate_v4', 'gen_random_bytes')`,
)
expect(extFns.rows).toHaveLength(2)
// Verify the public wrappers exist.
const pubFns = await pool.query<{ name: string; schema: string }>(
`SELECT p.proname AS name, n.nspname AS schema
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'public'
AND p.proname IN ('uuid_generate_v4', 'gen_random_bytes')`,
)
expect(pubFns.rows).toHaveLength(2)
})
it('wrappers are defined in SQL (not plpgsql or C)', async () => {
const pool = getPool()
const r = await pool.query<{ proname: string; prolang: string }>(
`SELECT proname, l.lanname AS prolang
FROM pg_proc p
JOIN pg_language l ON l.oid = p.prolang
WHERE p.proname IN ('uuid_generate_v4', 'gen_random_bytes')
AND p.pronamespace = 'public'::regnamespace`,
)
for (const row of r.rows) {
expect(row.prolang).toBe('sql')
}
})
it('wrappers are idempotent — CREATE OR REPLACE succeeds on re-run', async () => {
const pool = getPool()
// Simulate re-applying the migration: re-running the same CREATE OR REPLACE
// should succeed without errors and the functions should still work.
await pool.query(`
CREATE OR REPLACE FUNCTION public.uuid_generate_v4()
RETURNS uuid
LANGUAGE sql
VOLATILE
PARALLEL SAFE
SET search_path = ''
AS $$ SELECT extensions.uuid_generate_v4(); $$
`)
await pool.query(`
CREATE OR REPLACE FUNCTION public.gen_random_bytes(size integer)
RETURNS bytea
LANGUAGE sql
VOLATILE
PARALLEL SAFE
SET search_path = ''
AS $$ SELECT extensions.gen_random_bytes(size); $$
`)
// Functions must still work after re-creation.
const r = await pool.query<{ uuid: string; bytes_len: number }>(
`SELECT public.uuid_generate_v4() AS uuid,
octet_length(public.gen_random_bytes(32)) AS bytes_len`,
)
expect(r.rows[0]!.uuid).toBeTruthy()
expect(r.rows[0]!.bytes_len).toBe(32)
})
})