top_n_loader 1.0.2 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6eb6d3a03ffce49b32f78bb04809b3ce0b9c0cd114c3e32dae441c815873c8a5
4
- data.tar.gz: 1b5426e4d320baed476489d56928480d46604028b798a975faa7d104533d7606
3
+ metadata.gz: d905751360ae053d80faa6332daad7e5fce710c2b44031483d71018e15d67c1a
4
+ data.tar.gz: dc175dd9e135880cb26c1c81c2784500e8ee0950dda9b03a39799af6d0b98710
5
5
  SHA512:
6
- metadata.gz: 7adc3640113ca456040bfcdc08c1c35d9c9ed18d7ddc54d865785ee8a49a06531b2c0067ae84f90ecb5198c088683226892e6c03e3b38edbdcd48635d669dc7a
7
- data.tar.gz: f6c99dac0cbe3cd32fc02393f085baf853d88f8680b848e99efccf42ce9eebfffd7a8605192a04e75a396e45bda642a53a0372aa78241101b12680d6d93e6a8a
6
+ metadata.gz: fb24707fdea8d9162a87b6960cce21ee38d5f9c5ab8cadee549a97518e0da9c1f1fea5e2c6ae50b0b1db0171d868ec5cb2c5f3c058b8e1cefba14491b058d2ec
7
+ data.tar.gz: a69198a3697ebc97e49c5c36d71103b9c8afb837b1416a7668e0190323042b3af51af3cd14bd1e22351646f271b825e385db53a80bb8090f3baf34e6e47626d4
@@ -5,15 +5,10 @@ jobs:
5
5
  strategy:
6
6
  fail-fast: false
7
7
  matrix:
8
- ruby: [ '2.5', '2.6', '2.7' ]
8
+ ruby: [ '2.7', '3.0', '3.1' ]
9
9
  gemfiles:
10
- - gemfiles/Gemfile-rails-5.2
11
10
  - gemfiles/Gemfile-rails-6.0
12
- exclude:
13
- - ruby: '2.6'
14
- gemfiles: gemfiles/Gemfile-rails-5.2
15
- - ruby: '2.7'
16
- gemfiles: gemfiles/Gemfile-rails-5.2
11
+ - gemfiles/Gemfile-rails-7.0
17
12
  runs-on: ubuntu-latest
18
13
  steps:
19
14
  - uses: actions/checkout@v2
data/Gemfile.lock CHANGED
@@ -1,61 +1,59 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- top_n_loader (1.0.2)
4
+ top_n_loader (1.0.3)
5
5
  activerecord
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (7.0.1)
11
- activesupport (= 7.0.1)
12
- activerecord (7.0.1)
13
- activemodel (= 7.0.1)
14
- activesupport (= 7.0.1)
15
- activesupport (7.0.1)
10
+ activemodel (7.0.3)
11
+ activesupport (= 7.0.3)
12
+ activerecord (7.0.3)
13
+ activemodel (= 7.0.3)
14
+ activesupport (= 7.0.3)
15
+ activesupport (7.0.3)
16
16
  concurrent-ruby (~> 1.0, >= 1.0.2)
17
17
  i18n (>= 1.6, < 2)
18
18
  minitest (>= 5.1)
19
19
  tzinfo (~> 2.0)
20
- ast (2.4.0)
21
- coderay (1.1.2)
22
- concurrent-ruby (1.1.9)
23
- docile (1.1.5)
24
- i18n (1.8.11)
20
+ ast (2.4.2)
21
+ concurrent-ruby (1.1.10)
22
+ docile (1.4.0)
23
+ i18n (1.10.0)
25
24
  concurrent-ruby (~> 1.0)
26
- jaro_winkler (1.5.4)
27
- json (2.1.0)
28
- method_source (0.9.0)
29
- minitest (5.11.3)
30
- parallel (1.19.1)
31
- parser (2.7.1.2)
32
- ast (~> 2.4.0)
33
- pry (0.11.3)
34
- coderay (~> 1.1.0)
35
- method_source (~> 0.9.0)
36
- rainbow (3.0.0)
37
- rake (10.5.0)
38
- rexml (3.2.4)
39
- rubocop (0.82.0)
40
- jaro_winkler (~> 1.5.1)
25
+ minitest (5.16.1)
26
+ parallel (1.22.1)
27
+ parser (3.1.2.0)
28
+ ast (~> 2.4.1)
29
+ rainbow (3.1.1)
30
+ rake (13.0.6)
31
+ regexp_parser (2.5.0)
32
+ rexml (3.2.5)
33
+ rubocop (1.31.0)
41
34
  parallel (~> 1.10)
