top_n_loader 1.0.2 → 1.0.3

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,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