randumb 0.4.0 → 0.6.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 CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- OTI0OTBlMWNhNTc3MWQxYzU5MDM0YzAwNmM5NWI3OTFmNzhlYzA0Nw==
5
- data.tar.gz: !binary |-
6
- YzNlMmUzYTA3ZDRmZDI4ZmE5YTIyMzFiMGNlMTA2ZDNiMjRjZDFjNQ==
7
- !binary "U0hBNTEy":
8
- metadata.gz: !binary |-
9
- OTkzMmIzZmQ1ZDg0YjhjYmFiODhhNDRhZWFkN2RkNzdjYTQ4NTE5ZDgzOTIx
10
- NTY4NTk4YjVhOTZmMTFhNWJlZjM5Zjg0OGE3YzMyNmFmM2QwZDA1ZDRjYTY3
11
- YTRmYzVkNjExY2UwMTc4MDBhZWFmZjFjNzkzYTgwMzI2MjQzYjM=
12
- data.tar.gz: !binary |-
13
- OWJjZjUzODMzOWY2MWYwZWU1YWQzZDAxMWNkOWYyM2E3ZTQyZTNmMmE4ODI4
14
- ZjY4NjE5ZjBjMzkyNDFmZTFhZWJlMGQ4YWNlOTg2YjdiNzA3M2NmNzhhNGZj
15
- ZDUzMTZjNjgwYTI3ZWYzNjE0MDdkMWNmNjNlOWY4MGQ0ZWU2ZWY=
2
+ SHA256:
3
+ metadata.gz: 9fd0e54ae7de8a15766df8a33d65edd4a5d1eab2cb3ba36b27c6f9541ab39a72
4
+ data.tar.gz: ed1afd68fa829e7e236d92d44fda28439492b004eda2c8d45b11b1b85f8feee3
5
+ SHA512:
6
+ metadata.gz: 7031236bbde306b4c508d019956514ebe0a9b93b037f0d2cc2a539ea694b55c33c05b6728cfaae952f663b2727f919b974a863ddc905b15c77e70ca99c149c91
7
+ data.tar.gz: 7431df3f579c21e3076507b85564375646db6e78135ac0420d399c62e50474bafe3a1d827bc1f174edb57f22c8a68dbbaf7eb6b8e656e845e23476dfbb4bc310
@@ -1,4 +1,5 @@
1
1
  require 'randumb/version'
2
+ require 'randumb/syntax'
2
3
  require 'randumb/relation'
3
4
 
4
5
  # Mix it in
@@ -9,47 +9,44 @@ module Randumb
9
9
 
10
10
  # If the max_items argument is omitted, one random entity will be returned.
11
11
  # If you provide the integer argument, you will get back an array of records.
12
- def random(max_items = nil)
13
- random_weighted(nil, max_items)
12
+ def random(max_items = nil, opts={})
13
+ ActiveSupport::Deprecation.warn "The random() method will be depricated in randumb 1.0 in favor of the order_by_rand scope."
14
+ relation = clone
15
+ return random_by_id_shuffle(max_items, opts) if is_randumb_postges_case?(relation)
16
+ scope = relation.order_by_rand(opts)
17
+
18
+ scope = scope.limit(max_items) if override_limit?(max_items, relation)
19
+
20
+ # return first record if method was called without parameters
21
+ max_items ? scope.to_a : scope.first
14
22
  end
15
23
 
16
24
  # If ranking_column is provided, that named column wil be multiplied
17
25
  # by a random number to determine probability of order. The ranking column must be numeric.
18
- def random_weighted(ranking_column, max_items = nil)
26
+ def random_weighted(ranking_column, max_items = nil, opts={})
27
+ ActiveSupport::Deprecation.warn "The random_weighted() method will be depricated in randumb 1.0 in favor of the order_by_rand_weighted scope."
19
28
  relation = clone
20
- return random_by_id_shuffle(max_items) if is_randumb_postges_case?(relation, ranking_column)
29
+ return random_by_id_shuffle(max_items, opts) if is_randumb_postges_case?(relation, ranking_column)
21
30
  raise_unless_valid_ranking_column(ranking_column)
22
- # get clause for current db type
23
- order_clause = random_order_clause(ranking_column)
24
31
 
25
- the_scope = if ::ActiveRecord::VERSION::MAJOR == 3 && ::ActiveRecord::VERSION::MINOR < 2
26
- # AR 3.0.0 support
27
- relation.order(order_clause)
28
- else
29
- # keep prior orders and append random
30
- all_orders = (relation.orders + [order_clause]).join(", ")
31
- # override all previous orders
32
- relation.reorder(all_orders)
33
- end
34
-
32
+ scope = relation.order_by_rand_weighted(ranking_column, opts)
33
+
35
34
  # override the limit if they are requesting multiple records
36
- if max_items && (!relation.limit_value || relation.limit_value > max_items)
37
- the_scope = the_scope.limit(max_items)
38
- end
35
+ scope = scope.limit(max_items) if override_limit?(max_items, relation)
39
36
 
40
37
  # return first record if method was called without parameters
41
- max_items ? the_scope.to_a : the_scope.first
38
+ max_items ? scope.to_a : scope.first
42
39
  end
43
40
 
44
41
 
