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 diff --git a/lib/rails_cursor_pagination/paginator.rb b/lib/rails_cursor_pagination/paginator.rb index 99806c4..5301e2b 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_field_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_field_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_field_name) end end @@ -413,7 +420,7 @@ def relation_with_cursor_fields end if custom_order_field? && !@relation.select_values.include?(@order_field) - relation = relation.select(@order_field) + relation = relation.select("#{@order_field} AS #{order_field_name}") end relation @@ -445,6 +452,23 @@ 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 + 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 + end + end + # Applies the filtering based on the provided cursor and order column to the # sorted relation. # @@ -472,11 +496,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