Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,8 +10,21 @@
-- Set the default base URL for the current (production) project.
-- This value must be changed for staging/dev environments via:
-- ALTER DATABASE postgres SET app.indexing_v3_agent_base_url = '...';
alter database postgres
set app.indexing_v3_agent_base_url = 'https://sjrfecxgysukkwxsowpy.supabase.co';
--
-- Guarded: hosted Supabase denies ALTER DATABASE SET to the migration role
-- (42501). We swallow insufficient_privilege so the migration still succeeds on
-- hosted; the function below already falls back to the hardcoded project URL via
-- current_setting(..., true), so behaviour is unchanged when the GUC is unset.
-- On self-hosted / local (where the role is superuser) the GUC is set normally.
do $$
begin
execute format('alter database %I set app.indexing_v3_agent_base_url = %L',
current_database(), 'https://sjrfecxgysukkwxsowpy.supabase.co');
exception
when insufficient_privilege then
raise notice 'Skipping ALTER DATABASE SET app.indexing_v3_agent_base_url (insufficient privilege on hosted Supabase); invoke_indexing_v3_agent falls back to the hardcoded URL.';
end
$$;

create or replace function public.invoke_indexing_v3_agent(p_limit integer default 1)
returns bigint
Expand Down
162 changes: 20 additions & 142 deletions supabase/migrations/20260702170000_fix_match_chunks_text_n1.sql
Original file line numberDiff line numberDiff line change
@@ -1,144 +1,22 @@
-- Fix #7: Eliminate N+1 scalar function calls in match_document_chunks_text.
-- SUPERSEDED / INTENTIONAL NO-OP (neutralized 2026-07-03).
--
-- The original outer SELECT called document_label_metadata(document_id) and
-- document_summary_text(document_id) once per row returned from the `ranked`
-- CTE. With match_count=12 and duplicate document_ids, this could fire 24+
-- independent SELECTs against document_labels and document_summaries.
-- This migration originally rewrote public.match_document_chunks_text to batch
-- the N+1 label/summary lookups AND changed its RETURNS TABLE shape (it added a
-- `lexical_score` output column). It must NOT be applied, for two reasons:
--
-- Fix: add two CTEs that batch-fetch the data for all distinct document_ids
-- in the result set, then LEFT JOIN them back. chunk_image_metadata() accepts
-- a uuid[] so it is already a single call per row (no N+1); it is unchanged.

