dbee-active_record 2.0.0 → 2.1.0.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,68 @@
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
+ class ExpressionBuilder
14
+ # Derives Arel#where predicates.
15
+ class Where
16
+ include Singleton
17
+
18
+ def make(filter, arel_column)
19
+ values = normalize(filter.value)
20
+
21
+ if filter.is_a?(Query::Filters::Equals) && values.length > 1
22
+ arel_column.in(values)
23
+ elsif filter.is_a?(Query::Filters::NotEquals) && values.length > 1
24
+ arel_column.not_in(values)
25
+ else
26
+ use_or(filter, arel_column)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ FILTER_EVALUATORS = {
33
+ Query::Filters::Contains => ->(node, val) { node.matches("%#{val}%") },
34
+ Query::Filters::Equals => ->(node, val) { node.eq(val) },
35
+ Query::Filters::GreaterThan => ->(node, val) { node.gt(val) },
36
+ Query::Filters::GreaterThanOrEqualTo => ->(node, val) { node.gteq(val) },
37
+ Query::Filters::LessThan => ->(node, val) { node.lt(val) },
38
+ Query::Filters::LessThanOrEqualTo => ->(node, val) { node.lteq(val) },
39
+ Query::Filters::NotContain => ->(node, val) { node.does_not_match("%#{val}%") },
40
+ Query::Filters::NotEquals => ->(node, val) { node.not_eq(val) },
41
+ Query::Filters::NotStartWith => ->(node, val) { node.does_not_match("#{val}%") },
42
+ Query::Filters::StartsWith => ->(node, val) { node.matches("#{val}%") }
43
+ }.freeze
44
+
45
+ private_constant :FILTER_EVALUATORS
46
+
47
+ def normalize(value)
48
+ value ? Array(value).flatten : [nil]
49
+ end
50
+
51
+ def use_or(filter, arel_column)
52
+ predicates = normalize(filter.value).map do |coerced_value|
53
+ method = FILTER_EVALUATORS[filter.class]
54
+
55
+ raise ArgumentError, "cannot compile filter: #{filter}" unless method
56
+
57
+ method.call(arel_column, coerced_value)
58
+ end
59
+
60
+ predicates.inject(predicates.shift) do |memo, predicate|
61
+ memo.or(predicate)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -10,7 +10,7 @@
10
10
  module Dbee
11
11
  module Providers
12
12
  class ActiveRecordProvider
13
- VERSION = '2.0.0'
13
+ VERSION = '2.1.0-alpha'
14
14
  end
15
15
  end
16
16
  end
@@ -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,99 @@ 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
+ create_table :patients do |t|
110
+ t.column :first, :string
111
+ t.column :middle, :string
112
+ t.column :last, :string
113
+ t.timestamps
114
+ end
115
+
116
+ create_table :patient_field_values do |t|
117
+ t.column :patient_id, :integer
118
+ t.column :field_id, :integer
119
+ t.column :value, :string
120
+ t.timestamps
121
+ end
122
+
123
+ create_table :patient_payments do |t|
124
+ t.column :patient_id, :integer
125
+ t.column :amount, :decimal
126
+ t.timestamps
127
+ end
79
128
  end
80
129
  end
