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
@@ -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
@@ -0,0 +1,111 @@
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#where predicates.
15
+ class Where
16
+ include Singleton
17
+
18
+ def make(filter, arel_column)
19
+ # If the filter has a value of nil, then simply return an IS (NOT) NULL predicate
20
+ return make_is_null_predicate(arel_column, filter.class) if filter.value.nil?
21
+
22
+ values = Array(filter.value).flatten
23
+
24
+ # This logic helps ensure that if a null exists that it translates to an IS NULL
25
+ # predicate and does not get put into an in or not_in clause.
26
+ predicates =
27
+ if values.include?(nil)
28
+ [make_is_null_predicate(arel_column, filter.class)]
29
+ else
30
+ []
31
+ end
32
+
33
+ predicates += make_predicates(filter, arel_column, values - [nil])
34
+
35
+ # Chain all predicates together
36
+ predicates.inject(predicates.shift) do |memo, predicate|
37
+ memo.or(predicate)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ FILTER_EVALUATORS = {
44
+ Query::Filters::Contains => ->(node, val) { node.matches("%#{val}%") },
45
+ Query::Filters::Equals => ->(node, val) { node.eq(val) },
46
+ Query::Filters::GreaterThan => ->(node, val) { node.gt(val) },
47
+ Query::Filters::GreaterThanOrEqualTo => ->(node, val) { node.gteq(val) },
48
+ Query::Filters::LessThan => ->(node, val) { node.lt(val) },
49
+ Query::Filters::LessThanOrEqualTo => ->(node, val) { node.lteq(val) },
50
+ Query::Filters::NotContain => ->(node, val) { node.does_not_match("%#{val}%") },
51
+ Query::Filters::NotEquals => ->(node, val) { node.not_eq(val) },
52
+ Query::Filters::NotStartWith => ->(node, val) { node.does_not_match("#{val}%") },
53
+ Query::Filters::StartsWith => ->(node, val) { node.matches("#{val}%") }
54
+ }.freeze
55
+
56
+ NULL_PREDICATE_MAP = {
57
+ Query::Filters::Contains => Query::Filters::Equals,
58
+ Query::Filters::Equals => Query::Filters::Equals,
59
+ Query::Filters::GreaterThan => Query::Filters::Equals,
60
+ Query::Filters::GreaterThanOrEqualTo => Query::Filters::Equals,
61
+ Query::Filters::LessThan => Query::Filters::Equals,
62
+ Query::Filters::LessThanOrEqualTo => Query::Filters::Equals,
63
+ Query::Filters::NotContain => Query::Filters::NotEquals,
64
+ Query::Filters::NotEquals => Query::Filters::NotEquals,
65
+ Query::Filters::NotStartWith => Query::Filters::NotEquals,
66
+ Query::Filters::StartsWith => Query::Filters::Equals
67
+ }.freeze
68
+
69
+ private_constant :FILTER_EVALUATORS, :NULL_PREDICATE_MAP
70
+
71
+ def make_predicates(filter, arel_column, values)
72
+ if use_in?(filter, values)
73
+ [arel_column.in(values)]
74
+ elsif use_not_in?(filter, values)
75
+ [arel_column.not_in(values)]
76
+ else
77
+ make_or_predicates(filter, arel_column, values)
78
+ end
79
+ end
80
+
81
+ def use_in?(filter, values)
82
+ filter.is_a?(Query::Filters::Equals) && values.length > 1
83
+ end
84
+
85
+ def use_not_in?(filter, values)
86
+ filter.is_a?(Query::Filters::NotEquals) && values.length > 1
87
+ end
88
+
89
+ def make_or_predicates(filter, arel_column, values)
90
+ values.map do |value|
91
+ make_predicate(arel_column, filter.class, value)
92
+ end
93
+ end
94
+
95
+ def make_predicate(arel_column, filter_class, value)
96
+ method = FILTER_EVALUATORS[filter_class]
97
+
98
+ raise ArgumentError, "cannot compile filter: #{filter}" unless method
99
+
100
+ method.call(arel_column, value)
101
+ end
102
+
103
+ def make_is_null_predicate(arel_column, requested_filter_class)
104
+ actual_filter_class = NULL_PREDICATE_MAP[requested_filter_class]
105
+ make_predicate(arel_column, actual_filter_class, nil)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -10,7 +10,7 @@
10
10
  module Dbee
11
11
  module Providers
12
12
  class ActiveRecordProvider
13
- VERSION = '2.0.4'
13
+ VERSION = '2.2.0'
14
14
  end
15
15
  end
16
16
  end
data/spec/db_helper.rb CHANGED
@@ -10,6 +10,27 @@
10
10
  # Enable logging using something like:
11
11
  # ActiveRecord::Base.logger = Logger.new(STDERR)
12
12
 
13
+ class Field < ActiveRecord::Base
14
+ has_many :patient_field_values
15
+ end
16
+
17
+ class Patient < ActiveRecord::Base
18
+ has_many :patient_field_values
19
+ has_many :patient_payments
20
+
21
+ accepts_nested_attributes_for :patient_field_values
22
+ accepts_nested_attributes_for :patient_payments
23
+ end
24
+
25
+ class PatientFieldValue < ActiveRecord::Base
26
+ belongs_to :patient
27
+ belongs_to :field
28
+ end
29
+
30
+ class PatientPayment < ActiveRecord::Base
31
+ belongs_to :patient
32
+ end
33
+
13
34
  def connect_to_db(name)
14
35
  config = yaml_file_read('spec', 'config', 'database.yaml')[name.to_s]
