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:
@@ -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';
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user