42
- parser (>= 2.7.0.1)
35
+ parser (>= 3.1.0.0)
43
36
  rainbow (>= 2.2.2, < 4.0)
44
- rexml
37
+ regexp_parser (>= 1.8, < 3.0)
38
+ rexml (>= 3.2.5, < 4.0)
39
+ rubocop-ast (>= 1.18.0, < 2.0)
45
40
  ruby-progressbar (~> 1.7)
46
- unicode-display_width (>= 1.4.0, < 2.0)
47
- rubocop-rubycw (0.1.4)
48
- rubocop
49
- ruby-progressbar (1.10.1)
50
- simplecov (0.15.1)
51
- docile (~> 1.1.0)
52
- json (>= 1.8, < 3)
53
- simplecov-html (~> 0.10.0)
54
- simplecov-html (0.10.2)
55
- sqlite3 (1.4.2)
41
+ unicode-display_width (>= 1.4.0, < 3.0)
42
+ rubocop-ast (1.18.0)
43
+ parser (>= 3.1.1.0)
44
+ rubocop-rubycw (0.1.6)
45
+ rubocop (~> 1.0)
46
+ ruby-progressbar (1.11.0)
47
+ simplecov (0.21.2)
48
+ docile (~> 1.1)
49
+ simplecov-html (~> 0.11)
50
+ simplecov_json_formatter (~> 0.1)
51
+ simplecov-html (0.12.3)
52
+ simplecov_json_formatter (0.1.4)
53
+ sqlite3 (1.4.4)
56
54
  tzinfo (2.0.4)
57
55
  concurrent-ruby (~> 1.0)
58
- unicode-display_width (1.7.0)
56
+ unicode-display_width (2.2.0)
59
57
 
60
58
  PLATFORMS
61
59
  ruby
@@ -63,7 +61,6 @@ PLATFORMS
63
61
  DEPENDENCIES
64
62
  bundler
65
63
  minitest
66
- pry
67
64
  rake
68
65
  rubocop
69
66
  rubocop-rubycw
data/bin/console CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'bundler/setup'
4
4
  require 'top_n_loader'
5
- require 'pry'
6
5
  require_relative '../test/db'
7
6
 
8
- Pry.start
7
+ DB.connect DB::DATABASE_CONFIG_SQLITE3
8
+ binding.irb
@@ -5,4 +5,4 @@ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
5
5
  # Specify your gem's dependencies in top_n_loader.gemspec
6
6
  gemspec path: ".."
7
7
 
8
- gem "activerecord", "~> 5.2.0"
8
+ gem "activerecord", "~> 7.0.0"
@@ -9,71 +9,92 @@ module TopNLoader::SQLBuilder
9
9
  end
10
10
 
11
11
  def self.top_n_association_sql(klass, target_klass, relation, limit:, order_mode:, order_key:)
12
+ limit = limit.to_i
12
13
  parent_table = klass.table_name
13
14
  joins = klass.joins relation.to_sym
14
15
  target_table = target_klass.table_name
16
+ nullable = nullable_column? target_klass, order_key
15
17
  if target_table == klass.table_name
16
18
  target_table = "#{joins.joins_values.first.to_s.pluralize}_#{target_table}"
17
19
  end
18
20
  join_sql = joins.to_sql.match(/FROM.+/)[0]