45
42
  # This was my first implementation, adding it as an option for people to use
46
43
  # and to fall back on for pesky DB one off situations...
47
44
  # https://github.com/spilliton/randumb/issues/7
48
- def random_by_id_shuffle(max_items = nil)
45
+ def random_by_id_shuffle(max_items = nil, opts={})
49
46
  return_first_record = max_items.nil? # see return switch at end
50
47
  max_items ||= 1
51
48
  relation = clone
52
- ids = fetch_random_ids(relation, max_items)
49
+ ids = fetch_random_ids(relation, max_items, opts)
53
50
 
54
51
  # build new scope for final query
55
52
  the_scope = klass.includes(includes_values)
@@ -58,21 +55,54 @@ module Randumb
58
55
  the_scope = the_scope.select(select_values) unless select_values.empty?
59
56
 
60
57
  # get the records and shuffle since the order of the ids
61
- # passed to find_all_by_id isn't retained in the result set
62
- records = the_scope.find_all_by_id(ids).shuffle!
58
+ # passed to where() isn't retained in the result set
59
+ rng = random_number_generator(opts)
60
+ records = the_scope.where(:id => ids).to_a.shuffle!(:random => rng)
63
61
 
64
62
  # return first record if method was called without parameters
65
63
  return_first_record ? records.first : records
66
64
  end
67
65
 
66
+ def order_by_rand(opts = {})
67
+ if opts.is_a?(Hash)
68
+ build_order_scope(opts)
69
+ else
70
+ raise ArgumentError.new(
71
+ "order_by_rand() expects a hash of options. If you need to limit "\
72
+ "results simply add a limit to your scope ex: Artist.order_by_rand.limit(1)"
73
+ )
74
+ end
75
+ end
76
+
77
+ def order_by_rand_weighted(ranking_column, opts={})
78
+ raise_unless_valid_ranking_column(ranking_column)
79
+ is_randumb_postges_case?(self, ranking_column)
80
+ build_order_scope(opts, ranking_column)
81
+ end
82
+
68
83
  private
69
84
 
85
+ def build_order_scope(options, ranking_column=nil)
86
+ opts = options.reverse_merge(connection: connection, table_name: table_name)
87
+
88
+ order_clause = if ranking_column
89
+ Randumb::Syntax.random_weighted_order_clause(ranking_column, opts)
90
+ else
91
+ Randumb::Syntax.random_order_clause(opts)
92
+ end
93
+
94
+ # keep prior orders and append random
95
+ all_orders = (arel.orders + [order_clause])
96
+ # override all previous orders
97
+ reorder(all_orders)
98
+ end
99
+
70
100
  # postgres won't let you do an order_by when also doing a distinct
71
101
  # let's just use the in-memory option in this case
72
- def is_randumb_postges_case?(relation, ranking_column)
102
+ def is_randumb_postges_case?(relation, ranking_column=nil)
73
103
  if relation.respond_to?(:uniq_value) && relation.uniq_value && connection.adapter_name =~ /(postgres|postgis)/i
74
104
  if ranking_column
75
- raise Exception, "random_weighted: not possible when using .uniq and the postgres/postgis db adapter"
105
+ raise Exception, "order_by_rand_weighted: not possible when using .uniq and the postgres/postgis db adapter"
76
106
  else
77
107
  return true
78
108
  end
@@ -88,36 +118,9 @@ module Randumb
88
118
  end
89
119
  end
90
120
 
91
- # sligtly different for each DB
92
- def random_syntax
93
- if connection.adapter_name =~ /(sqlite|postgres|postgis)/i
94
- "RANDOM()"
95
- elsif connection.adapter_name =~ /mysql/i
96
- "RAND()"
97
- else
98
- raise Exception, "ActiveRecord adapter: '#{connection.adapter_name}' not supported by randumb. Send a pull request or open a ticket: https://github.com/spilliton/randumb"
99
- end
100
- end
101
-
102
- # builds the order clause to be appended in where clause
103
- def random_order_clause(ranking_column)
104
- if ranking_column.nil?
105
- random_syntax
106
- else
107
- if connection.adapter_name =~ /sqlite/i
108
- # computer multiplication is faster than division I was once taught...so translate here
109
- max_int = 9223372036854775807.0
110
- multiplier = 1.0 / max_int
111
- "(#{ranking_column} * ABS(#{random_syntax} * #{multiplier}) ) DESC"
112
- else
113
- "(#{ranking_column} * #{random_syntax}) DESC"
114
- end
115
- end
116
- end
117
-
118
121
  # Returns all matching ids from the db, shuffles them,
119
122
  # then returns an array containing at most max_ids
120
- def fetch_random_ids(relation, max_ids)
123
+ def fetch_random_ids(relation, max_ids, opts = {})
121
124
  # clear these for our id only query
122
125
  relation.select_values = []
123
126
  relation.includes_values = []
@@ -127,30 +130,54 @@ module Randumb
127
130
 
128
131
  id_results = connection.select_all(id_only_relation.to_sql)
129
132
 
133
+ rng = random_number_generator(opts)
130
134
  if max_ids == 1 && id_results.count > 0
131
- [ id_results[ rand(id_results.count) ]['id'] ]
135
+ rand_index = rng.rand(id_results.count)
136
+ [id_results[rand_index]["id"]]
132
137
  else