create or replace function public.match_document_chunks_text(
query_text text,
match_count integer default 12,
document_filters uuid[] default null,
owner_filter uuid default null
)
returns table (
id uuid,
document_id uuid,
title text,
file_name text,
page_number integer,
chunk_index integer,
section_heading text,
content text,
retrieval_synopsis text,
image_ids uuid[],
source_metadata jsonb,
document_labels jsonb,
document_summary text,
similarity double precision,
text_rank double precision,
hybrid_score double precision,
lexical_score double precision,
images jsonb
)
language sql
stable
set search_path = public, extensions, pg_temp
as $$
with query as (
select websearch_to_tsquery('english', coalesce(query_text, '')) as tsq
),
ranked as (
select
c.id,
c.document_id,
d.title,
d.file_name,
c.page_number,
c.chunk_index,
c.section_heading,
c.content,
c.retrieval_synopsis,
c.image_ids,
d.metadata as source_metadata,
(
ts_rank_cd(c.search_tsv, query.tsq) +
(ts_rank_cd(d.title_search_tsv, query.tsq) * 3.0)
)::double precision as text_rank
from public.document_chunks c
join public.documents d on d.id = c.document_id
cross join query
where (document_filters is null or c.document_id = any(document_filters))
and (owner_filter is null or d.owner_id = owner_filter)
and d.status = 'indexed'
and public.is_committed_document_generation(c.index_generation_id, d.metadata)
and (c.search_tsv @@ query.tsq or d.title_search_tsv @@ query.tsq)
order by (
ts_rank_cd(c.search_tsv, query.tsq) +
(ts_rank_cd(d.title_search_tsv, query.tsq) * 3.0)
) desc
limit least(greatest(match_count * 2, 24), 96)
),
-- Batch-fetch label metadata for all distinct document_ids in the result set.
-- One query replaces N per-row calls to document_label_metadata().
doc_labels as (
select
l.document_id,
coalesce(
jsonb_agg(
jsonb_build_object(
'id', l.id,
'document_id', l.document_id,
'owner_id', l.owner_id,
'label', l.label,
'label_type', l.label_type,
'source', l.source,
'confidence', l.confidence,
'metadata', l.metadata,
'created_at', l.created_at,
'updated_at', l.updated_at
)
order by l.confidence desc, l.label
),
'[]'::jsonb
) as labels
from public.document_labels l
where l.document_id in (select distinct ranked.document_id from ranked)
group by l.document_id
),
-- Batch-fetch summary text for all distinct document_ids in the result set.
-- One query replaces N per-row calls to document_summary_text().
doc_summaries as (
select distinct on (s.document_id)
s.document_id,
s.summary
from public.document_summaries s
where s.document_id in (select distinct ranked.document_id from ranked)
order by s.document_id
)
select
ranked.id,
ranked.document_id,
ranked.title,
ranked.file_name,
ranked.page_number,
ranked.chunk_index,
ranked.section_heading,
ranked.content,
ranked.retrieval_synopsis,
ranked.image_ids,
ranked.source_metadata,
coalesce(doc_labels.labels, '[]'::jsonb) as document_labels,
doc_summaries.summary as document_summary,
-- Text-only fallback has NO vector cosine similarity. Do not fabricate one:
-- a synthetic value here was read downstream as a real semantic score and
-- could label a pure keyword hit as "strong"/"moderate" evidence (>=0.64).
-- Leave similarity at 0; the lexical signal lives in lexical_score.
0::double precision as similarity,
ranked.text_rank,
-- Cap hybrid_score well below the 0.64 "moderate" threshold so a lexical-only
-- row can order amongst its peers but can never masquerade as a moderate/strong
-- cosine match when merged with vector results.
least(0.5, 0.18 + (least(ranked.text_rank, 1) * 0.3))::double precision as hybrid_score,
least(0.99, 0.4 + (least(ranked.text_rank, 1) * 0.59))::double precision as lexical_score,
public.chunk_image_metadata(ranked.image_ids) as images
from ranked
left join doc_labels on doc_labels.document_id = ranked.document_id
left join doc_summaries on doc_summaries.document_id = ranked.document_id
order by lexical_score desc, text_rank desc
limit match_count;
$$;
-- 1. It would ERROR. Adding an output column to a set-returning function via
-- CREATE OR REPLACE is illegal ("cannot change return type of existing
-- function") without a DROP FUNCTION first — so this file cannot apply on
-- any database whose match_document_chunks_text already exists.
--
-- 2. It would REGRESS the live function. The deployed match_document_chunks_text
-- already carries the N+1 batching AND a superior title-boost dual-path
-- implementation (chunk_seed / title_docs / title_matches) that this file
-- does not contain. Applying it would replace the better function with this
-- older single-path one.
--
-- The canonical retrieval-RPC definitions live in supabase/schema.sql. Fully
-- reconciling the live retrieval RPCs into migrations/schema.sql is tracked as a
-- separate, golden-eval-gated backlog item (the known hybrid-RPC drift). This
-- file is kept as a documented no-op purely to preserve migration ordering.
select 1;
14 changes: 12 additions & 2 deletions supabase/schema.sql
Original file line numberDiff line numberDiff line change
Expand Up@@ -3452,8 +3452,18 @@ $$;
revoke execute on function public.complete_strict_enrichment_job(uuid, uuid, text, text, text) from public, anon, authenticated;
grant execute on function public.complete_strict_enrichment_job(uuid, uuid, text, text, text) to service_role;

alter database postgres
set app.indexing_v3_agent_base_url = 'https://sjrfecxgysukkwxsowpy.supabase.co';
-- Guarded: hosted Supabase denies ALTER DATABASE SET to the migration role
-- (42501); swallow insufficient_privilege so schema replay succeeds on hosted.
-- invoke_indexing_v3_agent falls back to the hardcoded URL when the GUC is unset.
do $$
begin
execute format('alter database %I set app.indexing_v3_agent_base_url = %L',
current_database(), 'https://sjrfecxgysukkwxsowpy.supabase.co');
exception
when insufficient_privilege then
raise notice 'Skipping ALTER DATABASE SET app.indexing_v3_agent_base_url (insufficient privilege on hosted Supabase).';
end
$$;

create or replace function public.invoke_indexing_v3_agent(p_limit integer default 1)
returns bigint
Expand Down
5 changes: 4 additions & 1 deletion tests/supabase-schema.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -286,7 +286,10 @@ describe("Supabase schema Data API grants", () => {
expect(schema).toContain("set search_path = public, extensions, vault, pg_temp");
expect(schema).toContain("from vault.decrypted_secrets");
expect(schema).toContain("where name = 'indexing_v3_agent_secret'");
expect(schema).toContain("set app.indexing_v3_agent_base_url = 'https://sjrfecxgysukkwxsowpy.supabase.co';");
// The GUC default is set through a privilege-guarded DO block so schema
// replay succeeds on hosted Supabase (ALTER DATABASE SET is denied there).
expect(schema).toContain("alter database %I set app.indexing_v3_agent_base_url = %L");
expect(schema).toContain("when insufficient_privilege then");
expect(schema).toContain("nullif(current_setting('app.indexing_v3_agent_base_url', true), '')");
expect(schema).toContain("select net.http_post(");
expect(schema).toContain("v_base_url || '/functions/v1/indexing-v3-agent?limit='");
Expand Down
Loading