Skip to content

feat: v5-native filter system — complete rewrite with declarative API, full test coverage, and new plugins#813

Open
pyramation wants to merge 42 commits intomainfrom
devin/1773429791-v5-filter-system-complete
Open

feat: v5-native filter system — complete rewrite with declarative API, full test coverage, and new plugins#813
pyramation wants to merge 42 commits intomainfrom
devin/1773429791-v5-filter-system-complete

Conversation

@pyramation
Copy link
Contributor

@pyramation pyramation commented Mar 13, 2026

feat: v5-native filter system — complete rewrite with declarative API, tests, and new plugins

Summary

Replaces the upstream v4-ported postgraphile-plugin-connection-filter with a from-scratch v5-native implementation and migrates the entire filter stack to a declarative operator API. This is the combined work of 11 incremental PRs (#797#812) squashed into a single merge.

Core changes:

  • New graphile-connection-filter package (~2,000 lines): scalar operators, logical operators (and/or/not), relation filters, computed column filters, array filters, pattern matching, all built on v5 behavior system
  • Declarative connectionFilterOperatorFactories API: replaces imperative addConnectionFilterOperator. Satellite plugins declare operators in their preset config instead of mutating build state at runtime
  • conditionfilter migration: all GraphQL queries (including send-email-link function) migrated off condition:
  • PostGIS filter consolidation: graphile-plugin-connection-filter-postgis merged into graphile-postgis
  • New graphile-trgm plugin: pg_trgm fuzzy text matching with similarTo/wordSimilarTo filter operators and similarity scoring
  • Package renames — all plugin packages renamed to match their underlying algorithm:
    • graphile-search-plugingraphile-tsvector
    • graphile-pg-textsearch-plugingraphile-bm25
    • graphile-pg-trgm-plugingraphile-trgm
    • graphile-pgvector-plugingraphile-pgvector
  • Field naming convention refactor: All generated GraphQL field names now follow {column}{Algorithm}{Metric} pattern with algorithm deduplication
    • bm25: bm25BodyScorebodyBm25Score, BM25_BODY_SCORE_ASCBODY_BM25_SCORE_ASC
    • tsvector: bodyRankbodyTsvRank, BODY_RANK_ASCBODY_TSV_RANK_ASC
    • trgm: nameSimilaritynameTrgmSimilarity, SIMILARITY_NAME_ASCNAME_TRGM_SIMILARITY_ASC
    • pgvector: embeddingDistanceembeddingVectorDistance, EMBEDDING_DISTANCE_ASCEMBEDDING_VECTOR_DISTANCE_ASC
    • Filter input fields keep {algorithm}{Column} pattern (e.g. bm25Body)
    • Deduplication: When column names already end with the algorithm suffix (e.g. full_text_tsv), the suffix is not duplicated in generated names (e.g. fullTextTsvRank not fullTextTsvTsvRank)
  • Integration test suite: mega query exercising BM25 + tsvector + PostGIS + pgvector + trgm + relation filters simultaneously
  • CI image: pyramation/postgres:17ghcr.io/constructive-io/docker/postgres-plus:18

Test coverage: 84 runtime tests in connection-filter (scalar ops, array ops, negation, string ops, declarative factory, edge cases) + 35 integration tests in graphile-settings.

Updates since last revision

Schema snapshot fix (latest):

  • Fixed remaining SIMILARITY_EMAIL_ASC/SIMILARITY_EMAIL_DESC orderBy enums → EMAIL_TRGM_SIMILARITY_ASC/EMAIL_TRGM_SIMILARITY_DESC in the schema snapshot file. This was the last CI-blocking snapshot mismatch — all 42/42 checks now pass.

Field naming convention refactor (previous):

  • Score fields: Changed from {algorithm}{Column}{Metric} to {column}{Algorithm}{Metric} pattern (e.g. bm25BodyScorebodyBm25Score)
  • OrderBy enums: Changed from {ALGORITHM}_{COLUMN}_{METRIC} to {COLUMN}_{ALGORITHM}_{METRIC} pattern (e.g. BM25_BODY_SCORE_ASCBODY_BM25_SCORE_ASC)
  • Filter inputs: Unchanged — still use {algorithm}{Column} pattern (e.g. bm25Body) to group related search operators
  • Deduplication logic: All inflection methods now check if the column/field name already ends with the algorithm name (case-insensitive) and avoid duplicating the suffix
  • trgm filterPrefix option: Added configurable filterPrefix option to TrgmSearchPluginOptions interface (defaults to 'trgm')
  • Updated all test files: Used bulk sed replacements to update all field name references in bm25-search.test.ts, trgm-search.test.ts, vector-search.test.ts, and preset-integration.test.ts
  • Updated READMEs and comments: Fixed GraphQL query examples and source code comments to use new field names

Doc & naming consistency fixes (earlier):

  • GRAPHILE.md: Added missing package entries for graphile-bm25, graphile-trgm, and graphile-connection-filter with descriptions
  • bm25 conditionPrefixfilterPrefix: Renamed the plugin option in types.ts, bm25-search.ts, preset.ts, and the test file. This is a breaking change to the Bm25SearchPluginOptions interface — any code passing conditionPrefix will now silently fall back to the 'bm25' default
  • bm25 README: Updated all GraphQL examples from condition: to filter:, changed "Condition fields" → "Filter fields" in feature list
  • tsvector README: Updated GraphQL example from condition: to filter:

Package renames (earlier): Renamed 4 plugin directories and updated all package.json name fields, TypeScript imports in graphile-settings (constructive-preset.ts, plugins/index.ts), CI workflow matrix entries, README docs, source code JSDoc comments, and GRAPHILE.md. CHANGELOG.md files intentionally left unchanged (historical). pnpm install regenerated the lockfile and pnpm build passes clean.

Review & Testing Checklist for Human

⚠️ High-risk items (must verify):

  • Generated field name breaking changes — ALL GraphQL queries using the old score field names (bm25BodyScore, nameSimilarity, embeddingDistance, bodyRank) or orderBy enums (BM25_BODY_SCORE_ASC, SIMILARITY_NAME_DESC, etc.) will break immediately. Search ALL consumer codebases (internal apps, client queries, generated types) for these old names and update them. No deprecation warnings were added. This affects every query that uses search/scoring plugins.

  • Deduplication edge cases — The dedup logic uses case-insensitive .endsWith() checks. Verify this doesn't cause false positives for columns like mybm25 (would incorrectly trigger bm25 dedup) or embedding_vector (would incorrectly trigger pgvector dedup). Test with real schema column names to ensure dedup only applies to genuine algorithm suffix columns like full_text_tsv or body_bm25.

  • README feature list inconsistencies — The BM25 README feature list bullet points (lines showing "Score fields" and "OrderBy") were NOT updated to match the new naming convention. They still show old patterns like bm25<Column>Score and BM25_<COLUMN>_SCORE_ASC/DESC instead of <column>Bm25Score and <COLUMN>_BM25_SCORE_ASC/DESC. The query examples are correct but the feature descriptions are wrong. Similar issue may exist in other plugin READMEs.

  • Bulk sed replacement accuracy — All field name changes were done with sed find-and-replace across 11 files. Manually audit at least one test file (e.g. bm25-search.test.ts) to verify the replacements were contextually correct and didn't over-match strings that shouldn't have been changed (e.g. test descriptions, variable names that happened to contain the old field names).

  • Package rename completeness — Verify no stale references to old package names (graphile-search-plugin, graphile-pg-textsearch-plugin, graphile-pg-trgm-plugin, graphile-pgvector-plugin) remain in any TypeScript imports, package.json dependencies, or CI config. Grep the repo excluding CHANGELOG.md, pnpm-lock.yaml, and node_modules/. Any missed reference will cause build or runtime failures.

🟡 Medium-risk items:

  • conditionPrefixfilterPrefix breaking change — The bm25 plugin option was renamed. Any external code (other repos, consumer apps) that passes { conditionPrefix: 'custom' } to createBm25SearchPlugin() will silently ignore the option and fall back to the 'bm25' default. Search for any usage of this option in consumer codebases and update them. No deprecation warning was added.

  • connectionFilterAllowEmptyObjectInput removal — This option was removed in favor of applyPlan-level handling. If any downstream code (outside this repo) depended on setting this to false, it will break. Search for any references to this option in consumer apps.

  • condition deprecation impact — All queries in this repo migrated to filter:, but external consumers (other repos, client apps) that use condition: will break. No deprecation warnings were added. Document the breaking change and plan a migration guide.

  • Breaking change: package renames — Any code importing by the old package names (graphile-search-plugin, etc.) will break immediately. No re-export aliases were added. Consumers must update imports. Verify the lockfile correctly maps new workspace package names to the renamed directories.

  • Filter input naming pattern preservation — Verify that filter inputs still use the {algorithm}{Column} pattern (e.g. bm25Body, trgmName) as intended, and the refactor only affected score/orderBy naming. Test a query with filter inputs to ensure they still work and match the convention documented in READMEs.

Testing plan:

  1. Local test run: Set up local Postgres with PostGIS + pgvector + pg_trgm extensions, run pnpm test in graphile/graphile-connection-filter and graphile/graphile-settings
  2. GraphQL playground smoke test: Start dev server, run the mega query from graphile/graphile-settings/__tests__/integration.test.ts:126-174 with the NEW field names and verify results match expected
  3. Schema introspection: Query __type(name: "ItemFilter") and verify all expected operators appear, then query __type(name: "Article") and verify score field names follow new pattern (e.g. bodyBm25Score not bm25BodyScore)
  4. Package resolution verification: Run pnpm list graphile-tsvector graphile-bm25 graphile-trgm graphile-pgvector to confirm workspace packages resolve to the renamed directories
  5. Breaking change audit: Search codebase for condition: usage, old field names (bm25BodyScore, nameSimilarity), old enum names (BM25_BODY_SCORE_ASC), old package names, conditionPrefix option usage, and connectionFilterAllowEmptyObjectInput
  6. Dedup logic verification: Test queries with columns that have algorithm suffixes (e.g. full_text_tsv) and verify generated field names don't double the suffix (e.g. should be fullTextTsvRank not fullTextTsvTsvRank)

Notes

  • The old graphile-plugin-connection-filter-postgis package is deleted (merged into graphile-postgis)
  • The pnpm-lock.yaml diff is mostly formatting churn (pnpm version bump) — actual dependency changes are minimal
  • Seven dead directories exist in graphile/ (graphile-i18n, graphile-many-to-many, graphile-meta-schema, graphile-pg-type-mappings, graphile-plugin-connection-filter, graphile-plugin-fulltext-filter, graphile-simple-inflector) — these are just local build artifacts (dist/, node_modules/), not tracked in git
  • The isLongerThan custom operator test uses build.dataplanPg.TYPES.int and build.sql — if these property paths ever change in PostGraphile core, operators will silently fail to register
  • Package renames, conditionPrefixfilterPrefix change, and field name convention changes are breaking changes for any external consumers. CHANGELOG updates and npm publish will need to reflect the new package names, option names, and generated field names.
  • The field naming convention was designed to be self-documenting (bodyBm25Score clearly shows it's the BM25 score of the body column) and avoid collisions between different algorithms on the same column
  • Filter inputs intentionally keep the {algorithm}{Column} pattern to group related search operators together in the schema (e.g. all BM25 filters start with bm25)

Session: https://app.devin.ai/sessions/cf88f3fd383b4421a5169ed01612899d
Requested by: @pyramation

Implements a from-scratch PostGraphile v5 native connection filter plugin,
replacing the upstream postgraphile-plugin-connection-filter dependency.

New package: graphile/graphile-connection-filter/

Plugin architecture (7 plugins):
- ConnectionFilterInflectionPlugin: filter type naming conventions
- ConnectionFilterTypesPlugin: registers per-table and per-scalar filter types
- ConnectionFilterArgPlugin: injects filter arg on connections via applyPlan
- ConnectionFilterAttributesPlugin: adds per-column filter fields
- ConnectionFilterOperatorsPlugin: standard/sort/pattern/jsonb/inet/array/range operators
- ConnectionFilterCustomOperatorsPlugin: addConnectionFilterOperator API for satellite plugins
- ConnectionFilterLogicalOperatorsPlugin: and/or/not logical composition

Key features:
- Full v5 native: uses Grafast planning, PgCondition, codec system, behavior registry
- EXPORTABLE pattern for schema caching
- Preserves addConnectionFilterOperator API for PostGIS, search, pgvector, textsearch plugins
- No relation filter plugins (simplifies configuration vs upstream)
- Preset factory: ConnectionFilterPreset(options)

Also updates graphile-settings to use the new workspace package.
…Operator

filterType is for table-level filter types (UserFilter), while filterFieldType
is for scalar operator types (StringFilter). Satellite plugins pass scalar type
names, so the lookup must use filterFieldType to match the registration in
ConnectionFilterTypesPlugin. Previously worked by coincidence since both
inflections produce the same output, but would silently fail if a consumer
overrode one inflection but not the other.
Adds computed column filter support — allows filtering on PostgreSQL functions
that take a table row as their first argument and return a scalar.

Controlled by connectionFilterComputedColumns schema option. The preset factory
includes the plugin only when the option is truthy (default in preset: true,
but constructive-preset sets it to false).
- Remove phantom postgraphile-plugin-connection-filter dep from graphile-pgvector-plugin (never used)
- Remove phantom postgraphile-plugin-connection-filter dep from graphile-pg-textsearch-plugin (never used)
- Update graphile-plugin-connection-filter-postgis to use graphile-connection-filter workspace dep with typed imports
- Update graphile-search-plugin to use graphile-connection-filter workspace dep with typed imports
- Replace (build as any).addConnectionFilterOperator casts with properly typed build.addConnectionFilterOperator
…on-filter

- Update search plugin, pgvector, and postgis test files to import from
  graphile-connection-filter instead of postgraphile-plugin-connection-filter
- Use ConnectionFilterPreset() factory instead of PostGraphileConnectionFilterPreset
- Import ConnectionFilterOperatorSpec type from graphile-connection-filter
- Fix smart quote characters in filter descriptions to match existing snapshots
…ion filter tests

- Add graphile-connection-filter as devDependency in graphile-pgvector-plugin
  (test file imports ConnectionFilterPreset but package had no dependency)
- Skip connectionFilterRelations tests in search plugin (relation filters
  are intentionally not included in the v5-native plugin; they were disabled
  in production via disablePlugins with the old plugin)
…toggle

- ConnectionFilterForwardRelationsPlugin: filter by FK parent relations
- ConnectionFilterBackwardRelationsPlugin: filter by backward relations (one-to-one + one-to-many with some/every/none)
- connectionFilterRelations toggle in preset (default: false)
- Un-skip relation filter tests in search plugin
- Updated augmentations, types, and exports
… at runtime

The preset factory now always includes relation plugins in the plugin list.
Each plugin checks build.options.connectionFilterRelations at runtime and
early-returns if disabled. This allows the toggle to be set by any preset
in the chain, not just the ConnectionFilterPreset() call.
Enables relation filter fields in the production schema:
- Forward: filter by FK parent (e.g. clientByClientId on OrderFilter)
- Backward: filter by children with some/every/none
- Codegen will pick up the new filter fields automatically
- Search plugin: isPgCondition → isPgConnectionFilter scope
- BM25 plugin: isPgCondition → isPgConnectionFilter scope
- Disable PgConditionArgumentPlugin and PgConditionCustomFieldsPlugin in preset
- Update all tests from condition: {...} to filter: {...}
- Add graphile-connection-filter devDependency to BM25 plugin
- Update search plugin graceful degradation tests to use filter

BREAKING CHANGE: The condition argument has been removed entirely.
All filtering now uses the filter argument exclusively.
- Search plugin plugin.test.ts: condition → filter syntax, add ConnectionFilterPreset
- Server-test: condition → filter in query with equalTo operator
- Clear stale snapshots (schema-snapshot, introspection) for regeneration
- Search plugin: update snapshot keys to match renamed filter-based tests
- Schema snapshot: remove all condition arguments and XxxCondition input types
- Introspection snapshot: remove condition arg and UserCondition type
- Kept conditionType in _meta schema (unrelated to deprecated condition arg)
… behavior for pgCodecRelation, update schema snapshot with relation filter types
…y filter at applyPlan level

Top-level empty filter {} is now treated as 'no filter' (skipped) instead of
throwing an error. Nested empty objects in and/or/not and relation filters are
still rejected. This removes the need for the connectionFilterAllowEmptyObjectInput
workaround in pgvector tests.
- Extract shared getQueryBuilder utility into graphile-connection-filter/src/utils.ts
- Remove duplicate getQueryBuilder from search, BM25, and pgvector plugins
- Replace (build as any).dataplanPg with build.dataplanPg (already typed on Build)
- Replace (build as any).behavior with build.behavior (already typed on Build)
- Replace (build as any).input.pgRegistry with build.input.pgRegistry (already typed)
- Remove scope destructuring as any casts (pgCodec already typed on ScopeInputObject)
- Add pgCodec comment to augmentations.ts noting it's already declared by graphile-build-pg
- Export getQueryBuilder from graphile-connection-filter for satellite plugin use
Adds index safety check for relation filter fields. When enabled (default: true),
relation filter fields are only created for FKs with supporting indexes.
This prevents generating EXISTS subqueries that would cause sequential scans
on large tables.

Uses PgIndexBehaviorsPlugin's existing relation.extensions.isIndexed metadata
which is set at gather time. The check runs at schema build time with zero
runtime cost.

Applied to both forward and backward relation filter plugins.
Comprehensive test coverage using graphile-test infrastructure:
- Scalar operators: equalTo, notEqualTo, distinctFrom, isNull, in/notIn,
  lessThan, greaterThan, like, iLike, includes, startsWith, endsWith
- Logical operators: and, or, not, nested combinations
- Relation filters: forward (child->parent), backward one-to-one,
  backward one-to-many (some/every/none), exists fields
- Computed column filters
- Schema introspection: filter types, operator fields, relation fields
- Options toggles: connectionFilterRelations, connectionFilterComputedColumns,
  connectionFilterLogicalOperators, connectionFilterAllowedOperators,
  connectionFilterOperatorNames

Also adds graphile/graphile-connection-filter to CI matrix (41 jobs).
Exercises multiple plugins working together in a single test database:
- Connection filter (scalar operators, logical operators, relation filters)
- PostGIS spatial filters (geometry column)
- pgvector (vector column, search function, distance ordering)
- tsvector search plugin (fullText matches, rank, orderBy)
- BM25 search (pg_textsearch body index, score, orderBy)
- Kitchen sink queries combining multiple plugins

34 test cases across 8 describe blocks, all passing locally.
Added postgres-plus CI job for tests requiring PostGIS/pgvector/pg_textsearch.
… test

The mega query now exercises all SIX plugin types in a single filter:
- tsvector (fullTextTsv)
- BM25 (bm25Body)
- relation filter (category name)
- scalar filter (isActive)
- pgvector (vectorEmbedding nearby)
- PostGIS (geom intersects polygon bbox)

Also validates returned coordinates fall within the bounding box.
New package: graphile-pg-trgm-plugin — a PostGraphile v5 plugin for pg_trgm
trigram-based fuzzy text matching. Zero config, works on any text column.

Features:
- similarTo / wordSimilarTo filter operators on StringFilter
- trgm<Column> direct filter fields on connection filter types
- <column>Similarity computed score fields (0-1, null when inactive)
- SIMILARITY_<COLUMN>_ASC/DESC orderBy enum values
- TrgmSearchPreset for easy composition into presets
- connectionFilterTrgmRequireIndex option (default: false)
- 14 dedicated tests + integrated into mega query as 7th plugin type

Mega query now exercises ALL 7 plugin types in one GraphQL query:
tsvector + BM25 + pgvector + PostGIS + pg_trgm + relation filter + scalar
Updated introspection and SDL snapshots to include new fields from
TrgmSearchPlugin: similarTo/wordSimilarTo operators on StringFilter,
*Similarity computed fields, trgm* filter fields, and SIMILARITY_*
orderBy enum values.
- orderBy: [BM25_BODY_SCORE_ASC, SIMILARITY_NAME_DESC] demonstrates
  multi-signal relevance ranking in a single query
- Added comprehensive JSDoc explaining all 7 plugin types, the 2-phase
  meta system, and ORDER BY priority semantics
- Inline GraphQL comments explain each filter and score field
- Assertion verifies BM25 ASC ordering (primary sort)
- Documents important subtlety: ORDER BY priority follows schema field
  processing order, not the orderBy array order
… connectionFilterOperatorFactories

Migrates all 3 satellite plugins (search, pg_trgm, PostGIS) from the
imperative build.addConnectionFilterOperator() API to the new declarative
connectionFilterOperatorFactories preset configuration.

Changes:
- Core: Add ConnectionFilterOperatorFactory type, ConnectionFilterOperatorRegistration
  interface, and connectionFilterOperatorFactories schema option
- Core: ConnectionFilterCustomOperatorsPlugin processes factories during init hook
- Search plugin: Extract matches operator into createMatchesOperatorFactory
- pg_trgm plugin: Extract similarTo/wordSimilarTo into createTrgmOperatorFactories
- PostGIS plugin: Refactor entire plugin into createPostgisOperatorFactory factory
- PostGIS tests: Rewrite to call factory directly instead of mocking init hook
- README: Update custom operator docs to show declarative API
- Remove all imperative addConnectionFilterOperator calls from satellite plugins
…t to fix array merging

graphile-config replaces (not concatenates) arrays when merging presets,
so each satellite plugin's connectionFilterOperatorFactories was overwriting
the previous one. Only the last preset's factories survived.

Fix: export factory creators from each satellite plugin and aggregate all
factories explicitly in ConstructivePreset's top-level schema options.
…hile-postgis

- Move createPostgisOperatorFactory() into graphile-postgis/src/plugins/connection-filter-operators.ts
- Move operator and integration tests into graphile-postgis/__tests__/
- Update GraphilePostgisPreset to include connectionFilterOperatorFactories
- Export createPostgisOperatorFactory from graphile-postgis
- Add graphile-connection-filter as optional peer dependency
- Update ConstructivePreset to import from graphile-postgis
- Remove graphile-plugin-connection-filter-postgis package entirely
- Remove graphile-settings dependency on old package
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

- graphile-search-plugin → graphile-tsvector
- graphile-pg-textsearch-plugin → graphile-bm25
- graphile-pg-trgm-plugin → graphile-trgm
- graphile-pgvector-plugin → graphile-pgvector

Updated package.json names, all imports in graphile-settings,
CI workflow references, README docs, and source code comments.
pnpm build passes clean.
- GRAPHILE.md: add missing packages (graphile-bm25, graphile-trgm, graphile-connection-filter)
- bm25: rename conditionPrefix option to filterPrefix for consistency
- bm25 README: update examples from condition: to filter:
- tsvector README: update example from condition: to filter:
…uplication

- bm25: bm25BodyScore → bodyBm25Score, BM25_BODY_SCORE_ASC → BODY_BM25_SCORE_ASC
- tsvector: bodyRank → bodyTsvRank, BODY_RANK_ASC → BODY_TSV_RANK_ASC
- trgm: nameSimilarity → nameTrgmSimilarity, SIMILARITY_NAME_ASC → NAME_TRGM_SIMILARITY_ASC
- pgvector: embeddingDistance → embeddingVectorDistance, EMBEDDING_DISTANCE_ASC → EMBEDDING_VECTOR_DISTANCE_ASC
- Dedup: strip trailing algorithm from column names (e.g. fullTextTsv → fullTextTsvRank not fullTextTsvTsvRank)
- Add configurable filterPrefix option to trgm plugin
- Update all tests, READMEs, and source comments
…ing convention

- fullTextRank → fullTextTsvRank in filter.test.ts
- FULL_TEXT_RANK_ASC/DESC → FULL_TEXT_TSV_RANK_ASC/DESC in filter.test.ts
- nameSimilarity → nameTrgmSimilarity in schema-snapshot.test.ts.snap
- SIMILARITY_NAME → NAME_TRGM_SIMILARITY in schema-snapshot.test.ts.snap
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant