dbee-active_record 2.0.2 → 2.1.1

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.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +13 -12
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +3 -10
  5. data/CHANGELOG.md +22 -0
  6. data/dbee-active_record.gemspec +14 -6
  7. data/exe/.gitkeep +0 -0
  8. data/lib/dbee/providers/active_record_provider/expression_builder.rb +55 -24
  9. data/lib/dbee/providers/active_record_provider/maker.rb +37 -0
  10. data/lib/dbee/providers/active_record_provider/{expression_builder/constraint_maker.rb → makers/constraint.rb} +12 -12
  11. data/lib/dbee/providers/active_record_provider/{expression_builder/order_maker.rb → makers/order.rb} +9 -9
  12. data/lib/dbee/providers/active_record_provider/makers/select.rb +81 -0
  13. data/lib/dbee/providers/active_record_provider/makers/where.rb +91 -0
  14. data/lib/dbee/providers/active_record_provider/version.rb +1 -1
  15. data/spec/db_helper.rb +134 -14
  16. data/spec/dbee/providers/active_record_provider/expression_builder_spec.rb +117 -0
  17. data/spec/dbee/providers/active_record_provider_spec.rb +101 -3
  18. data/spec/fixtures/active_record_snapshots/one_table_empty_query.yaml +10 -0
  19. data/spec/fixtures/active_record_snapshots/one_table_query_with_filters.yaml +24 -16
  20. data/spec/fixtures/active_record_snapshots/two_table_query_with_aggregation.yaml +71 -0
  21. data/spec/fixtures/active_record_snapshots/two_table_query_with_pivoting.yaml +88 -0
  22. data/spec/fixtures/models.yaml +20 -0
  23. data/spec/spec_helper.rb +4 -0
  24. metadata +33 -19
  25. data/lib/dbee/providers/active_record_provider/expression_builder/select_maker.rb +0 -33
  26. data/lib/dbee/providers/active_record_provider/expression_builder/where_maker.rb +0 -56
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c9ceb85ce9f32169d54a6b29b39462e6a0331f3b033ecc6aebb30493b80e531
4
- data.tar.gz: 88c79f007ffd85c1eeb8bf955f0c6804754cca94b8651dccd0316ccd61d0b8b7
3
+ metadata.gz: e71321e2f9366122dfbcc7bbfa5a18be43693e6d0c7eda44cf06221e646bef33
4
+ data.tar.gz: f59ad369064af84ba7cd387df7b0c072eb38bf6658d7342320e158e5d297c65a
5
5
  SHA512:
6
- metadata.gz: f4ea1efa24ad9cf7a6d0df953e487237e9381a0a7016b978c833044ea45cf680e315d60a93fe94c9a28555c68af9ec5d2c733500b8bf25be3c488a9c27f81e23
7
- data.tar.gz: 383a0071de4503d75106a85ba8255070b0dd8402012421ae2aba8c315716d966d759fdbfdbe81865e9c9bce14b83d0cf370d0e19ec2b685dc30ce79ea5f7e0c2
6
+ metadata.gz: 6ad043966cba813e731ab4e7d53cf22fc359ec6a2000097b18719d0bf7a317c3521c8785999cfc99f31b5d091724337afad62ff62c02aca19f448d37989eecbc
7
+ data.tar.gz: eb496b07348bbb79a2cd97be2816ea696186ac8068172ca38f521d22343cf3f80f67c71bf99adeb0022f723643dbc579e145641afb814a1772237ff92e35ef82
@@ -1,6 +1,15 @@
1
- Metrics/LineLength:
1
+ AllCops:
2
+ TargetRubyVersion: 2.5
3
+ NewCops: enable
4
+
5
+ Layout/LineLength:
2
6
  Max: 100
3
7
 
8
+ Metrics/AbcSize:
9
+ Max: 16
10
+ Exclude:
11
+ - spec/db_helper.rb
12
+
4
13
  Metrics/BlockLength:
5
14
  ExcludedMethods:
6
15
  - let
@@ -13,18 +22,10 @@ Metrics/BlockLength:
13
22
  - spec/dbee/**/*
14
23
  - dbee-active_record.gemspec
15
24
 
