1- import { installMembershipKey } from '@sim/db/script-migrations/0019_tin_keyword_projection'
1+ import {
2+ installMembershipKey ,
3+ installTinChunkSync ,
4+ } from '@sim/db/script-migrations/0019_tin_keyword_projection'
25import { resolveMigrationDatabaseUrl } from '@sim/db/script-migrations/database-url'
36import type { ScriptMigration } from '@sim/db/script-migrations/types'
47import { createLogger } from '@sim/logger'
@@ -12,10 +15,23 @@ const TRIGGER_LOCK_TIMEOUT = '5s'
1215const TRIGGER_LOCK_RETRY_BUDGET_MS = 20 * 60_000
1316const TRIGGER_LOCK_RETRY_BACKOFF = { baseMs : 2_000 , maxMs : 30_000 } as const
1417
15- /** How both GIN keyword writers bring an existing row up to its chunk, as `0016_backfill_search_vectors` does. */
18+ /** The columns a GIN keyword row copies from its chunk. */
19+ const KEYWORD_SEARCH_COLUMNS = [
20+ 'knowledge_base_id' ,
21+ 'document_id' ,
22+ 'enabled' ,
23+ 'content_tsv' ,
24+ ] as const
25+
26+ /**
27+ * How both GIN keyword writers bring an existing row (aliased `s`) up to its chunk. A row already
28+ * current is left unwritten, as the projector's upserts leave it, so adopting a base whose rows
29+ * survived rewrites only what changed while it holds the membership lock.
30+ */
1631const KEYWORD_SEARCH_UPSERT = `ON CONFLICT (id) DO UPDATE SET
17- knowledge_base_id = EXCLUDED.knowledge_base_id, document_id = EXCLUDED.document_id,
18- enabled = EXCLUDED.enabled, content_tsv = EXCLUDED.content_tsv`
32+ ${ KEYWORD_SEARCH_COLUMNS . map ( ( column ) => `${ column } = EXCLUDED.${ column } ` ) . join ( ', ' ) }
33+ WHERE (${ KEYWORD_SEARCH_COLUMNS . map ( ( column ) => `s.${ column } ` ) . join ( ', ' ) } )
34+ IS DISTINCT FROM (${ KEYWORD_SEARCH_COLUMNS . map ( ( column ) => `EXCLUDED.${ column } ` ) . join ( ', ' ) } )`
1935
2036/**
2137 * The GIN keyword projection's chunk trigger, scoped to search indexes as the Tin projection is:
@@ -36,7 +52,7 @@ async function installKeywordSearchSync(tx: TransactionSql): Promise<void> {
3652 END IF;
3753 RETURN NEW;
3854 END IF;
39- INSERT INTO embedding_keyword_search (id, knowledge_base_id, document_id, enabled, content_tsv)
55+ INSERT INTO embedding_keyword_search AS s (id, knowledge_base_id, document_id, enabled, content_tsv)
4056 VALUES (NEW.id, NEW.knowledge_base_id, NEW.document_id, NEW.enabled, NEW.content_tsv)
4157 ${ KEYWORD_SEARCH_UPSERT } ;
4258 RETURN NEW;
@@ -58,7 +74,7 @@ async function installKeywordSearchMembership(tx: TransactionSql): Promise<void>
5874 DELETE FROM embedding_keyword_search WHERE knowledge_base_id = NEW.id;
5975 RETURN NEW;
6076 END IF;
61- INSERT INTO embedding_keyword_search (id, knowledge_base_id, document_id, enabled, content_tsv)
77+ INSERT INTO embedding_keyword_search AS s (id, knowledge_base_id, document_id, enabled, content_tsv)
6278 SELECT id, knowledge_base_id, document_id, enabled, content_tsv
6379 FROM embedding WHERE knowledge_base_id = NEW.id
6480 ${ KEYWORD_SEARCH_UPSERT } ;
@@ -72,35 +88,13 @@ async function installKeywordSearchMembership(tx: TransactionSql): Promise<void>
7288}
7389
7490/**
75- * The Tin chunk trigger's body from `0019_tin_keyword_projection`, where that migration installed
76- * it, without the delete an insert outside a search index ran: a new chunk id has no Tin row, and a
77- * base adopted meanwhile waits on the membership lock the insert holds.
91+ * The Tin chunk trigger's body as `0019_tin_keyword_projection` now installs it, where that
92+ * migration installed it: without the delete an insert outside a search index ran.
7893 */
7994async function installTinSync ( tx : TransactionSql ) : Promise < void > {
8095 const [ row ] = await tx < Array < { installed : boolean } > > `
8196 SELECT to_regprocedure('sync_embedding_keyword_tin()') IS NOT NULL AS installed`
82- if ( ! row ?. installed ) return
83- await tx . unsafe ( `CREATE OR REPLACE FUNCTION sync_embedding_keyword_tin()
84- RETURNS trigger LANGUAGE plpgsql AS $$
85- BEGIN
86- PERFORM pg_advisory_xact_lock_shared(knowledge_tin_membership_key(NEW.knowledge_base_id));
87- IF NOT EXISTS (
88- SELECT 1 FROM knowledge_base WHERE id = NEW.knowledge_base_id AND is_search_index
89- ) THEN
90- IF TG_OP = 'UPDATE' THEN
91- DELETE FROM embedding_keyword_tin WHERE id = NEW.id;
92- END IF;
93- RETURN NEW;
94- END IF;
95- INSERT INTO embedding_keyword_tin (id, knowledge_base_id, document_id, enabled, content)
96- VALUES (NEW.id, NEW.knowledge_base_id, NEW.document_id, NEW.enabled,
97- knowledge_tin_base_token(NEW.knowledge_base_id) || ' ' || knowledge_tin_stream(NEW.content_tsv))
98- ON CONFLICT (id) DO UPDATE SET
99- knowledge_base_id = EXCLUDED.knowledge_base_id, document_id = EXCLUDED.document_id,
100- enabled = EXCLUDED.enabled, content = EXCLUDED.content;
101- RETURN NEW;
102- END;
103- $$` )
97+ if ( row ?. installed ) await installTinChunkSync ( tx )
10498}
10599
106100/**
0 commit comments