From 46896812f2b43d7dbad119437e786ffd4a49f1e9 Mon Sep 17 00:00:00 2001 From: Derys Rodriguez Date: Thu, 27 Mar 2025 11:29:42 -0400 Subject: [PATCH 1/5] Add order_column method to properly escape order field with table name --- lib/rails_cursor_pagination/paginator.rb | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/rails_cursor_pagination/paginator.rb b/lib/rails_cursor_pagination/paginator.rb index 99806c4..04b692a 100644 --- a/lib/rails_cursor_pagination/paginator.rb +++ b/lib/rails_cursor_pagination/paginator.rb @@ -445,6 +445,19 @@ def id_column "#{escaped_table_name}.#{escaped_id_column}".freeze end + # Return a properly escaped reference to the order column prefixed with the + # table name. This prefixing is important in case of another model having + # been joined to the passed relation. + # + # @return [String (frozen)] + + def order_column + escaped_table_name = @relation.quoted_table_name + escaped_order_column = @relation.connection.quote_column_name(@order_field) + + "#{escaped_table_name}.#{escaped_order_column}".freeze + end + # Applies the filtering based on the provided cursor and order column to the # sorted relation. # @@ -472,11 +485,11 @@ def filtered_and_sorted_relation end sorted_relation - .where("#{@order_field} #{filter_operator} ?", + .where("#{order_column} #{filter_operator} ?", decoded_cursor.order_field_value) .or( sorted_relation - .where("#{@order_field} = ?", decoded_cursor.order_field_value) + .where("#{order_column} = ?", decoded_cursor.order_field_value) .where("#{id_column} #{filter_operator} ?", decoded_cursor.id) ) end From 1990c88c04ddfd8cf2e4d9b721c0328252681ea3 Mon Sep 17 00:00:00 2001 From: Derys Rodriguez Date: Fri, 28 Mar 2025 15:02:10 -0400 Subject: [PATCH 2/5] Refactor order_column and cursor methods to improve handling of order field names including table names --- lib/rails_cursor_pagination/paginator.rb | 31 ++++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/rails_cursor_pagination/paginator.rb b/lib/rails_cursor_pagination/paginator.rb index 04b692a..8ce41ae 100644 --- a/lib/rails_cursor_pagination/paginator.rb +++ b/lib/rails_cursor_pagination/paginator.rb @@ -353,6 +353,13 @@ def filter_value "#{decoded_cursor.order_field_value}-#{decoded_cursor.id}" end + # Extract the column name from "table.column" if necessary + # + # @return [Symbol] + def order_column_name + @order_field.to_s.split('.').last.to_sym + end + # Generate a cursor for the given record and ordering field. The cursor # encodes all the data required to then paginate based on it with the given # ordering field. @@ -365,7 +372,7 @@ def filter_value # @param record [ActiveRecord] Model instance for which we want the cursor # @return [String] def cursor_for_record(record) - cursor_class.from_record(record: record, order_field: @order_field).encode + cursor_class.from_record(record: record, order_field: order_column_name).encode end # Decode the provided cursor. Either just returns the cursor's ID or in case @@ -375,7 +382,7 @@ def cursor_for_record(record) # @return [Integer, Array] def decoded_cursor memoize(:decoded_cursor) do - cursor_class.decode(encoded_string: @cursor, order_field: @order_field) + cursor_class.decode(encoded_string: @cursor, order_field: order_column_name) end end @@ -405,17 +412,17 @@ def cursor_class def relation_with_cursor_fields return @relation if @relation.select_values.blank? || @relation.select_values.include?('*') - + relation = @relation - + unless @relation.select_values.include?(:id) relation = relation.select(:id) end - + if custom_order_field? && !@relation.select_values.include?(@order_field) - relation = relation.select(@order_field) + relation = relation.select("#{@order_field} AS #{order_column_name}") end - + relation end @@ -452,10 +459,14 @@ def id_column # @return [String (frozen)] def order_column - escaped_table_name = @relation.quoted_table_name - escaped_order_column = @relation.connection.quote_column_name(@order_field) + if @order_field.to_s.include?('.') + return @order_field + else + escaped_table_name = @relation.quoted_table_name + escaped_order_column = @relation.connection.quote_column_name(@order_field) - "#{escaped_table_name}.#{escaped_order_column}".freeze + "#{escaped_table_name}.#{escaped_order_column}".freeze + end end # Applies the filtering based on the provided cursor and order column to the From 844ddb2daf67a63e4b675873761b2f4ffc49697b Mon Sep 17 00:00:00 2001 From: Derys Rodriguez Date: Fri, 28 Mar 2025 15:03:42 -0400 Subject: [PATCH 3/5] Clean up whitespace --- lib/rails_cursor_pagination/paginator.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/rails_cursor_pagination/paginator.rb b/lib/rails_cursor_pagination/paginator.rb index 8ce41ae..50f2107 100644 --- a/lib/rails_cursor_pagination/paginator.rb +++ b/lib/rails_cursor_pagination/paginator.rb @@ -412,17 +412,17 @@ def cursor_class def relation_with_cursor_fields return @relation if @relation.select_values.blank? || @relation.select_values.include?('*') - + relation = @relation - + unless @relation.select_values.include?(:id) relation = relation.select(:id) end - + if custom_order_field? && !@relation.select_values.include?(@order_field) relation = relation.select("#{@order_field} AS #{order_column_name}") end - + relation end From de244e0b489f1b59b8afdb3e7c17fe3439127aea Mon Sep 17 00:00:00 2001 From: Derys Rodriguez Date: Fri, 28 Mar 2025 15:34:44 -0400 Subject: [PATCH 4/5] Rename order_column_name method to order_field_name for clarity and consistency --- lib/rails_cursor_pagination/paginator.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/rails_cursor_pagination/paginator.rb b/lib/rails_cursor_pagination/paginator.rb index 50f2107..5301e2b 100644 --- a/lib/rails_cursor_pagination/paginator.rb +++ b/lib/rails_cursor_pagination/paginator.rb @@ -356,7 +356,7 @@ def filter_value # Extract the column name from "table.column" if necessary # # @return [Symbol] - def order_column_name + def order_field_name @order_field.to_s.split('.').last.to_sym end @@ -372,7 +372,7 @@ def order_column_name # @param record [ActiveRecord] Model instance for which we want the cursor # @return [String] def cursor_for_record(record) - cursor_class.from_record(record: record, order_field: order_column_name).encode + cursor_class.from_record(record: record, order_field: order_field_name).encode end # Decode the provided cursor. Either just returns the cursor's ID or in case @@ -382,7 +382,7 @@ def cursor_for_record(record) # @return [Integer, Array] def decoded_cursor memoize(:decoded_cursor) do - cursor_class.decode(encoded_string: @cursor, order_field: order_column_name) + cursor_class.decode(encoded_string: @cursor, order_field: order_field_name) end end @@ -420,7 +420,7 @@ def relation_with_cursor_fields end if custom_order_field? && !@relation.select_values.include?(@order_field) - relation = relation.select("#{@order_field} AS #{order_column_name}") + relation = relation.select("#{@order_field} AS #{order_field_name}") end relation From e4cb1eeb17681d5e985b13c18b9200219784c60a Mon Sep 17 00:00:00 2001 From: Derys Rodriguez Date: Fri, 28 Mar 2025 16:21:15 -0400 Subject: [PATCH 5/5] Update CHANGELOG.md to reflect added support for sorting by joined table columns and fixes for SQL ambiguity errors --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b15ec3..53a94ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,13 @@ These are the latest changes on the project's `master` branch that have not yet Follow the same format as previous releases by categorizing your feature into "Added", "Changed", "Deprecated", "Removed", "Fixed", or "Security". ---> +### Added +- Added support for sorting by columns from joined tables. The `order_by` parameter now accepts fully qualified column names, allowing pagination to work seamlessly with joined table columns. + +### Fixed +- Ensure `order_by` columns are properly prefixed with the table name to avoid SQL ambiguity errors when joining multiple tables with columns of the same name (e.g., `created_at`). +- Fixed cursor decoding and encoding to handle fully qualified column names (e.g., `table.column`) correctly. + ## [0.4.0] - 2023-10-06 ### Changed