dbee-active_record 2.0.1 → 2.1.0.pre.alpha.1
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +24 -9
- data/.ruby-version +1 -1
- data/.travis.yml +3 -7
- data/CHANGELOG.md +26 -0
- data/dbee-active_record.gemspec +13 -5
- data/exe/.gitkeep +0 -0
- data/lib/dbee/providers/active_record_provider/expression_builder.rb +47 -19
- data/lib/dbee/providers/active_record_provider/expression_builder/{constraint_maker.rb → constraint.rb} +11 -11
- data/lib/dbee/providers/active_record_provider/expression_builder/{order_maker.rb → order.rb} +8 -8
- data/lib/dbee/providers/active_record_provider/expression_builder/select.rb +71 -0
- data/lib/dbee/providers/active_record_provider/expression_builder/where.rb +91 -0
- data/lib/dbee/providers/active_record_provider/version.rb +1 -1
- data/spec/db_helper.rb +130 -14
- data/spec/dbee/providers/active_record_provider_spec.rb +102 -1
- data/spec/fixtures/active_record_snapshots/one_table_query_with_filters.yaml +24 -16
- data/spec/fixtures/active_record_snapshots/two_table_query_with_aggregation.yaml +71 -0
- data/spec/fixtures/active_record_snapshots/two_table_query_with_pivoting.yaml +88 -0
- data/spec/fixtures/models.yaml +20 -0
- metadata +30 -21
- data/lib/dbee/providers/active_record_provider/expression_builder/select_maker.rb +0 -33
- data/lib/dbee/providers/active_record_provider/expression_builder/where_maker.rb +0 -56
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,
|
43
|
+
t.column :name, :string
|
22
44
|
t.column :partition, :string
|
23
|
-
t.column :active,
|
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,
|
51
|
+
t.column :tid, :integer
|
30
52
|
t.column :account_number, :string
|
31
|
-
t.column :partition,
|
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,
|
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,
|
44
|
-
t.column :phone_number,
|
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,
|
51
|
-
t.column :genre,
|
52
|
-
t.column :favorite,
|
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,
|
64
|
-
t.column :type,
|
65
|
-
t.column :name,
|
66
|
-
t.column :deleted,
|
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 '
|
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
|
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
|
-
(
|
67
|
-
(
|
70
|
+
("theaters"."active" IS NULL OR "theaters"."active" IN ('t', 'f')) AND
|
71
|
+
("theaters"."inspected" IS NULL OR "theaters"."inspected" IN ('f', 't')) 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
|
-
|
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
|
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
|
-
(
|
84
|
-
(
|
88
|
+
("t0"."active" IS NULL OR "t0"."active" IN ('t', 'f')) AND
|
89
|
+
("t0"."inspected" IS NULL OR "t0"."inspected" IN ('f', 't')) 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
|
-
|
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
|
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
|
-
(
|
101
|
-
(
|
106
|
+
(`theaters`.`active` IS NULL OR `theaters`.`active` IN (TRUE, FALSE)) AND
|
107
|
+
(`theaters`.`inspected` IS NULL OR `theaters`.`inspected` IN (FALSE, TRUE)) 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
|
-
|
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
|
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
|
-
(
|
118
|
-
(
|
124
|
+
(`t0`.`active` IS NULL OR `t0`.`active` IN (TRUE, FALSE)) AND
|
125
|
+
(`t0`.`inspected` IS NULL OR `t0`.`inspected` IN (FALSE, TRUE)) 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
|
-
|
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%')
|
@@ -0,0 +1,71 @@
|
|
1
|
+
model_name: Patients
|
2
|
+
query:
|
3
|
+
fields:
|
4
|
+
- key_path: id
|
5
|
+
display: 'ID #'
|
6
|
+
- key_path: first
|
7
|
+
display: First Name
|
8
|
+
- key_path: patient_payments.amount
|
9
|
+
display: Ave Payment
|
10
|
+
aggregator: ave
|
11
|
+
- key_path: patient_payments.amount
|
12
|
+
display: Number of Payments
|
13
|
+
aggregator: count
|
14
|
+
- key_path: patient_payments.amount
|
15
|
+
display: Max Payment
|
16
|
+
aggregator: max
|
17
|
+
- key_path: patient_payments.amount
|
18
|
+
display: Min Payment
|
19
|
+
aggregator: min
|
20
|
+
- key_path: patient_payments.amount
|
21
|
+
display: Total Paid
|
22
|
+
aggregator: sum
|
23
|
+
|
24
|
+
sqlite_readable: |+
|
25
|
+
SELECT
|
26
|
+
"patients"."id" AS 'ID #',
|
27
|
+
"patients"."first" AS 'First Name',
|
28
|
+
AVG("patient_payments"."amount") AS 'Ave Payment',
|
29
|
+
COUNT("patient_payments"."amount") AS 'Number of Payments',
|
30
|
+
MAX("patient_payments"."amount") AS 'Max Payment',
|
31
|
+
MIN("patient_payments"."amount") AS 'Min Payment',
|
32
|
+
SUM("patient_payments"."amount") AS 'Total Paid'
|
33
|
+
FROM "patients" "patients"
|
34
|
+
LEFT OUTER JOIN "patient_payments" "patient_payments" ON "patient_payments"."patient_id" = "patients"."id"
|
35
|
+
GROUP BY "patients"."id", "patients"."first"
|
36
|
+
sqlite_not_readable: |+
|
37
|
+
SELECT
|
38
|
+
"t0"."id" AS 'c0',
|
39
|
+
"t0"."first" AS 'c1',
|
40
|
+
AVG("t1"."amount") AS 'c2',
|
41
|
+
COUNT("t1"."amount") AS 'c3',
|
42
|
+
MAX("t1"."amount") AS 'c4',
|
43
|
+
MIN("t1"."amount") AS 'c5',
|
44
|
+
SUM("t1"."amount") AS 'c6'
|
45
|
+
FROM "patients" "t0"
|
46
|
+
LEFT OUTER JOIN "patient_payments" "t1" ON "t1"."patient_id" = "t0"."id"
|
47
|
+
GROUP BY "t0"."id", "t0"."first"
|
48
|
+
mysql_readable: |+
|
49
|
+
SELECT
|
50
|
+
`patients`.`id` AS 'ID #',
|
51
|
+
`patients`.`first` AS 'First Name',
|
52
|
+
AVG(`patient_payments`.`amount`) AS 'Ave Payment',
|
53
|
+
COUNT(`patient_payments`.`amount`) AS 'Number of Payments',
|
54
|
+
MAX(`patient_payments`.`amount`) AS 'Max Payment',
|
55
|
+
MIN(`patient_payments`.`amount`) AS 'Min Payment',
|
56
|
+
SUM(`patient_payments`.`amount`) AS 'Total Paid'
|
57
|
+
FROM `patients` `patients`
|
58
|
+
LEFT OUTER JOIN `patient_payments` `patient_payments` ON `patient_payments`.`patient_id` = `patients`.`id`
|
59
|
+
GROUP BY `patients`.`id`, `patients`.`first`
|
60
|
+
mysql_not_readable: |+
|
61
|
+
SELECT
|
62
|
+
`t0`.`id` AS 'c0',
|
63
|
+
`t0`.`first` AS 'c1',
|
64
|
+
AVG(`t1`.`amount`) AS 'c2',
|
65
|
+
COUNT(`t1`.`amount`) AS 'c3',
|
66
|
+
MAX(`t1`.`amount`) AS 'c4',
|
67
|
+
MIN(`t1`.`amount`) AS 'c5',
|
68
|
+
SUM(`t1`.`amount`) AS 'c6'
|
69
|
+
FROM `patients` `t0`
|
70
|
+
LEFT OUTER JOIN `patient_payments` `t1` ON `t1`.`patient_id` = `t0`.`id`
|
71
|
+
GROUP BY `t0`.`id`, `t0`.`first`
|