From 542f2bc61a21d8b033f37997c896b98bb36030ad Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Wed, 25 Mar 2026 16:44:11 -0600 Subject: [PATCH] migraciones para soportar el full text search --- supabase/.temp/cli-latest | 2 +- supabase/functions/_shared/database.types.ts | 66 +++++++++++++-- ...606_full_text_search_sobre_asignaturas.sql | 82 +++++++++++-------- 3 files changed, 111 insertions(+), 39 deletions(-) diff --git a/supabase/.temp/cli-latest b/supabase/.temp/cli-latest index 0cce2dc..47c148f 100644 --- a/supabase/.temp/cli-latest +++ b/supabase/.temp/cli-latest @@ -1 +1 @@ -v2.78.1 \ No newline at end of file +v2.84.2 \ No newline at end of file diff --git a/supabase/functions/_shared/database.types.ts b/supabase/functions/_shared/database.types.ts index 35f2364..7f378ed 100644 --- a/supabase/functions/_shared/database.types.ts +++ b/supabase/functions/_shared/database.types.ts @@ -1,4 +1,4 @@ -export type Json = +export type Json = | string | number | boolean @@ -7,10 +7,30 @@ export type Json = | Json[] export type Database = { - // Allows to automatically instantiate createClient with right options - // instead of createClient(URL, KEY) - __InternalSupabase: { - PostgrestVersion: "14.1" + graphql_public: { + Tables: { + [_ in never]: never + } + Views: { + [_ in never]: never + } + Functions: { + graphql: { + Args: { + extensions?: Json + operationName?: string + query?: string + variables?: Json + } + Returns: Json + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } } public: { Tables: { @@ -135,6 +155,7 @@ export type Database = { orden_celda: number | null plan_estudio_id: string prerrequisito_asignatura_id: string | null + search_vector: unknown tipo: Database["public"]["Enums"]["tipo_asignatura"] tipo_origen: Database["public"]["Enums"]["tipo_origen"] | null } @@ -161,6 +182,7 @@ export type Database = { orden_celda?: number | null plan_estudio_id: string prerrequisito_asignatura_id?: string | null + search_vector?: unknown tipo?: Database["public"]["Enums"]["tipo_asignatura"] tipo_origen?: Database["public"]["Enums"]["tipo_origen"] | null } @@ -187,6 +209,7 @@ export type Database = { orden_celda?: number | null plan_estudio_id?: string prerrequisito_asignatura_id?: string | null + search_vector?: unknown tipo?: Database["public"]["Enums"]["tipo_asignatura"] tipo_origen?: Database["public"]["Enums"]["tipo_origen"] | null } @@ -1373,6 +1396,35 @@ export type Database = { Args: { p_append: Json; p_id: string } Returns: undefined } + build_asignaturas_prefix_tsquery: { + Args: { p_search: string } + Returns: unknown + } + recalcular_vectores_asignaturas: { Args: never; Returns: undefined } + search_asignaturas: { + Args: { + p_carrera_id?: string + p_facultad_id?: string + p_limit?: number + p_offset?: number + p_plan_estudio_id?: string + p_search?: string + } + Returns: { + codigo: string + contenido_tematico: Json + creditos: number + datos: Json + estado: Database["public"]["Enums"]["estado_asignatura"] + id: string + nombre: string + numero_ciclo: number + plan_estudio_id: string + rank: number + tipo: Database["public"]["Enums"]["tipo_asignatura"] + total_count: number + }[] + } suma_porcentajes: { Args: { "": Json }; Returns: number } unaccent: { Args: { "": string }; Returns: string } unaccent_immutable: { Args: { "": string }; Returns: string } @@ -1548,6 +1600,9 @@ export type CompositeTypes< : never export const Constants = { + graphql_public: { + Enums: {}, + }, public: { Enums: { estado_asignatura: ["borrador", "revisada", "aprobada", "generando"], @@ -1607,3 +1662,4 @@ export const Constants = { }, }, } as const + diff --git a/supabase/migrations/20260325024606_full_text_search_sobre_asignaturas.sql b/supabase/migrations/20260325024606_full_text_search_sobre_asignaturas.sql index 12638b9..9c737f6 100644 --- a/supabase/migrations/20260325024606_full_text_search_sobre_asignaturas.sql +++ b/supabase/migrations/20260325024606_full_text_search_sobre_asignaturas.sql @@ -52,27 +52,18 @@ for each row execute function public.fn_asignaturas_update_search_vector(); -- Paso 5 -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' - ); +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 @@ -117,7 +108,9 @@ $$; -- Paso 8 create or replace function public.search_asignaturas( - p_search text, + 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 @@ -133,14 +126,19 @@ returns table ( datos jsonb, contenido_tematico jsonb, estado public.estado_asignatura, - rank real + rank real, + total_count bigint -- 👈 Total para la paginación del frontend ) -language sql +language plpgsql stable as $$ - with q as ( - select public.build_asignaturas_prefix_tsquery(p_search) as tsq - ) +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, @@ -152,16 +150,34 @@ as $$ a.datos, a.contenido_tematico, a.estado, - ts_rank(a.search_vector, q.tsq) as rank + 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 - cross join q + -- 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 - q.tsq is not null - and a.search_vector @@ q.tsq + -- 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 - rank desc, + (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; -$$; \ No newline at end of file +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; \ No newline at end of file