133
- # ActiveRecord 4 requires .to_ary
134
- arr = id_results.respond_to?(:to_ary) ? id_results.to_ary : id_results
135
- arr.shuffle![0,max_ids].collect!{ |h| h['id'] }
138
+ # ActiveRecord 4 requires .to_a
139
+ arr = id_results.respond_to?(:to_a) ? id_results.to_a : id_results
140
+ arr.shuffle!(random: rng)[0, max_ids].collect! { |h| h["id"] }
136
141
  end
137
142
  end
138
143
 
144
+ def random_number_generator(opts={})
145
+ if seed = opts[:seed]
146
+ Random.new(seed)
147
+ else
148
+ Random.new
149
+ end
150
+ end
151
+
152
+ def override_limit?(max_items, relation)
153
+ max_items && (!relation.limit_value || relation.limit_value > max_items)
154
+ end
155
+
139
156
  end
140
157
 
141
158
 
142
159
  # Class methods
160
+ # where(nil) is because:
161
+ # http://stackoverflow.com/questions/18198963/with-rails-4-model-scoped-is-deprecated-but-model-all-cant-replace-it
143
162
  module Base
144
- def random(max_items = nil)
145
- relation.random(max_items)
163
+ def random(max_items = nil, opts = {})
164
+ where(nil).random(max_items, opts)
165
+ end
166
+
167
+ def random_weighted(ranking_column, max_items = nil, opts = {})
168
+ where(nil).random_weighted(ranking_column, max_items, opts)
169
+ end
170
+
171
+ def random_by_id_shuffle(max_items = nil, opts = {})
172
+ where(nil).random_by_id_shuffle(max_items, opts)
146
173
  end
147
174
 
148
- def random_weighted(ranking_column, max_items = nil)
149
- relation.random_weighted(ranking_column, max_items)
175
+ def order_by_rand(opts = {})
176
+ where(nil).order_by_rand(opts)
150
177
  end
151
178
 
152
- def random_by_id_shuffle(max_items = nil)
153
- relation.random_by_id_shuffle(max_items)
179
+ def order_by_rand_weighted(ranking_column, opts={})
180
+ where(nil).order_by_rand_weighted(ranking_column, opts)
154
181
  end
155
182
  end
156
183
 
@@ -159,6 +186,7 @@ module Randumb
159
186
  module MethodMissingMagicks
160
187
  def method_missing(symbol, *args)
161
188
  if symbol.to_s =~ /^random_weighted_by_(\w+)$/
189
+ ActiveSupport::Deprecation.warn "Dynamic finders will be removed in randumb 1.0 http://guides.rubyonrails.org/active_record_querying.html#dynamic-finders"
162
190
  random_weighted($1, *args)
163
191
  else
164
192
  super
@@ -167,6 +195,7 @@ module Randumb
167
195
 
168
196
  def respond_to?(symbol, include_private=false)
169
197
  if symbol.to_s =~ /^random_weighted_by_(\w+)$/
198
+ ActiveSupport::Deprecation.warn "Dynamic finders will be removed in randumb 1.0 http://guides.rubyonrails.org/active_record_querying.html#dynamic-finders"
170
199
  true
171
200
  else
172
201
  super
@@ -0,0 +1,82 @@
1
+ module Randumb
2
+ class Syntax
3
+ class << self
4
+
5
+ # builds the order clause to be appended in where clause
6
+ def random_order_clause(opts={})
7
+ random_for(opts)
8
+ end
9
+
10
+ # builds the order clause to be appended in where clause
11
+ def random_weighted_order_clause(ranking_column, opts={})
12
+ connection = opts[:connection]
13
+
14
+ if connection.adapter_name =~ /sqlite/i
15
+ # computer multiplication is faster than division I was once taught...so translate here
16
+ max_int = 9223372036854775807.0
17
+ multiplier = 1.0 / max_int
18
+ "(#{ranking_column} * ABS(#{random_for(opts)} * #{multiplier}) ) DESC"
19
+ else
20
+ "(#{ranking_column} * #{random_for(opts)}) DESC"
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ # sligtly different for each DB
27
+ def random_for(opts)
28
+ connection = opts[:connection]
29
+ adapter_name = connection.adapter_name
30
+ if adapter_name =~ /(sqlite)/i
31
+ random_for_sqlite(opts)
32
+ elsif adapter_name =~ /(postgres|postgis)/i
33
+ random_for_postgres(opts)
34
+ elsif adapter_name =~ /mysql/i
35
+ random_for_mysql(opts)
36
+ else
37
+ raise Exception, "ActiveRecord adapter: '#{adapter_name}' not supported by randumb. Send a pull request or open a ticket: https://github.com/spilliton/randumb"
38
+ end
39
+ end
40
+
41
+ def random_for_sqlite(opts)
42
+ if seed = opts[:seed]
43
+ table_name = opts[:table_name]
44
+ # SQLLite does not support a random seed. However, pseudo-randomness
45
+ # can be achieved by sorting on a hash of the id field (generated by
46
+ # multiplying the id by the random seed and ignoring everything before
47
+ # the decimal).
48
+ # See http://stackoverflow.com/questions/2171578/seeding-sqlite-random
49
+ seed_value = Random.new(seed.to_i).rand
50
+ "(SUBSTR(#{table_name}.id * #{seed_value}, LENGTH(#{table_name}.id) + 2))"
51
+ else
52
+ "RANDOM()"
53
+ end
54
+ end
55
+
56
+ def random_for_postgres(opts)
57
+ # Postgres random seeding requires executing an extra "SELECT SETSEED(value)" on the connection.
58
+ # See http://www.postgresql.org/docs/8.3/static/sql-set.html:
59
+ #
60
+ # Sets the internal seed for the random number generator (the
61
+ # function random). Allowed values are floating-point numbers
62
+ # between 0 and 1, which are then multiplied by 2^(31)-1.
63
+ #
64
+ if seed = opts[:seed]
65
+ connection = opts[:connection]
66
+ seed_value = Random.new(seed.to_i).rand # map integer seed to a value: 0 <= value < 1
67
+ connection.execute "SELECT SETSEED(#{seed_value})"
68
+ end
69
+ "RANDOM()"
70
+ end
71
+
72
+ def random_for_mysql(opts)
73
+ if seed = opts[:seed]
74
+ "RAND(#{seed.to_i})"
75
+ else
76
+ "RAND()"
77
+ end
78
+ end
79
+
80
+ end
81
+ end
82
+ end
@@ -1,3 +1,3 @@
1
1
  module Randumb
