From af00b1eb99a5888924c5d51b736eef9f142f1ebf Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Tue, 17 Mar 2026 08:57:06 +0000 Subject: [PATCH 1/5] feat: scope trgm operators to tables with intentional search - Change createTrgmOperatorFactories() to target 'StringTrgm' type instead of global 'String', so trgm operators register on StringTrgmFilter rather than the shared StringFilter used by every string column everywhere. - Register StringTrgmFilter type in unified search plugin init hook with pgConnectionFilterOperators scope so standard operators auto-propagate. - In GraphQLInputObjectType_fields hook, swap StringFilter to StringTrgmFilter for string attribute fields on tables that qualify for trgm (via the existing supplementary adapter gating logic). - Add @trgmSearch smart tag support: tables or columns tagged with @trgmSearch will activate supplementary adapters (including trgm) even without tsvector or BM25 intentional search infrastructure. --- .../src/codecs/operator-factories.ts | 10 ++- graphile/graphile-search/src/plugin.ts | 64 ++++++++++++++++++- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/graphile/graphile-search/src/codecs/operator-factories.ts b/graphile/graphile-search/src/codecs/operator-factories.ts index 2ad3c6079..a8e8a80b0 100644 --- a/graphile/graphile-search/src/codecs/operator-factories.ts +++ b/graphile/graphile-search/src/codecs/operator-factories.ts @@ -49,6 +49,12 @@ export function createMatchesOperatorFactory( * Creates the `similarTo` and `wordSimilarTo` filter operator factories * for pg_trgm fuzzy text matching. Declared here so they're registered * via the declarative `connectionFilterOperatorFactories` API. + * + * These operators target 'StringTrgm' (resolved to 'StringTrgmFilter'), + * NOT the global 'String' type. The unified search plugin registers + * 'StringTrgmFilter' and selectively assigns it to string columns on + * tables that qualify for trgm (via intentional search or @trgmSearch tag). + * This prevents trgm operators from appearing on every string field. */ export function createTrgmOperatorFactories(): ConnectionFilterOperatorFactory { return (build) => { @@ -56,7 +62,7 @@ export function createTrgmOperatorFactories(): ConnectionFilterOperatorFactory { return [ { - typeNames: 'String', + typeNames: 'StringTrgm', operatorName: 'similarTo', spec: { description: @@ -81,7 +87,7 @@ export function createTrgmOperatorFactories(): ConnectionFilterOperatorFactory { }, }, { - typeNames: 'String', + typeNames: 'StringTrgm', operatorName: 'wordSimilarTo', spec: { description: diff --git a/graphile/graphile-search/src/plugin.ts b/graphile/graphile-search/src/plugin.ts index 0758705d9..db66ddb6c 100644 --- a/graphile/graphile-search/src/plugin.ts +++ b/graphile/graphile-search/src/plugin.ts @@ -122,10 +122,18 @@ export function createUnifiedSearchPlugin( } } - // Phase 2: Only run supplementary adapters if at least one primary - // adapter with isIntentionalSearch found columns on this codec. + // Phase 2: Run supplementary adapters if intentional search exists + // OR if the table/column has a @trgmSearch smart tag. // pgvector (isIntentionalSearch: false) alone won't trigger trgm. - if (hasIntentionalSearch) { + const hasTrgmSearchTag = + // Table-level tag + (codec.extensions as any)?.tags?.trgmSearch || + // Column-level tag + (codec.attributes && Object.values(codec.attributes as Record).some( + (attr: any) => attr?.extensions?.tags?.trgmSearch + )); + + if (hasIntentionalSearch || hasTrgmSearchTag) { for (const adapter of supplementaryAdapters) { const columns = adapter.detectColumns(codec, build); if (columns.length > 0) { @@ -149,6 +157,8 @@ export function createUnifiedSearchPlugin( 'PgConnectionArgFilterAttributesPlugin', 'PgConnectionArgFilterOperatorsPlugin', 'AddConnectionFilterOperatorPlugin', + 'ConnectionFilterTypesPlugin', + 'ConnectionFilterCustomOperatorsPlugin', // Allow individual codec plugins to load first (e.g. Bm25CodecPlugin) 'Bm25CodecPlugin', 'VectorCodecPlugin', @@ -229,6 +239,34 @@ export function createUnifiedSearchPlugin( for (const adapter of adapters) { adapter.registerTypes(build); } + + // Register StringTrgmFilter — a variant of StringFilter that includes + // trgm operators (similarTo, wordSimilarTo). Only string columns on + // tables that qualify for trgm will use this type instead of StringFilter. + const hasTrgmAdapter = adapters.some((a) => a.name === 'trgm'); + if (hasTrgmAdapter) { + const DPTYPES = (build as any).dataplanPg?.TYPES; + const textCodec = DPTYPES?.text ?? TYPES.text; + build.registerInputObjectType( + 'StringTrgmFilter', + { + pgConnectionFilterOperators: { + isList: false, + pgCodecs: [textCodec], + inputTypeName: 'String', + rangeElementInputTypeName: null, + domainBaseTypeName: null, + }, + }, + () => ({ + description: + 'A filter to be used against String fields with pg_trgm support. ' + + 'All fields are combined with a logical \u2018and.\u2019', + }), + 'UnifiedSearchPlugin (StringTrgmFilter)' + ); + } + return _; }, @@ -610,6 +648,26 @@ export function createUnifiedSearchPlugin( let newFields = fields; + // ── StringFilter → StringTrgmFilter type swapping ── + // For tables that qualify for trgm, swap the type of string attribute + // filter fields so they get similarTo/wordSimilarTo operators. + const hasTrgm = adapterColumns.some((ac) => ac.adapter.name === 'trgm'); + if (hasTrgm) { + const StringTrgmFilterType = build.getTypeByName('StringTrgmFilter'); + const StringFilterType = build.getTypeByName('StringFilter'); + if (StringTrgmFilterType && StringFilterType) { + const swapped: Record = {}; + for (const [key, field] of Object.entries(newFields)) { + if (field && typeof field === 'object' && (field as any).type === StringFilterType) { + swapped[key] = Object.assign({}, field, { type: StringTrgmFilterType }); + } else { + swapped[key] = field; + } + } + newFields = swapped; + } + } + for (const { adapter, columns } of adapterColumns) { for (const column of columns) { const fieldName = inflection.camelCase( From f08a328bf4c3d0e0a59ac9253d1ecb9f7772a355 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Tue, 17 Mar 2026 09:08:23 +0000 Subject: [PATCH 2/5] fix: update snapshots to reflect trgm operators removed from StringFilter --- .../schema-snapshot.test.ts.snap | 10 ---------- .../__snapshots__/graphile-test.test.ts.snap | 20 ------------------- 2 files changed, 30 deletions(-) diff --git a/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap b/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap index b70e8d045..711711d8f 100644 --- a/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap +++ b/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap @@ -666,16 +666,6 @@ input StringFilter { """Greater than or equal to the specified value (case-insensitive).""" greaterThanOrEqualToInsensitive: String - - """ - Fuzzy matches using pg_trgm trigram similarity. Tolerates typos and misspellings. - """ - similarTo: TrgmSearchInput - - """ - Fuzzy matches using pg_trgm word_similarity. Finds the best matching substring within the column value. - """ - wordSimilarTo: TrgmSearchInput } """ diff --git a/graphql/test/__tests__/__snapshots__/graphile-test.test.ts.snap b/graphql/test/__tests__/__snapshots__/graphile-test.test.ts.snap index d2fae7909..23d706fb3 100644 --- a/graphql/test/__tests__/__snapshots__/graphile-test.test.ts.snap +++ b/graphql/test/__tests__/__snapshots__/graphile-test.test.ts.snap @@ -1121,26 +1121,6 @@ based pagination. May not be used with \`last\`.", "ofType": null, }, }, - { - "defaultValue": null, - "description": "Fuzzy matches using pg_trgm trigram similarity. Tolerates typos and misspellings.", - "name": "similarTo", - "type": { - "kind": "INPUT_OBJECT", - "name": "TrgmSearchInput", - "ofType": null, - }, - }, - { - "defaultValue": null, - "description": "Fuzzy matches using pg_trgm word_similarity. Finds the best matching substring within the column value.", - "name": "wordSimilarTo", - "type": { - "kind": "INPUT_OBJECT", - "name": "TrgmSearchInput", - "ofType": null, - }, - }, ], "interfaces": null, "kind": "INPUT_OBJECT", From 63e5593e03a3fd9df0431a70d0ea48c6ec188a1c Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Tue, 17 Mar 2026 09:14:42 +0000 Subject: [PATCH 3/5] fix: remove TrgmSearchInput type from snapshots (no longer registered without intentional search) --- .../schema-snapshot.test.ts.snap | 15 +------ .../__snapshots__/graphile-test.test.ts.snap | 45 ------------------- 2 files changed, 1 insertion(+), 59 deletions(-) diff --git a/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap b/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap index 711711d8f..77a04915b 100644 --- a/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap +++ b/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap @@ -669,20 +669,7 @@ input StringFilter { } """ -Input for pg_trgm fuzzy text matching. Provide a search value and optional similarity threshold. -""" -input TrgmSearchInput { - """The text to fuzzy-match against. Typos and misspellings are tolerated.""" - value: String! - - """ - Minimum similarity threshold (0.0 to 1.0). Higher = stricter matching. Default is 0.3. - """ - threshold: Float -} - -""" -A filter to be used against Boolean fields. All fields are combined with a logical ‘and.’ +A filter to be used against Boolean fields. All fields are combined with a logical 'and.'.’ """ input BooleanFilter { """ diff --git a/graphql/test/__tests__/__snapshots__/graphile-test.test.ts.snap b/graphql/test/__tests__/__snapshots__/graphile-test.test.ts.snap index 23d706fb3..d3c1f0f58 100644 --- a/graphql/test/__tests__/__snapshots__/graphile-test.test.ts.snap +++ b/graphql/test/__tests__/__snapshots__/graphile-test.test.ts.snap @@ -1127,51 +1127,6 @@ based pagination. May not be used with \`last\`.", "name": "StringFilter", "possibleTypes": null, }, - { - "description": "Input for pg_trgm fuzzy text matching. Provide a search value and optional similarity threshold.", - "enumValues": null, - "fields": null, - "inputFields": [ - { - "defaultValue": null, - "description": "The text to fuzzy-match against. Typos and misspellings are tolerated.", - "name": "value", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null, - }, - }, - }, - { - "defaultValue": null, - "description": "Minimum similarity threshold (0.0 to 1.0). Higher = stricter matching. Default is 0.3.", - "name": "threshold", - "type": { - "kind": "SCALAR", - "name": "Float", - "ofType": null, - }, - }, - ], - "interfaces": null, - "kind": "INPUT_OBJECT", - "name": "TrgmSearchInput", - "possibleTypes": null, - }, - { - "description": "The \`Float\` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).", - "enumValues": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "kind": "SCALAR", - "name": "Float", - "possibleTypes": null, - }, { "description": "Methods to use when ordering \`User\`.", "enumValues": [ From 0b77a70843939388a3d5e526891c3e4179dd267b Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Tue, 17 Mar 2026 09:20:28 +0000 Subject: [PATCH 4/5] fix: correct typo in server-test snapshot (extra period) --- .../__tests__/__snapshots__/schema-snapshot.test.ts.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap b/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap index 77a04915b..4ac1ca6a5 100644 --- a/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap +++ b/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap @@ -669,7 +669,7 @@ input StringFilter { } """ -A filter to be used against Boolean fields. All fields are combined with a logical 'and.'.’ +A filter to be used against Boolean fields. All fields are combined with a logical 'and.' """ input BooleanFilter { """ From f47c0c5399b4b3cb7f6d98be01c90ff47a4f229c Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Tue, 17 Mar 2026 09:27:54 +0000 Subject: [PATCH 5/5] fix: use unicode smart quotes in BooleanFilter snapshot description --- .../__tests__/__snapshots__/schema-snapshot.test.ts.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap b/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap index 4ac1ca6a5..5de8a96a9 100644 --- a/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap +++ b/graphql/server-test/__tests__/__snapshots__/schema-snapshot.test.ts.snap @@ -669,7 +669,7 @@ input StringFilter { } """ -A filter to be used against Boolean fields. All fields are combined with a logical 'and.' +A filter to be used against Boolean fields. All fields are combined with a logical ‘and.’ """ input BooleanFilter { """