25
+ Metrics/ClassLength:
26
+ Max: 140
27
+
16
28
  Metrics/MethodLength:
17
29
  Max: 25
18
30
  Exclude:
19
31
  - spec/db_helper.rb
20
-
21
- AllCops:
22
- TargetRubyVersion: 2.3
23
-
24
- Metrics/AbcSize:
25
- Max: 16
26
- Exclude:
27
- - spec/db_helper.rb
28
-
29
- Metrics/ClassLength:
30
- Max: 125
@@ -1 +1 @@
1
- 2.6.3
1
+ 2.6.6
@@ -6,19 +6,12 @@ services:
6
6
  - mysql
7
7
  rvm:
8
8
  # Build on the latest stable of all supported Rubies (https://www.ruby-lang.org/en/downloads/):
9
- - 2.3.8
10
- - 2.4.6
11
- - 2.5.5
12
- - 2.6.3
9
+ - 2.5.8
10
+ - 2.6.6
11
+ - 2.7.1
13
12
  env:
14
13
  - AR_VERSION=5
15
14
  - AR_VERSION=6
16
- matrix:
17
- exclude:
18
- - rvm: 2.3.8
19
- env: AR_VERSION=6
20
- - rvm: 2.4.6
21
- env: AR_VERSION=6
22
15
  cache: bundler
23
16
  before_script:
24
17
  - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
@@ -1,3 +1,25 @@
1
+ # 2.1.1 (July 15th, 2020)
2
+
3
+ ### Additions:
4
+
5
+ * Implemented Dbee::Query::Field#aggregator
6
+ * Implemented Dbee::Query::Field#filters
7
+ * Implemented base case when a Dbee::Query contains no fields
8
+
9
+ ### Changes:
10
+
11
+ * Bumped minimum Ruby version to 2.5
12
+
13
+ # 2.0.4 (February 13th, 2020)
14
+
15
+ * use Arel#in for Equal filters when there is more than one value
16
+ * use Arel#not_in for NotEqual filters when there are is than one value
17
+
18
+ # 2.0.3 (January 7th, 2020)
19
+
20
+ * Added/tested support for Dbee 2.0.3
21
+ * Added support for Ruby 2.6.5
22
+
1
23
  # 2.0.2 (November 7th, 2019)
2
24
 
3
25
  * Added/tested support for Dbee 2.0.2
@@ -15,11 +15,19 @@ Gem::Specification.new do |s|
15
15
  s.email = ['mruggio@bluemarblepayroll.com']
16
16
  s.files = `git ls-files`.split("\n")
17
17
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
- s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
18
+ s.bindir = 'exe'
19
+ s.executables = []
19
20
  s.homepage = 'https://github.com/bluemarblepayroll/dbee-active_record'
20
21
  s.license = 'MIT'
22
+ s.metadata = {
23
+ 'bug_tracker_uri' => 'https://github.com/bluemarblepayroll/dbee-active_record/issues',
24
+ 'changelog_uri' => 'https://github.com/bluemarblepayroll/dbee-active_record/blob/master/CHANGELOG.md',
25
+ 'documentation_uri' => 'https://www.rubydoc.info/gems/dbee-active_record',
26
+ 'homepage_uri' => s.homepage,
27
+ 'source_code_uri' => s.homepage
28
+ }
21
29
 
22
- s.required_ruby_version = '>= 2.3.8'
30
+ s.required_ruby_version = '>= 2.5'
23
31
 
24
32
  ar_version = ENV['AR_VERSION'] || ''
25
33
 
@@ -34,15 +42,15 @@ Gem::Specification.new do |s|
34
42
  end
35
43
 
36
44
  s.add_dependency('activerecord', activerecord_version)
37
- s.add_dependency('dbee', '~>2', '>=2.0.2')
45
+ s.add_dependency('dbee', '~>2', '>=2.1.1')
38
46
 
39
47
  s.add_development_dependency('guard-rspec', '~>4.7')
40
48
  s.add_development_dependency('mysql2', '~>0.5')
41
49
  s.add_development_dependency('pry', '~>0')
42
50
  s.add_development_dependency('rake', '~> 13')
43
51
  s.add_development_dependency('rspec', '~> 3.8')