2
- VERSION = "0.4.0"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -1,5 +1,11 @@
1
1
  class Artist < ActiveRecord::Base
2
2
  has_many :albums
3
-
3
+
4
+ if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("3.2")
5
+ default_scope { where("artists.deleted_at IS NULL") }
6
+ else
7
+ default_scope where("artists.deleted_at IS NULL")
8
+ end
9
+
4
10
  scope :at_least_three_views, -> { where("views >= 3") }
5
- end
11
+ end
@@ -1,4 +1,4 @@
1
- FactoryGirl.define do
1
+ FactoryBot.define do
2
2
  factory :artist do
3
3
  name { Faker::Lorem.words(3).join(' ') }
4
4
  views { Random.rand(50) }
@@ -1,13 +1,17 @@
1
1
  $:.unshift '.'; require File.dirname(__FILE__) + '/test_helper'
2
2
 
3
- class RandumbTest < Test::Unit::TestCase
4
-
5
- def assert_equal_for_both_methods(expected, obj, params = nil)
6
- assert_equal expected, obj.send(:random, params), "when calling random"
7
- assert_equal expected, obj.send(:random_by_id_shuffle, params), "when calling random_by_id_shuffle"
3
+ class RandumbTest < Minitest::Test
4
+
5
+ def assert_equal_for_both_methods(expected, obj, *params)
6
+ if expected.nil?
7
+ assert_nil obj.send(:random, *params), "when calling random"
8
+ assert_nil obj.send(:random_by_id_shuffle, *params), "when calling random_by_id_shuffle"
9
+ else
10
+ assert_equal expected, obj.send(:random, *params), "when calling random"
11
+ assert_equal expected, obj.send(:random_by_id_shuffle, *params), "when calling random_by_id_shuffle"
12
+ end
8
13
  end
9
14
 
10
-
11
15
  should "should return empty when no record in table" do
12
16
  assert_equal 0, Artist.count
13
17
 
@@ -15,36 +19,56 @@ class RandumbTest < Test::Unit::TestCase
15
19
  # above is equivalent to:
16
20
  # assert_equal nil, Artist.random
17
21
  # assert_equal nil, Artist.random_by_id_shuffle
22
+ assert_nil Artist.order_by_rand.first
18
23
 
19
24
  assert_equal_for_both_methods [], Artist, 1
20
25
  # above is equivalent to:
21
26
  # assert_equal [], Artist.random(1)
22
27
  # assert_equal [], Artist.random_by_id_shuffle(1)
28
+ assert_equal [], Artist.order_by_rand.limit(1).all
23
29
 
24
30
  assert_equal_for_both_methods nil, Artist.limit(50)
25
31
  end
26
32
 
33
+ should "raise helpful error if non-hash passed" do
34
+ error = assert_raises ArgumentError do
35
+ Artist.order_by_rand(10)
36
+ end
37
+ assert error.message.include?("order_by_rand")
38
+ end
39
+
27
40
  context "1 record in the table" do
28
41
  setup do
29
- @high_on_fire = FactoryGirl.create(:artist, :name => "High On Fire", :views => 1)
42
+ @high_on_fire = FactoryBot.create(:artist, :name => "High On Fire", :views => 1)
30
43
  end
31
44
 
32
45
  should "select only 1 record even when you request more" do
33
46
  assert_equal 1, Artist.count
34
47
 
35
48
  assert_equal_for_both_methods @high_on_fire, Artist
49
+ assert_equal @high_on_fire, Artist.order_by_rand.first
50
+
36
51
  assert_equal_for_both_methods [@high_on_fire], Artist, 1
37
52
  assert_equal_for_both_methods [@high_on_fire], Artist, 30
53
+ assert_equal [@high_on_fire], Artist.limit(30).order_by_rand.all
38
54
  end
39
55
 
40
56
  should "not return a record that doesnt match where" do
