dbee 1.1.0 → 1.2.0

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