44
- s.add_development_dependency('rubocop', '~>0.76.0')
45
- s.add_development_dependency('simplecov', '~>0.17.0')
46
- s.add_development_dependency('simplecov-console', '~>0.5.0')
52
+ s.add_development_dependency('rubocop', '~>0.88.0')
53
+ s.add_development_dependency('simplecov', '~>0.18.5')
54
+ s.add_development_dependency('simplecov-console', '~>0.7.0')
47
55
  s.add_development_dependency('sqlite3', '~>1')
48
56
  end
File without changes
@@ -7,32 +7,29 @@
7
7
  # LICENSE file in the root directory of this source tree.
8
8
  #
9
9
 
10
- require_relative 'expression_builder/constraint_maker'
11
- require_relative 'expression_builder/order_maker'
12
- require_relative 'expression_builder/select_maker'
13
- require_relative 'expression_builder/where_maker'
10
+ require_relative 'maker'
14
11
 
15
12
  module Dbee
16
13
  module Providers
17
14
  class ActiveRecordProvider
18
15
  # This class can generate an Arel expression tree.
19
- class ExpressionBuilder
20
- extend Forwardable
21
-
16
+ class ExpressionBuilder < Maker # :nodoc: all
22
17
  class MissingConstraintError < StandardError; end
23
18
 
24
- def_delegators :statement, :to_sql
25
-
26
19
  def initialize(model, table_alias_maker, column_alias_maker)
27
- @model = model
28
- @table_alias_maker = table_alias_maker
29
- @column_alias_maker = column_alias_maker
20
+ super(column_alias_maker)
21
+
22
+ @model = model
23
+ @table_alias_maker = table_alias_maker
30
24
 
31
25
  clear
32
26
  end
33
27
 
34
28
  def clear
35
- @base_table = make_table(model.table, model.name)
29
+ @requires_group_by = false
30
+ @group_by_columns = []
31
+ @base_table = make_table(model.table, model.name)
32
+ @select_all = true
36
33
 
37
34
  build(base_table)
38
35
 
@@ -40,6 +37,8 @@ module Dbee
40
37
  end
41
38
 
42
39
  def add(query)
40
+ return self unless query
41
+
43
42
  query.fields.each { |field| add_field(field) }
44
43
  query.sorters.each { |sorter| add_sorter(sorter) }
45
44
  query.filters.each { |filter| add_filter(filter) }
@@ -49,13 +48,27 @@ module Dbee
49
48
  self
50
49
  end
51
50
 
51
+ def to_sql
52
+ if requires_group_by
53
+ @requires_group_by = false
54
+ statement.group(group_by_columns) unless group_by_columns.empty?
55
+ @group_by_columns = []
56
+ end
57
+
58
+ return statement.project(select_maker.star(base_table)).to_sql if select_all
59
+
60
+ statement.to_sql
61
+ end
62
+
52
63
  private
53
64
 
54
65
  attr_reader :base_table,
55
66
  :statement,
56
67
  :model,
57
68
  :table_alias_maker,
58
- :column_alias_maker
69
+ :requires_group_by,
70
+ :group_by_columns,
71
+ :select_all
59
72
 
60
73
  def tables
61
74
  @tables ||= {}
@@ -70,7 +83,7 @@ module Dbee
70
83
 
71
84
  key_path = filter.key_path
72
85
  arel_column = key_paths_to_arel_columns[key_path]
73
- predicate = WhereMaker.instance.make(filter, arel_column)
86
+ predicate = where_maker.make(filter, arel_column)
74
87
 
75
88
  build(statement.where(predicate))
76
89
 
@@ -82,22 +95,40 @@ module Dbee
82
95
 
83
96
  key_path = sorter.key_path
84
97
  arel_column = key_paths_to_arel_columns[key_path]
85
- predicate = OrderMaker.instance.make(sorter, arel_column)
98
+ predicate = order_maker.make(sorter, arel_column)
86
99
 
87
100
  build(statement.order(predicate))
88
101
 
89
102
  self
90
103
  end
91
104
 
105
+ def add_filter_key_paths(filters)
106
+ filters.each_with_object({}) do |filter, memo|
107
+ arel_key_column = add_key_path(filter.key_path)
108
+
109
+ memo[arel_key_column] = filter
110
+ end
111
+ end
112
+
92
113
  def add_field(field)