130
+
131
+ def load_data
132
+ demo_dob_field = Field.create!(section: 'demographics', key: 'dob')
133
+ demo_drivers_license_field = Field.create!(section: 'demographics', key: 'drivers_license')
134
+ demo_notes_field = Field.create!(section: 'demographics', key: 'notes')
135
+
136
+ contact_phone_number_field = Field.create!(section: 'contact', key: 'phone_number')
137
+ contact_notes_field = Field.create!(section: 'contact', key: 'notes')
138
+
139
+ Patient.create!(
140
+ first: 'Bozo',
141
+ middle: 'The',
142
+ last: 'Clown',
143
+ patient_field_values_attributes: [
144
+ {
145
+ field: demo_dob_field,
146
+ value: '1904-04-04'
147
+ },
148
+ {
149
+ field: demo_notes_field,
150
+ value: 'The patient is funny!'
151
+ },
152
+ {
153
+ field: demo_drivers_license_field,
154
+ value: '82-54-hut-hut-hike!'
155
+ },
156
+ {
157
+ field: contact_phone_number_field,
158
+ value: '555-555-5555'
159
+ },
160
+ {
161
+ field: contact_notes_field,
162
+ value: 'Do not call this patient at night!'
163
+ }
164
+ ],
165
+ patient_payments_attributes: [
166
+ { amount: 5 },
167
+ { amount: 10 },
168
+ { amount: 15 }
169
+ ]
170
+ )
171
+
172
+ Patient.create!(
173
+ first: 'Frank',
174
+ last: 'Rizzo',
175
+ patient_payments_attributes: [
176
+ { amount: 50 },
177
+ { amount: 150 }
178
+ ]
179
+ )
180
+
181
+ Patient.create!(
182
+ first: 'Bugs',
183
+ middle: 'The',
184
+ last: 'Bunny',
185
+ patient_field_values_attributes: [
186
+ {
187
+ field: demo_dob_field,
188
+ value: '2040-01-01'
189
+ },
190
+ {
191
+ field: contact_notes_field,
192
+ value: 'Call anytime!!'
193
+ }
194
+ ]
195
+ )
196
+ end
@@ -79,7 +79,7 @@ describe Dbee::Providers::ActiveRecordProvider do
79
79
  end
80
80
  end
81
81
 
82
- context 'Executing SQL' do
82
+ context 'Shallow SQL Execution' do
83
83
  %w[sqlite].each do |dbms|
84
84
  context dbms do
85
85
  before(:all) do
@@ -108,4 +108,105 @@ describe Dbee::Providers::ActiveRecordProvider do
108
108
  end
109
109
  end
110
110
  end
111
+
112
+ describe 'Deep SQL execution' do
113
+ before(:all) do
114
+ connect_to_db(:sqlite)
115
+ load_schema
116
+ load_data
117
+ end
118
+
119
+ describe 'pivoting' do
120
+ let(:snapshot_path) do
121
+ %w[
122
+ spec
123
+ fixtures
124
+ active_record_snapshots
125
+ two_table_query_with_pivoting.yaml
126
+ ]
127
+ end
128
+
129
+ let(:snapshot) { yaml_file_read(*snapshot_path) }
130
+ let(:query) { Dbee::Query.make(snapshot['query']) }
131
+ let(:model) { Dbee::Model.make(models['Patients']) }
132
+
133
+ it 'pivots table rows into columns' do
134
+ sql = described_class.new.sql(model, query)
135
+
136
+ results = ActiveRecord::Base.connection.execute(sql)
137
+
138
+ expect(results[0]).to include(
139
+ 'First Name' => 'Bozo',
140
+ 'Date of Birth' => '1904-04-04',
141
+ 'Drivers License #' => '82-54-hut-hut-hike!',
142
+ 'Demographic Notes' => 'The patient is funny!',
143
+ 'Contact Notes' => 'Do not call this patient at night!'
144
+ )
145
+
146
+ expect(results[1]).to include(
147
+ 'First Name' => 'Frank',
148
+ 'Date of Birth' => nil,
149
+ 'Drivers License #' => nil,
150
+ 'Demographic Notes' => nil,
151
+ 'Contact Notes' => nil
152
+ )
153
+
154
+ expect(results[2]).to include(
155
+ 'First Name' => 'Bugs',
156
+ 'Date of Birth' => '2040-01-01',
157
+ 'Drivers License #' => nil,
158
+ 'Demographic Notes' => nil,
159
+ 'Contact Notes' => 'Call anytime!!'
160
+ )
161
+ end
162
+ end
163
+
164
+ describe 'aggregation' do
165
+ let(:snapshot_path) do
166
+ %w[
167
+ spec
168
+ fixtures
169
+ active_record_snapshots
170
+ two_table_query_with_aggregation.yaml
171
+ ]
172
+ end
173
+
174
+ let(:snapshot) { yaml_file_read(*snapshot_path) }
175
+ let(:query) { Dbee::Query.make(snapshot['query']) }
176
+ let(:model) { Dbee::Model.make(models['Patients']) }
177
+
178
+ it 'executes correct SQL aggregate functions' do
179
+ sql = described_class.new.sql(model, query)
180
+
181
+ results = ActiveRecord::Base.connection.execute(sql)
182
+
183
+ expect(results[0]).to include(
184
+ 'First Name' => 'Bozo',
185
+ 'Ave Payment' => 10,
186
+ 'Number of Payments' => 3,
187
+ 'Max Payment' => 15,
188
+ 'Min Payment' => 5,
189
+ 'Total Paid' => 30
190
+ )
191
+
192
+ expect(results[1]).to include(
193
+ 'First Name' => 'Frank',
194
+ 'Ave Payment' => 100,
195
+ 'Number of Payments' => 2,
196
+ 'Max Payment' => 150,
197
+ 'Min Payment' => 50,
198
+ 'Total Paid' => 200
199
+ )
200
+
201
+ expect(results[2]).to include(
202
+ 'First Name' => 'Bugs',
203
+ 'Ave Payment' => nil,
204
+ 'Number of Payments' => 0,
205
+ 'Max Payment' => nil,
206
+ 'Min Payment' => nil,
207
+ 'Total Paid' => nil
208
+ )
209
+ end
210
+ end
211
+ end
111
212
  end
