174 lines
6.7 KiB
PL/PgSQL
174 lines
6.7 KiB
PL/PgSQL
-- 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';
|