dbee-active_record 2.0.4 → 2.1.0.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f50827b6f31ffb9749796b4532b8a48f704c522be4e0011b7e9a58ed716dd26
4
- data.tar.gz: e9469a99514bfdfbe03da6cdcda339aef1b12b2d829a82d7e2278a22b006f0b3
3
+ metadata.gz: f8de8c388637ba455ffca406ff42751ecfb62f4a40bbcb9f560a085b585568a1
4
+ data.tar.gz: 9723886e37e886d7df7fa4661df4c81d35fe186d08a7d82de66083e08ed44bac
5
5
  SHA512:
6
- metadata.gz: 7162ee879530dd36b2078a347b6fade9a5e5ce8e79f47709b33b11cb308303195cbafb3e6096a1529ddb7b7c23da6a19caccdc61356f29dd14b48b4e077cc041
7
- data.tar.gz: 6d19989ed13fd0e2d7a88b3341309cf517aede0f4924c8f76f1325685a099b4b20985774adb445b0bb3487c9d91568500a86c767237618f57194e2806ee0fa93
6
+ metadata.gz: be75ae14b3c4b0154a0cc4f63950fa86a5f9f16272cd2d5133a4fc4649e2bcd6f7dc5836cf1706c7a9a6b9dba1f1183783038b013bc9e78f5cc5f25e9c980f78
7
+ data.tar.gz: 7f466202c164f0da7fb936010bc73393a0643682588f14093979fa1e8844f36bd0978020f3f435624d6bea25d95767af35884c6e453a35f41d219dd4331b22d7
@@ -1,6 +1,20 @@
1
- Metrics/LineLength:
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
- AllCops:
22
- TargetRubyVersion: 2.3
38
+ Style/HashEachMethods:
39
+ Enabled: True
23
40
 
24
- Metrics/AbcSize:
25
- Max: 16
26
- Exclude:
27
- - spec/db_helper.rb
41
+ Style/HashTransformKeys:
42
+ Enabled: True
28
43
 
29
- Metrics/ClassLength:
30
- Max: 125
44
+ Style/HashTransformValues:
45
+ Enabled: True
@@ -1 +1 @@
1
- 2.6.5
1
+ 2.6.6
@@ -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.3.8
10
- - 2.4.6
11
- - 2.5.5
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
@@ -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
@@ -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.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
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.3.8'
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', '~>2', '>=2.0.3')
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.79.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.6.0')
54
+ s.add_development_dependency('simplecov-console', '~>0.7.0')
47
55
  s.add_development_dependency('sqlite3', '~>1')
48
56
  end
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/constraint_maker'
11
- require_relative 'expression_builder/order_maker'
12
- require_relative 'expression_builder/select_maker'
13
- require_relative 'expression_builder/where_maker'
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 = WhereMaker.instance.make(filter, arel_column)
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 = OrderMaker.instance.make(sorter, arel_column)
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
- key_path = field.key_path
96
- arel_column = key_paths_to_arel_columns[key_path]
97
- predicate = SelectMaker.instance.make(field, arel_column, column_alias_maker)
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 = ConstraintMaker.instance.make(model.constraints, table, previous_table)
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
- self
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 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
@@ -12,16 +12,9 @@ module Dbee
12
12
  class ActiveRecordProvider
13
13
  class ExpressionBuilder
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,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
@@ -12,24 +12,9 @@ module Dbee
12
12
  class ActiveRecordProvider
13
13
  class ExpressionBuilder
14
14
  # Derives Arel#where predicates.
15
- class WhereMaker
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
@@ -10,7 +10,7 @@
10
10
  module Dbee
11
11
  module Providers
12
12
  class ActiveRecordProvider
13
- VERSION = '2.0.4'
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
@@ -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`
@@ -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
4
+ version: 2.1.0.pre.alpha
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Ruggio
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2020-02-13 00:00:00.000000000 Z
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.3
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: '2'
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.79.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.79.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.6.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.6.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/constraint_maker.rb
205
- - lib/dbee/providers/active_record_provider/expression_builder/order_maker.rb
206
- - lib/dbee/providers/active_record_provider/expression_builder/select_maker.rb
207
- - lib/dbee/providers/active_record_provider/expression_builder/where_maker.rb
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.3.8
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: '0'
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