19
- %(
21
+ parent_primary_key = "#{qt parent_table}.#{q klass.primary_key}"
22
+ target_order_key = "#{qt target_table}.#{q order_key}"
23
+ target_primary_key = "#{qt target_table}.#{q target_klass.primary_key}"
24
+ top_n_key, top_n_alias, t_join_cond = limit == 1 ? [
25
+ target_primary_key, :top_n_primary_key, "#{target_primary_key} = top_n_primary_key"
26
+ ] : [
27
+ target_order_key, :top_n_order_key, compare_cond(target_order_key, order_mode, includes_nil: nullable)
28
+ ]
29
+ order_columns = limit == 1 ? [target_order_key, target_primary_key].uniq : [target_order_key]
30
+ order_cond = order_columns.map {|column| "#{column} #{order_mode.to_s.upcase}"}.join(', ')
31
+ <<~SQL.squish
20
32
  SELECT #{qt target_table}.*, top_n_group_key
21
33
  #{join_sql}
22
34
  INNER JOIN
23
35
  (
24
- SELECT T.#{q klass.primary_key} as top_n_group_key,
36
+ SELECT T.#{q klass.primary_key} AS top_n_group_key,
25
37
  (
26
- SELECT #{qt target_table}.#{q order_key}
38
+ SELECT #{top_n_key}
27
39
  #{join_sql}
28
- WHERE #{qt parent_table}.#{q klass.primary_key} = T.#{q klass.primary_key}
29
- ORDER BY #{qt target_table}.#{q order_key} #{order_mode.upcase}
30
- LIMIT 1 OFFSET #{limit.to_i - 1}
31
- ) AS last_value_of_key
32
- FROM #{qt parent_table} as T where T.#{q klass.primary_key} in (?)
40
+ WHERE #{parent_primary_key} = T.#{q klass.primary_key}
41
+ ORDER BY #{order_cond}
42
+ LIMIT 1#{" OFFSET #{limit - 1}" if limit != 1}
43
+ ) AS #{top_n_alias}
44
+ FROM #{qt parent_table} AS T WHERE T.#{q klass.primary_key} IN (?)
33
45
  ) T
34
- ON #{qt parent_table}.#{q klass.primary_key} = T.top_n_group_key
35
- AND (
36
- T.last_value_of_key IS NULL
37
- OR #{qt target_table}.#{q order_key} #{{ asc: :<=, desc: :>= }[order_mode]} T.last_value_of_key
38
- OR #{qt target_table}.#{q order_key} is NULL
39
- )
40
- )
46
+ ON #{parent_primary_key} = T.top_n_group_key AND #{t_join_cond}
47
+ SQL
41
48
  end
42
49
 
43
-
44
50
  def self.top_n_group_sql(klass:, group_column:, group_keys:, condition:, limit:, order_mode:, order_key:)
45
- order_op = order_mode == :asc ? :<= : :>=
46
- group_key_table = value_table(:T, :top_n_group_key, group_keys)
51
+ limit = limit.to_i
47
52
  table_name = klass.table_name
48
53
  sql = condition_sql klass, condition
