order_as_specified 0.1.0 → 1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|