93
- add_key_path(field.key_path)
114
+ @select_all = false
115
+ arel_value_column = add_key_path(field.key_path)
116
+ arel_key_columns_to_filters = add_filter_key_paths(field.filters)
94
117
 
95
- key_path = field.key_path
96
- arel_column = key_paths_to_arel_columns[key_path]
97
- predicate = SelectMaker.instance.make(field, arel_column, column_alias_maker)
118
+ predicate = select_maker.make(
119
+ field,
120
+ arel_key_columns_to_filters,
121
+ arel_value_column
122
+ )
98
123
 
99
124
  build(statement.project(predicate))
100
125
 
126
+ if field.aggregator?
127
+ @requires_group_by = true
128
+ else
129
+ group_by_columns << arel_value_column
130
+ end
131
+
101
132
  self
102
133
  end
103
134
 
@@ -123,7 +154,7 @@ module Dbee
123
154
  def table(name, model, previous_table)
124
155
  table = make_table(model.table, name)
125
156
 
126
- on = ConstraintMaker.instance.make(model.constraints, table, previous_table)
157
+ on = constraint_maker.make(model.constraints, table, previous_table)
127
158
 
128
159
  raise MissingConstraintError, "for: #{name}" unless on
129
160
 
@@ -142,16 +173,16 @@ module Dbee
142
173
  end
143
174
 
144
175
  def add_key_path(key_path)
145
- return if key_paths_to_arel_columns.key?(key_path)
176
+ return key_paths_to_arel_columns[key_path] if key_paths_to_arel_columns.key?(key_path)
146
177
 
147
178
  ancestors = model.ancestors!(key_path.ancestor_names)
148
179
 
149
180
  table = traverse_ancestors(ancestors)
150
181
 
151
182
  arel_column = table[key_path.column_name]
152
- key_paths_to_arel_columns[key_path] = arel_column
153
183
 
154
- self
184
+ # Note that this returns arel_column
185
+ key_paths_to_arel_columns[key_path] = arel_column
155
186
  end
156
187
 
157
188
  def build(new_expression)
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require_relative 'makers/constraint'
11
+ require_relative 'makers/order'
12
+ require_relative 'makers/select'
13
+ require_relative 'makers/where'
14
+
15
+ module Dbee
16
+ module Providers
17
+ class ActiveRecordProvider
18
+ # This class composes all the maker instances into one for use together.
19
+ class Maker # :nodoc: all
20
+ def initialize(column_alias_maker)
21
+ @column_alias_maker = column_alias_maker
22
+ @constraint_maker = Makers::Constraint.instance
23
+ @order_maker = Makers::Order.instance
24
+ @select_maker = Makers::Select.new(column_alias_maker)
25
+ @where_maker = Makers::Where.instance
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :constraint_maker,
31
+ :order_maker,
32
+ :select_maker,
33
+ :where_maker
34
+ end
35
+ end
36
+ end
37
+ end
@@ -10,11 +10,21 @@
10
10
  module Dbee
11
11
  module Providers
12
12
  class ActiveRecordProvider
13
- class ExpressionBuilder
13
+ module Makers # :nodoc: all
14
14
  # Can derive constraints for Arel table JOIN statements.
15
- class ConstraintMaker
15
+ class Constraint
16
16
  include Singleton
17
17
 
18
+ def make(constraints, table, previous_table)
19
+ constraints.inject(nil) do |memo, constraint|
20
+ method = CONSTRAINT_RESOLVERS[constraint.class]
21
+
22
+ raise ArgumentError, "constraint unhandled: #{constraint.class.name}" unless method
23
+
24
+ method.call(constraint, memo, table, previous_table)
25
+ end
26
+ end
27
+
18
28
  CONCAT_METHOD = lambda do |on, arel_column, value|
19
29
  on ? on.and(arel_column.eq(value)) : arel_column.eq(value)
20
30
  end
@@ -44,16 +54,6 @@ module Dbee
44
54
  }.freeze
45
55
 
46
56
  private_constant :CONSTRAINT_RESOLVERS
