183 lines
5.0 KiB
PL/PgSQL
183 lines
5.0 KiB
PL/PgSQL
-- Paso 1
|
|
drop text search configuration if exists public.es_simple_unaccent cascade;
|
|
|
|
create text search configuration public.es_simple_unaccent (copy = pg_catalog.simple);
|
|
|
|
alter text search configuration public.es_simple_unaccent
|
|
alter mapping for hword, hword_part, word
|
|
with unaccent, simple;
|
|
|
|
-- Paso 2
|
|
alter table public.asignaturas
|
|
add column if not exists search_vector tsvector;
|
|
|
|
-- Paso 3
|
|
create or replace function public.fn_asignaturas_update_search_vector()
|
|
returns trigger
|
|
language plpgsql
|
|
as $$
|
|
begin
|
|
new.search_vector :=
|
|
setweight(
|
|
to_tsvector('public.es_simple_unaccent', coalesce(new.nombre, '')),
|
|
'A'
|
|
)
|
|
||
|
|
setweight(
|
|
to_tsvector('public.es_simple_unaccent', coalesce(new.codigo, '')),
|
|
'A'
|
|
)
|
|
||
|
|
setweight(
|
|
to_tsvector('public.es_simple_unaccent', coalesce(new.datos, '{}'::jsonb)::text),
|
|
'B'
|
|
)
|
|
||
|
|
setweight(
|
|
to_tsvector('public.es_simple_unaccent', coalesce(new.contenido_tematico, '[]'::jsonb)::text),
|
|
'B'
|
|
);
|
|
|
|
return new;
|
|
end;
|
|
$$;
|
|
|
|
-- Paso 4
|
|
drop trigger if exists trg_asignaturas_search_vector on public.asignaturas;
|
|
|
|
create trigger trg_asignaturas_search_vector
|
|
before insert or update of nombre, codigo, datos, contenido_tematico
|
|
on public.asignaturas
|
|
for each row
|
|
execute function public.fn_asignaturas_update_search_vector();
|
|
|
|
-- Paso 5
|
|
create or replace function public.recalcular_vectores_asignaturas()
|
|
returns void
|
|
language sql
|
|
as $$
|
|
UPDATE public.asignaturas
|
|
SET search_vector =
|
|
setweight(to_tsvector('public.es_simple_unaccent', coalesce(nombre, '')), 'A') ||
|
|
setweight(to_tsvector('public.es_simple_unaccent', coalesce(codigo, '')), 'A') ||
|
|
setweight(to_tsvector('public.es_simple_unaccent', coalesce(datos, '{}'::jsonb)::text), 'B') ||
|
|
setweight(to_tsvector('public.es_simple_unaccent', coalesce(contenido_tematico, '[]'::jsonb)::text), 'B')
|
|
WHERE id IS NOT NULL;
|
|
$$;
|
|
|
|
-- Paso 6
|
|
create index if not exists asignaturas_search_vector_gin_idx
|
|
on public.asignaturas
|
|
using gin (search_vector);
|
|
|
|
-- Paso 7
|
|
create or replace function public.build_asignaturas_prefix_tsquery(p_search text)
|
|
returns tsquery
|
|
language plpgsql
|
|
stable
|
|
as $$
|
|
declare
|
|
cleaned text;
|
|
tokens text[];
|
|
query_text text;
|
|
begin
|
|
cleaned := trim(coalesce(p_search, ''));
|
|
|
|
if cleaned = '' then
|
|
return null;
|
|
end if;
|
|
|
|
cleaned := lower(public.unaccent(cleaned));
|
|
cleaned := regexp_replace(cleaned, '[^[:alnum:]\s]+', ' ', 'g');
|
|
cleaned := regexp_replace(cleaned, '\s+', ' ', 'g');
|
|
|
|
tokens := regexp_split_to_array(cleaned, '\s+');
|
|
|
|
select string_agg(token || ':*', ' & ')
|
|
into query_text
|
|
from unnest(tokens) as token
|
|
where token <> '';
|
|
|
|
if query_text is null or query_text = '' then
|
|
return null;
|
|
end if;
|
|
|
|
return to_tsquery('public.es_simple_unaccent', query_text);
|
|
end;
|
|
$$;
|
|
|
|
-- Paso 8
|
|
create or replace function public.search_asignaturas(
|
|
p_search text default '',
|
|
p_facultad_id uuid default null,
|
|
p_carrera_id uuid default null,
|
|
p_plan_estudio_id uuid default null,
|
|
p_limit integer default 20,
|
|
p_offset integer default 0
|
|
)
|
|
returns table (
|
|
id uuid,
|
|
plan_estudio_id uuid,
|
|
codigo text,
|
|
nombre text,
|
|
tipo public.tipo_asignatura,
|
|
creditos numeric,
|
|
numero_ciclo integer,
|
|
datos jsonb,
|
|
contenido_tematico jsonb,
|
|
estado public.estado_asignatura,
|
|
rank real,
|
|
total_count bigint -- 👈 Total para la paginación del frontend
|
|
)
|
|
language plpgsql
|
|
stable
|
|
as $$
|
|
declare
|
|
v_tsq tsquery;
|
|
begin
|
|
-- 1. Construimos el query solo si hay texto
|
|
v_tsq := public.build_asignaturas_prefix_tsquery(p_search);
|
|
|
|
return query
|
|
select
|
|
a.id,
|
|
a.plan_estudio_id,
|
|
a.codigo,
|
|
a.nombre,
|
|
a.tipo,
|
|
a.creditos,
|
|
a.numero_ciclo,
|
|
a.datos,
|
|
a.contenido_tematico,
|
|
a.estado,
|
|
coalesce(ts_rank(a.search_vector, v_tsq), 0)::real as rank,
|
|
count(*) OVER() as total_count -- 👈 Cuenta total ignorando el LIMIT
|
|
from public.asignaturas a
|
|
-- 2. JOINS para poder filtrar por la jerarquía superior
|
|
left join public.planes_estudio p on a.plan_estudio_id = p.id
|
|
left join public.carreras c on p.carrera_id = c.id
|
|
where
|
|
-- 3. Si no hay búsqueda, trae todo. Si hay búsqueda, usa el FTS
|
|
(v_tsq is null or a.search_vector @@ v_tsq)
|
|
-- 4. Filtros jerárquicos dinámicos
|
|
and (p_plan_estudio_id is null or a.plan_estudio_id = p_plan_estudio_id)
|
|
and (p_carrera_id is null or p.carrera_id = p_carrera_id)
|
|
and (p_facultad_id is null or c.facultad_id = p_facultad_id)
|
|
order by
|
|
(case when v_tsq is not null then ts_rank(a.search_vector, v_tsq) else 0 end) desc,
|
|
a.nombre asc
|
|
limit p_limit
|
|
offset p_offset;
|
|
end;
|
|
$$;
|
|
|
|
-- Corrección de bug con triggers
|
|
-- 1. Borramos la llave foránea estricta actual
|
|
ALTER TABLE public.cambios_asignatura
|
|
DROP CONSTRAINT cambios_asignatura_asignatura_id_fkey;
|
|
|
|
-- 2. La volvemos a crear, pero le decimos que espere al final de la transacción para validar
|
|
ALTER TABLE public.cambios_asignatura
|
|
ADD CONSTRAINT cambios_asignatura_asignatura_id_fkey
|
|
FOREIGN KEY (asignatura_id) REFERENCES public.asignaturas(id)
|
|
DEFERRABLE INITIALLY DEFERRED; |