@@ -45,6 +45,10 @@ query:
45
45
  - 'Netflix'
46
46
  - 'Hulu'
47
47
  key_path: name
48
+ - type: not_equals
49
+ value:
50
+ - 'YouTube Super Video'
51
+ key_path: name
48
52
  - type: not_contain
49
53
  value:
50
54
  - 'tfli'
@@ -59,67 +63,71 @@ sqlite_readable: |+
59
63
  SELECT "theaters"."id" AS 'ID #',
60
64
  "theaters"."name" AS 'name'
61
65
  FROM "theaters" "theaters"
62
- WHERE ("theaters"."name" = 'AMC' OR "theaters"."name" = 'Regal') AND
66
+ WHERE "theaters"."name" IN ('AMC', 'Regal') AND
63
67
  "theaters"."name" LIKE 'A%' AND
64
68
  "theaters"."name" LIKE '%m%' AND
65
69
  "theaters"."active" = 'false' AND
66
- (("theaters"."active" = 't' OR "theaters"."active" = 'f') OR "theaters"."active" IS NULL) AND
67
- (("theaters"."inspected" = 'f' OR "theaters"."inspected" = 't') OR "theaters"."inspected" IS NULL) AND
70
+ "theaters"."active" IN ('t', 'f', NULL) AND
71
+ "theaters"."inspected" IN ('f', 't', NULL) AND
68
72
  "theaters"."created_at" <= '2019-03-04' AND
69
73
  "theaters"."created_at" < '2018-03-04' AND
70
74
  "theaters"."created_at" >= '2001-03-04' AND
71
75
  "theaters"."created_at" > '2002-03-04' AND
72
- ("theaters"."name" != 'Netflix' OR "theaters"."name" != 'Hulu') AND
76
+ "theaters"."name" NOT IN ('Netflix', 'Hulu') AND
77
+ "theaters"."name" != 'YouTube Super Video' AND
73
78
  ("theaters"."name" NOT LIKE '%tfli%' OR "theaters"."name" NOT LIKE '%ul%') AND
74
79
  ("theaters"."name" NOT LIKE 'netf%' OR "theaters"."name" NOT LIKE 'hu%')
75
80
  sqlite_not_readable: |+
76
81
  SELECT "t0"."id" AS 'c0',
77
82
  "t0"."name" AS 'c1'
78
83
  FROM "theaters" "t0"
79
- WHERE ("t0"."name" = 'AMC' OR "t0"."name" = 'Regal') AND
84
+ WHERE "t0"."name" IN ('AMC', 'Regal') AND
80
85
  "t0"."name" LIKE 'A%' AND
81
86
  "t0"."name" LIKE '%m%' AND
82
87
  "t0"."active" = 'false' AND
83
- (("t0"."active" = 't' OR "t0"."active" = 'f') OR "t0"."active" IS NULL) AND
84
- (("t0"."inspected" = 'f' OR "t0"."inspected" = 't') OR "t0"."inspected" IS NULL) AND
88
+ "t0"."active" IN ('t', 'f', NULL) AND
89
+ "t0"."inspected" IN ('f', 't', NULL) AND
85
90
  "t0"."created_at" <= '2019-03-04' AND
86
91
  "t0"."created_at" < '2018-03-04' AND
87
92
  "t0"."created_at" >= '2001-03-04' AND
