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

Large diffs are not rendered by default.

384 changes: 384 additions & 0 deletions backend/migrations/20260812_01_collection_pagination_queries.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,384 @@
-- Migration date: 2026-08-12

-- Server-side pagination, search/filter facets, and lightweight collection
-- queries introduced by the overview and directory refactor.

-- ============================================================================
-- Library search and filter facets
-- ============================================================================

-- Flat, server-side Library search used when the Library table has an active
-- search, file-type filter, or sort. Browsing remains level-based through the
-- hierarchical directory view on the existing /library/:kind endpoint.
create or replace function public.search_library_documents(
p_user_id text,
p_library_kind text,
p_limit integer,
p_offset integer,
p_search_term text default null,
p_file_type text default null,
p_sort_key text default 'updated',
p_sort_direction text default 'desc'
)
returns table (
id uuid,
project_id uuid,
user_id text,
status text,
folder_id uuid,
library_kind text,
library_folder_id uuid,
current_version_id uuid,
created_at timestamptz,
updated_at timestamptz,
filename text,
file_type text,
storage_path text,
pdf_storage_path text,
size_bytes integer,
page_count integer,
active_version_number integer
)
language sql
stable
as $$
select
d.id,
d.project_id,
d.user_id,
d.status,
d.folder_id,
d.library_kind,
d.library_folder_id,
d.current_version_id,
d.created_at,
d.updated_at,
coalesce(nullif(trim(v.filename), ''), 'Untitled document') as filename,
v.file_type,
v.storage_path,
v.pdf_storage_path,
v.size_bytes,
v.page_count,
v.version_number as active_version_number
from public.documents d
left join public.document_versions v
on v.id = d.current_version_id
and v.deleted_at is null
where d.user_id = p_user_id
and d.project_id is null
and (
(p_library_kind = 'file' and coalesce(d.library_kind, 'file') = 'file')
or d.library_kind = p_library_kind
)
and (
p_search_term is null
or p_search_term = ''
or lower(coalesce(v.filename, '')) like
'%' || replace(replace(replace(lower(p_search_term), '\', '\\'), '%', '\%'), '_', '\_') || '%'
escape '\'
)
and (
p_file_type is null
or lower(coalesce(v.file_type, '')) = lower(p_file_type)
)
order by
case when p_sort_key = 'name' and p_sort_direction = 'asc' then lower(coalesce(v.filename, '')) else null end asc,
case when p_sort_key = 'name' and p_sort_direction = 'desc' then lower(coalesce(v.filename, '')) else null end desc,
case when p_sort_key = 'type' and p_sort_direction = 'asc' then lower(coalesce(v.file_type, '')) else null end asc,
case when p_sort_key = 'type' and p_sort_direction = 'desc' then lower(coalesce(v.file_type, '')) else null end desc,
case when p_sort_key = 'size' and p_sort_direction = 'asc' then coalesce(v.size_bytes, 0) else null end asc,
case when p_sort_key = 'size' and p_sort_direction = 'desc' then coalesce(v.size_bytes, 0) else null end desc,
case when p_sort_key = 'version' and p_sort_direction = 'asc' then coalesce(v.version_number, 0) else null end asc,
case when p_sort_key = 'version' and p_sort_direction = 'desc' then coalesce(v.version_number, 0) else null end desc,
case when p_sort_key = 'created' and p_sort_direction = 'asc' then d.created_at else null end asc,
case when p_sort_key = 'created' and p_sort_direction = 'desc' then d.created_at else null end desc,
case when p_sort_key = 'updated' and p_sort_direction = 'asc' then d.updated_at else null end asc,
case when p_sort_key = 'updated' and p_sort_direction = 'desc' then d.updated_at else null end desc,
d.updated_at desc,
d.id asc
limit greatest(coalesce(p_limit, 50), 1)
offset greatest(coalesce(p_offset, 0), 0);
$$;

create or replace function public.get_library_filter_options(
p_user_id text,
p_library_kind text
)
returns table (file_types text[])
language sql
stable
as $$
select coalesce(
array_agg(distinct lower(v.file_type) order by lower(v.file_type))
filter (where nullif(trim(v.file_type), '') is not null),
array[]::text[]
) as file_types
from public.documents d
left join public.document_versions v
on v.id = d.current_version_id
and v.deleted_at is null
where d.user_id = p_user_id
and d.project_id is null
and (
(p_library_kind = 'file' and coalesce(d.library_kind, 'file') = 'file')
or d.library_kind = p_library_kind
);
$$;

-- Small, distinct filter facets for Projects. This avoids downloading every
-- project overview row (and calculating all of its counts) just to populate
-- two dropdown menus.
create or replace function public.get_project_filter_options(
p_user_id text,
p_user_email text default null
)
returns table (practices text[], owners jsonb)
language sql
stable
as $$
with visible_projects as (
select p.user_id, nullif(trim(p.practice), '') as practice
from public.projects p
where p.user_id = p_user_id
or (
coalesce(p_user_email, '') <> ''
and p.user_id <> p_user_id
and p.shared_with @> jsonb_build_array(p_user_email)
)
),
distinct_owners as (
select distinct vp.user_id
from visible_projects vp
),
owner_options as (
select
o.user_id,
case
when o.user_id = p_user_id then 'Me'
else coalesce(
nullif(trim(up.display_name), ''),
nullif(trim(up.email), ''),
'Shared'
)
end as label
from distinct_owners o
left join public.user_profiles up
on up.user_id::text = o.user_id
)
select
coalesce(
(select array_agg(distinct practice order by practice)
from visible_projects
where practice is not null),
array[]::text[]
) as practices,
coalesce(
(select jsonb_agg(
jsonb_build_object('value', user_id, 'label', label)
order by label, user_id
) from owner_options),
'[]'::jsonb
) as owners;
$$;

-- Scope/type-aware Workflow facets. Static system-workflow facets stay in
-- memory on the frontend and are merged with these DB-backed values.
create or replace function public.get_workflow_filter_options(
p_user_id text,
p_user_email text default null,
p_type text default null,
p_scope text default 'all'
)
returns table (
practices text[],
languages text[],
jurisdictions text[]
)
language sql
stable
as $$
with owned as (
select w.practice, w.language, w.jurisdictions, 'owned'::text as source
from public.workflows w
where w.user_id::text = p_user_id
and (p_type is null or w.type = p_type)
),
shared as (
select w.practice, w.language, w.jurisdictions, 'shared'::text as source
from public.workflow_shares ws
join public.workflows w on w.id = ws.workflow_id
where lower(ws.shared_with_email) = lower(coalesce(p_user_email, ''))
and (p_type is null or w.type = p_type)
),
visible as (
select * from owned
union all
select * from shared
),
scoped as (
select * from visible
where coalesce(p_scope, 'all') = 'all' or source = p_scope
)
select
coalesce(
array_agg(distinct nullif(trim(practice), '') order by nullif(trim(practice), ''))
filter (where nullif(trim(practice), '') is not null),
array[]::text[]
) as practices,
coalesce(
array_agg(distinct nullif(trim(language), '') order by nullif(trim(language), ''))
filter (where nullif(trim(language), '') is not null),
array[]::text[]
) as languages,
coalesce(
(select array_agg(distinct jurisdiction order by jurisdiction)
from scoped s
cross join lateral unnest(coalesce(s.jurisdictions, array[]::text[])) jurisdiction
where nullif(trim(jurisdiction), '') is not null),
array[]::text[]
) as jurisdictions
from scoped;
$$;

create index if not exists document_versions_filename_trgm_idx
on public.document_versions using gin (lower(filename) gin_trgm_ops)
where deleted_at is null;

-- ============================================================================
-- Chat sidebar pagination
-- ============================================================================

-- Offset pagination for the sidebar's recent-chat list. Project names are
-- returned with each row so the sidebar does not need to download every
-- project merely to label project chats.
create index if not exists chats_user_created_idx
on public.chats(user_id, created_at desc, id);

drop function if exists public.get_chats_overview(text, integer);

create or replace function public.get_chats_overview(
p_user_id text,
p_limit integer default null,
p_offset integer default 0
)
returns table (
id uuid,
project_id uuid,
user_id text,
title text,
created_at timestamptz,
project_name text
)
language sql
stable
as $$
select
c.id,
c.project_id,
c.user_id,
c.title,
c.created_at,
p.name as project_name
from public.chats c
left join public.projects p on p.id = c.project_id
where c.user_id = p_user_id
or (
p.id is not null
and p.user_id = p_user_id
)
order by c.created_at desc, c.id asc
limit case
when p_limit is null then null
else greatest(1, least(p_limit, 100))
end
offset greatest(coalesce(p_offset, 0), 0);
$$;

-- ============================================================================
-- Recent projects and Library bulk-selection helpers
-- ============================================================================

-- Lightweight sidebar project feed. The Projects overview RPC intentionally
-- computes file/chat/review counts for table sorting; the sidebar needs none
-- of those aggregates.
drop function if exists public.get_recent_projects(text, text, integer, integer);

create or replace function public.get_project_summaries(
p_user_id text,
p_user_email text,
p_limit integer,
p_offset integer
)
returns table (
id uuid,
user_id text,
name text,
created_at timestamptz,
updated_at timestamptz,
is_owner boolean
)
language sql
stable
as $$
select
p.id,
p.user_id,
p.name,
p.created_at,
p.updated_at,
p.user_id = p_user_id as is_owner
from public.projects p
where p.user_id = p_user_id
or (
coalesce(p_user_email, '') <> ''
and p.user_id <> p_user_id
and p.shared_with @> jsonb_build_array(p_user_email)
)
order by p.updated_at desc, p.created_at desc, p.id asc
limit greatest(coalesce(p_limit, 11), 1)
offset greatest(coalesce(p_offset, 0), 0);
$$;

-- ID-only Library query for select-all and bulk actions. This mirrors the
-- flat Library search predicate without returning document/version payloads.
create or replace function public.get_library_document_ids(
p_user_id text,
p_library_kind text,
p_search_term text,
p_file_type text,
p_limit integer,
p_offset integer
)
returns table (
id uuid,
user_id text
)
language sql
stable
as $$
select d.id, d.user_id
from public.documents d
left join public.document_versions v
on v.id = d.current_version_id
and v.deleted_at is null
where d.user_id = p_user_id
and d.project_id is null
and (
(p_library_kind = 'file' and coalesce(d.library_kind, 'file') = 'file')
or d.library_kind = p_library_kind
)
and (
p_search_term is null
or p_search_term = ''
or lower(coalesce(v.filename, '')) like
'%' || replace(replace(replace(lower(p_search_term), '\', '\\'), '%', '\%'), '_', '\_') || '%'
escape '\'
)
and (
p_file_type is null
or lower(coalesce(v.file_type, '')) = lower(p_file_type)
)
order by d.updated_at desc, d.id asc
limit greatest(coalesce(p_limit, 1000), 1)
offset greatest(coalesce(p_offset, 0), 0);
$$;
Loading
Loading