-- RPC writer controllate per pgvector via Supabase/PostgREST. -- Eseguire sul database Supabase centrale come owner dello schema `vectors`. -- Prerequisiti: -- - schema `vectors` con tabelle schema_records/evidence/memory -- - colonne: record_key text unique, kind text, content_hash text, metadata jsonb, -- embedding vector(N), indexed_at timestamptz -- - ruolo/key PostgREST dedicato `vector_writer` -- -- La key writer deve ricevere solo EXECUTE su queste funzioni, non privilegi raw di DELETE -- sulle tabelle. Cleanup/rebuild restano via terminale sul server. create or replace function public._assert_vector_write_table(table_name text, kinds text[]) returns void language plpgsql stable as $$ begin if table_name not in ('schema_records', 'evidence', 'memory') then raise sqlstate 'PGRST' using message = json_build_object('message', 'Unsupported vector table')::text, detail = json_build_object('status', 400)::text; end if; if table_name = 'schema_records' and exists (select 1 from unnest(kinds) k where k not in ('schema_table', 'schema_column')) then raise sqlstate 'PGRST' using message = json_build_object('message', 'Invalid kind for schema_records')::text, detail = json_build_object('status', 400)::text; elsif table_name = 'evidence' and exists (select 1 from unnest(kinds) k where k <> 'evidence') then raise sqlstate 'PGRST' using message = json_build_object('message', 'Invalid kind for evidence')::text, detail = json_build_object('status', 400)::text; elsif table_name = 'memory' and exists (select 1 from unnest(kinds) k where k <> 'memory') then raise sqlstate 'PGRST' using message = json_build_object('message', 'Invalid kind for memory')::text, detail = json_build_object('status', 400)::text; end if; end; $$; create or replace function public.list_evidence_generations(table_name text, kind text) returns table(generation text) language plpgsql security definer set search_path = public, vectors, extensions as $$ begin if table_name <> 'evidence' or kind <> 'evidence' then raise exception 'only exact Evidence generations may be listed'; end if; return query select distinct e.metadata->>'vector_generation' from vectors.evidence e where e.kind = 'evidence' and e.metadata->>'vector_generation' ~ '^gen:[0-9a-f]{32}$' order by 1; end; $$; create or replace function public.existing_vector_hashes(table_name text, kinds text[]) returns table(record_key text, content_hash text) language plpgsql security definer set search_path = public, vectors, extensions as $$ begin perform public._assert_vector_write_table(table_name, kinds); return query execute format( 'select record_key, content_hash from vectors.%I where kind = any ($1)', table_name ) using kinds; end; $$; create or replace function public.upsert_vector_records(table_name text, rows jsonb) returns jsonb language plpgsql security definer set search_path = public, vectors, extensions as $$ declare r jsonb; affected integer := 0; kinds text[]; begin if jsonb_typeof(rows) <> 'array' then raise sqlstate 'PGRST' using message = json_build_object('message', 'rows must be a JSON array')::text, detail = json_build_object('status', 400)::text; end if; select coalesce(array_agg(distinct value->>'kind'), array[]::text[]) into kinds from jsonb_array_elements(rows) value; perform public._assert_vector_write_table(table_name, kinds); for r in select * from jsonb_array_elements(rows) loop execute format( 'insert into vectors.%I (record_key, kind, content_hash, metadata, embedding) values ($1, $2, $3, $4::jsonb, $5::vector) on conflict (record_key) do update set kind = excluded.kind, content_hash = excluded.content_hash, metadata = excluded.metadata, embedding = excluded.embedding, indexed_at = now()', table_name ) using r->>'record_key', r->>'kind', r->>'content_hash', coalesce(r->'metadata', '{}'::jsonb), ('[' || (select string_agg(value::text, ',') from jsonb_array_elements(r->'embedding')) || ']'); affected := affected + 1; end loop; return jsonb_build_object('upserted', affected); end; $$; create or replace function public.delete_vector_generation( table_name text, kind text, generation text ) returns jsonb language plpgsql security definer set search_path = public, vectors, extensions as $$ declare affected integer; begin if table_name <> 'evidence' or kind <> 'evidence' or generation !~ '^gen:[0-9a-f]{32}$' then raise exception 'only an exact Evidence generation may be deleted'; end if; delete from vectors.evidence e where e.kind = 'evidence' and e.metadata->>'vector_generation' = generation; get diagnostics affected = row_count; return jsonb_build_object('deleted', affected); end; $$; revoke all on function public._assert_vector_write_table(text, text[]) from public; revoke all on function public.existing_vector_hashes(text, text[]) from public; revoke all on function public.upsert_vector_records(text, jsonb) from public; revoke all on function public.delete_vector_generation(text, text, text) from public; revoke all on function public.list_evidence_generations(text, text) from public; -- Su alcuni progetti Supabase le funzioni in `public` ricevono grant automatici: revoca -- esplicitamente dai ruoli client generici, poi abilita solo il writer dedicato. do $$ begin if exists (select 1 from pg_roles where rolname = 'anon') then revoke all on function public.existing_vector_hashes(text, text[]) from anon; revoke all on function public.upsert_vector_records(text, jsonb) from anon; revoke all on function public.delete_vector_generation(text, text, text) from anon; revoke all on function public.list_evidence_generations(text, text) from anon; end if; if exists (select 1 from pg_roles where rolname = 'authenticated') then revoke all on function public.existing_vector_hashes(text, text[]) from authenticated; revoke all on function public.upsert_vector_records(text, jsonb) from authenticated; revoke all on function public.delete_vector_generation(text, text, text) from authenticated; revoke all on function public.list_evidence_generations(text, text) from authenticated; end if; if exists (select 1 from pg_roles where rolname = 'vector_writer') then grant execute on function public.existing_vector_hashes(text, text[]) to vector_writer; grant execute on function public.upsert_vector_records(text, jsonb) to vector_writer; grant execute on function public.delete_vector_generation(text, text, text) to vector_writer; grant execute on function public.list_evidence_generations(text, text) to vector_writer; end if; end $$; notify pgrst, 'reload schema';