feat(harness): port vectorstore dual-key + reader RPC (D11, §5.4)
Ports vectorstore/{rest_client,rest_writer,store,reader,embeddings,records},
evidence/model (leaf dep of records), and cli/_guards (require_vector_write_allowed
workstation write-guard). Renamed psdwp3->nsp, verbatim.
VectorRestClient gains an api_key property so reader/writer clients carry their
distinct keys visibly (spec D11: vector_reader / vector_writer on the same endpoint).
scripts/create_vector_reader_rpc.sql is NEW: the reader RPCs (search_similar,
list_tables) lived server-side in Supabase and were never versioned. Authored now
mirroring the writer allowlist pattern (table allowlist, security definer, revoke
from anon/authenticated, grant to vector_reader only). Writer RPC ported verbatim.
L1: test_vector_dual_key (7 tests) pins the dual-key construction + the workstation
write-guard (exit 4 without writer key).
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
-- 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.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;
|
||||
$$;
|
||||
|
||||
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;
|
||||
|
||||
-- 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;
|
||||
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;
|
||||
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;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
notify pgrst, 'reload schema';
|
||||
Reference in New Issue
Block a user