41
57
  assert_equal_for_both_methods nil, Artist.where(:name => "The Little Gentlemen")
42
58
  end
43
59
 
60
+ should "respect default scope" do
61
+ @high_on_fire.deleted_at = Time.now.utc
62
+ @high_on_fire.save!
63
+ assert_equal 0, Artist.count
64
+ assert_nil Artist.order_by_rand.first
65
+ assert_equal @high_on_fire, Artist.unscoped.order_by_rand.first
66
+ end
67
+
44
68
  context "3 records in table" do
45
69
  setup do
46
- @fiona_apple = FactoryGirl.create(:artist, :name => "Fiona Apple", :views => 3)
47
- @magnetic_fields = FactoryGirl.create(:artist, :name => "The Magnetic Fields", :views => 2)
70
+ @fiona_apple = FactoryBot.create(:artist, :name => "Fiona Apple", :views => 3)
71
+ @magnetic_fields = FactoryBot.create(:artist, :name => "The Magnetic Fields", :views => 2)
48
72
  end
49
73
 
50
74
  should "apply randomness after other orders when using sql method" do
@@ -61,19 +85,27 @@ class RandumbTest < Test::Unit::TestCase
61
85
  end
62
86
 
63
87
  should "respect selecting certain columns" do
88
+ # this version doesn't throw for some reason...
89
+ skip_throw_missing = ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 0
90
+
64
91
  assert_equal 3, Artist.find(@fiona_apple.id).views
65
92
 
66
93
  artists = Artist.select(:name).random(3)
67
94
  assert_equal false, artists.first.name.nil?
68
- assert_raise (ActiveModel::MissingAttributeError) {artists.first.views}
95
+ assert_raises (ActiveModel::MissingAttributeError) { artists.first.views } unless skip_throw_missing
96
+
97
+ artists = Artist.select(:name).order_by_rand.limit(3)
98
+ assert_equal false, artists.first.name.nil?
99
+ assert_raises (ActiveModel::MissingAttributeError) { artists.first.views } unless skip_throw_missing
69
100
 
70
101
  artists = Artist.select(:name).random_by_id_shuffle(3)
71
102
  assert_equal false, artists.first.name.nil?
72
- assert_raise (ActiveModel::MissingAttributeError) {artists.first.views}
103
+ assert_raises (ActiveModel::MissingAttributeError) { artists.first.views } unless skip_throw_missing
73
104
  end
74
105
 
75
106
  should "respect scopes" do
76
107
  assert_equal_for_both_methods [@fiona_apple], Artist.at_least_three_views, 3
108
+ assert_equal [@fiona_apple], Artist.at_least_three_views.order_by_rand.limit(3)
77
109
  end
78
110
 
79
111
  should "select only as many as in the db if we request more" do
@@ -92,10 +124,10 @@ class RandumbTest < Test::Unit::TestCase
92
124
 
93
125
  context "with some albums" do
94
126
  setup do
95
- @tidal = FactoryGirl.create(:album, :name => "Tidal", :artist => @fiona_apple)
96
- @extraordinary_machine = FactoryGirl.create(:album, :name => "Extraordinary Machine", :artist => @fiona_apple)
97
- @sixty_nine_love_songs = FactoryGirl.create(:album, :name => "69 Love Songs", :artist => @magnetic_fields)
98
- @snakes_for_the_divine = FactoryGirl.create(:album, :name => "Snakes For the Divine", :artist => @high_on_fire)
127
+ @tidal = FactoryBot.create(:album, :name => "Tidal", :artist => @fiona_apple)
128
+ @extraordinary_machine = FactoryBot.create(:album, :name => "Extraordinary Machine", :artist => @fiona_apple)
129
+ @sixty_nine_love_songs = FactoryBot.create(:album, :name => "69 Love Songs", :artist => @magnetic_fields)
130
+ @snakes_for_the_divine = FactoryBot.create(:album, :name => "Snakes For the Divine", :artist => @high_on_fire)
99
131
  end
100
132
 
101
133
 
@@ -103,7 +135,7 @@ class RandumbTest < Test::Unit::TestCase
103
135
  artists = Artist.includes(:albums).random(10)
104
136
  fiona_apple = artists.find { |a| a.name == "Fiona Apple" }
105
137
  # if I add a new album now, it shouldn't be in the albums assocation yet b/c it was already loaded
106
- FactoryGirl.create(:album, :name => "When The Pawn", :artist => @fiona_apple)
138
+ FactoryBot.create(:album, :name => "When The Pawn", :artist => @fiona_apple)
107
139
 
108
140
  assert_equal 2, fiona_apple.albums.length
109
141
  assert_equal 3, @fiona_apple.reload.albums.length
@@ -113,7 +145,7 @@ class RandumbTest < Test::Unit::TestCase
113
145
  artists = Artist.includes(:albums).random_by_id_shuffle(10)
114
146
  fiona_apple = artists.find { |a| a.name == "Fiona Apple" }
115
147
  # if I add a new album now, it shouldn't be in the albums assocation yet b/c it was already loaded
116
- FactoryGirl.create(:album, :name => "When The Pawn", :artist => @fiona_apple)
148
+ FactoryBot.create(:album, :name => "When The Pawn", :artist => @fiona_apple)
117
149
 
