dbee 1.1.0 → 1.2.0

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: 01e542a4c7d9eb3f9641aa7b54f776f32b88e178129ad0e4af219b3965315090
4
- data.tar.gz: a94879eb871f87ed9b9c3f7bccbe3ddfa5a177f558a842572f7632d09feab1fc
3
+ metadata.gz: ae136319b1de6f828e34e6e0546b7c6663848efecbf8bfb42f8fe6204bebb20c
4
+ data.tar.gz: 1690b0f58befeade514c2949163d37ada74f673235ab64a49ec18f7ffde58e1d
5
5
  SHA512:
6
- metadata.gz: af62cb3558f7557e939be6b5b4606c299436f782b40441bad3dac27c18560c9c9e55b1884c3645dcfada969f8d704c0aeb313e9731d0dd1cfcab8d7e634efa9e
7
- data.tar.gz: 9f87776b0c84ff7c31c0df4d08d7f0725064adcdec21c8653995d1370426bd7d5a3b13c33946e8c1b72816cf0d9906ea0bc970ba61656876d84b3e0081fb93bc
6
+ metadata.gz: 82c42cf23dbaed18644da3b2e02bc406833a65ca6429c8d31d824de44cf08fe2b503c4af32c6199646fbe797abe7af9dc434d75acff91b1fb7b502d2f7eb6d35
7
+ data.tar.gz: '07482747b169ccfde19405381d5128bd07ee269374e75c0f577ee34c26b3a0d877654c70da3a0b1bdd8ce31a1e591a4d0d8c11050390b3a5be18e06f7b312b11'
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ # 1.2.0 (August 29th, 2019)
2
+
3
+ Additions:
4
+
5
+ * Added partitioners to a model specification. A Partitioner is essentially a way of explicitly specifying that a data model must meet a specific equality for a column.
6
+
7
+ Changes:
8
+
9
+ * Model#ancestors renamed to Model#ancestors!
10
+ * Model#ancestor now returns a hash where the key is an array of strings and not just a pre-concatenated string.
11
+
1
12
  # 1.1.0 (August 28th, 2019)
2
13
 
3
14
  Additions:
data/README.md CHANGED
@@ -175,6 +175,44 @@ models:
175
175
 
176
176
  It is up to you to determine which modeling technique to use as both are equivalent. Technically speaking, the code-first DSL is nothing more than syntactic sugar on top of Dbee::Model.
177
177
 
