dbee-active_record 2.0.4 → 2.2.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +14 -13
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +4 -10
  5. data/CHANGELOG.md +24 -0
  6. data/Guardfile +2 -1
  7. data/README.md +1 -1
  8. data/dbee-active_record.gemspec +21 -8
  9. data/exe/.gitkeep +0 -0
  10. data/lib/dbee/providers/active_record_provider.rb +3 -3
  11. data/lib/dbee/providers/active_record_provider/expression_builder.rb +96 -55
  12. data/lib/dbee/providers/active_record_provider/maker.rb +37 -0
  13. data/lib/dbee/providers/active_record_provider/{expression_builder/constraint_maker.rb → makers/constraint.rb} +12 -12
  14. data/lib/dbee/providers/active_record_provider/{expression_builder/order_maker.rb → makers/order.rb} +9 -9
  15. data/lib/dbee/providers/active_record_provider/makers/select.rb +81 -0
  16. data/lib/dbee/providers/active_record_provider/makers/where.rb +111 -0
  17. data/lib/dbee/providers/active_record_provider/version.rb +1 -1
  18. data/spec/db_helper.rb +134 -14
  19. data/spec/dbee/providers/active_record_provider/expression_builder_spec.rb +90 -0
  20. data/spec/dbee/providers/active_record_provider/makers/where_spec.rb +260 -0
  21. data/spec/dbee/providers/active_record_provider_spec.rb +112 -14
  22. data/spec/fixtures/active_record_snapshots/five_table_query.yaml +1 -0
  23. data/spec/fixtures/active_record_snapshots/multiple_same_table_query_with_static_constraints.yaml +1 -0
  24. data/spec/fixtures/active_record_snapshots/one_table_empty_query.yaml +11 -0
  25. data/spec/fixtures/active_record_snapshots/one_table_query.yaml +1 -0
  26. data/spec/fixtures/active_record_snapshots/one_table_query_with_ascending_sort.yaml +1 -0
  27. data/spec/fixtures/active_record_snapshots/one_table_query_with_descending_sort.yaml +1 -0
  28. data/spec/fixtures/active_record_snapshots/one_table_query_with_filters.yaml +9 -8
  29. data/spec/fixtures/active_record_snapshots/one_table_query_with_limit.yaml +1 -0
  30. data/spec/fixtures/active_record_snapshots/one_table_query_with_multiple_sorts.yaml +1 -0
  31. data/spec/fixtures/active_record_snapshots/partitioner_example_1_query.yaml +1 -0
  32. data/spec/fixtures/active_record_snapshots/partitioner_example_2_query.yaml +1 -0
  33. data/spec/fixtures/active_record_snapshots/reverse_polymorphic_query.yaml +1 -0
  34. data/spec/fixtures/active_record_snapshots/two_table_query.yaml +1 -0
  35. data/spec/fixtures/active_record_snapshots/two_table_query_with_aggregation.yaml +72 -0
  36. data/spec/fixtures/active_record_snapshots/two_table_query_with_pivoting.yaml +89 -0
  37. data/spec/fixtures/models.yaml +112 -84
  38. data/spec/spec_helper.rb +13 -2
  39. metadata +96 -28
  40. data/lib/dbee/providers/active_record_provider/expression_builder/select_maker.rb +0 -33
  41. data/lib/dbee/providers/active_record_provider/expression_builder/where_maker.rb +0 -68
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f50827b6f31ffb9749796b4532b8a48f704c522be4e0011b7e9a58ed716dd26
4
- data.tar.gz: e9469a99514bfdfbe03da6cdcda339aef1b12b2d829a82d7e2278a22b006f0b3
3
+ metadata.gz: a9ad362290b7ac95e9ad1c466868f81bad354df55b9772742c9bf6591f9f762d
4
+ data.tar.gz: 0ff078fc8e40af83f984729f3320856af95cf95b3562f97bc17b0b8787342a12
5
5
  SHA512:
6
- metadata.gz: 7162ee879530dd36b2078a347b6fade9a5e5ce8e79f47709b33b11cb308303195cbafb3e6096a1529ddb7b7c23da6a19caccdc61356f29dd14b48b4e077cc041
7
- data.tar.gz: 6d19989ed13fd0e2d7a88b3341309cf517aede0f4924c8f76f1325685a099b4b20985774adb445b0bb3487c9d91568500a86c767237618f57194e2806ee0fa93
6
+ metadata.gz: 93b68e1d07bfe82c469d1169d7e94fa2dece7a7f32fc01adcf895356bdfa285d7d1352b1a37e3bd7b50e1023551160988b4dc7a49c97d904b1b4c37d77d1e676
7
+ data.tar.gz: 44aa0eda58e8d5f50f9d99d1b44a81ade7781ba9cad9cf35b47e6b3dcab2f2699c69898e774b33c3a00c535b4927481b3bb62411856f607ff78b24e34dd320a5
data/.rubocop.yml CHANGED
@@ -1,8 +1,17 @@
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: 17
10
+ Exclude:
11
+ - spec/db_helper.rb
12
+
4
13
  Metrics/BlockLength:
5
- ExcludedMethods:
14
+ IgnoredMethods:
6
15
  - let
7
16
  - it
8
17
  - describe
@@ -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
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.6.5
1
+ 2.6.6
data/.travis.yml CHANGED
@@ -1,24 +1,18 @@
1
1
  env:
2
2
  global:
3
3
  - CC_TEST_REPORTER_ID=036a8fd92cf0c323c9704c041015837d14889e47de936bab18287626ff3372c1
4
+ - DISABLE_RSPEC_FOCUS=true
4
5
  language: ruby
5
6
  services:
6
7
  - mysql
7
8
  rvm:
8
9
  # 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.5
10
+ - 2.5.8
11
+ - 2.6.6
12
+ - 2.7.2
13
13
  env:
14
14
  - AR_VERSION=5
15
15
  - 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
16
  cache: bundler
23
17
  before_script:
24
18
  - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
data/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ # 2.2.0 (March 11th, 2021)
2
+
3
+ ### Additions:
4
+
5
+ * Support for graph based models.
6
+
7
+ # 2.1.2 (October 15th, 2020)
8
+
9
+ * Improved test coverage for Where maker
10
+ * Fixed bug in Where maker which resulted in only IS NULL predicates being generated, even in cases
11
+ of when IS NOT NULL intended.
12
+
13
+ # 2.1.1 (July 15th, 2020)
14
+
15
+ ### Additions:
16
+
17
+ * Implemented Dbee::Query::Field#aggregator
18
+ * Implemented Dbee::Query::Field#filters
19
+ * Implemented base case when a Dbee::Query contains no fields
20
+
21
+ ### Changes:
22
+
23
+ * Bumped minimum Ruby version to 2.5
24
+
1
25
  # 2.0.4 (February 13th, 2020)
2
26
 
3
27
  * use Arel#in for Equal filters when there is more than one value
data/Guardfile CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- guard :rspec, cmd: 'DISABLE_SIMPLECOV=true bundle exec rspec --format=documentation' do
3
+ command = 'DISABLE_SIMPLECOV=true bundle exec rspec --format=documentation --order=defined'
4
+ guard :rspec, cmd: command do
4
5
  require 'guard/rspec/dsl'
5
6
  dsl = Guard::RSpec::Dsl.new(self)
6
7
 