118
150
  assert_equal 2, fiona_apple.albums.length
119
151
  assert_equal 3, @fiona_apple.reload.albums.length
@@ -144,8 +176,8 @@ class RandumbTest < Test::Unit::TestCase
144
176
  should "work with uniq" do
145
177
  assert_equal 2, Artist.uniq.random(2).length
146
178
  assert_equal 2, Artist.uniq.random_by_id_shuffle(2).length
147
- assert_not_nil Artist.uniq.random
148
- assert_not_nil Artist.uniq.random_by_id_shuffle
179
+ assert !Artist.uniq.random.nil?
180
+ assert !Artist.uniq.random_by_id_shuffle.nil?
149
181
  end
150
182
  end
151
183
 
@@ -157,8 +189,8 @@ class RandumbTest < Test::Unit::TestCase
157
189
 
158
190
  context "2 records in table" do
159
191
  setup do
160
- @hum = FactoryGirl.create(:artist, :name => "Hum", :views => 3)
161
- @minutemen = FactoryGirl.create(:artist, :name => "Minutemen", :views => 2)
192
+ @hum = FactoryBot.create(:artist, :name => "Hum", :views => 3)
193
+ @minutemen = FactoryBot.create(:artist, :name => "Minutemen", :views => 2)
162
194
  end
163
195
 
164
196
  should "eventually render the 2 possible orders using default method" do
@@ -190,6 +222,29 @@ class RandumbTest < Test::Unit::TestCase
190
222
  assert order1_found
191
223
  assert order2_found
192
224
  end
193
- end
194
225
 
226
+ context "using seed" do
227
+ setup do
228
+ @seed = 123
229
+ end
230
+
231
+ should "always return the same order using default method" do
232
+ seeded_order = Artist.random(2, seed: @seed)
233
+ 10.times do
234
+ assert_equal seeded_order, Artist.random(2, seed: @seed)
235
+ end
236
+
237
+ 10.times do
238
+ assert_equal seeded_order, Artist.order_by_rand(seed: @seed).limit(2)
239
+ end
240
+ end
241
+
242
+ should "always return the same order using shuffle method" do
243
+ seeded_order = Artist.random_by_id_shuffle(2, seed: @seed)
244
+ 10.times do
245
+ assert_equal seeded_order, Artist.random_by_id_shuffle(2, seed: @seed)
246
+ end
247
+ end
248
+ end
249
+ end
195
250
  end
@@ -1,33 +1,29 @@
1
1
  $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
2
2
  require 'rubygems'
3
- require 'test/unit'
3
+ require "minitest/autorun"
4
4
  require 'shoulda'
5
- require 'factory_girl'
5
+ require 'factory_bot'
6
6
  require 'faker'
7
7
  require 'active_record'
8
8
  require 'active_support/dependencies'
9
- require 'active_support/core_ext/logger'
10
- # require 'active_record/fixtures'
11
9
  require 'randumb'
12
10
 
13
11
  MODELS_PATH = File.join(File.dirname(__FILE__), 'models')
14
12
 
15
-
16
- ActiveRecord::Base.logger = Logger.new(STDERR)
17
- ActiveRecord::Base.logger.level = Logger::WARN
18
-
19
13
  config = YAML::load(File.open(File.expand_path("../databases.yml", __FILE__)))
20
14
  version = ActiveRecord::VERSION::STRING
21
15
  driver = (ENV["DB"] or "sqlite3").downcase
22
16
  in_memory = config[driver]["database"] == ":memory:"
23
-
17
+
24
18
  # http://about.travis-ci.org/docs/user/database-setup/
25
19
  commands = {
26
20
  "mysql" => "mysql -e 'create database randumb_test;'",
27
21
  "postgres" => "psql -c 'create database randumb_test;' -U postgres"
28
22
  }
