rotulus 0.2.0 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -2
- data/README.md +7 -6
- data/lib/rotulus/order.rb +24 -2
- data/lib/rotulus/version.rb +1 -1
- data/lib/rotulus.rb +2 -1
- metadata +2 -3
- data/Gemfile +0 -46
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2c74625ece0961a25549797386fa66abb146564211b4dc1640abd92ac4c0e2a0
|
4
|
+
data.tar.gz: 567562c553bbe914cdf5740e4262bd76b59ef04493658a68c69bd420e3109283
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e1380f553f3a0756b36b21f2a6193ee7031fe29bc8b3478ec349f6c35d2b9292ed5afd295b6b5250b2dc4498e1d86b66d13d639ea5e3d36db78fc8868fb4e9f
|
7
|
+
data.tar.gz: 625359b428c792c30a2a2ee2b6b702a2c7270e495111c284c82a1d1433eddf5d0ac485d6d5ae961128006f161c4cb01b8fa76f7ab3bd14d63cb761306a9392f6
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
-
## 0.
|
1
|
+
## 0.2.0
|
2
|
+
- Initial release.
|
2
3
|
|
3
|
-
|
4
|
+
## 0.2.1
|
5
|
+
- Drop unnecessary ORDER BY columns following a non-nullable and distinct column.
|
6
|
+
|
7
|
+
## 0.2.2
|
8
|
+
- Raise error when there is no non-nullable and distinct column in the configured order definition.
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Rotulus
|
2
2
|
|
3
|
-
[![CI](https://github.com/jsonb-uy/rotulus/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jsonb-uy/rotulus/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/jsonb-uy/rotulus/branch/main/graph/badge.svg?token=OKGOWP4SH9)](https://codecov.io/gh/jsonb-uy/rotulus)
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/rotulus.svg)](https://badge.fury.io/rb/rotulus) [![CI](https://github.com/jsonb-uy/rotulus/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jsonb-uy/rotulus/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/jsonb-uy/rotulus/branch/main/graph/badge.svg?token=OKGOWP4SH9)](https://codecov.io/gh/jsonb-uy/rotulus)
|
4
4
|
|
5
5
|
### Cursor-based pagination for apps built on Rails/ActiveRecord
|
6
6
|
|
@@ -295,18 +295,19 @@ page = Rotulus::Page.new(items, order: order_by, limit: 2)
|
|
295
295
|
|
296
296
|
| Class | Description |
|
297
297
|
| ----------- | ----------- |
|
298
|
-
| `Rotulus::InvalidCursor` | Cursor token received is invalid e.g., unrecognized token, token data has been tampered/updated, base ActiveRecord relation filter/sorting/limit is no longer consistent to the token
|
298
|
+
| `Rotulus::InvalidCursor` | Cursor token received is invalid e.g., unrecognized token, token data has been tampered/updated, base ActiveRecord relation filter/sorting/limit is no longer consistent to the token. |
|
299
299
|
| `Rotulus::Expired` | Cursor token received has expired based on the configured `token_expires_in` |
|
300
300
|
| `Rotulus::InvalidLimit` | Limit set to Rotulus::Page is not valid. e.g., exceeds the configured limit. see `config.page_max_limit` |
|
301
301
|
| `Rotulus::CursorError` | Generic error for cursor related validations |
|
302
|
-
| `Rotulus::
|
302
|
+
| `Rotulus::InvalidColumnError` | Column provided in the :order param can't be found. |
|
303
|
+
| `Rotulus::MissingTiebreakerError` | There is no non-nullable and distinct column in the configured order definition. |
|
303
304
|
| `Rotulus::ConfigurationError` | Generic error for missing/invalid configurations. |
|
304
305
|
|
305
306
|
## How it works
|
306
307
|
Cursor-based pagination uses a reference point/record to fetch the previous or next set of records. This gem takes care of the SQL query and cursor generation needed for the pagination. To ensure that the pagination results are stable, it requires that:
|
307
308
|
|
308
309
|
* Records are sorted (`ORDER BY`).
|
309
|
-
* In case multiple records with the same column value(s) exists in the result, a unique column is needed as tie-breaker. Usually, the table PK suffices for this but for complex queries(e.g. with table joins and with nullable columns, etc.), combining and using multiple columns that would uniquely identify the row in the result is needed.
|
310
|
+
* In case multiple records with the same column value(s) exists in the result, a unique non-nullable column is needed as tie-breaker. Usually, the table PK suffices for this but for complex queries(e.g. with table joins and with nullable columns, etc.), combining and using multiple columns that would uniquely identify the row in the result is needed.
|
310
311
|
* Columns used in `ORDER BY` would need to be indexed as they will be used in filtering.
|
311
312
|
|
312
313
|
|
@@ -352,13 +353,13 @@ page = Rotulus::Page.new(User.all, order: { first_name: { direction: :asc, nulls
|
|
352
353
|
|
353
354
|
###### SQL:
|
354
355
|
```sql
|
355
|
-
-- if
|
356
|
+
-- if last_name value of the current page's last record is not null:
|
356
357
|
WHERE ((users.last_name >= ? OR users.last_name IS NULL) AND
|
357
358
|
((users.last_name > ? OR users.last_name IS NULL)
|
358
359
|
OR (users.last_name = ? AND users.id > ?)))
|
359
360
|
ORDER BY users.last_name asc nulls last, users.id asc LIMIT 3
|
360
361
|
|
361
|
-
-- if
|
362
|
+
-- if last_name value of the current page's last record is null:
|
362
363
|
WHERE users.last_name IS NULL AND users.id > ?
|
363
364
|
ORDER BY users.last_name asc nulls last, users.id asc LIMIT 3
|
364
365
|
```
|
data/lib/rotulus/order.rb
CHANGED
@@ -11,6 +11,9 @@ module Rotulus
|
|
11
11
|
@definition = {}
|
12
12
|
|
13
13
|
build_column_definitions
|
14
|
+
|
15
|
+
return if has_tiebreaker?
|
16
|
+
raise Rotulus::MissingTiebreakerError.new('A non-nullable and distinct column is required.')
|
14
17
|
end
|
15
18
|
|
16
19
|
# Returns an array of the ordered columns
|
@@ -62,14 +65,14 @@ module Rotulus
|
|
62
65
|
#
|
63
66
|
# @return [String] the ORDER BY clause
|
64
67
|
def reversed_sql
|
65
|
-
Arel.sql(
|
68
|
+
Arel.sql(columns_for_order.map(&:reversed_order_sql).join(', '))
|
66
69
|
end
|
67
70
|
|
68
71
|
# Returns the ORDER BY sort expression(s) to sort the records
|
69
72
|
#
|
70
73
|
# @return [String] the ORDER BY clause
|
71
74
|
def sql
|
72
|
-
Arel.sql(
|
75
|
+
Arel.sql(columns_for_order.map(&:order_sql).join(', '))
|
73
76
|
end
|
74
77
|
|
75
78
|
# Generate a 'state' so we can detect whether the order definition has changed.
|
@@ -92,6 +95,19 @@ module Rotulus
|
|
92
95
|
|
93
96
|
attr_reader :ar_model, :definition, :raw_hash
|
94
97
|
|
98
|
+
def columns_for_order
|
99
|
+
return @columns_for_order if instance_variable_defined?(:@columns_for_order)
|
100
|
+
|
101
|
+
@columns_for_order = []
|
102
|
+
columns.each do |col|
|
103
|
+
@columns_for_order << col
|
104
|
+
|
105
|
+
break if col.distinct? && !col.nullable?
|
106
|
+
end
|
107
|
+
|
108
|
+
@columns_for_order
|
109
|
+
end
|
110
|
+
|
95
111
|
def ar_model_primary_key
|
96
112
|
ar_model.primary_key
|
97
113
|
end
|
@@ -124,6 +140,12 @@ module Rotulus
|
|
124
140
|
)
|
125
141
|
end
|
126
142
|
|
143
|
+
def has_tiebreaker?
|
144
|
+
last_column = columns_for_order.last
|
145
|
+
|
146
|
+
last_column.distinct? && !last_column.nullable?
|
147
|
+
end
|
148
|
+
|
127
149
|
def primary_key_ordered?
|
128
150
|
!definition["#{ar_table}.#{ar_model_primary_key}"].nil?
|
129
151
|
end
|
data/lib/rotulus/version.rb
CHANGED
data/lib/rotulus.rb
CHANGED
@@ -21,7 +21,8 @@ module Rotulus
|
|
21
21
|
class InvalidCursorDirection < CursorError; end
|
22
22
|
class InvalidLimit < BaseError; end
|
23
23
|
class ConfigurationError < BaseError; end
|
24
|
-
class
|
24
|
+
class MissingTiebreakerError < ConfigurationError; end
|
25
|
+
class InvalidColumnError < ConfigurationError; end
|
25
26
|
|
26
27
|
def self.db
|
27
28
|
@db ||= case ActiveRecord::Base.connection.adapter_name.downcase
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rotulus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Uy Jayson B
|
@@ -73,7 +73,6 @@ extensions: []
|
|
73
73
|
extra_rdoc_files: []
|
74
74
|
files:
|
75
75
|
- CHANGELOG.md
|
76
|
-
- Gemfile
|
77
76
|
- LICENSE
|
78
77
|
- README.md
|
79
78
|
- lib/rotulus.rb
|
@@ -97,7 +96,7 @@ metadata:
|
|
97
96
|
homepage_uri: https://github.com/jsonb-uy/rotulus
|
98
97
|
source_code_uri: https://github.com/jsonb-uy/rotulus
|
99
98
|
changelog_uri: https://github.com/jsonb-uy/rotulus/blob/main/CHANGELOG.md
|
100
|
-
documentation_uri: https://rubydoc.info/github/jsonb-uy/rotulus
|
99
|
+
documentation_uri: https://rubydoc.info/github/jsonb-uy/rotulus/main
|
101
100
|
bug_tracker_uri: https://github.com/jsonb-uy/rotulus/issues
|
102
101
|
post_install_message:
|
103
102
|
rdoc_options: []
|
data/Gemfile
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
source 'https://rubygems.org'
|
2
|
-
gemspec
|
3
|
-
|
4
|
-
rails_version = ENV['RAILS_VERSION'] || '7-0-stable'
|
5
|
-
|
6
|
-
gem 'rake'
|
7
|
-
gem 'rspec'
|
8
|
-
|
9
|
-
if ENV.fetch('COVERAGE', nil) == 'true'
|
10
|
-
gem 'simplecov'
|
11
|
-
gem 'simplecov-cobertura'
|
12
|
-
end
|
13
|
-
|
14
|
-
case rails_version
|
15
|
-
when '4-2-stable'
|
16
|
-
# Ruby 2.2 or newer.
|
17
|
-
gem 'pg', '~> 0.15'
|
18
|
-
gem 'sqlite3', '~> 1.3.6'
|
19
|
-
gem 'mysql2', '~> 0.4.4'
|
20
|
-
when '5-0-stable', '5-1-stable', '5-2-stable'
|
21
|
-
# Ruby 2.2.2 or newer.
|
22
|
-
gem 'pg', '~> 0.18'
|
23
|
-
gem 'sqlite3', '~> 1.3.6'
|
24
|
-
gem 'mysql2', '~> 0.4.4'
|
25
|
-
when '6-0-stable'
|
26
|
-
# Ruby 2.5.0 or newer.
|
27
|
-
gem 'pg', '~> 0.18'
|
28
|
-
gem 'sqlite3', '~> 1.4'
|
29
|
-
gem 'mysql2', '>= 0.4.4'
|
30
|
-
when '6-1-stable'
|
31
|
-
# Ruby 2.5.0 or newer.
|
32
|
-
gem 'pg', '~> 1.1'
|
33
|
-
gem 'sqlite3', '~> 1.4'
|
34
|
-
gem 'mysql2', '~> 0.5'
|
35
|
-
when '7-0-stable'
|
36
|
-
# Ruby 2.7.0 or newer.
|
37
|
-
gem 'pg', '~> 1.1'
|
38
|
-
gem 'sqlite3', '~> 1.4'
|
39
|
-
gem 'mysql2', '~> 0.5'
|
40
|
-
else
|
41
|
-
gem 'pg'
|
42
|
-
gem 'sqlite3'
|
43
|
-
gem 'mysql2'
|
44
|
-
end
|
45
|
-
|
46
|
-
gem 'rails', git: 'https://github.com/rails/rails', branch: rails_version
|