15
36
  ActiveRecord::Base.establish_connection(config)
@@ -17,39 +38,40 @@ end
17
38
 
18
39
  def load_schema
19
40
  ActiveRecord::Schema.define do
41
+ # Movie Theater Schema
20
42
  create_table :theaters do |t|
21
- t.column :name, :string
43
+ t.column :name, :string
22
44
  t.column :partition, :string
23
- t.column :active, :boolean
45
+ t.column :active, :boolean
24
46
  t.column :inspected, :boolean
25
47
  t.timestamps
26
48
  end
27
49
 
28
50
  create_table :members do |t|
29
- t.column :tid, :integer
51
+ t.column :tid, :integer
30
52
  t.column :account_number, :string
31
- t.column :partition, :string
53
+ t.column :partition, :string
32
54
  t.timestamps
33
55
  end
34
56
 
35
57
  create_table :demographics do |t|
36
58
  t.column :member_id, :integer
37
- t.column :name, :string
59
+ t.column :name, :string
38
60
  t.timestamps
39
61
  end
40
62
 
41
63
  create_table :phone_numbers do |t|
42
64
  t.column :demographic_id, :integer
43
- t.column :phone_type, :string
44
- t.column :phone_number, :string
65
+ t.column :phone_type, :string
66
+ t.column :phone_number, :string
45
67
  t.timestamps
46
68
  end
47
69
 
48
70
  create_table :movies do |t|
49
71
  t.column :member_id, :integer
50
- t.column :name, :string
51
- t.column :genre, :string
52
- t.column :favorite, :boolean, default: false, null: false
72
+ t.column :name, :string
73
+ t.column :genre, :string
74
+ t.column :favorite, :boolean, default: false, null: false
53
75
  t.timestamps
54
76
  end
55
77
 
@@ -60,10 +82,10 @@ def load_schema
60
82
 
61
83
  create_table :animals do |t|
62
84
  t.column :owner_id, :integer
63
- t.column :toy_id, :integer
64
- t.column :type, :string
65
- t.column :name, :string
66
- t.column :deleted, :boolean
85
+ t.column :toy_id, :integer
86
+ t.column :type, :string
87
+ t.column :name, :string
88
+ t.column :deleted, :boolean
67
89
  t.timestamps
68
90
  end
69
91
 
@@ -76,5 +98,103 @@ def load_schema
76
98
  t.column :laser, :boolean
77
99
  t.timestamps
78
100
  end
101
+
102
+ # Patient Schema
103
+ create_table :fields do |t|
104
+ t.column :section, :string
105
+ t.column :key, :string
106
+ t.timestamps
107
+ end
108
+
109
+ add_index :fields, %i[section key], unique: true
110
+
111
+ create_table :patients do |t|
112
+ t.column :first, :string
113
+ t.column :middle, :string
114
+ t.column :last, :string
115
+ t.timestamps
116
+ end
117
+
118
+ create_table :patient_field_values do |t|
119
+ t.column :patient_id, :integer, foreign_key: true
120
+ t.column :field_id, :integer, foreign_key: true
121
+ t.column :value, :string
122
+ t.timestamps
123
+ end
124
+
125
+ add_index :patient_field_values, %i[patient_id field_id], unique: true
126
+
127
+ create_table :patient_payments do |t|
128
+ t.column :patient_id, :integer, foreign_key: true
129
+ t.column :amount, :decimal
130
+ t.timestamps
131
+ end
79
132
  end
80
133
  end
134
+
135
+ def load_data
136
+ demo_dob_field = Field.create!(section: 'demographics', key: 'dob')
137
+ demo_drivers_license_field = Field.create!(section: 'demographics', key: 'drivers_license')
138
+ demo_notes_field = Field.create!(section: 'demographics', key: 'notes')
139
+
140
+ contact_phone_number_field = Field.create!(section: 'contact', key: 'phone_number')
141
+ contact_notes_field = Field.create!(section: 'contact', key: 'notes')
142
+
143
+ Patient.create!(
144
+ first: 'Bozo',
145
+ middle: 'The',
146
+ last: 'Clown',
147
+ patient_field_values_attributes: [
148
+ {
149
+ field: demo_dob_field,
150
+ value: '1904-04-04'
151
+ },
152
+ {
153
+ field: demo_notes_field,
154
+ value: 'The patient is funny!'
155
+ },
156
+ {
157
+ field: demo_drivers_license_field,
158
+ value: '82-54-hut-hut-hike!'
159
+ },
160
+ {
161
+ field: contact_phone_number_field,
162
+ value: '555-555-5555'
163
+ },
164
+ {
165
+ field: contact_notes_field,
166
+ value: 'Do not call this patient at night!'
167
+ }
168
+ ],
169
+ patient_payments_attributes: [
170
+ { amount: 5 },
171
+ { amount: 10 },
172
+ { amount: 15 }
173
+ ]
174
+ )
175
+
176
+ Patient.create!(
177
+ first: 'Frank',
178
+ last: 'Rizzo',
179
+ patient_payments_attributes: [
180
+ { amount: 50 },
181
+ { amount: 150 }
182
+ ]
183
+ )
184
+
185
+ Patient.create!(
186
+ first: 'Bugs',
187
+ middle: 'The',
188
+ last: 'Bunny',
189
+ patient_field_values_attributes: [
190
+ {
191
+ field: demo_dob_field,
192
+ value: '2040-01-01'
193
+ },
194
+ {
195
+ field: contact_notes_field,
196
+ value: 'Call anytime!!'
197
+ }
198
+ ]
199
+ )
200
+ end