You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Feature request: Ability to refer to joined tables columns in sorting keys #16
Hi, as I mentioned, I can't really provide a PR at this time, as we have just solved it for our specific use case, but I can past you the monkey patched code :)
# frozen_string_literal: truemoduleGraphQLmoduleConnectionsmoduleKeyset# Implements keyset pagination by two fields with asc orderclassAsc < ::GraphQL::Connections::Keyset::Basedefhas_previous_pageiflastnodes.any? &&
items.where(_field.eq(nodes.first[field_key])).where(arel_table[primary_key].lt(nodes.first[primary_key])).or(items.where(_field.lt(nodes.first[field_key]))).exists?elsifafteritems.where(_field.lt(after_cursor_date)).or(items.where(_field.eq(after_cursor_date)).where(arel_table[primary_key].lt(after_cursor_primary_key))).exists?elsefalseendenddefhas_next_pageiffirstnodes.any? &&
items.where(_field.eq(nodes.last[field_key])).where(arel_table[primary_key].gt(nodes.last[primary_key])).or(items.where(_field.gt(nodes.last[field_key]))).exists?elsifbeforeitems.where(_field.gt(before_cursor_date)).or(items.where(_field.eq(before_cursor_date)).where(arel_table[primary_key].gt(before_cursor_primary_key))).exists?elsefalseendendprivate# standard:disable Metrics/AbcSize, Metrics/MethodLengthdeflimited_relationscope=sliced_relationnodes=[]iffirstnodes |= scope.reorder(_field.asc,arel_table[primary_key].asc).limit(first).to_aendiflastnodes |= scope.reorder(_field.desc,arel_table[primary_key].desc).limit(last).to_a.reverse!endnodesenddefsliced_relation_after(relation)relation.where(_field.eq(after_cursor_date)).where(arel_table[primary_key].gt(after_cursor_primary_key)).or(relation.where(_field.gt(after_cursor_date)))enddefsliced_relation_before(relation)relation.where(_field.eq(before_cursor_date)).where(arel_table[primary_key].lt(before_cursor_primary_key)).or(relation.where(_field.lt(before_cursor_date)))enddef_fieldiffield_key.to_s.include?(".")table,field=field_key.to_s.split('.')Arel::Table.new(table.to_sym)[field.to_sym]elsearel_table[field_key]endendend# Implements keyset pagination by two fields with desc orderclassDesc < ::GraphQL::Connections::Keyset::Basedefhas_previous_pageiflastnodes.any? &&
items.where(_field.eq(nodes.first[field_key])).where(arel_table[primary_key].gt(nodes.first[primary_key])).or(items.where(_field.gt(nodes.first[field_key]))).exists?elsifafteritems.where(_field.gt(after_cursor_date)).or(items.where(_field.eq(after_cursor_date)).where(arel_table[primary_key].gt(after_cursor_primary_key))).exists?elsefalseendenddefhas_next_pageiffirstnodes.any? &&
items.where(_field.eq(nodes.last[field_key])).where(arel_table[primary_key].lt(nodes.last[primary_key])).or(items.where(_field.lt(nodes.last[field_key]))).exists?elsifbeforeitems.where(_field.lt(before_cursor_date)).or(items.where(_field.eq(before_cursor_date)).where(arel_table[primary_key].lt(before_cursor_primary_key))).exists?elsefalseendend# standard:disable Metrics/AbcSize, Metrics/MethodLengthdefcursor_for(item)cursor=[item[field_key],item[primary_key]].map{ |value| serialize(value)}.join(@separator)cursor=encode(cursor)ifopaque_cursorcursorendprivatedeflimited_relationscope=sliced_relationnodes=[]iffirstnodes |= scope.reorder(_field.desc,arel_table[primary_key].desc).limit(first).tap{ |a|
putsa.to_sql}.to_aendiflastnodes |= scope.reorder(_field.asc,arel_table[primary_key].asc).limit(last).to_a.reverse!endnodesenddefsliced_relation_after(relation)relation.where(_field.eq(after_cursor_date)).where(arel_table[primary_key].lt(after_cursor_primary_key)).or(relation.where(_field.lt(after_cursor_date)))enddefsliced_relation_before(relation)relation.where(_field.eq(before_cursor_date)).where(arel_table[primary_key].gt(before_cursor_primary_key)).or(relation.where(_field.gt(before_cursor_date)))enddef_fieldiffield_key.to_s.include?(".")table,field=field_key.to_s.split('.')Arel::Table.new(table.to_sym)[field.to_sym]elsearel_table[field_key]endendendendendend
Issues with this code:
Still untested, this has not been used in production yet
It is possible for ActiveRecord to change the name of a table it joins, this code does not take that into account
We ran into this exact issue. We needed to sort and paginate by columns from joined tables (e.g., authors.name, categories.title) as well as computed columns (a CASE expression for priority ordering).
We found a workaround that doesn't require patching the gem. The idea is to materialize the aliased columns via a subquery, then tell ActiveRecord the columns exist with .select("table.*"):
defbuild_connection(posts,sort_by)# 1. Add JOINs and SELECT aliases as neededcasesort_by[:field]when"author_name"posts=posts.left_joins(:author).select("posts.*","authors.name AS author_name")when"category_title"posts=posts.left_joins(:category).select("posts.*","categories.title AS category_title")when"priority_order"posts=posts.select("posts.*, (CASE WHEN posts.priority = 'high' THEN 1 WHEN posts.priority = 'medium' THEN 2 WHEN posts.priority = 'low' THEN 3 END) as priority_order")end# 2. Wrap in subquery to materialize aliased columns as real columnsresult=Post.from("(#{posts.to_sql}) as posts")# 3. This is the key line — it makes Stable aware of the materialized columnsresult=result.select("posts.*")# 4. Now Stable can reference the aliased columns in cursor WHERE clausesGraphQL::Connections::Stable.new(result,desc: sort_by[:direction] == "DESC",keys: [sort_by[:field],:id])end
Why it works: The subquery materializes the aliased columns (e.g., author_name, category_title) as real columns in the result set. The .select("posts.*") tells ActiveRecord that all columns — including the materialized ones — are available on the relation. Stable can then reference them in its cursor WHERE clauses (WHERE (author_name, id) > (?, ?)) without error.
Without the .select("posts.*") line, Stable's introspection can't detect the materialized columns and hasNextPage breaks.
Activity
DmitryTsepelev commentedon Jun 6, 2024
Hi! Yes indeed, sounds very useful, could you please make a PR with your implementation? 🙂
Bertg commentedon Jun 6, 2024
Hi, as I mentioned, I can't really provide a PR at this time, as we have just solved it for our specific use case, but I can past you the monkey patched code :)
Issues with this code:
mjacobus commentedon Feb 12, 2026
We ran into this exact issue. We needed to sort and paginate by columns from joined tables (e.g.,
authors.name,categories.title) as well as computed columns (aCASEexpression for priority ordering).We found a workaround that doesn't require patching the gem. The idea is to materialize the aliased columns via a subquery, then tell ActiveRecord the columns exist with
.select("table.*"):Why it works: The subquery materializes the aliased columns (e.g.,
author_name,category_title) as real columns in the result set. The.select("posts.*")tells ActiveRecord that all columns — including the materialized ones — are available on the relation. Stable can then reference them in its cursor WHERE clauses (WHERE (author_name, id) > (?, ?)) without error.Without the
.select("posts.*")line, Stable's introspection can't detect the materialized columns andhasNextPagebreaks.