Skip to content

Commit

Permalink
Add the ability to skip implicitly appending a primary key to the lis…
Browse files Browse the repository at this point in the history
…t of sorting columns
  • Loading branch information
fatkodima committed Mar 10, 2024
1 parent 52091db commit 6724e76
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 5 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
## master (unreleased)

- Add the ability to skip implicitly appending a primary key to the list of sorting columns.

It may be useful to disable it for the table with a UUID primary key or when the sorting
is done by a combination of columns that are already unique.

```ruby
paginator = UserSettings.cursor_paginate(order: :user_id, append_primary_key: false)
```

## 0.1.0 (2024-03-08)

- First release
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ paginator = posts.cursor_paginate(order: [:author, :title])
paginator = posts.cursor_paginate(order: { author: :asc, title: :desc })
```

The gem implicitly appends a primary key column to the list of sorting columns. It may be useful
to disable it for the table with a UUID primary key or when the sorting is done by a combination
of columns that are already unique.

```ruby
paginator = UserSettings.cursor_paginate(order: :user_id, append_primary_key: false)
```

**Important:**
If your app regularly orders by another column, you might want to add a database index for this.
Say that your order column is `author` then you'll want to add a compound index on `(author, id)`.
Expand Down
7 changes: 4 additions & 3 deletions lib/activerecord_cursor_paginate/extension.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ module ActiveRecordCursorPaginate
module Extension
# Convenient method to use on ActiveRecord::Relation to get a paginator.
# @return [ActiveRecordCursorPaginate::Paginator]
# @see ActiveRecordCursorPaginate::Paginator#initialize
#
# @example
# paginator = Post.all.cursor_paginate(limit: 2, after: "Mg==")
# paginator = Post.cursor_paginate(limit: 2, after: "Mg==")
# page = paginator.fetch
#
def cursor_paginate(after: nil, before: nil, limit: nil, order: nil)
def cursor_paginate(after: nil, before: nil, limit: nil, order: nil, append_primary_key: true)
relation = (is_a?(ActiveRecord::Relation) ? self : all)
Paginator.new(relation, after: after, before: before, limit: limit, order: order)
Paginator.new(relation, after: after, before: before, limit: limit, order: order, append_primary_key: append_primary_key)
end
alias cursor_pagination cursor_paginate
end
Expand Down
15 changes: 13 additions & 2 deletions lib/activerecord_cursor_paginate/paginator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ class Paginator
# ```sql
# CREATE INDEX <index_name> ON <table_name> (<order_fields>..., id)
# ```
# @param append_primary_key [Boolean] (true). Specifies whether the primary column(s)
# should be implicitly appended to the list of sorting columns. It may be useful
# to disable it for the table with a UUID primary key or when the sorting is done by a
# combination of columns that are already unique.
# @raise [ArgumentError] If any parameter is not valid
#
def initialize(relation, before: nil, after: nil, limit: nil, order: nil)
def initialize(relation, before: nil, after: nil, limit: nil, order: nil, append_primary_key: true)
unless relation.is_a?(ActiveRecord::Relation)
raise ArgumentError, "relation is not an ActiveRecord::Relation"
end
Expand All @@ -54,6 +58,7 @@ def initialize(relation, before: nil, after: nil, limit: nil, order: nil)
@page_size = limit || config.default_page_size
@page_size = [@page_size, config.max_page_size].min if config.max_page_size

@append_primary_key = append_primary_key
order = normalize_order(order)
@columns = order.keys
@directions = order.values
Expand Down Expand Up @@ -153,7 +158,13 @@ def normalize_order(order)

result = result.with_indifferent_access
result.transform_values! { |direction| direction.downcase.to_sym }
Array(@primary_key).each { |column| result[column] ||= default_direction }

if @append_primary_key
Array(@primary_key).each { |column| result[column] ||= default_direction }
end

raise ArgumentError, ":order must contain columns to order by" if result.blank?

result
end

Expand Down
17 changes: 17 additions & 0 deletions test/paginator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ def test_raises_when_before_and_after_present
assert_equal("Only one of :before and :after can be provided", error.message)
end

def test_raises_when_no_primary_key_and_order_is_empty
error = assert_raises(ArgumentError) do
NoPkTable.cursor_paginate
end
assert_equal(":order must contain columns to order by", error.message)
end

def test_paginates_by_id_by_default
p = User.cursor_paginate
users = p.fetch.records
Expand Down Expand Up @@ -75,6 +82,16 @@ def test_order_by_different_columns_without_directions
assert_equal([[3, 1], [7, 1], [9, 1], [1, 2]], users.pluck(:id, :company_id))
end

def test_does_not_append_id_if_asked
p = User.cursor_paginate(limit: 1, order: :company_id, append_primary_key: false)

records = []
p.pages.each do |page|
records.concat(page.records)
end
assert_equal([1, 2, 3, 4], records.map(&:company_id))
end

def test_paginates_forward_after_cursor
p1 = User.cursor_paginate(limit: 3)
page1 = p1.fetch
Expand Down
8 changes: 8 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
t.integer :user_id
t.integer :stars
end

create_table :no_pk_table, id: false do |t|
t.integer :some_column
end
end

class User < ActiveRecord::Base
Expand All @@ -43,6 +47,10 @@ class CpkUser < ActiveRecord::Base
class Project < ActiveRecord::Base
end

class NoPkTable < ActiveRecord::Base
self.table_name = :no_pk_table
end

Minitest::Test.class_eval do
alias_method :assert_not, :refute
alias_method :assert_not_empty, :refute_empty
Expand Down

0 comments on commit 6724e76

Please sign in to comment.