Skip to content

Support composite primary keys in search scopes - #601

Merged
nertzy merged 21 commits into
masterfrom
composite-primary-keys
Oct 16, 2026
Merged

nertzy merged 21 commits into
masterfrom
composite-primary-keys

Conversation

@nertzy

@nertzy nertzy commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Supersedes #537 by @causztic and keeps their commits. Fixes #531.

On a model with a composite primary key, pg_search_scope sent the key Array to SQL as a single quoted column name, so PostgreSQL rejected the query. The scope now projects, joins, groups, and orders by each primary-key column:

  • Rank join: the rank subquery selects every key column and joins back on all of them. Each key column gets a hashed pg_search_… alias, for single and composite keys alike, so it cannot collide with an unqualified column in the caller's SQL or with the rank column. This replaces the undocumented pg_search_id alias; a GitHub code search found no code that depends on it.
  • Default order within rank: ties are ordered by every key column.
  • associated_against: the association subquery selects and groups by every key column of the searching model and joins on all of them. Searching through an association to a composite-key model already worked once the rank join was fixed, because Rails builds that association join itself.

#537 was written against the string-built SQL, which master has since replaced with Arel. The merge commit rebuilds its changes as Arel expressions. Later commits simplify the spec fixtures, add composite-key support for associations searched from a composite-key model, which #537 deferred, and credit the change to yaojie in the changelog. Association#to_arel now reads the key columns from its own model instead of taking them as arguments, and its subquery keeps their column names rather than aliasing them.

Multisearch is unchanged. pg_search_documents.searchable_id holds a single value, and Rails already raises CompositePrimaryKeyMismatchError for the polymorphic association on a composite-key model.

Copilot AI balanced review requested due to automatic review settings October 14, 2026 23:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

Static review found no blocking issues, and focused regression examples cover the changed behavior; runtime tests were not executed.

0 open findings

What changed in this PR

Adds composite primary-key support to pg_search_scope, addressing #531 while leaving multisearch unchanged.

Changes:

  • Uses every key column for rank joins and default tie ordering.
  • Updates association subqueries to join and group by all key columns.
  • Adds regression examples and documents the support contract.
File Description
spec/​integration/​composite_primary_key_spec.rb Tests composite-key searches, ordering, and associations.
lib/​pg_search/​scope_options.rb Projects, joins, and orders all primary-key columns.
lib/​pg_search/​configuration/​association.rb Handles multiple keys in association joins and grouping.
doc/​plans/​2026-10-14-composite-primary-keys.md Documents scope, limitations, and verification plans.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Merge #537 onto master, which moved scope SQL construction to Arel after
the pull request branched. Rebuild its composite-key rank subquery,
rank join, and default order within rank as Arel expressions, and apply
the module-inclusion spacing required by the current Standard.
Model the examples on books keyed by author and number, letting Rails
infer the composite key from the schema. Give the keys a shared leading
column so a rank join or ordering on only part of the key fails.
Searching associated_against from a composite-key model selected,
grouped, and joined the association subquery on the inspected key
Array, which PostgreSQL rejects as a missing column. Give each key
column its own subquery alias and join on all of them.

Association#to_arel now takes the key attributes as arguments. Its
remaining caller passes Arel attributes, so drop the orphaned trusted
SQL String coercion.
@nertzy
nertzy force-pushed the composite-primary-keys branch from a4235e6 to fa4816f Compare October 16, 2026 21:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

Static review identified no blocking issues, and regression specs cover the changed search paths; tests were not run.

No open issues 🎉

Review effort: Balanced

Association#to_arel took the model's primary-key attributes from its only
caller, though the association already holds the model. It now derives them
itself and selects and joins the primary-key columns under their own names:
the subquery's other columns carry hashed pg_search aliases, so the id_N
aliases guarded against no collision.
@nertzy
nertzy force-pushed the composite-primary-keys branch from cdd25f8 to 9d89474 Compare October 16, 2026 21:42
@nertzy
nertzy requested a balanced review from Copilot October 16, 2026 21:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

Static review found no blocking issues, and regression specs cover the principal composite-key behaviors; tests were not executed.

No open issues 🎉

Review effort: Balanced

Association#to_arel built its subquery and ON condition in locals. Both now
live in private methods. The subquery is memoized, so the condition's column
references do not rebuild its relation.
ScopeOptions built its rank key selects, rank join condition, and primary-key
attributes inline. Each now has a named private method, matching
Association#primary_keys.
The rank subquery is joined into the caller's query, so its key columns
need aliases that cannot collide with unqualified user SQL or with the
rank column. Hashed Configuration aliases do that for single and
composite keys alike, replacing the pg_search_id and pg_search_id_N
branches. Chained scopes stay distinct because each rank subquery has
its own counted table alias.
The composite-key helpers grew endless methods past 80 columns, most
wrapping a block. Give them conventional def/end bodies.
index_by states the alias-to-attribute mapping directly instead of
building pairs for to_h.
@nertzy
nertzy force-pushed the composite-primary-keys branch from 9d89474 to ad65b30 Compare October 16, 2026 21:54
@nertzy
nertzy merged commit 9a5c3a0 into master Oct 16, 2026
13 checks passed
@nertzy
nertzy deleted the composite-primary-keys branch October 16, 2026 21:57
@nertzy nertzy mentioned this pull request Oct 16, 2026
2 tasks done
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.

Error when searching a table with a composite primary key

3 participants