From 3bbc1b9cbb7b1c0e50b6c27b1ad705fbf6404cd5 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Sun, 14 Jun 2026 01:13:29 +0100 Subject: [PATCH] cleanup tsvector usage --- .../base/components/kanban/kanban-column.tsx | 2 +- .../src/ee/base/queries/base-row-query.ts | 20 +-- .../src/ee/base/services/base-service.ts | 2 - apps/client/src/ee/base/types/base.types.ts | 5 - .../migrations/20260529T125146-bases.ts | 125 ------------------ apps/server/src/database/types/db.d.ts | 2 - .../server/src/database/types/entity.types.ts | 15 +-- 7 files changed, 10 insertions(+), 161 deletions(-) diff --git a/apps/client/src/ee/base/components/kanban/kanban-column.tsx b/apps/client/src/ee/base/components/kanban/kanban-column.tsx index a85e9a2fe..95c1a1858 100644 --- a/apps/client/src/ee/base/components/kanban/kanban-column.tsx +++ b/apps/client/src/ee/base/components/kanban/kanban-column.tsx @@ -44,7 +44,7 @@ export function KanbanColumn({ [viewFilter, groupByPropertyId, column.key], ); - const rowsQuery = useBaseRowsQuery(pageId, filter, undefined, undefined); + const rowsQuery = useBaseRowsQuery(pageId, filter, undefined); const createCard = useKanbanCreateCardMutation(); const rows = useMemo(() => { diff --git a/apps/client/src/ee/base/queries/base-row-query.ts b/apps/client/src/ee/base/queries/base-row-query.ts index 9c22361e1..0df2781e1 100644 --- a/apps/client/src/ee/base/queries/base-row-query.ts +++ b/apps/client/src/ee/base/queries/base-row-query.ts @@ -24,7 +24,6 @@ import { DeleteRowsInput, ReorderRowInput, FilterNode, - SearchSpec, ViewSortConfig, RowReferences, NO_VALUE_CHOICE_ID, @@ -59,14 +58,12 @@ export function baseRowsQueryKey( pageId: string | undefined, filter: FilterNode | undefined, sorts: ViewSortConfig[] | undefined, - search: SearchSpec | undefined, ) { return [ "base-rows", pageId, normalizeFilter(filter), sorts?.length ? sorts : undefined, - search?.query ? search : undefined, ] as const; } @@ -87,21 +84,18 @@ export function useBaseRowsQuery( pageId: string | undefined, filter?: FilterNode, sorts?: ViewSortConfig[], - search?: SearchSpec, ) { const activeFilter = normalizeFilter(filter); const activeSorts = sorts?.length ? sorts : undefined; - const activeSearch = search?.query ? search : undefined; const query = useInfiniteQuery({ - queryKey: baseRowsQueryKey(pageId, filter, sorts, search), + queryKey: baseRowsQueryKey(pageId, filter, sorts), queryFn: ({ pageParam }) => listRows(pageId!, { cursor: pageParam, limit: 100, filter: activeFilter, sorts: activeSorts, - search: activeSearch, }), enabled: !!pageId, initialPageParam: undefined as string | undefined, @@ -428,7 +422,6 @@ type KanbanMoveCardInput = { groupByPropertyId: string; destChoiceValue: string | null; position: string; - search?: SearchSpec; }; type KanbanMoveCardContext = { @@ -447,12 +440,12 @@ export function useKanbanMoveCardMutation() { requestId: newRequestId(), }), onMutate: async (variables) => { - const { pageId, rowId, sourceColumnFilter, destColumnFilter, columnChanged, groupByPropertyId, destChoiceValue, position, search } = variables; + const { pageId, rowId, sourceColumnFilter, destColumnFilter, columnChanged, groupByPropertyId, destChoiceValue, position } = variables; await queryClient.cancelQueries({ queryKey: ["base-rows", pageId] }); - const sourceKey = baseRowsQueryKey(pageId, sourceColumnFilter, undefined, search); - const destKey = baseRowsQueryKey(pageId, destColumnFilter, undefined, search); + const sourceKey = baseRowsQueryKey(pageId, sourceColumnFilter, undefined); + const destKey = baseRowsQueryKey(pageId, destColumnFilter, undefined); const sourceSnapshot = queryClient.getQueryData>(sourceKey); const destSnapshot = queryClient.getQueryData>(destKey); @@ -531,7 +524,6 @@ type KanbanCreateCardInput = { groupByPropertyId: string; columnKey: string; position?: string; - search?: SearchSpec; }; export function useKanbanCreateCardMutation() { @@ -545,8 +537,8 @@ export function useKanbanCreateCardMutation() { requestId: newRequestId(), }), onSuccess: (newRow, variables) => { - const { pageId, destColumnFilter, search } = variables; - const destKey = baseRowsQueryKey(pageId, destColumnFilter, undefined, search); + const { pageId, destColumnFilter } = variables; + const destKey = baseRowsQueryKey(pageId, destColumnFilter, undefined); queryClient.setQueryData>(destKey, (old) => { if (!old) return old; const lastPageIndex = old.pages.length - 1; diff --git a/apps/client/src/ee/base/services/base-service.ts b/apps/client/src/ee/base/services/base-service.ts index 856fdba08..dfe998332 100644 --- a/apps/client/src/ee/base/services/base-service.ts +++ b/apps/client/src/ee/base/services/base-service.ts @@ -21,7 +21,6 @@ import { DeleteViewInput, UpdatePropertyResult, FilterNode, - SearchSpec, ViewSortConfig, RowReferences, } from "@/ee/base/types/base.types"; @@ -146,7 +145,6 @@ export async function listRows( limit?: number; filter?: FilterNode; sorts?: ViewSortConfig[]; - search?: SearchSpec; }, ): Promise { const req = await api.post("/bases/rows", { pageId, ...params }); diff --git a/apps/client/src/ee/base/types/base.types.ts b/apps/client/src/ee/base/types/base.types.ts index 1426fcc47..05bb7c4e2 100644 --- a/apps/client/src/ee/base/types/base.types.ts +++ b/apps/client/src/ee/base/types/base.types.ts @@ -188,11 +188,6 @@ export type FilterGroup = { export type FilterNode = FilterCondition | FilterGroup; -export type SearchSpec = { - query: string; - mode?: 'trgm' | 'fts'; -}; - export const NO_VALUE_CHOICE_ID = '__no_value'; export const KANBAN_CARD_DRAG_TYPE = "base-kanban-card"; diff --git a/apps/server/src/database/migrations/20260529T125146-bases.ts b/apps/server/src/database/migrations/20260529T125146-bases.ts index 8dd8f513a..8d7341fb8 100644 --- a/apps/server/src/database/migrations/20260529T125146-bases.ts +++ b/apps/server/src/database/migrations/20260529T125146-bases.ts @@ -79,8 +79,6 @@ export async function up(db: Kysely): Promise { col.notNull().defaultTo(sql`'{}'::jsonb`), ) .addColumn('position', 'varchar', (col) => col.notNull()) - .addColumn('search_text', 'text') - .addColumn('search_tsv', sql`tsvector`) .addColumn('creator_id', 'uuid', (col) => col.references('users.id').onDelete('set null'), ) @@ -117,12 +115,6 @@ export async function up(db: Kysely): Promise { WHERE deleted_at IS NULL `.execute(db); - await sql` - CREATE INDEX IF NOT EXISTS idx_base_rows_search_tsv - ON base_rows USING gin (search_tsv) - WHERE deleted_at IS NULL - `.execute(db); - await db.schema .createTable('base_views') .ifNotExists() @@ -236,121 +228,6 @@ export async function up(db: Kysely): Promise { $$ `.execute(db); - // Resolves select/multiSelect cells to their choice names. The property list - // per page is cached in a transaction-local setting so bulk writes look it up - // once instead of per row. - await sql` - CREATE OR REPLACE FUNCTION build_base_row_search_text( - _cells jsonb, - _page_id uuid - ) RETURNS text - LANGUAGE plpgsql STABLE PARALLEL SAFE - AS $$ - DECLARE - _parts text[] := ARRAY[]::text[]; - _prop jsonb; - _value text; - _arr jsonb; - _elem jsonb; - _resolved text; - _cache_key text; - _cached text; - _props jsonb; - BEGIN - IF _cells IS NULL OR _cells = '{}'::jsonb OR _page_id IS NULL THEN - RETURN NULL; - END IF; - - _cache_key := 'bases.prop_cache_' || replace(_page_id::text, '-', '_'); - _cached := current_setting(_cache_key, true); - IF _cached IS NULL OR _cached = '' THEN - SELECT coalesce( - jsonb_agg(jsonb_build_object( - 'id', id, - 'type', type, - 'type_options', type_options - )), - '[]'::jsonb - ) - INTO _props - FROM base_properties - WHERE page_id = _page_id AND deleted_at IS NULL; - PERFORM set_config(_cache_key, _props::text, true); - ELSE - _props := _cached::jsonb; - END IF; - - FOR _prop IN SELECT * FROM jsonb_array_elements(_props) - LOOP - IF (_prop->>'type') IN ('text', 'url', 'email') THEN - _value := _cells->>(_prop->>'id'); - IF _value IS NOT NULL AND _value <> '' THEN - _parts := array_append(_parts, _value); - END IF; - ELSIF (_prop->>'type') IN ('select', 'status') THEN - _value := _cells->>(_prop->>'id'); - IF _value IS NOT NULL AND _value <> '' THEN - SELECT c->>'name' INTO _resolved - FROM jsonb_array_elements(coalesce(_prop->'type_options'->'choices', '[]'::jsonb)) AS c - WHERE c->>'id' = _value - LIMIT 1; - IF _resolved IS NOT NULL AND _resolved <> '' THEN - _parts := array_append(_parts, _resolved); - END IF; - END IF; - ELSIF (_prop->>'type') = 'multiSelect' THEN - _arr := _cells->(_prop->>'id'); - IF jsonb_typeof(_arr) = 'array' THEN - FOR _elem IN SELECT * FROM jsonb_array_elements(_arr) - LOOP - SELECT c->>'name' INTO _resolved - FROM jsonb_array_elements(coalesce(_prop->'type_options'->'choices', '[]'::jsonb)) AS c - WHERE c->>'id' = _elem#>>'{}' - LIMIT 1; - IF _resolved IS NOT NULL AND _resolved <> '' THEN - _parts := array_append(_parts, _resolved); - END IF; - END LOOP; - END IF; - END IF; - END LOOP; - - IF array_length(_parts, 1) IS NULL THEN - RETURN NULL; - END IF; - - RETURN f_unaccent(array_to_string(_parts, ' ')); - END; - $$ - `.execute(db); - - await sql` - CREATE OR REPLACE FUNCTION base_rows_search_trigger() RETURNS trigger - LANGUAGE plpgsql AS $$ - BEGIN - NEW.search_text := substring(build_base_row_search_text(NEW.cells, NEW.page_id), 1, 25000); - NEW.search_tsv := to_tsvector('english', coalesce(NEW.search_text, '')); - RETURN NEW; - END; - $$ - `.execute(db); - - await sql` - CREATE OR REPLACE TRIGGER base_rows_search_insert - BEFORE INSERT ON base_rows - FOR EACH ROW EXECUTE FUNCTION base_rows_search_trigger() - `.execute(db); - - // Position-only reorders and metadata touches must not pay the - // search_text recompute; OLD is not referenceable on INSERT, hence - // the split triggers. - await sql` - CREATE OR REPLACE TRIGGER base_rows_search_update - BEFORE UPDATE ON base_rows - FOR EACH ROW - WHEN (OLD.cells IS DISTINCT FROM NEW.cells) - EXECUTE FUNCTION base_rows_search_trigger() - `.execute(db); } export async function down(db: Kysely): Promise { @@ -358,8 +235,6 @@ export async function down(db: Kysely): Promise { await db.schema.dropTable('base_rows').execute(); await db.schema.dropTable('base_properties').execute(); - await sql`DROP FUNCTION base_rows_search_trigger()`.execute(db); - await sql`DROP FUNCTION build_base_row_search_text(jsonb, uuid)`.execute(db); await sql`DROP FUNCTION jsonb_set_many(jsonb, jsonb)`.execute(db); await sql`DROP FUNCTION base_cell_array(jsonb, uuid)`.execute(db); await sql`DROP FUNCTION base_cell_bool(jsonb, uuid)`.execute(db); diff --git a/apps/server/src/database/types/db.d.ts b/apps/server/src/database/types/db.d.ts index af8a1e49f..294a0eb2c 100644 --- a/apps/server/src/database/types/db.d.ts +++ b/apps/server/src/database/types/db.d.ts @@ -153,8 +153,6 @@ export interface BaseRows { lastUpdatedById: string | null; pageId: string; position: string; - searchText: string | null; - searchTsv: string | null; updatedAt: Generated; workspaceId: string; } diff --git a/apps/server/src/database/types/entity.types.ts b/apps/server/src/database/types/entity.types.ts index 0419f0382..e8aa0572c 100644 --- a/apps/server/src/database/types/entity.types.ts +++ b/apps/server/src/database/types/entity.types.ts @@ -248,18 +248,9 @@ export type InsertableBaseProperty = Insertable; export type UpdatableBaseProperty = Updateable>; // Base Row -// `searchText` and `searchTsv` are internal fulltext-index columns maintained -// by a trigger. They are omitted from the public types so they never leak into -// HTTP responses or write payloads. -export type BaseRow = Omit, 'searchText' | 'searchTsv'>; -export type InsertableBaseRow = Omit< - Insertable, - 'searchText' | 'searchTsv' ->; -export type UpdatableBaseRow = Omit< - Updateable>, - 'searchText' | 'searchTsv' ->; +export type BaseRow = Selectable; +export type InsertableBaseRow = Insertable; +export type UpdatableBaseRow = Updateable>; // Base View export type BaseView = Selectable;