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).
100 lines
3.7 KiB
PL/PgSQL
100 lines
3.7 KiB
PL/PgSQL
-- RPC reader per pgvector via Supabase/PostgREST (spec D11, §5.4).
|
|
-- 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_reader` (lettura: search_similar, list_tables)
|
|
--
|
|
-- A differenza del writer RPC (che era gia' nel repo), i reader RPC vivevano lato server
|
|
-- in Supabase e non erano mai stati versionati. Li si riporta qui per completezza del
|
|
-- modello dual-key: la key reader riceve solo EXECUTE su queste due funzioni, niente
|
|
-- SELECT raw sulle tabelle. Il cleanup/rebuild resta solo-server (vedi writer RPC).
|
|
|
|
create or replace function public._assert_vector_read_table(table_name 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;
|
|
end;
|
|
$$;
|
|
|
|
create or replace function public.search_similar(
|
|
table_name text,
|
|
query_embedding vector,
|
|
limit_count integer
|
|
)
|
|
returns table(id bigint, similarity real, metadata jsonb)
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public, vectors, extensions
|
|
as $$
|
|
begin
|
|
perform public._assert_vector_read_table(table_name);
|
|
return query execute format(
|
|
'select t.id,
|
|
(1 - (t.embedding <=> $1))::real as similarity,
|
|
t.metadata
|
|
from vectors.%I t
|
|
order by t.embedding <=> $1
|
|
limit $2',
|
|
table_name
|
|
) using query_embedding, limit_count;
|
|
end;
|
|
$$;
|
|
|
|
create or replace function public.list_tables()
|
|
returns table(table_name text, vector_dimensions integer, record_count bigint)
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public, vectors, extensions
|
|
as $$
|
|
begin
|
|
return query
|
|
select c.relname::text as table_name,
|
|
-- dimensione ricavata dalla colonna embedding (vector(N)) se presente
|
|
coalesce(
|
|
(select (regexp_matches(format_type(a.atttypid, a.atttypmod), 'vector\((\d+)\)'))[1])::int,
|
|
0
|
|
) as vector_dimensions,
|
|
c.reltuples::bigint as record_count
|
|
from pg_class c
|
|
join pg_namespace n on n.oid = c.relnamespace
|
|
left join pg_attribute a on a.attrelid = c.oid and a.attname = 'embedding'
|
|
where n.nspname = 'vectors'
|
|
and c.relkind in ('r', 'p')
|
|
and c.relname in ('schema_records', 'evidence', 'memory')
|
|
order by c.relname;
|
|
end;
|
|
$$;
|
|
|
|
revoke all on function public._assert_vector_read_table(text) from public;
|
|
revoke all on function public.search_similar(text, vector, integer) from public;
|
|
revoke all on function public.list_tables() from public;
|
|
|
|
-- Revoca dai ruoli client generici, poi abilita solo il reader dedicato
|
|
-- (e il writer, che usa existing_vector_hashes per l'upsert incrementale).
|
|
do $$
|
|
begin
|
|
if exists (select 1 from pg_roles where rolname = 'anon') then
|
|
revoke all on function public.search_similar(text, vector, integer) from anon;
|
|
revoke all on function public.list_tables() from anon;
|
|
end if;
|
|
if exists (select 1 from pg_roles where rolname = 'authenticated') then
|
|
revoke all on function public.search_similar(text, vector, integer) from authenticated;
|
|
revoke all on function public.list_tables() from authenticated;
|
|
end if;
|
|
if exists (select 1 from pg_roles where rolname = 'vector_reader') then
|
|
grant execute on function public.search_similar(text, vector, integer) to vector_reader;
|
|
grant execute on function public.list_tables() to vector_reader;
|
|
end if;
|
|
end $$;
|
|
|
|
notify pgrst, 'reload schema';
|