data/README.md CHANGED
@@ -11,7 +11,7 @@ This library is a plugin for [Dbee](https://github.com/bluemarblepayroll/dbee).
11
11
  To install through Rubygems:
12
12
 
13
13
  ````
14
- gem install install dbee-active_record
14
+ gem install dbee-active_record
15
15
  ````
16
16
 
17
17
  You can also add this to your Gemfile:
@@ -11,15 +11,23 @@ Gem::Specification.new do |s|
11
11
  By default Dbee ships with no underlying SQL generator. This library will plug in ActiveRecord into Dbee and Dbee will use it for SQL generation.
12
12
  DESCRIPTION
13
13
 
14
- s.authors = ['Matthew Ruggio']
15
- s.email = ['mruggio@bluemarblepayroll.com']
14
+ s.authors = ['Matthew Ruggio', 'Craig Kattner']
15
+ s.email = ['mruggio@bluemarblepayroll.com', 'ckattner@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,20 @@ 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.3')
45
+ s.add_dependency('dbee', '~>3')
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')
50
+ s.add_development_dependency('pry-byebug')
42
51
  s.add_development_dependency('rake', '~> 13')
43
52
  s.add_development_dependency('rspec', '~> 3.8')
44
- s.add_development_dependency('rubocop', '~>0.79.0')
45
- s.add_development_dependency('simplecov', '~>0.17.0')
46
- s.add_development_dependency('simplecov-console', '~>0.6.0')
53
+ s.add_development_dependency('rubocop', '~> 1')
54
+ s.add_development_dependency('rubocop-rake')
55
+ s.add_development_dependency('rubocop-rspec')
56
+ s.add_development_dependency('simplecov', '~>0.19.0')
57
+ s.add_development_dependency('simplecov-console', '~>0.7.0')
47
58
  s.add_development_dependency('sqlite3', '~>1')
59
+ # Helpful to spot differences in longer SQL queries:
60
+ s.add_development_dependency('super_diff', '~>0.6')
48
61
  end
data/exe/.gitkeep ADDED
File without changes
@@ -35,12 +35,12 @@ module Dbee
35
35
  @column_alias_maker = alias_maker(column_prefix)
36
36
  end
37
37
 
38
- def sql(model, query)
38
+ def sql(schema, query)
39
39
  ExpressionBuilder.new(
40
- model,
40
+ schema,
41
41
  table_alias_maker,
42
42
  column_alias_maker
43
- ).add(query).to_sql
43
+ ).to_sql(query)
44
44
  end
45
45
 
46
46
  private
@@ -7,62 +7,75 @@
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
- # This class can generate an Arel expression tree.
19
- class ExpressionBuilder
20
- extend Forwardable
21
-
15
+ # This class can generate an Arel expression tree given a Dbee::Schema
16
+ # and Dbee::Query.
17
+ class ExpressionBuilder < Maker # :nodoc: all
22
18
  class MissingConstraintError < StandardError; end
23
19
 
24
- def_delegators :statement, :to_sql
25
-
26
- 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
+ def initialize(schema, table_alias_maker, column_alias_maker)
21
+ super(column_alias_maker)
30
22
 
31
- clear
23
+ @schema = schema
24
+ @table_alias_maker = table_alias_maker
32
25
  end
33
26
 
34
- def clear
35
- @base_table = make_table(model.table, model.name)
27
+ def to_sql(query)
28
+ reset_query_state
29
+ build_query(query)
36
30
 
37
- build(base_table)
31
+ return statement.project(select_maker.star(base_table)).to_sql if select_all
38
32
 
39
- add_partitioners(base_table, model.partitioners)
40
- end
41
-
42
- def add(query)
43
- query.fields.each { |field| add_field(field) }
44
- query.sorters.each { |sorter| add_sorter(sorter) }
45
- query.filters.each { |filter| add_filter(filter) }
46
-
47
- add_limit(query.limit)
48
-
49
- self
33
+ statement.to_sql
50
34
  end
51
35
 
52
36
  private
53
37
 
54
38
  attr_reader :base_table,
39
+ :key_paths_to_arel_columns,
40
+ :from_model,
55
41
  :statement,
56
- :model,
57
42
  :table_alias_maker,
