dbee-active_record 2.0.4 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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