49
- join_cond = %(#{qt table_name}.#{q group_column} = T.top_n_group_key)
50
- if group_keys.include? nil
51
- nil_join_cond = %((#{qt table_name}.#{q group_column} IS NULL AND T.top_n_group_key IS NULL))
52
- join_cond = %((#{join_cond} OR #{nil_join_cond}))
53
- end
54
- %(
54
+ group_key_nullable = group_keys.include?(nil) && nullable_column?(klass, group_column)
55
+ order_key_nullable = nullable_column? klass, order_key
56
+ group_key_table = value_table :T, :top_n_group_key, group_keys
57
+ table_order_key = "#{qt table_name}.#{q order_key}"
58
+ join_cond = equals_cond "#{qt table_name}.#{q group_column}", includes_nil: group_key_nullable
59
+ table_primary_key = "#{qt table_name}.#{q klass.primary_key}"
60
+ top_n_key, top_n_alias, t_join_cond = limit == 1 ? [
61
+ table_primary_key, :top_n_primary_key, "#{table_primary_key} = top_n_primary_key"
62
+ ] : [
63
+ table_order_key, :top_n_order_key,
64
+ "#{compare_cond table_order_key, order_mode, includes_nil: order_key_nullable}#{" WHERE #{sql}" if sql}"
65
+ ]
66
+ order_columns = limit == 1 ? [table_order_key, table_primary_key].uniq : [table_order_key]
67
+ order_cond = order_columns.map {|column| "#{column} #{order_mode.to_s.upcase}"}.join(', ')
68
+ <<~SQL.squish
55
69
  SELECT #{qt table_name}.*, top_n_group_key
56
70
  FROM #{qt table_name}
57
71
  INNER JOIN
58
72
  (
59
73
  SELECT top_n_group_key,
60
74
  (
61
- SELECT #{qt table_name}.#{q order_key} FROM #{qt table_name}
62
- WHERE #{join_cond}
63
- #{"AND #{sql}" if sql}
64
- ORDER BY #{qt table_name}.#{q order_key} #{order_mode.to_s.upcase}
65
- LIMIT 1 OFFSET #{limit.to_i - 1}
66
- ) AS last_value_of_key
75
+ SELECT #{top_n_key} FROM #{qt table_name}
76
+ WHERE #{join_cond}#{" AND #{sql}" if sql}
77
+ ORDER BY #{order_cond}
78
+ LIMIT 1#{" OFFSET #{limit - 1}" if limit != 1}
79
+ ) AS #{top_n_alias}
67
80
  FROM #{group_key_table}
68
81
  ) T
69
- ON #{join_cond}
70
- AND (
71
- T.last_value_of_key IS NULL
72
- OR #{qt table_name}.#{q order_key} #{order_op} T.last_value_of_key
73
- OR #{qt table_name}.#{q order_key} is NULL
74
- )
75
- #{"WHERE #{sql}" if sql}
76
- )
82
+ ON #{join_cond} AND #{t_join_cond}
83
+ SQL
84
+ end
85
+
86
+ def self.equals_cond(column, includes_nil:, t_column: 'T.top_n_group_key')
87
+ cond = "#{column} = #{t_column}"
88
+ includes_nil ? "(#{cond} OR (#{column} IS NULL AND #{t_column} IS NULL))" : cond
89
+ end
90
+
91
+ def self.compare_cond(column, order_mode, includes_nil:, t_column: 'T.top_n_order_key')
92
+ op = order_mode == :asc ? '<=' : '>='
93
+ if includes_nil && (nil_first? ? order_mode == :asc : order_mode == :desc)
94
+ "(#{column} #{op} #{t_column} OR #{column} IS NULL OR #{t_column} IS NULL)"
95
+ else
96
+ "(#{column} #{op} #{t_column} OR #{t_column} IS NULL)"
97
+ end
77
98
  end
78
99
 
79
100
  def self.q(name)
@@ -84,24 +105,56 @@ module TopNLoader::SQLBuilder
84
105
  ActiveRecord::Base.connection.quote_table_name name
85
106
  end
86
107
 
108
+ def self.nullable_column?(klass, column)
109
+ klass.column_for_attribute(column).null
110
+ end
111
+
112
+ def self.type_values(values)
113
+ return [nil, values] if sqlite?
114
+ groups = values.group_by { _1.is_a?(Time) || _1.is_a?(DateTime) ? 0 : _1.is_a?(Date) ? 1 : 2 }
115
+ type = groups[0] ? :TIMESTAMP : groups[1] ? :DATE : nil
116
+ [type, groups.sort.flat_map(&:last)]
117
+ end
118
+
87
119
  def self.value_table(table, column, values)
88
- if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
89
- values_value_table(table, column, values)
120
+ type, values = type_values values
121
+ if postgres?
122
+ values_value_table table, column, values, type
90
123
  else
91
- union_value_table(table, column, values)
124
+ union_value_table table, column, values, type
92
125
  end
93
126
  end
94
127
 
95
- def self.union_value_table(table, column, values)
128
+ def self.values_table_batch_size
129
+ sqlite? ? 200 : 1000
130
+ end
131
+
132
+ def self.nil_first?
133
+ !postgres?
134
+ end
135
+
136
+ def self.adapter_name
137
+ ActiveRecord::Base.connection.adapter_name
138
+ end
139
+
140
+ def self.postgres?
141
+ adapter_name == 'PostgreSQL'
142
+ end
143
+
144
+ def self.sqlite?
145
+ adapter_name == 'SQLite'
146
+ end
147
+
148
+ def self.union_value_table(table, column, values, type)
96
149
  sanitize_sql_array [
97
- "(SELECT ? AS #{column}#{' UNION SELECT ?' * (values.size - 1)}) AS #{table}",
150
+ "(SELECT #{"#{type} " if type}? AS #{column}#{' UNION SELECT ?' * (values.size - 1)}) AS #{table}",
98
151
  *values
99
152
  ]
100
153
  end
101
154
 
102
- def self.values_value_table(table, column, values)
155
+ def self.values_value_table(table, column, values, type)
103
156
  sanitize_sql_array [
104
- "(VALUES #{(['(?)'] * values.size).join(',')}) AS #{table} (#{column})",
157
+ "(VALUES (#{"#{type} " if type}?) #{', (?)' * (values.size - 1)}) AS #{table} (#{column})",
105
158
  *values
106
159
  ]
107
160
  end
@@ -122,12 +175,12 @@ module TopNLoader::SQLBuilder
122
175
  sql_binds = begin
123
176
  case value
124
177
  when NilClass
125
- %(#{q key} IS NULL)
178
+ "#{q key} IS NULL"
126
179
  when Range
127
180
  if value.exclude_end?
128
- [%(#{q key} >= ? AND #{q key} < ?), value.begin, value.end]
181
+ ["#{q key} >= ? AND #{q key} < ?", value.begin, value.end]
129
182
  else
130
- [%(#{q key} BETWEEN ? AND ?), value.begin, value.end]
183
+ ["#{q key} BETWEEN ? AND ?", value.begin, value.end]
131
184
  end
132
185
  when Hash
133
186
  raise ArgumentError, '' unless value.keys == [:not]
@@ -135,12 +188,12 @@ module TopNLoader::SQLBuilder
135
188
  when Enumerable
136
189
  array = value.to_a
137
190
  if array.include? nil
138
- [%((#{q key} IS NULL OR #{q key} IN (?))), array.reject(&:nil?)]
191
+ ["(#{q key} IS NULL OR #{q key} IN (?))", array.reject(&:nil?)]
139
192
  else
140
- [%(#{q key} IN (?)), array]
193
+ ["#{q key} IN (?)", array]
141
194
  end
142
195
  else
143
- [%(#{q key} = ?), value]
196
+ ["#{q key} = ?", value]
144
197
  end
145
198
  end
146
199
  sanitize_sql_array sql_binds
@@ -1,3 +1,3 @@
1
1
  module TopNLoader
2
- VERSION = '1.0.2'
2
+ VERSION = '1.0.3'
3
3
  end
data/lib/top_n_loader.rb CHANGED
@@ -25,13 +25,17 @@ module TopNLoader
25
25
  limit: limit,
26
26
  **parse_order(klass, order)
27
27
  }
28
- records = klass.find_by_sql(
29
- SQLBuilder.top_n_group_sql(
30
- group_keys: keys,
31
- condition: condition,
32
- **options
28
+ keys = keys.uniq
29
+ batch_size = keys.size.fdiv(keys.size.fdiv(SQLBuilder.values_table_batch_size).ceil).ceil
30
+ records = keys.each_slice(batch_size).flat_map do |batch_keys|
31
+ klass.find_by_sql(
32
+ SQLBuilder.top_n_group_sql(
33
+ group_keys: batch_keys,
34
+ condition: condition,
35
+ **options
36
+ )
33
37
  )
34
- )
38
+ end
35
39
  format_result records, **options
36
40
  end
37
41
 
@@ -74,7 +78,7 @@ module TopNLoader
74
78
  existings, blanks = grouped_records.partition { |o| o[order_key] }
75
79
  existings.sort_by! { |o| [o[order_key], o[primary_key]] }
76
80
  blanks.sort_by! { |o| o[primary_key] }
77
- ordered = blanks + existings
81
+ ordered = SQLBuilder.nil_first? ? blanks + existings : existings + blanks
78
82
  ordered.reverse! if order_mode == :desc
79
83
  ordered.take limit
80
84
  end
data/top_n_loader.gemspec CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.add_dependency "activerecord"
24
24
 
25
- %w[bundler rake minitest sqlite3 pry simplecov rubocop rubocop-rubycw].each do |gem_name|
25
+ %w[bundler rake minitest sqlite3 simplecov rubocop rubocop-rubycw].each do |gem_name|
26
26
  spec.add_development_dependency gem_name
27
27
  end
28
28
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: top_n_loader
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - tompng
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-13 00:00:00.000000000 Z
11
+ date: 2022-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -80,20 +80,6 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: pry
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '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'
97
83
  - !ruby/object:Gem::Dependency
98
84
  name: simplecov
99
85
  requirement: !ruby/object:Gem::Requirement
@@ -146,7 +132,6 @@ files:
146
132
  - ".github/workflows/test.yml"
147
133
  - ".gitignore"
148
134
  - ".rubocop.yml"
149
- - ".travis.yml"
150
135
  - Gemfile
151
136
  - Gemfile.lock
152
137
  - LICENSE.txt
@@ -154,8 +139,8 @@ files:
154
139
  - Rakefile
155
140
  - bin/console
156
141
  - bin/setup
157
- - gemfiles/Gemfile-rails-5.2
158
142
  - gemfiles/Gemfile-rails-6.0
143
+ - gemfiles/Gemfile-rails-7.0
159
144
  - lib/top_n_loader.rb
160
145
  - lib/top_n_loader/sql_builder.rb
161
146
  - lib/top_n_loader/version.rb
data/.travis.yml DELETED
@@ -1,5 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.5.0
5
- before_install: gem install bundler -v 1.16.1