178
+ #### Table Partitioning
179
+
180
+ You can leverage the model partitioners for hard-coding partitioning by column=value. The initial use-case for this was to mirror how ActiveRecord deals with (Single Table Inheritance)[https://api.rubyonrails.org/v6.0.0/classes/ActiveRecord/Base.html#class-ActiveRecord::Base-label-Single+table+inheritance]. Here is a basic example of how to partition an `animals` table for different subclasses:
181
+
182
+ ##### Code-first:
183
+
184
+ ````ruby
185
+ class Dogs < Dbee::Base
186
+ table 'animals'
187
+
188
+ partitioner :type, 'Dog'
189
+ end
190
+
191
+ class Cats < Dbee::Base
192
+ table 'animals'
193
+
194
+ partitioner :type, 'Cat'
195
+ end
196
+ ````
197
+
198
+ ##### Configuration-first:
199
+
200
+ ````yaml
201
+ Dogs:
202
+ name: dogs
203
+ table: animals
204
+ partitioners:
205
+ - name: type
206
+ value: Dog
207
+
208
+ Cats:
209
+ name: cats
210
+ table: animals
211
+ partitioners:
212
+ - name: type
213
+ value: Cat
214
+ ````
215
+
178
216
  ### The Query API
179
217
 
180
218
  The Query API (Dbee::Query) is a simplified and abstract way to model an SQL query. A Query has the following components:
data/lib/dbee/base.rb CHANGED
@@ -12,6 +12,10 @@ module Dbee
12
12
  # Model declaration.
13
13
  class Base
14
14
  class << self
15
+ def partitioner(name, value)
16
+ partitioners << { name: name, value: value }
17
+ end
18
+
15
19
  def table(name)
16
20
  @table_name = name.to_s
17
21
 
@@ -51,6 +55,10 @@ module Dbee
51
55
  @associations_by_name ||= {}
52
56
  end
53
57
 
58
+ def partitioners
59
+ @partitioners ||= []
60
+ end
61
+
54
62
  def table_name?
55
63
  !table_name.empty?
56
64
  end
@@ -65,6 +73,12 @@ module Dbee
65
73
  end
66
74
  end
67
75
 
76
+ def inherited_partitioners
77
+ reversed_subclasses.inject([]) do |memo, subclass|
78
+ memo + subclass.partitioners
79
+ end
80
+ end
81
+
68
82
  private
69
83
 
70
84
  def subclasses
@@ -78,6 +92,7 @@ module Dbee
78
92
  def model_config(key_chain, name, constraints, path_parts)
79
93
  {
80
94
  constraints: constraints,
95
+ partitioners: inherited_partitioners,
81
96
  models: associations(key_chain, path_parts),
82
97
  name: name,
83
98
  table: derive_table
@@ -0,0 +1,44 @@
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
+ class Model
12
+ # An Partitioner is a way to explicitly define constraints on a model. For example, say we
13
+ # want to create a data model, but restrict the returned data to a subset based on a 'type'
14
+ # column like ActiveRecord does for Single Table Inheritance. We could use a partition
15
+ # to define this constraint.
16
+ class Partitioner
17
+ acts_as_hashable
18
+
19
+ attr_reader :name, :value
20
+
21
+ def initialize(name: '', value: nil)
22
+ raise ArgumentError, 'name is required' if name.to_s.empty?
23
+
24
+ @name = name.to_s
25
+ @value = value
26
+ end
27
+
28
+ def <=>(other)
29
+ "#{name}#{value}" <=> "#{other.name}#{other.value}"
30
+ end
31
+
32
+ def hash
33
+ "#{name}#{value}".hash
34
+ end
35
+
36
+ def ==(other)
37
+ other.instance_of?(self.class) &&
38
+ other.name == name &&
39
+ other.value == value
40
+ end
41
+ alias eql? ==
42
+ end
43
+ end
44
+ end
data/lib/dbee/model.rb CHANGED
@@ -8,6 +8,7 @@
8
8
  #
9
9
 
10
10
  require_relative 'model/constraints'
11
+ require_relative 'model/partitioner'
11
12
 
12
13
  module Dbee
13
14
  # In DB terms, a Model is usually a table, but it does not have to be. You can also re-model
@@ -16,53 +17,48 @@ module Dbee
16
17
  extend Forwardable
17
18
  acts_as_hashable
18
19
 
19
- JOIN_CHAR = '.'
20
-
21
- private_constant :JOIN_CHAR
22
-
23
20
  class ModelNotFoundError < StandardError; end
24
21
 
25
- attr_reader :constraints, :name, :table
26
-
27
- def_delegator :models_by_name, :values, :models
22
+ attr_reader :constraints, :filters, :name, :partitioners, :table
28
23
 
29
- def_delegator :models, :sort, :sorted_models
24
+ def_delegator :models_by_name, :values, :models
25
+ def_delegator :models, :sort, :sorted_models
26
+ def_delegator :constraints, :sort, :sorted_constraints
27
+ def_delegator :partitioners, :sort, :sorted_partitioners
30
28
 
31
- def_delegator :constraints, :sort, :sorted_constraints
32
-
33
- def initialize(name:, constraints: [], models: [], table: '')
29
+ def initialize(name:, constraints: [], models: [], partitioners: [], table: '')
34
30
  raise ArgumentError, 'name is required' if name.to_s.empty?
35
31
 
36
32
  @name = name.to_s
37
- @constraints = Constraints.array(constraints)
33
+ @constraints = Constraints.array(constraints).uniq
38
34
  @models_by_name = name_hash(Model.array(models))
35
+ @partitioners = Partitioner.array(partitioners).uniq
39
36
  @table = table.to_s.empty? ? @name : table.to_s
40
37
 
41
38
  freeze
42
39
  end
43
40
 
44
- def name_hash(array)
45
- array.map { |a| [a.name, a] }.to_h
46
- end
47
-
48
- def ancestors(parts = [], alias_chain = [], found = {})
41
+ # This recursive method will walk a path of model names (parts) and return back a
42
+ # flattened hash instead of a nested object structure.
43
+ # The hash key will be an array of strings (model names) and the value will be the
44
+ # identified model.
45
+ def ancestors!(parts = [], visited_parts = [], found = {})
49
46
  return found if Array(parts).empty?
50
47
 
51
- alias_chain = [] if Array(alias_chain).empty?
52
-
53
- model_name = parts.first
48
+ # Take the first entry in parts
49
+ model_name = parts.first.to_s
54
50
 
55
- model = models_by_name[model_name.to_s]
51
+ # Ensure we have it registered as a child, or raise error
52
+ model = assert_model(model_name, visited_parts)
56
53
 
57
- raise ModelNotFoundError, "Cannot traverse: #{model_name}" unless model
54
+ # Push onto visited list
55
+ visited_parts += [model_name]
58
56
 
59
- new_alias_chain = alias_chain + [model_name]
57
+ # Add found model to flattened structure
58
+ found[visited_parts] = model
60
59
 
61
- new_alias = new_alias_chain.join(JOIN_CHAR)
62
-
63
- found[new_alias] = model
64
-
65
- model.ancestors(parts[1..-1], new_alias_chain, found)
60
+ # Recursively call for next parts in the chain
61
+ model.ancestors!(parts[1..-1], visited_parts, found)
66
62
  end
67
63
 
68
64
  def ==(other)
@@ -70,6 +66,7 @@ module Dbee
70
66
  other.name == name &&
71
67
  other.table == table &&
72
68
  other.sorted_constraints == sorted_constraints &&
69
+ other.sorted_partitioners == sorted_partitioners &&
73
70
  other.sorted_models == sorted_models
74
71
  end
75
72
  alias eql? ==
@@ -81,5 +78,14 @@ module Dbee
81
78
  private
82
79
 
83
80
  attr_reader :models_by_name
81
+
82
+ def assert_model(model_name, visited_parts)
83
+ models_by_name[model_name] ||
84
+ raise(ModelNotFoundError, "Missing: #{model_name}, after: #{visited_parts}")
85
+ end
86
+
87
+ def name_hash(array)
88
+ array.map { |a| [a.name, a] }.to_h
89
+ end
84
90
  end
85
91
  end
@@ -21,7 +21,7 @@ require_relative 'filters/starts_with'
21
21
  module Dbee
22
22
  class Query
23
23
  # Top-level class that allows for the making of filters. For example, you can call this as:
24
- # - Filters.make(type: :contains, value: 'something')
24
+ # - Filters.make(key_path: :field, type: :contains, value: 'something')
25
25
  class Filters
26
26
  acts_as_hashable_factory
27
27
 
data/lib/dbee/version.rb CHANGED
@@ -8,5 +8,5 @@
8
8
  #
9
9
 
10
10
  module Dbee
11
- VERSION = '1.1.0'
11
+ VERSION = '1.2.0'
12
12
  end
@@ -72,4 +72,32 @@ describe Dbee::Base do
72
72
  expect(Models::E.inherited_table_name).to eq('table_set_to_e')
73
73
  end
74
74
  end
75
+
76
+ describe 'partitioners' do
77
+ it 'honors partitioners on a root model' do
78
+ model_name = 'Partitioner Example 1'
79
+ expected_config = yaml_fixture('models.yaml')[model_name]
80
+ expected_model = Dbee::Model.make(expected_config)
81
+
82
+ key_paths = %w[id]
83
+ key_chain = Dbee::KeyChain.new(key_paths)
84
+
85
+ actual_model = PartitionerExamples::Dogs.to_model(key_chain)
86
+
87
+ expect(actual_model).to eq(expected_model)
88
+ end
89
+
90
+ it 'honors partitioners on a child model' do
91
+ model_name = 'Partitioner Example 2'
92
+ expected_config = yaml_fixture('models.yaml')[model_name]
93
+ expected_model = Dbee::Model.make(expected_config)
94
+
95
+ key_paths = %w[id dogs.id]
96
+ key_chain = Dbee::KeyChain.new(key_paths)
97
+
98
+ actual_model = PartitionerExamples::Owners.to_model(key_chain)
99
+
100
+ expect(actual_model).to eq(expected_model)
101
+ end
102
+ end
75
103
  end
@@ -66,10 +66,10 @@ describe Dbee::Model do
66
66
  members = subject.models.first
67
67
 
68
68
  expected_plan = {
69
- 'members' => members
69
+ %w[members] => members
70
70
  }
71
71
 
72
- plan = subject.ancestors(%w[members])
72
+ plan = subject.ancestors!(%w[members])
73
73
 
74
74
  expect(plan).to eq(expected_plan)
75
75
  end
@@ -80,12 +80,12 @@ describe Dbee::Model do
80
80
  phone_numbers = demos.models.first
81
81
 
82
82
  expected_plan = {
83
- 'members' => members,
84
- 'members.demos' => demos,
85
- 'members.demos.phone_numbers' => phone_numbers
83
+ %w[members] => members,
84
+ %w[members demos] => demos,
85
+ %w[members demos phone_numbers] => phone_numbers
86
86
  }
87
87
 
88
- plan = subject.ancestors(%w[members demos phone_numbers])
88
+ plan = subject.ancestors!(%w[members demos phone_numbers])
89
89
 
90
90
  expect(plan).to eq(expected_plan)
91
91
  end
@@ -139,3 +139,20 @@ module Cycles
139
139
  association :a, model: 'Cycles::A'
140
140
  end
141
141
  end
142
+
143
+ # Examples of Rails Single Table Inheritance.
144
+ module PartitionerExamples
145
+ class Owners < Dbee::Base
146
+ association :dogs, model: 'PartitionerExamples::Dogs', constraints: {
147
+ name: :owner_id, parent: :id
148
+ }
149
+ end
150
+
151
+ class Dogs < Dbee::Base
152
+ table 'animals'
153
+
154
+ partitioner :type, 'Dog'
155
+
156
+ partitioner :deleted, false
157
+ end
158
+ end
@@ -186,3 +186,26 @@ Cycle Example:
186
186
  table: b
187
187
  models:
188
188
  - name: c
189
+
190
+ Partitioner Example 1:
191
+ name: dogs
192
+ table: animals
193
+ partitioners:
194
+ - name: type
195
+ value: Dog
196
+ - name: deleted
197
+ value: false
198
+
199
+ Partitioner Example 2:
200
+ name: owners
201
+ models:
202
+ - name: dogs
203
+ table: animals
204
+ constraints:
205
+ - name: owner_id
206
+ parent: id
207
+ partitioners:
208
+ - name: type
209
+ value: Dog
210
+ - name: deleted
211
+ value: false
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbee
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Ruggio
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-28 00:00:00.000000000 Z
11
+ date: 2019-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: acts_as_hashable
@@ -162,6 +162,7 @@ files:
162
162
  - lib/dbee/model/constraints/base.rb
163
163
  - lib/dbee/model/constraints/reference.rb
164
164
  - lib/dbee/model/constraints/static.rb
165
+ - lib/dbee/model/partitioner.rb
165
166
  - lib/dbee/providers.rb
166
167
  - lib/dbee/providers/null_provider.rb
167
168
  - lib/dbee/query.rb