47
-
48
- def make(constraints, table, previous_table)
49
- constraints.inject(nil) do |memo, constraint|
50
- method = CONSTRAINT_RESOLVERS[constraint.class]
51
-
52
- raise ArgumentError, "constraint unhandled: #{constraint.class.name}" unless method
53
-
54
- method.call(constraint, memo, table, previous_table)
55
- end
56
- end
57
57
  end
58
58
  end
59
59
  end
@@ -10,18 +10,11 @@
10
10
  module Dbee
11
11
  module Providers
12
12
  class ActiveRecordProvider
13
- class ExpressionBuilder
13
+ module Makers # :nodoc: all
14
14
  # Derives Arel#order predicates.
15
- class OrderMaker
15
+ class Order
16
16
  include Singleton
17
17
 
18
- SORTER_EVALUATORS = {
19
- Query::Sorters::Ascending => ->(column) { column },
20
- Query::Sorters::Descending => ->(column) { column.desc }
21
- }.freeze
22
-
23
- private_constant :SORTER_EVALUATORS
24
-
25
18
  def make(sorter, arel_column)
26
19
  method = SORTER_EVALUATORS[sorter.class]
27
20
 
@@ -29,6 +22,13 @@ module Dbee
29
22
 
30
23
  method.call(arel_column)
31
24
  end
25
+
26
+ SORTER_EVALUATORS = {
27
+ Query::Sorters::Ascending => ->(column) { column },
28
+ Query::Sorters::Descending => ->(column) { column.desc }
29
+ }.freeze
30
+
31
+ private_constant :SORTER_EVALUATORS
32
32
  end
33
33
  end
34
34
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Dbee
11
+ module Providers
12
+ class ActiveRecordProvider
13
+ module Makers # :nodoc: all
14
+ # Derives Arel#project predicates.
15
+ class Select
16
+ attr_reader :alias_maker
17
+
18
+ def initialize(alias_maker)
19
+ @alias_maker = alias_maker
20
+
21
+ freeze
22
+ end
23
+
24
+ def star(arel_table)
25
+ arel_table[Arel.star]
26
+ end
27
+
28
+ def make(field, arel_key_nodes_to_filters, arel_value_node)
29
+ column_alias = quote(alias_maker.make(field.display))
30
+ predicate = expression(field, arel_key_nodes_to_filters, arel_value_node)
31
+ predicate = aggregate(field, predicate)
32
+
33
+ predicate.as(column_alias)
34
+ end
35
+
36
+ private
37
+
38
+ AGGREGRATOR_EVALUATORS = {
39
+ nil => ->(arel_node) { arel_node },
40
+ Query::Field::Aggregator::AVE => ->(node) { Arel::Nodes::Avg.new([node]) },
41
+ Query::Field::Aggregator::COUNT => ->(node) { Arel::Nodes::Count.new([node]) },
42
+ Query::Field::Aggregator::MAX => ->(node) { Arel::Nodes::Max.new([node]) },
43
+ Query::Field::Aggregator::MIN => ->(node) { Arel::Nodes::Min.new([node]) },
44
+ Query::Field::Aggregator::SUM => ->(node) { Arel::Nodes::Sum.new([node]) }
45
+ }.freeze
46
+
47
+ private_constant :AGGREGRATOR_EVALUATORS
48
+
49
+ def quote(value)
50
+ ActiveRecord::Base.connection.quote(value)
51
+ end
52
+
53
+ def aggregate(field, predicate)
54
+ AGGREGRATOR_EVALUATORS[field.aggregator].call(predicate)
55
+ end
56
+
57
+ def expression(field, arel_key_nodes_to_filters, arel_value_node)
58
+ if field.filters?
59
+ case_statement = Arel::Nodes::Case.new
60
+ filter_predicate = make_filter_predicate(arel_key_nodes_to_filters)
61
+
62
+ case_statement.when(filter_predicate).then(arel_value_node)
63
+ else
64
+ arel_value_node
65
+ end
66
+ end
67
+
68
+ def make_filter_predicate(arel_key_nodes_to_filters)
69
+ predicates = arel_key_nodes_to_filters.map do |arel_key_node, filter|
70
+ Where.instance.make(filter, arel_key_node)
71
+ end
72
+
73
+ predicates.inject(predicates.shift) do |memo, predicate|
74
+ memo.and(predicate)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end