58
- :column_alias_maker
43
+ :requires_group_by,
44
+ :group_by_columns,
45
+ :schema,
46
+ :select_all,
47
+ :tables
48
+
49
+ def reset_query_state
50
+ @base_table = nil
51
+ @key_paths_to_arel_columns = {}
52
+ @from_model = nil
53
+ @group_by_columns = []
54
+ @requires_group_by = false
55
+ @select_all = true
56
+ @tables = {}
57
+ end
59
58
 
60
- def tables
61
- @tables ||= {}
59
+ def build_query(query)
60
+ establish_query_base(query)
61
+ process_fields_sorters_and_filters(query)
62
+
63
+ add_partitioners(base_table, from_model.partitioners)
64
+ add_limit(query.limit)
65
+
66
+ statement.group(group_by_columns) if requires_group_by && !group_by_columns.empty?
62
67
  end
63
68
 
64
- def key_paths_to_arel_columns
65
- @key_paths_to_arel_columns ||= {}
69
+ def establish_query_base(query)
70
+ @from_model = schema.model_for_name!(query.from)
71
+ @base_table = make_table(from_model.table, @from_model.name)
72
+ build(base_table)
73
+ end
74
+
75
+ def process_fields_sorters_and_filters(query)
76
+ query.fields.each { |field| add_field(field) }
77
+ query.sorters.each { |sorter| add_sorter(sorter) }
78
+ query.filters.each { |filter| add_filter(filter) }
66
79
  end
67
80
 
68
81
  def add_filter(filter)
@@ -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
 
@@ -120,38 +151,48 @@ module Dbee
120
151
  self
121
152
  end
122
153
 
123
- def table(name, model, previous_table)
124
- table = make_table(model.table, name)
154
+ def table(ancestor_names, relationship, model, previous_table)
155
+ table = make_table(model.table, ancestor_names)
125
156
 
126
- on = ConstraintMaker.instance.make(model.constraints, table, previous_table)
157
+ on = constraint_maker.make(relationship.constraints, table, previous_table)
127
158
 
128
- raise MissingConstraintError, "for: #{name}" unless on
159
+ raise MissingConstraintError, "for: #{ancestor_names}" unless on
129
160
 
130
161
  build(statement.join(table, ::Arel::Nodes::OuterJoin))
131
162
  build(statement.on(on))
132
163
 
133
164
  add_partitioners(table, model.partitioners)
134
165
 
135
- tables[name] = table
166
+ tables[ancestor_names] = table
136
167
  end
137
168
 
138
- def traverse_ancestors(ancestors)
139
- ancestors.each_pair.inject(base_table) do |memo, (name, model)|
140
- tables.key?(name) ? tables[name] : table(name, model, memo)
169
+ # Travel the query path returning the table at the end of the path.
170
+ #
171
+ # Side effect: intermediate tables are created along the way and are
172
+ # added to the "tables" hash keyed by path.
173
+ def traverse_query_path(expanded_query_path)
174
+ visited_path = []
175
+
176
+ expanded_query_path.inject(base_table) do |prev_model, (relationship, next_model)|
177
+ visited_path += [relationship.name]
178
+ if tables.key?(visited_path)
179
+ tables[visited_path]
180
+ else
181
+ table(visited_path, relationship, next_model, prev_model)
182
+ end
141
183
  end
142
184
  end
143
185
 
144
186
  def add_key_path(key_path)
145
- return if key_paths_to_arel_columns.key?(key_path)
187
+ return key_paths_to_arel_columns[key_path] if key_paths_to_arel_columns.key?(key_path)
146
188
 
147
- ancestors = model.ancestors!(key_path.ancestor_names)
148
-
149
- table = traverse_ancestors(ancestors)
189
+ expanded_query_path = schema.expand_query_path(from_model, key_path)
190
+ table = traverse_query_path(expanded_query_path)
150
191
 
151
192
  arel_column = table[key_path.column_name]
152
- key_paths_to_arel_columns[key_path] = arel_column
153
193
 
154
- self
194
+ # Note that this returns arel_column
195
+ key_paths_to_arel_columns[key_path] = arel_column
155
196
  end
156
197
 
157
198
  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