29
23
  %x{#{commands[driver] || true}}
30
-
24
+
25
+
26
+
31
27
  ActiveRecord::Base.establish_connection config[driver]
32
28
  puts "Using #{RUBY_VERSION} AR #{version} with #{driver}"
33
29
 
@@ -38,16 +34,18 @@ ActiveRecord::Base.connection.create_table(:artists, :force => true) do |t|
38
34
  t.float "rating"
39
35
  t.datetime "created_at"
40
36
  t.datetime "updated_at"
37
+ t.datetime "deleted_at"
41
38
  end
42
-
39
+
43
40
  ActiveRecord::Base.connection.create_table(:albums, :force => true) do |t|
44
41
  t.string "name"
45
42
  t.integer "views"
46
43
  t.integer "artist_id"
47
44
  t.datetime "created_at"
48
45
  t.datetime "updated_at"
46
+ t.datetime "deleted_at"
49
47
  end
50
-
48
+
51
49
  # setup models for lazy load
52
50
  dep = defined?(ActiveSupport::Dependencies) ? ActiveSupport::Dependencies : ::Dependencies
53
51
  dep.autoload_paths.unshift MODELS_PATH
@@ -56,7 +54,7 @@ dep.autoload_paths.unshift MODELS_PATH
56
54
  require 'test/models/factories'
57
55
 
58
56
  # clear db for every test
59
- class Test::Unit::TestCase
57
+ class Minitest::Test
60
58
 
61
59
  def setup
62
60
  Artist.delete_all
@@ -65,3 +63,6 @@ class Test::Unit::TestCase
65
63
 
66
64
  end
67
65
 
66
+
67
+ # Silence deprications
68
+ ActiveSupport::Deprecation.silenced = true
@@ -1,10 +1,10 @@
1
1
  $:.unshift '.'; require File.dirname(__FILE__) + '/test_helper'
2
2
 
3
- class WeightedTest < Test::Unit::TestCase
3
+ class WeightedTest < Minitest::Test
4
4
 
5
5
  should "raise exception when called with a non-existent column" do
6
6
  assert_raises(ArgumentError) do
7
- Artist.random_weighted(:blah)
7
+ Artist.order_by_rand_weighted(:blah)
8
8
  end
9
9
  assert_raises(ArgumentError) do
10
10
  Artist.random_weighted_by_blah
@@ -13,7 +13,7 @@ class WeightedTest < Test::Unit::TestCase
13
13
 
14
14
  should "raise exception when called with a non-numeric column" do
15
15
  assert_raises(ArgumentError) do
16
- Artist.random_weighted(:name)
16
+ Artist.order_by_rand_weighted(:name)
17
17
  end
18
18
  assert_raises(ArgumentError) do
19
19
  Artist.random_weighted_by_name
@@ -25,7 +25,7 @@ class WeightedTest < Test::Unit::TestCase
25
25
  if ENV["DB"] == "postgres"
26
26
  should "raise exception if being called with uniq/postgres" do
27
27
  assert_raises(Exception) do
28
- Artist.uniq.random_weighted(:name)
28
+ Artist.uniq.order_by_rand_weighted(:views)
29
29
  end
30
30
  end
31
31
  else
@@ -41,7 +41,7 @@ class WeightedTest < Test::Unit::TestCase
41
41
  end
42
42
 
43
43
  should "not interfere with active record dynamic methods that use method_missing" do
44
- @artist = FactoryGirl.create(:artist, :name => 'Spiritualized')
44
+ @artist = FactoryBot.create(:artist, :name => 'Spiritualized')
45
45
  assert_equal @artist, Artist.find_by_name('Spiritualized')
46
46
  end
47
47
 
@@ -59,13 +59,26 @@ class WeightedTest < Test::Unit::TestCase
59
59
  context "order by ranking_column" do
60
60
  setup do
61
61
  @view_counts = [1, 2, 3, 4, 5]
62
- @view_counts.each { |views| FactoryGirl.create(:artist, :views => views) }
62
+ @view_counts.each { |views| FactoryBot.create(:artist, views: views) }
63
+ end
64
+
65
+ should "respect default scope" do
66
+ artist = Artist.last
67
+ artist.deleted_at = Time.now.utc
68
+ artist.save!
69
+ 50.times do
70
+ assert Artist.random_weighted("views").id != artist.id, "should never pick deleted artist"
71
+ end
72
+ assert_equal 4, Artist.order_by_rand_weighted("views").all.length
63
73
  end
64
74
 
65
75
  should "order by ranking column with explicit method call" do
66
76
  assert_hits_per_views do
67
77
  Artist.random_weighted("views").views
68
78
  end
79
+ assert_hits_per_views do
80
+ Artist.order_by_rand_weighted("views").first.views
81
+ end
69
82
  end
70
83
 
71
84
  should "order by ranking column with method_missing" do
@@ -80,6 +93,12 @@ class WeightedTest < Test::Unit::TestCase
80
93
  assert(result.size == 5)
81
94
  result.first.views
82
95
  end
96
+
97
+ assert_hits_per_views do
98
+ result = Artist.order_by_rand_weighted("views").limit(5).all
99
+ assert(result.size == 5)
100
+ result.first.views
101
+ end
83
102
  end
84
103
 
85
104
  should "order by ranking column with method_missing using max_items" do
@@ -98,31 +117,45 @@ class WeightedTest < Test::Unit::TestCase
98
117
  result.last.views
99
118
  end
100
119
  end
120
+
121
+ assert_raises(MiniTest::Assertion) do
122
+ assert_hits_per_views do
123
+ result = Artist.order_by_rand_weighted(:views).limit(3)
124
+ assert(result.size == 3)
125
+ result.last.views
126
+ end
127
+ end
101
128
  end
102
129
 
103
130
  should "order by ranking column with method_missing using 1 max_items" do
104
131
  assert_hits_per_views do
105
132
  result = Artist.random_weighted_by_views(1)
106
- assert(result.size == 1)
133
+ assert_equal 1, result.length
107
134
  result.first.views
108
135
  end
109
- end
110
136
 
111
- def assert_hits_per_views
112
- hits_per_views = Hash.new
113
- @view_counts.each { |views| hits_per_views[views] = 0 }
114
-
115
- 1000.times do
116
- hits_per_views[yield] += 1
117
- end
118
- last_count = 0
119
- @view_counts.each do |views|
120
- hits = hits_per_views[views]
121
- assert(hits >= last_count, "#{hits} > #{last_count} : There were an unexpected number of visits: #{hits_per_views.to_yaml}")
122
- last_count = hits
137
+ assert_hits_per_views do
138
+ result = Artist.order_by_rand_weighted(:views).limit(1)
139
+ assert_equal 1, result.length
140
+ result.first.views
123
141
  end
124
142
  end
125
143
  end
126
144
 
145
+ def assert_hits_per_views
146
+ hits_per_views = Hash.new
147
+ @view_counts.each { |views| hits_per_views[views] = 0 }
148
+
149
+ 1000.times do
150
+ hits_per_views[yield] += 1
151
+ end
152
+ last_count = 0
153
+ @view_counts.each do |views|
154
+ hits = hits_per_views[views]
155
+ assert(hits >= last_count, "#{hits} > #{last_count} : There were an unexpected number of visits: #{hits_per_views.to_yaml}")
156
+ last_count = hits
157
+ end
158
+ end
159
+
127
160
 
128
- end
161
+ end
metadata CHANGED
@@ -1,111 +1,181 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: randumb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zachary Kloepping
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-07-20 00:00:00.000000000 Z
11
+ date: 2020-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ! '>='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '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: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ! '>='
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 3.0.0
33
+ version: 3.0.20
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ! '>='
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 3.0.0
40
+ version: 3.0.20
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: activerecord
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ! '>='
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 3.0.0
47
+ version: 3.0.20
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ! '>='
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 3.0.0
54
+ version: 3.0.20
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: sqlite3
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.3.6
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.3.6
69
+ - !ruby/object:Gem::Dependency
70
+ name: mysql2
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - '='
60
74
  - !ruby/object:Gem::Version
61
- version: 1.3.7
75
+ version: 0.4.10
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
80
  - - '='
67
81
  - !ruby/object:Gem::Version
68
- version: 1.3.7
82
+ version: 0.4.10
83
+ - !ruby/object:Gem::Dependency
84
+ name: pg
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 0.19.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '='
95
+ - !ruby/object:Gem::Version
96
+ version: 0.19.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: bigdecimal
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '='
102
+ - !ruby/object:Gem::Version
103
+ version: 1.4.2
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '='
109
+ - !ruby/object:Gem::Version
110
+ version: 1.4.2
111
+ - !ruby/object:Gem::Dependency
112
+ name: minitest
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
69
125
  - !ruby/object:Gem::Dependency
70
126
  name: shoulda
71
127
  requirement: !ruby/object:Gem::Requirement
72
128
  requirements:
73
- - - ! '>='
129
+ - - ">="
74
130
  - !ruby/object:Gem::Version
75
131
  version: '0'
76
132
  type: :development
77
133
  prerelease: false
78
134
  version_requirements: !ruby/object:Gem::Requirement
79
135
  requirements:
80
- - - ! '>='
136
+ - - ">="
81
137
  - !ruby/object:Gem::Version
82
138
  version: '0'
83
139
  - !ruby/object:Gem::Dependency
84
- name: factory_girl
140
+ name: factory_bot
85
141
  requirement: !ruby/object:Gem::Requirement
86
142
  requirements:
87
- - - ~>
143
+ - - ">="
88
144
  - !ruby/object:Gem::Version
89
- version: '3.0'
145
+ version: '0'
90
146
  type: :development
91
147
  prerelease: false
92
148
  version_requirements: !ruby/object:Gem::Requirement
93
149
  requirements:
94
- - - ~>
150
+ - - ">="
95
151
  - !ruby/object:Gem::Version
96
- version: '3.0'
152
+ version: '0'
97
153
  - !ruby/object:Gem::Dependency
98
154
  name: faker
99
155
  requirement: !ruby/object:Gem::Requirement
100
156
  requirements:
101
- - - ! '>='
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: pry
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
102
172
  - !ruby/object:Gem::Version
103
173
  version: '0'
104
174
  type: :development
105
175
  prerelease: false
106
176
  version_requirements: !ruby/object:Gem::Requirement
107
177
  requirements:
108
- - - ! '>='
178
+ - - ">="
109
179
  - !ruby/object:Gem::Version
110
180
  version: '0'
111
181
  description:
@@ -114,9 +184,10 @@ executables: []
114
184
  extensions: []
115
185
  extra_rdoc_files: []
116
186
  files:
187
+ - lib/randumb.rb
117
188
  - lib/randumb/relation.rb
189
+ - lib/randumb/syntax.rb
118
190
  - lib/randumb/version.rb
119
- - lib/randumb.rb
120
191
  - test/models/album.rb
121
192
  - test/models/artist.rb
122
193
  - test/models/factories.rb
@@ -133,17 +204,16 @@ require_paths:
133
204
  - lib
134
205
  required_ruby_version: !ruby/object:Gem::Requirement
135
206
  requirements:
136
- - - ! '>='
207
+ - - ">="
137
208
  - !ruby/object:Gem::Version
138
209
  version: '0'
139
210
  required_rubygems_version: !ruby/object:Gem::Requirement
140
211
  requirements:
141
- - - ! '>='
212
+ - - ">="
142
213
  - !ruby/object:Gem::Version
143
214
  version: '0'
144
215
  requirements: []
145
- rubyforge_project:
146
- rubygems_version: 2.0.5
216
+ rubygems_version: 3.0.3
147
217
  signing_key:
148
218
  specification_version: 4
149
219
  summary: Adds the ability to pull random records from ActiveRecord