88
93
  "t0"."created_at" > '2002-03-04' AND
89
- ("t0"."name" != 'Netflix' OR "t0"."name" != 'Hulu') AND
94
+ "t0"."name" NOT IN ('Netflix', 'Hulu') AND
95
+ "t0"."name" != 'YouTube Super Video' AND
90
96
  ("t0"."name" NOT LIKE '%tfli%' OR "t0"."name" NOT LIKE '%ul%') AND
91
97
  ("t0"."name" NOT LIKE 'netf%' OR "t0"."name" NOT LIKE 'hu%')
92
98
  mysql_readable: |+
93
99
  SELECT `theaters`.`id` AS 'ID #',
94
100
  `theaters`.`name` AS 'name'
95
101
  FROM `theaters` `theaters`
96
- WHERE (`theaters`.`name` = 'AMC' OR `theaters`.`name` = 'Regal') AND
102
+ WHERE `theaters`.`name` IN ('AMC', 'Regal') AND
97
103
  `theaters`.`name` LIKE 'A%' AND
98
104
  `theaters`.`name` LIKE '%m%' AND
99
105
  `theaters`.`active` = 'false' AND
100
- ((`theaters`.`active` = TRUE OR `theaters`.`active` = FALSE) OR `theaters`.`active` IS NULL) AND
101
- ((`theaters`.`inspected` = FALSE OR `theaters`.`inspected` = TRUE) OR `theaters`.`inspected` IS NULL) AND
106
+ `theaters`.`active` IN (TRUE, FALSE, NULL) AND
107
+ `theaters`.`inspected` IN (FALSE, TRUE, NULL) AND
102
108
  `theaters`.`created_at` <= '2019-03-04' AND
103
109
  `theaters`.`created_at` < '2018-03-04' AND
104
110
  `theaters`.`created_at` >= '2001-03-04' AND
105
111
  `theaters`.`created_at` > '2002-03-04' AND
106
- (`theaters`.`name` != 'Netflix' OR `theaters`.`name` != 'Hulu') AND
112
+ `theaters`.`name` NOT IN ('Netflix', 'Hulu') AND
113
+ `theaters`.`name` != 'YouTube Super Video' AND
107
114
  (`theaters`.`name` NOT LIKE '%tfli%' OR `theaters`.`name` NOT LIKE '%ul%') AND
108
115
  (`theaters`.`name` NOT LIKE 'netf%' OR `theaters`.`name` NOT LIKE 'hu%')
109
116
  mysql_not_readable: |+
110
117
  SELECT `t0`.`id` AS 'c0',
111
118
  `t0`.`name` AS 'c1'
112
119
  FROM `theaters` `t0`
113
- WHERE (`t0`.`name` = 'AMC' OR `t0`.`name` = 'Regal') AND
120
+ WHERE `t0`.`name` IN ('AMC', 'Regal') AND
114
121
  `t0`.`name` LIKE 'A%' AND
115
122
  `t0`.`name` LIKE '%m%' AND
116
123
  `t0`.`active` = 'false' AND
117
- ((`t0`.`active` = TRUE OR `t0`.`active` = FALSE) OR `t0`.`active` IS NULL) AND
118
- ((`t0`.`inspected` = FALSE OR `t0`.`inspected` = TRUE) OR `t0`.`inspected` IS NULL) AND
124
+ `t0`.`active` IN (TRUE, FALSE, NULL) AND
125
+ `t0`.`inspected` IN (FALSE, TRUE, NULL) AND
119
126
  `t0`.`created_at` <= '2019-03-04' AND
120
127
  `t0`.`created_at` < '2018-03-04' AND
121
128
  `t0`.`created_at` >= '2001-03-04' AND
122
129
  `t0`.`created_at` > '2002-03-04' AND
123
- (`t0`.`name` != 'Netflix' OR `t0`.`name` != 'Hulu') AND
130
+ `t0`.`name` NOT IN ('Netflix', 'Hulu') AND
131
+ `t0`.`name` != 'YouTube Super Video' AND
124
132
  (`t0`.`name` NOT LIKE '%tfli%' OR `t0`.`name` NOT LIKE '%ul%') AND
125
133
  (`t0`.`name` NOT LIKE 'netf%' OR `t0`.`name` NOT LIKE 'hu%')