order_as_specified 0.1.0 → 1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +3 -0
- data/README.md +17 -0
- data/lib/order_as_specified.rb +15 -5
- data/lib/order_as_specified/error.rb +4 -0
- data/lib/order_as_specified/version.rb +1 -1
- data/order_as_specified.gemspec +3 -2
- data/spec/config/database.yml +9 -0
- data/spec/config/test_setup_migration.rb +18 -0
- data/spec/order_as_specified_spec.rb +5 -87
- data/spec/postgresql_spec.rb +32 -0
- data/spec/shared/order_as_specified_examples.rb +80 -0
- data/spec/spec_helper.rb +3 -4
- data/spec/sqlite3_spec.rb +14 -0
- data/spec/support/association_test_class.rb +5 -0
- data/spec/support/test_class.rb +5 -0
- metadata +39 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aa46391db1a92ccc0095e732300f8ff9dff05219
|
4
|
+
data.tar.gz: 54dc39bd5f341a8a0245c25cfbbdb56a72590b4e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 92a5c23cd6dff0657c08cf71387b30fa8ce6ae4a1113c51f3aeb7ad8c6f75786f9449b49ebc1d8e445341bf65b3a9bd431febb00436c8d7c566a0c30247097e8
|
7
|
+
data.tar.gz: d0ce1a2a19c3f1d03d80d4c2ef940551423ea3fb986b30685d99966ad1b71cc91d94c60881a61c068af8b6927e78cbb4d909f8b9df5ffc42f2d88dd3542e108b
|
data/.travis.yml
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm:
|
3
3
|
- 2.2.1
|
4
|
+
before_script:
|
5
|
+
- psql -c 'create database order_as_specified_test;' -U postgres
|
4
6
|
script: bundle exec rspec
|
5
7
|
addons:
|
8
|
+
postgresql: "9.3"
|
6
9
|
code_climate:
|
7
10
|
repo_token: 781c439d68cbb928316deaec1c7136f98423e1db87238f99cbc95183de94df9e
|
8
11
|
notifications:
|
data/README.md
CHANGED
@@ -103,6 +103,23 @@ TestObject.order_as_specified(language: ["fr", "es"])
|
|
103
103
|
]>
|
104
104
|
```
|
105
105
|
|
106
|
+
In databases that support it (such as PostgreSQL), you can also use an option to
|
107
|
+
add a `DISTINCT ON` to your query when you would otherwise have duplicates:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
TestObject.order_as_specified(distinct_on: true, language: ["fr", "en"])
|
111
|
+
=> #<ActiveRecord::Relation [
|
112
|
+
#<TestObject id: 2, language: "fr">,
|
113
|
+
#<TestObject id: 3, language: "en">,
|
114
|
+
#<TestObject id: 4, language: "es">
|
115
|
+
]>
|
116
|
+
```
|
117
|
+
|
118
|
+
Note that if a `nil` value is passed in the ordering an error is raised, because
|
119
|
+
databases do not have good or consistent support for ordering with `NULL` values
|
120
|
+
in an arbitrary order, so we don't permit this behavior instead of allowing an
|
121
|
+
unexpected result.
|
122
|
+
|
106
123
|
## Documentation
|
107
124
|
|
108
125
|
We have documentation on [RubyDoc](http://www.rubydoc.info/github/panorama-ed/order_as_specified/master).
|
data/lib/order_as_specified.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require "order_as_specified/version"
|
2
|
+
require "order_as_specified/error"
|
2
3
|
|
3
4
|
# This module adds the ability to query an ActiveRecord class for results from
|
4
5
|
# the database in an arbitrary order, without having to store anything extra
|
@@ -9,19 +10,28 @@ module OrderAsSpecified
|
|
9
10
|
# @param hash [Hash] the ActiveRecord arguments hash
|
10
11
|
# @return [ActiveRecord::Relation] the objects, ordered as specified
|
11
12
|
def order_as_specified(hash)
|
13
|
+
distinct_on = hash.delete(:distinct_on)
|
12
14
|
params = extract_params(hash)
|
13
15
|
|
14
16
|
table = params[:table]
|
15
17
|
attribute = params[:attribute]
|
16
18
|
|
17
19
|
# We have to explicitly quote for now because SQL sanitization for ORDER BY
|
18
|
-
# queries
|
20
|
+
# queries isn't in less current versions of Rails.
|
19
21
|
# See: https://github.com/rails/rails/pull/13008
|
20
|
-
|
21
|
-
"
|
22
|
-
end.join(", ")
|
22
|
+
conditions = params[:values].map do |value|
|
23
|
+
raise OrderAsSpecified::Error, "Cannot order by `nil`" if value.nil?
|
23
24
|
|
24
|
-
|
25
|
+
"#{table}.#{attribute}='#{value}'"
|
26
|
+
end
|
27
|
+
|
28
|
+
scope = order(conditions.map { |cond| "#{cond} DESC" }.join(", "))
|
29
|
+
|
30
|
+
if distinct_on
|
31
|
+
scope = scope.select("DISTINCT ON (#{conditions.join(', ')}) #{table}.*")
|
32
|
+
end
|
33
|
+
|
34
|
+
scope
|
25
35
|
end
|
26
36
|
|
27
37
|
private
|
data/order_as_specified.gemspec
CHANGED
@@ -19,13 +19,14 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
20
|
spec.require_paths = ["lib"]
|
21
21
|
|
22
|
-
spec.add_dependency "activerecord", "
|
22
|
+
spec.add_dependency "activerecord", ">= 4.0"
|
23
23
|
|
24
24
|
spec.add_development_dependency "bundler", "~> 1.7"
|
25
25
|
spec.add_development_dependency "codeclimate-test-reporter", "~> 0.4"
|
26
26
|
spec.add_development_dependency "overcommit", "~> 0.23"
|
27
|
+
spec.add_development_dependency "pg", ">= 0.18"
|
27
28
|
spec.add_development_dependency "rspec", "~> 3.2"
|
28
29
|
spec.add_development_dependency "rspec-rails", "~> 3.2"
|
29
30
|
spec.add_development_dependency "rubocop", "~> 0.29"
|
30
|
-
spec.add_development_dependency "
|
31
|
+
spec.add_development_dependency "sqlite3", ">= 1.3"
|
31
32
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class TestSetupMigration < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
return if ActiveRecord::Base.connection.table_exists? :test_classes
|
4
|
+
|
5
|
+
create_table :test_classes do |t|
|
6
|
+
t.string :field
|
7
|
+
end
|
8
|
+
|
9
|
+
create_table :association_test_classes do |t|
|
10
|
+
t.integer :test_class_id
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def down
|
15
|
+
drop_table :test_classes
|
16
|
+
drop_table :association_test_classes
|
17
|
+
end
|
18
|
+
end
|
@@ -1,87 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
RSpec
|
4
|
-
|
5
|
-
|
6
|
-
with_columns do |t|
|
7
|
-
t.string :field
|
8
|
-
end
|
9
|
-
|
10
|
-
extend OrderAsSpecified
|
11
|
-
|
12
|
-
has_one :association_test_class
|
13
|
-
end
|
14
|
-
|
15
|
-
Temping.create :association_test_class do
|
16
|
-
with_columns do |t|
|
17
|
-
t.integer :test_class_id
|
18
|
-
end
|
19
|
-
|
20
|
-
belongs_to :test_class
|
21
|
-
end
|
22
|
-
|
23
|
-
# Clean up after each test. This is a lot lighter for these few tests than
|
24
|
-
# trying to wrangle with RSpec-Rails to get transactional tests to work.
|
25
|
-
after :each do
|
26
|
-
TestClass.delete_all
|
27
|
-
AssociationTestClass.delete_all
|
28
|
-
end
|
29
|
-
|
30
|
-
let(:shuffled_objects) do
|
31
|
-
5.times.map { |i| TestClass.create(field: "Field #{i}") }.shuffle
|
32
|
-
end
|
33
|
-
let(:shuffled_object_fields) { shuffled_objects.map(&:field) }
|
34
|
-
let(:shuffled_object_ids) { shuffled_objects.map(&:id) }
|
35
|
-
let(:omitted_object) { TestClass.create }
|
36
|
-
|
37
|
-
context "with no table name specified" do
|
38
|
-
subject { TestClass.order_as_specified(field: shuffled_object_fields) }
|
39
|
-
|
40
|
-
it "returns results including unspecified objects" do
|
41
|
-
omitted_object # Build an object that isn't sorted in this list.
|
42
|
-
expect(subject).to include omitted_object
|
43
|
-
end
|
44
|
-
|
45
|
-
it "returns results in the given order" do
|
46
|
-
omitted_object # Build an object that isn't sorted in this list.
|
47
|
-
expect(subject.map(&:id)).
|
48
|
-
to eq [*shuffled_object_ids, omitted_object.id]
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
context "with another table name specified" do
|
53
|
-
let(:associated_objects) do
|
54
|
-
shuffled_objects.map do |object|
|
55
|
-
AssociationTestClass.create(test_class: object)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
let(:associated_object_ids) { associated_objects.map(&:id) }
|
59
|
-
let(:omitted_associated_object) do
|
60
|
-
AssociationTestClass.create(test_class: omitted_object)
|
61
|
-
end
|
62
|
-
|
63
|
-
subject do
|
64
|
-
TestClass.
|
65
|
-
joins(:association_test_class).
|
66
|
-
order_as_specified(
|
67
|
-
association_test_classes: { id: associated_object_ids }
|
68
|
-
)
|
69
|
-
end
|
70
|
-
|
71
|
-
it "returns results including unspecified objects" do
|
72
|
-
# Build an object that isn't sorted in this list.
|
73
|
-
omitted_associated_object
|
74
|
-
|
75
|
-
expect(subject).to include omitted_object
|
76
|
-
end
|
77
|
-
|
78
|
-
it "returns results in the given order" do
|
79
|
-
# Build an object that isn't sorted in this list.
|
80
|
-
omitted_associated_object
|
81
|
-
|
82
|
-
expect(subject.map(&:id)).
|
83
|
-
to eq [*shuffled_object_ids, omitted_object.id]
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
1
|
+
# Instead of defining specs here, we define them in
|
2
|
+
# shared/order_as_specified_examples.rb. We then have a different spec file for
|
3
|
+
# each supported database adapter, whch runs the shared RSpec tests found in
|
4
|
+
# the shared/order_as_specified_examples.rb file, along with any adapter-
|
5
|
+
# specific tests.
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "shared/order_as_specified_examples"
|
3
|
+
require "config/test_setup_migration"
|
4
|
+
|
5
|
+
RSpec.describe "PostgreSQL" do
|
6
|
+
before :all do
|
7
|
+
ActiveRecord::Base.establish_connection(:postgresql_test)
|
8
|
+
TestSetupMigration.migrate(:up)
|
9
|
+
end
|
10
|
+
|
11
|
+
after(:all) { ActiveRecord::Base.remove_connection }
|
12
|
+
|
13
|
+
include_examples ".order_as_specified"
|
14
|
+
|
15
|
+
context "when using DISTINCT ON" do
|
16
|
+
subject do
|
17
|
+
TestClass.order_as_specified(
|
18
|
+
distinct_on: true,
|
19
|
+
field: shuffled_object_fields
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
let(:shuffled_objects) do
|
24
|
+
fields = 3.times.map { |i| "Field #{i}" } * 2
|
25
|
+
5.times.map { |i| TestClass.create(field: fields[i]) }.shuffle
|
26
|
+
end
|
27
|
+
|
28
|
+
it "returns distinct objects" do
|
29
|
+
expect(subject.length).to eq 3
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "support/test_class"
|
2
|
+
require "support/association_test_class"
|
3
|
+
|
4
|
+
RSpec.shared_examples ".order_as_specified" do
|
5
|
+
# Clean up after each test. This is a lot lighter for these few tests than
|
6
|
+
# trying to wrangle with RSpec-Rails to get transactional tests to work.
|
7
|
+
after :each do
|
8
|
+
TestClass.delete_all
|
9
|
+
AssociationTestClass.delete_all
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:shuffled_objects) do
|
13
|
+
5.times.map { |i| TestClass.create(field: "Field #{i}") }.shuffle
|
14
|
+
end
|
15
|
+
let(:shuffled_object_fields) { shuffled_objects.map(&:field) }
|
16
|
+
let(:shuffled_object_ids) { shuffled_objects.map(&:id) }
|
17
|
+
let(:omitted_object) { TestClass.create(field: "Nothing") }
|
18
|
+
|
19
|
+
context "with no table name specified" do
|
20
|
+
subject { TestClass.order_as_specified(field: shuffled_object_fields) }
|
21
|
+
|
22
|
+
it "returns results including unspecified objects" do
|
23
|
+
omitted_object # Build an object that isn't sorted in this list.
|
24
|
+
expect(subject).to include omitted_object
|
25
|
+
end
|
26
|
+
|
27
|
+
it "returns results in the given order" do
|
28
|
+
omitted_object # Build an object that isn't sorted in this list.
|
29
|
+
expect(subject.map(&:id)).
|
30
|
+
to eq [*shuffled_object_ids, omitted_object.id]
|
31
|
+
end
|
32
|
+
|
33
|
+
context "when the order includes nil" do
|
34
|
+
let(:shuffled_objects) do
|
35
|
+
5.times.map do |i|
|
36
|
+
TestClass.create(field: (i == 0 ? nil : "Field #{i}"))
|
37
|
+
end.shuffle
|
38
|
+
end
|
39
|
+
|
40
|
+
it "raises an error" do
|
41
|
+
expect { subject }.to raise_error(OrderAsSpecified::Error)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "with another table name specified" do
|
47
|
+
subject do
|
48
|
+
TestClass.
|
49
|
+
joins(:association_test_class).
|
50
|
+
order_as_specified(
|
51
|
+
association_test_classes: { id: associated_object_ids }
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
let(:associated_objects) do
|
56
|
+
shuffled_objects.map do |object|
|
57
|
+
AssociationTestClass.create(test_class: object)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
let(:associated_object_ids) { associated_objects.map(&:id) }
|
61
|
+
let(:omitted_associated_object) do
|
62
|
+
AssociationTestClass.create(test_class: omitted_object)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "returns results including unspecified objects" do
|
66
|
+
# Build an object that isn't sorted in this list.
|
67
|
+
omitted_associated_object
|
68
|
+
|
69
|
+
expect(subject).to include omitted_object
|
70
|
+
end
|
71
|
+
|
72
|
+
it "returns results in the given order" do
|
73
|
+
# Build an object that isn't sorted in this list.
|
74
|
+
omitted_associated_object
|
75
|
+
|
76
|
+
expect(subject.map(&:id)).
|
77
|
+
to eq [*shuffled_object_ids, omitted_object.id]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
require "codeclimate-test-reporter"
|
2
2
|
CodeClimate::TestReporter.start
|
3
3
|
|
4
|
-
|
5
|
-
require "temping"
|
6
|
-
ActiveRecord::Base.
|
7
|
-
establish_connection(adapter: "sqlite3", database: ":memory:")
|
4
|
+
require "active_record"
|
8
5
|
|
9
6
|
require "order_as_specified"
|
10
7
|
|
8
|
+
ActiveRecord::Base.configurations = YAML.load_file("spec/config/database.yml")
|
9
|
+
|
11
10
|
RSpec.configure do |config|
|
12
11
|
# These two settings work together to allow you to limit a spec run
|
13
12
|
# to individual examples or groups you care about by tagging them with
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "shared/order_as_specified_examples"
|
3
|
+
require "config/test_setup_migration"
|
4
|
+
|
5
|
+
RSpec.describe "SQLite3" do
|
6
|
+
before :all do
|
7
|
+
ActiveRecord::Base.establish_connection(:sqlite3_test)
|
8
|
+
TestSetupMigration.migrate(:up)
|
9
|
+
end
|
10
|
+
|
11
|
+
after(:all) { ActiveRecord::Base.remove_connection }
|
12
|
+
|
13
|
+
include_examples ".order_as_specified"
|
14
|
+
end
|
metadata
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: order_as_specified
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: '1.0'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jacob Evelyn
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-04-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - '>='
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '4.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - '>='
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '4.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - ~>
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0.23'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pg
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.18'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.18'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: rspec
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -109,19 +123,19 @@ dependencies:
|
|
109
123
|
- !ruby/object:Gem::Version
|
110
124
|
version: '0.29'
|
111
125
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
126
|
+
name: sqlite3
|
113
127
|
requirement: !ruby/object:Gem::Requirement
|
114
128
|
requirements:
|
115
|
-
- -
|
129
|
+
- - '>='
|
116
130
|
- !ruby/object:Gem::Version
|
117
|
-
version: '3
|
131
|
+
version: '1.3'
|
118
132
|
type: :development
|
119
133
|
prerelease: false
|
120
134
|
version_requirements: !ruby/object:Gem::Requirement
|
121
135
|
requirements:
|
122
|
-
- -
|
136
|
+
- - '>='
|
123
137
|
- !ruby/object:Gem::Version
|
124
|
-
version: '3
|
138
|
+
version: '1.3'
|
125
139
|
description: Obtain ActiveRecord results with a custom ordering with no need to store
|
126
140
|
anything in the database.
|
127
141
|
email:
|
@@ -138,10 +152,18 @@ files:
|
|
138
152
|
- LICENSE.txt
|
139
153
|
- README.md
|
140
154
|
- lib/order_as_specified.rb
|
155
|
+
- lib/order_as_specified/error.rb
|
141
156
|
- lib/order_as_specified/version.rb
|
142
157
|
- order_as_specified.gemspec
|
158
|
+
- spec/config/database.yml
|
159
|
+
- spec/config/test_setup_migration.rb
|
143
160
|
- spec/order_as_specified_spec.rb
|
161
|
+
- spec/postgresql_spec.rb
|
162
|
+
- spec/shared/order_as_specified_examples.rb
|
144
163
|
- spec/spec_helper.rb
|
164
|
+
- spec/sqlite3_spec.rb
|
165
|
+
- spec/support/association_test_class.rb
|
166
|
+
- spec/support/test_class.rb
|
145
167
|
homepage: https://github.com/panorama-ed/order_as_specified
|
146
168
|
licenses:
|
147
169
|
- MIT
|
@@ -167,5 +189,13 @@ signing_key:
|
|
167
189
|
specification_version: 4
|
168
190
|
summary: Add arbitrary ordering to ActiveRecord queries.
|
169
191
|
test_files:
|
192
|
+
- spec/config/database.yml
|
193
|
+
- spec/config/test_setup_migration.rb
|
170
194
|
- spec/order_as_specified_spec.rb
|
195
|
+
- spec/postgresql_spec.rb
|
196
|
+
- spec/shared/order_as_specified_examples.rb
|
171
197
|
- spec/spec_helper.rb
|
198
|
+
- spec/sqlite3_spec.rb
|
199
|
+
- spec/support/association_test_class.rb
|
200
|
+
- spec/support/test_class.rb
|
201
|
+
has_rdoc:
|