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