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