dbee-active_record 2.0.4 → 2.1.0.pre.alpha
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 -10
- data/CHANGELOG.md +11 -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_maker.rb → where.rb} +16 -16
- 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/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 -27
- data/lib/dbee/providers/active_record_provider/expression_builder/select_maker.rb +0 -33
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8de8c388637ba455ffca406ff42751ecfb62f4a40bbcb9f560a085b585568a1
|
|
4
|
+
data.tar.gz: 9723886e37e886d7df7fa4661df4c81d35fe186d08a7d82de66083e08ed44bac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: be75ae14b3c4b0154a0cc4f63950fa86a5f9f16272cd2d5133a4fc4649e2bcd6f7dc5836cf1706c7a9a6b9dba1f1183783038b013bc9e78f5cc5f25e9c980f78
|
|
7
|
+
data.tar.gz: 7f466202c164f0da7fb936010bc73393a0643682588f14093979fa1e8844f36bd0978020f3f435624d6bea25d95767af35884c6e453a35f41d219dd4331b22d7
|
data/.rubocop.yml
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 2.5
|
|
3
|
+
|
|
4
|
+
Layout/LineLength:
|
|
2
5
|
Max: 100
|
|
3
6
|
|
|
7
|
+
Lint/RaiseException:
|
|
8
|
+
Enabled: True
|
|
9
|
+
|
|
10
|
+
Lint/StructNewOverride:
|
|
11
|
+
Enabled: True
|
|
12
|
+
|
|
13
|
+
Metrics/AbcSize:
|
|
14
|
+
Max: 16
|
|
15
|
+
Exclude:
|
|
16
|
+
- spec/db_helper.rb
|
|
17
|
+
|
|
4
18
|
Metrics/BlockLength:
|
|
5
19
|
ExcludedMethods:
|
|
6
20
|
- let
|
|
@@ -13,18 +27,19 @@ Metrics/BlockLength:
|
|
|
13
27
|
- spec/dbee/**/*
|
|
14
28
|
- dbee-active_record.gemspec
|
|
15
29
|
|
|
30
|
+
Metrics/ClassLength:
|
|
31
|
+
Max: 125
|
|
32
|
+
|
|
16
33
|
Metrics/MethodLength:
|
|
17
34
|
Max: 25
|
|
18
35
|
Exclude:
|
|
19
36
|
- spec/db_helper.rb
|
|
20
37
|
|
|
21
|
-
|
|
22
|
-
|
|
38
|
+
Style/HashEachMethods:
|
|
39
|
+
Enabled: True
|
|
23
40
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Exclude:
|
|
27
|
-
- spec/db_helper.rb
|
|
41
|
+
Style/HashTransformKeys:
|
|
42
|
+
Enabled: True
|
|
28
43
|
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
Style/HashTransformValues:
|
|
45
|
+
Enabled: True
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.6.
|
|
1
|
+
2.6.6
|
data/.travis.yml
CHANGED
|
@@ -6,19 +6,12 @@ services:
|
|
|
6
6
|
- mysql
|
|
7
7
|
rvm:
|
|
8
8
|
# Build on the latest stable of all supported Rubies (https://www.ruby-lang.org/en/downloads/):
|
|
9
|
-
- 2.
|
|
10
|
-
- 2.
|
|
11
|
-
- 2.
|
|
12
|
-
- 2.6.5
|
|
9
|
+
- 2.5.8
|
|
10
|
+
- 2.6.6
|
|
11
|
+
- 2.7.1
|
|
13
12
|
env:
|
|
14
13
|
- AR_VERSION=5
|
|
15
14
|
- 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
15
|
cache: bundler
|
|
23
16
|
before_script:
|
|
24
17
|
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
# 2.1.0 (TBD)
|
|
2
|
+
|
|
3
|
+
### Additions:
|
|
4
|
+
|
|
5
|
+
* Implemented Dbee::Query::Field#aggregator
|
|
6
|
+
* Implemented Dbee::Query::Field#filters
|
|
7
|
+
|
|
8
|
+
### Changes:
|
|
9
|
+
|
|
10
|
+
* Bumped minimum Ruby version to 2.5
|
|
11
|
+
|
|
1
12
|
# 2.0.4 (February 13th, 2020)
|
|
2
13
|
|
|
3
14
|
* use Arel#in for Equal filters when there is more than one value
|
data/dbee-active_record.gemspec
CHANGED
|
@@ -15,11 +15,19 @@ Gem::Specification.new do |s|
|
|
|
15
15
|
s.email = ['mruggio@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.
|
|
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.
|
|
30
|
+
s.required_ruby_version = '>= 2.5'
|
|
23
31
|
|
|
24
32
|
ar_version = ENV['AR_VERSION'] || ''
|
|
25
33
|
|
|
@@ -34,15 +42,15 @@ Gem::Specification.new do |s|
|
|
|
34
42
|
end
|
|
35
43
|
|
|
36
44
|
s.add_dependency('activerecord', activerecord_version)
|
|
37
|
-
s.add_dependency('dbee', '
|
|
45
|
+
s.add_dependency('dbee', '=2.1.0.pre.alpha')
|
|
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')
|
|
42
50
|
s.add_development_dependency('rake', '~> 13')
|
|
43
51
|
s.add_development_dependency('rspec', '~> 3.8')
|
|
44
|
-
s.add_development_dependency('rubocop', '~>0.
|
|
52
|
+
s.add_development_dependency('rubocop', '~>0.81.0')
|
|
45
53
|
s.add_development_dependency('simplecov', '~>0.17.0')
|
|
46
|
-
s.add_development_dependency('simplecov-console', '~>0.
|
|
54
|
+
s.add_development_dependency('simplecov-console', '~>0.7.0')
|
|
47
55
|
s.add_development_dependency('sqlite3', '~>1')
|
|
48
56
|
end
|
data/exe/.gitkeep
ADDED
|
File without changes
|
|
@@ -7,26 +7,24 @@
|
|
|
7
7
|
# LICENSE file in the root directory of this source tree.
|
|
8
8
|
#
|
|
9
9
|
|
|
10
|
-
require_relative 'expression_builder/
|
|
11
|
-
require_relative 'expression_builder/
|
|
12
|
-
require_relative 'expression_builder/
|
|
13
|
-
require_relative 'expression_builder/
|
|
10
|
+
require_relative 'expression_builder/constraint'
|
|
11
|
+
require_relative 'expression_builder/order'
|
|
12
|
+
require_relative 'expression_builder/select'
|
|
13
|
+
require_relative 'expression_builder/where'
|
|
14
14
|
|
|
15
15
|
module Dbee
|
|
16
16
|
module Providers
|
|
17
17
|
class ActiveRecordProvider
|
|
18
18
|
# This class can generate an Arel expression tree.
|
|
19
19
|
class ExpressionBuilder
|
|
20
|
-
extend Forwardable
|
|
21
|
-
|
|
22
20
|
class MissingConstraintError < StandardError; end
|
|
23
21
|
|
|
24
|
-
def_delegators :statement, :to_sql
|
|
25
|
-
|
|
26
22
|
def initialize(model, table_alias_maker, column_alias_maker)
|
|
27
23
|
@model = model
|
|
28
24
|
@table_alias_maker = table_alias_maker
|
|
29
25
|
@column_alias_maker = column_alias_maker
|
|
26
|
+
@requires_group_by = false
|
|
27
|
+
@group_by_columns = []
|
|
30
28
|
|
|
31
29
|
clear
|
|
32
30
|
end
|
|
@@ -49,13 +47,25 @@ module Dbee
|
|
|
49
47
|
self
|
|
50
48
|
end
|
|
51
49
|
|
|
50
|
+
def to_sql
|
|
51
|
+
if requires_group_by
|
|
52
|
+
@requires_group_by = false
|
|
53
|
+
statement.group(group_by_columns) unless group_by_columns.empty?
|
|
54
|
+
@group_by_columns = []
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
statement.to_sql
|
|
58
|
+
end
|
|
59
|
+
|
|
52
60
|
private
|
|
53
61
|
|
|
54
62
|
attr_reader :base_table,
|
|
55
63
|
:statement,
|
|
56
64
|
:model,
|
|
57
65
|
:table_alias_maker,
|
|
58
|
-
:column_alias_maker
|
|
66
|
+
:column_alias_maker,
|
|
67
|
+
:requires_group_by,
|
|
68
|
+
:group_by_columns
|
|
59
69
|
|
|
60
70
|
def tables
|
|
61
71
|
@tables ||= {}
|
|
@@ -70,7 +80,7 @@ module Dbee
|
|
|
70
80
|
|
|
71
81
|
key_path = filter.key_path
|
|
72
82
|
arel_column = key_paths_to_arel_columns[key_path]
|
|
73
|
-
predicate =
|
|
83
|
+
predicate = Where.instance.make(filter, arel_column)
|
|
74
84
|
|
|
75
85
|
build(statement.where(predicate))
|
|
76
86
|
|
|
@@ -82,22 +92,40 @@ module Dbee
|
|
|
82
92
|
|
|
83
93
|
key_path = sorter.key_path
|
|
84
94
|
arel_column = key_paths_to_arel_columns[key_path]
|
|
85
|
-
predicate =
|
|
95
|
+
predicate = Order.instance.make(sorter, arel_column)
|
|
86
96
|
|
|
87
97
|
build(statement.order(predicate))
|
|
88
98
|
|
|
89
99
|
self
|
|
90
100
|
end
|
|
91
101
|
|
|
102
|
+
def add_filter_key_paths(filters)
|
|
103
|
+
filters.each_with_object({}) do |filter, memo|
|
|
104
|
+
arel_key_column = add_key_path(filter.key_path)
|
|
105
|
+
|
|
106
|
+
memo[arel_key_column] = filter
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
92
110
|
def add_field(field)
|
|
93
|
-
add_key_path(field.key_path)
|
|
111
|
+
arel_value_column = add_key_path(field.key_path)
|
|
112
|
+
arel_key_columns_to_filters = add_filter_key_paths(field.filters)
|
|
94
113
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
114
|
+
predicate = Select.instance.make(
|
|
115
|
+
field,
|
|
116
|
+
arel_key_columns_to_filters,
|
|
117
|
+
arel_value_column,
|
|
118
|
+
column_alias_maker
|
|
119
|
+
)
|
|
98
120
|
|
|
99
121
|
build(statement.project(predicate))
|
|
100
122
|
|
|
123
|
+
if field.aggregator?
|
|
124
|
+
@requires_group_by = true
|
|
125
|
+
else
|
|
126
|
+
group_by_columns << arel_value_column
|
|
127
|
+
end
|
|
128
|
+
|
|
101
129
|
self
|
|
102
130
|
end
|
|
103
131
|
|
|
@@ -123,7 +151,7 @@ module Dbee
|
|
|
123
151
|
def table(name, model, previous_table)
|
|
124
152
|
table = make_table(model.table, name)
|
|
125
153
|
|
|
126
|
-
on =
|
|
154
|
+
on = Constraint.instance.make(model.constraints, table, previous_table)
|
|
127
155
|
|
|
128
156
|
raise MissingConstraintError, "for: #{name}" unless on
|
|
129
157
|
|
|
@@ -142,16 +170,16 @@ module Dbee
|
|
|
142
170
|
end
|
|
143
171
|
|
|
144
172
|
def add_key_path(key_path)
|
|
145
|
-
return if key_paths_to_arel_columns.key?(key_path)
|
|
173
|
+
return key_paths_to_arel_columns[key_path] if key_paths_to_arel_columns.key?(key_path)
|
|
146
174
|
|
|
147
175
|
ancestors = model.ancestors!(key_path.ancestor_names)
|
|
148
176
|
|
|
149
177
|
table = traverse_ancestors(ancestors)
|
|
150
178
|
|
|
151
179
|
arel_column = table[key_path.column_name]
|
|
152
|
-
key_paths_to_arel_columns[key_path] = arel_column
|
|
153
180
|
|
|
154
|
-
|
|
181
|
+
# Note that this returns arel_column
|
|
182
|
+
key_paths_to_arel_columns[key_path] = arel_column
|
|
155
183
|
end
|
|
156
184
|
|
|
157
185
|
def build(new_expression)
|
|
@@ -12,9 +12,19 @@ module Dbee
|
|
|
12
12
|
class ActiveRecordProvider
|
|
13
13
|
class ExpressionBuilder
|
|
14
14
|
# Can derive constraints for Arel table JOIN statements.
|
|
15
|
-
class
|
|
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
|
data/lib/dbee/providers/active_record_provider/expression_builder/{order_maker.rb → order.rb}
RENAMED
|
@@ -12,16 +12,9 @@ module Dbee
|
|
|
12
12
|
class ActiveRecordProvider
|
|
13
13
|
class ExpressionBuilder
|
|
14
14
|
# Derives Arel#order predicates.
|
|
15
|
-
class
|
|
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,71 @@
|
|
|
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#project predicates.
|
|
15
|
+
class Select
|
|
16
|
+
include Singleton
|
|
17
|
+
|
|
18
|
+
def make(field, arel_key_nodes_to_filters, arel_value_node, alias_maker)
|
|
19
|
+
column_alias = quote(alias_maker.make(field.display))
|
|
20
|
+
predicate = expression(field, arel_key_nodes_to_filters, arel_value_node)
|
|
21
|
+
predicate = aggregate(field, predicate)
|
|
22
|
+
|
|
23
|
+
predicate.as(column_alias)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
AGGREGRATOR_EVALUATORS = {
|
|
29
|
+
nil => ->(arel_node) { arel_node },
|
|
30
|
+
Query::Field::Aggregator::AVE => ->(node) { Arel::Nodes::Avg.new([node]) },
|
|
31
|
+
Query::Field::Aggregator::COUNT => ->(node) { Arel::Nodes::Count.new([node]) },
|
|
32
|
+
Query::Field::Aggregator::MAX => ->(node) { Arel::Nodes::Max.new([node]) },
|
|
33
|
+
Query::Field::Aggregator::MIN => ->(node) { Arel::Nodes::Min.new([node]) },
|
|
34
|
+
Query::Field::Aggregator::SUM => ->(node) { Arel::Nodes::Sum.new([node]) }
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
private_constant :AGGREGRATOR_EVALUATORS
|
|
38
|
+
|
|
39
|
+
def quote(value)
|
|
40
|
+
ActiveRecord::Base.connection.quote(value)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def aggregate(field, predicate)
|
|
44
|
+
AGGREGRATOR_EVALUATORS[field.aggregator].call(predicate)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def expression(field, arel_key_nodes_to_filters, arel_value_node)
|
|
48
|
+
if field.filters?
|
|
49
|
+
case_statement = Arel::Nodes::Case.new
|
|
50
|
+
filter_predicate = make_filter_predicate(arel_key_nodes_to_filters)
|
|
51
|
+
|
|
52
|
+
case_statement.when(filter_predicate).then(arel_value_node)
|
|
53
|
+
else
|
|
54
|
+
arel_value_node
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def make_filter_predicate(arel_key_nodes_to_filters)
|
|
59
|
+
predicates = arel_key_nodes_to_filters.map do |arel_key_node, filter|
|
|
60
|
+
Where.instance.make(filter, arel_key_node)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
predicates.inject(predicates.shift) do |memo, predicate|
|
|
64
|
+
memo.and(predicate)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/dbee/providers/active_record_provider/expression_builder/{where_maker.rb → where.rb}
RENAMED
|
@@ -12,24 +12,9 @@ module Dbee
|
|
|
12
12
|
class ActiveRecordProvider
|
|
13
13
|
class ExpressionBuilder
|
|
14
14
|
# Derives Arel#where predicates.
|
|
15
|
-
class
|
|
15
|
+
class Where
|
|
16
16
|
include Singleton
|
|
17
17
|
|
|
18
|
-
FILTER_EVALUATORS = {
|
|
19
|
-
Query::Filters::Contains => ->(column, val) { column.matches("%#{val}%") },
|
|
20
|
-
Query::Filters::Equals => ->(column, val) { column.eq(val) },
|
|
21
|
-
Query::Filters::GreaterThan => ->(column, val) { column.gt(val) },
|
|
22
|
-
Query::Filters::GreaterThanOrEqualTo => ->(column, val) { column.gteq(val) },
|
|
23
|
-
Query::Filters::LessThan => ->(column, val) { column.lt(val) },
|
|
24
|
-
Query::Filters::LessThanOrEqualTo => ->(column, val) { column.lteq(val) },
|
|
25
|
-
Query::Filters::NotContain => ->(column, val) { column.does_not_match("%#{val}%") },
|
|
26
|
-
Query::Filters::NotEquals => ->(column, val) { column.not_eq(val) },
|
|
27
|
-
Query::Filters::NotStartWith => ->(column, val) { column.does_not_match("#{val}%") },
|
|
28
|
-
Query::Filters::StartsWith => ->(column, val) { column.matches("#{val}%") }
|
|
29
|
-
}.freeze
|
|
30
|
-
|
|
31
|
-
private_constant :FILTER_EVALUATORS
|
|
32
|
-
|
|
33
18
|
def make(filter, arel_column)
|
|
34
19
|
values = normalize(filter.value)
|
|
35
20
|
|
|
@@ -44,6 +29,21 @@ module Dbee
|
|
|
44
29
|
|
|
45
30
|
private
|
|
46
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
47
|
def normalize(value)
|
|
48
48
|
value ? Array(value).flatten : [nil]
|
|
49
49
|
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,
|
|
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
|
|
@@ -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`
|
|
@@ -0,0 +1,88 @@
|
|
|
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_field_values.value
|
|
9
|
+
display: Date of Birth
|
|
10
|
+
aggregator: max
|
|
11
|
+
filters:
|
|
12
|
+
- key_path: patient_field_values.fields.section
|
|
13
|
+
value: demographics
|
|
14
|
+
- key_path: patient_field_values.fields.key
|
|
15
|
+
value: dob
|
|
16
|
+
- key_path: patient_field_values.value
|
|
17
|
+
display: Demographic Notes
|
|
18
|
+
aggregator: max
|
|
19
|
+
filters:
|
|
20
|
+
- key_path: patient_field_values.fields.section
|
|
21
|
+
value: demographics
|
|
22
|
+
- key_path: patient_field_values.fields.key
|
|
23
|
+
value: notes
|
|
24
|
+
- key_path: patient_field_values.value
|
|
25
|
+
display: 'Drivers License #'
|
|
26
|
+
aggregator: max
|
|
27
|
+
filters:
|
|
28
|
+
- key_path: patient_field_values.fields.section
|
|
29
|
+
value: demographics
|
|
30
|
+
- key_path: patient_field_values.fields.key
|
|
31
|
+
value: drivers_license
|
|
32
|
+
- key_path: patient_field_values.value
|
|
33
|
+
display: Contact Notes
|
|
34
|
+
aggregator: max
|
|
35
|
+
filters:
|
|
36
|
+
- key_path: patient_field_values.fields.section
|
|
37
|
+
value: contact
|
|
38
|
+
- key_path: patient_field_values.fields.key
|
|
39
|
+
value: notes
|
|
40
|
+
|
|
41
|
+
sqlite_readable: |+
|
|
42
|
+
SELECT
|
|
43
|
+
"patients"."id" AS 'ID #',
|
|
44
|
+
"patients"."first" AS 'First Name',
|
|
45
|
+
MAX(CASE WHEN "patient_field_values_fields"."section" = 'demographics' AND "patient_field_values_fields"."key" = 'dob' THEN "patient_field_values"."value" END) AS 'Date of Birth',
|
|
46
|
+
MAX(CASE WHEN "patient_field_values_fields"."section" = 'demographics' AND "patient_field_values_fields"."key" = 'notes' THEN "patient_field_values"."value" END) AS 'Demographic Notes',
|
|
47
|
+
MAX(CASE WHEN "patient_field_values_fields"."section" = 'demographics' AND "patient_field_values_fields"."key" = 'drivers_license' THEN "patient_field_values"."value" END) AS 'Drivers License #',
|
|
48
|
+
MAX(CASE WHEN "patient_field_values_fields"."section" = 'contact' AND "patient_field_values_fields"."key" = 'notes' THEN "patient_field_values"."value" END) AS 'Contact Notes'
|
|
49
|
+
FROM "patients" "patients"
|
|
50
|
+
LEFT OUTER JOIN "patient_field_values" "patient_field_values" ON "patient_field_values"."patient_id" = "patients"."id"
|
|
51
|
+
LEFT OUTER JOIN "fields" "patient_field_values_fields" ON "patient_field_values_fields"."id" = "patient_field_values"."field_id"
|
|
52
|
+
GROUP BY "patients"."id", "patients"."first"
|
|
53
|
+
sqlite_not_readable: |+
|
|
54
|
+
SELECT
|
|
55
|
+
"t0"."id" AS 'c0',
|
|
56
|
+
"t0"."first" AS 'c1',
|
|
57
|
+
MAX(CASE WHEN "t2"."section" = 'demographics' AND "t2"."key" = 'dob' THEN "t1"."value" END) AS 'c2',
|
|
58
|
+
MAX(CASE WHEN "t2"."section" = 'demographics' AND "t2"."key" = 'notes' THEN "t1"."value" END) AS 'c3',
|
|
59
|
+
MAX(CASE WHEN "t2"."section" = 'demographics' AND "t2"."key" = 'drivers_license' THEN "t1"."value" END) AS 'c4',
|
|
60
|
+
MAX(CASE WHEN "t2"."section" = 'contact' AND "t2"."key" = 'notes' THEN "t1"."value" END) AS 'c5'
|
|
61
|
+
FROM "patients" "t0"
|
|
62
|
+
LEFT OUTER JOIN "patient_field_values" "t1" ON "t1"."patient_id" = "t0"."id"
|
|
63
|
+
LEFT OUTER JOIN "fields" "t2" ON "t2"."id" = "t1"."field_id"
|
|
64
|
+
GROUP BY "t0"."id", "t0"."first"
|
|
65
|
+
mysql_readable: |+
|
|
66
|
+
SELECT
|
|
67
|
+
`patients`.`id` AS 'ID #',
|
|
68
|
+
`patients`.`first` AS 'First Name',
|
|
69
|
+
MAX(CASE WHEN `patient_field_values_fields`.`section` = 'demographics' AND `patient_field_values_fields`.`key` = 'dob' THEN `patient_field_values`.`value` END) AS 'Date of Birth',
|
|
70
|
+
MAX(CASE WHEN `patient_field_values_fields`.`section` = 'demographics' AND `patient_field_values_fields`.`key` = 'notes' THEN `patient_field_values`.`value` END) AS 'Demographic Notes',
|
|
71
|
+
MAX(CASE WHEN `patient_field_values_fields`.`section` = 'demographics' AND `patient_field_values_fields`.`key` = 'drivers_license' THEN `patient_field_values`.`value` END) AS 'Drivers License #',
|
|
72
|
+
MAX(CASE WHEN `patient_field_values_fields`.`section` = 'contact' AND `patient_field_values_fields`.`key` = 'notes' THEN `patient_field_values`.`value` END) AS 'Contact Notes'
|
|
73
|
+
FROM `patients` `patients`
|
|
74
|
+
LEFT OUTER JOIN `patient_field_values` `patient_field_values` ON `patient_field_values`.`patient_id` = `patients`.`id`
|
|
75
|
+
LEFT OUTER JOIN `fields` `patient_field_values_fields` ON `patient_field_values_fields`.`id` = `patient_field_values`.`field_id`
|
|
76
|
+
GROUP BY `patients`.`id`, `patients`.`first`
|
|
77
|
+
mysql_not_readable: |+
|
|
78
|
+
SELECT
|
|
79
|
+
`t0`.`id` AS 'c0',
|
|
80
|
+
`t0`.`first` AS 'c1',
|
|
81
|
+
MAX(CASE WHEN `t2`.`section` = 'demographics' AND `t2`.`key` = 'dob' THEN `t1`.`value` END) AS 'c2',
|
|
82
|
+
MAX(CASE WHEN `t2`.`section` = 'demographics' AND `t2`.`key` = 'notes' THEN `t1`.`value` END) AS 'c3',
|
|
83
|
+
MAX(CASE WHEN `t2`.`section` = 'demographics' AND `t2`.`key` = 'drivers_license' THEN `t1`.`value` END) AS 'c4',
|
|
84
|
+
MAX(CASE WHEN `t2`.`section` = 'contact' AND `t2`.`key` = 'notes' THEN `t1`.`value` END) AS 'c5'
|
|
85
|
+
FROM `patients` `t0`
|
|
86
|
+
LEFT OUTER JOIN `patient_field_values` `t1` ON `t1`.`patient_id` = `t0`.`id`
|
|
87
|
+
LEFT OUTER JOIN `fields` `t2` ON `t2`.`id` = `t1`.`field_id`
|
|
88
|
+
GROUP BY `t0`.`id`, `t0`.`first`
|
data/spec/fixtures/models.yaml
CHANGED
|
@@ -103,3 +103,23 @@ Partitioner Example 2:
|
|
|
103
103
|
value: Dog
|
|
104
104
|
- name: deleted
|
|
105
105
|
value: false
|
|
106
|
+
|
|
107
|
+
Patients:
|
|
108
|
+
name: patients
|
|
109
|
+
models:
|
|
110
|
+
- name: patient_payments
|
|
111
|
+
constraints:
|
|
112
|
+
- type: reference
|
|
113
|
+
parent: id
|
|
114
|
+
name: patient_id
|
|
115
|
+
- name: patient_field_values
|
|
116
|
+
constraints:
|
|
117
|
+
- type: reference
|
|
118
|
+
parent: id
|
|
119
|
+
name: patient_id
|
|
120
|
+
models:
|
|
121
|
+
- name: fields
|
|
122
|
+
constraints:
|
|
123
|
+
- type: reference
|
|
124
|
+
parent: field_id
|
|
125
|
+
name: id
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dbee-active_record
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.1.0.pre.alpha
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matthew Ruggio
|
|
8
8
|
autorequire:
|
|
9
|
-
bindir:
|
|
9
|
+
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2020-
|
|
11
|
+
date: 2020-07-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -34,22 +34,16 @@ dependencies:
|
|
|
34
34
|
name: dbee
|
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
|
-
- -
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '2'
|
|
40
|
-
- - ">="
|
|
37
|
+
- - '='
|
|
41
38
|
- !ruby/object:Gem::Version
|
|
42
|
-
version: 2.0.
|
|
39
|
+
version: 2.1.0.pre.alpha
|
|
43
40
|
type: :runtime
|
|
44
41
|
prerelease: false
|
|
45
42
|
version_requirements: !ruby/object:Gem::Requirement
|
|
46
43
|
requirements:
|
|
47
|
-
- -
|
|
44
|
+
- - '='
|
|
48
45
|
- !ruby/object:Gem::Version
|
|
49
|
-
version:
|
|
50
|
-
- - ">="
|
|
51
|
-
- !ruby/object:Gem::Version
|
|
52
|
-
version: 2.0.3
|
|
46
|
+
version: 2.1.0.pre.alpha
|
|
53
47
|
- !ruby/object:Gem::Dependency
|
|
54
48
|
name: guard-rspec
|
|
55
49
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -126,14 +120,14 @@ dependencies:
|
|
|
126
120
|
requirements:
|
|
127
121
|
- - "~>"
|
|
128
122
|
- !ruby/object:Gem::Version
|
|
129
|
-
version: 0.
|
|
123
|
+
version: 0.81.0
|
|
130
124
|
type: :development
|
|
131
125
|
prerelease: false
|
|
132
126
|
version_requirements: !ruby/object:Gem::Requirement
|
|
133
127
|
requirements:
|
|
134
128
|
- - "~>"
|
|
135
129
|
- !ruby/object:Gem::Version
|
|
136
|
-
version: 0.
|
|
130
|
+
version: 0.81.0
|
|
137
131
|
- !ruby/object:Gem::Dependency
|
|
138
132
|
name: simplecov
|
|
139
133
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -154,14 +148,14 @@ dependencies:
|
|
|
154
148
|
requirements:
|
|
155
149
|
- - "~>"
|
|
156
150
|
- !ruby/object:Gem::Version
|
|
157
|
-
version: 0.
|
|
151
|
+
version: 0.7.0
|
|
158
152
|
type: :development
|
|
159
153
|
prerelease: false
|
|
160
154
|
version_requirements: !ruby/object:Gem::Requirement
|
|
161
155
|
requirements:
|
|
162
156
|
- - "~>"
|
|
163
157
|
- !ruby/object:Gem::Version
|
|
164
|
-
version: 0.
|
|
158
|
+
version: 0.7.0
|
|
165
159
|
- !ruby/object:Gem::Dependency
|
|
166
160
|
name: sqlite3
|
|
167
161
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -180,8 +174,7 @@ description: " By default Dbee ships with no underlying SQL generator. This
|
|
|
180
174
|
will plug in ActiveRecord into Dbee and Dbee will use it for SQL generation.\n"
|
|
181
175
|
email:
|
|
182
176
|
- mruggio@bluemarblepayroll.com
|
|
183
|
-
executables:
|
|
184
|
-
- console
|
|
177
|
+
executables: []
|
|
185
178
|
extensions: []
|
|
186
179
|
extra_rdoc_files: []
|
|
187
180
|
files:
|
|
@@ -199,12 +192,13 @@ files:
|
|
|
199
192
|
- Rakefile
|
|
200
193
|
- bin/console
|
|
201
194
|
- dbee-active_record.gemspec
|
|
195
|
+
- exe/.gitkeep
|
|
202
196
|
- lib/dbee/providers/active_record_provider.rb
|
|
203
197
|
- lib/dbee/providers/active_record_provider/expression_builder.rb
|
|
204
|
-
- lib/dbee/providers/active_record_provider/expression_builder/
|
|
205
|
-
- lib/dbee/providers/active_record_provider/expression_builder/
|
|
206
|
-
- lib/dbee/providers/active_record_provider/expression_builder/
|
|
207
|
-
- lib/dbee/providers/active_record_provider/expression_builder/
|
|
198
|
+
- lib/dbee/providers/active_record_provider/expression_builder/constraint.rb
|
|
199
|
+
- lib/dbee/providers/active_record_provider/expression_builder/order.rb
|
|
200
|
+
- lib/dbee/providers/active_record_provider/expression_builder/select.rb
|
|
201
|
+
- lib/dbee/providers/active_record_provider/expression_builder/where.rb
|
|
208
202
|
- lib/dbee/providers/active_record_provider/obfuscated_alias_maker.rb
|
|
209
203
|
- lib/dbee/providers/active_record_provider/safe_alias_maker.rb
|
|
210
204
|
- lib/dbee/providers/active_record_provider/version.rb
|
|
@@ -223,12 +217,19 @@ files:
|
|
|
223
217
|
- spec/fixtures/active_record_snapshots/partitioner_example_2_query.yaml
|
|
224
218
|
- spec/fixtures/active_record_snapshots/reverse_polymorphic_query.yaml
|
|
225
219
|
- spec/fixtures/active_record_snapshots/two_table_query.yaml
|
|
220
|
+
- spec/fixtures/active_record_snapshots/two_table_query_with_aggregation.yaml
|
|
221
|
+
- spec/fixtures/active_record_snapshots/two_table_query_with_pivoting.yaml
|
|
226
222
|
- spec/fixtures/models.yaml
|
|
227
223
|
- spec/spec_helper.rb
|
|
228
224
|
homepage: https://github.com/bluemarblepayroll/dbee-active_record
|
|
229
225
|
licenses:
|
|
230
226
|
- MIT
|
|
231
|
-
metadata:
|
|
227
|
+
metadata:
|
|
228
|
+
bug_tracker_uri: https://github.com/bluemarblepayroll/dbee-active_record/issues
|
|
229
|
+
changelog_uri: https://github.com/bluemarblepayroll/dbee-active_record/blob/master/CHANGELOG.md
|
|
230
|
+
documentation_uri: https://www.rubydoc.info/gems/dbee-active_record
|
|
231
|
+
homepage_uri: https://github.com/bluemarblepayroll/dbee-active_record
|
|
232
|
+
source_code_uri: https://github.com/bluemarblepayroll/dbee-active_record
|
|
232
233
|
post_install_message:
|
|
233
234
|
rdoc_options: []
|
|
234
235
|
require_paths:
|
|
@@ -237,12 +238,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
237
238
|
requirements:
|
|
238
239
|
- - ">="
|
|
239
240
|
- !ruby/object:Gem::Version
|
|
240
|
-
version: 2.
|
|
241
|
+
version: '2.5'
|
|
241
242
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
242
243
|
requirements:
|
|
243
|
-
- - "
|
|
244
|
+
- - ">"
|
|
244
245
|
- !ruby/object:Gem::Version
|
|
245
|
-
version:
|
|
246
|
+
version: 1.3.1
|
|
246
247
|
requirements: []
|
|
247
248
|
rubygems_version: 3.0.3
|
|
248
249
|
signing_key:
|
|
@@ -264,5 +265,7 @@ test_files:
|
|
|
264
265
|
- spec/fixtures/active_record_snapshots/partitioner_example_2_query.yaml
|
|
265
266
|
- spec/fixtures/active_record_snapshots/reverse_polymorphic_query.yaml
|
|
266
267
|
- spec/fixtures/active_record_snapshots/two_table_query.yaml
|
|
268
|
+
- spec/fixtures/active_record_snapshots/two_table_query_with_aggregation.yaml
|
|
269
|
+
- spec/fixtures/active_record_snapshots/two_table_query_with_pivoting.yaml
|
|
267
270
|
- spec/fixtures/models.yaml
|
|
268
271
|
- spec/spec_helper.rb
|
|
@@ -1,33 +0,0 @@
|
|
|
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#project predicates.
|
|
15
|
-
class SelectMaker
|
|
16
|
-
include Singleton
|
|
17
|
-
|
|
18
|
-
def make(column, arel_column, alias_maker)
|
|
19
|
-
column_alias = quote(alias_maker.make(column.display))
|
|
20
|
-
|
|
21
|
-
arel_column.as(column_alias)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def quote(value)
|
|
27
|
-
ActiveRecord::Base.connection.quote(value)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|