dbee 2.0.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/dbee.gemspec CHANGED
@@ -15,20 +15,31 @@ 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'
20
21
  s.license = 'MIT'
22
+ s.metadata = {
23
+ 'bug_tracker_uri' => 'https://github.com/bluemarblepayroll/dbee/issues',
24
+ 'changelog_uri' => 'https://github.com/bluemarblepayroll/dbee/blob/master/CHANGELOG.md',
25
+ 'documentation_uri' => 'https://www.rubydoc.info/gems/dbee',
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
- s.add_dependency('acts_as_hashable', '~>1', '>=1.1.0')
32
+ s.add_dependency('acts_as_hashable', '~>1', '>=1.2.0')
25
33
  s.add_dependency('dry-inflector', '~>0')
26
34
 
27
35
  s.add_development_dependency('guard-rspec', '~>4.7')
28
36
  s.add_development_dependency('pry', '~>0')
37
+ s.add_development_dependency('pry-byebug')
29
38
  s.add_development_dependency('rake', '~> 13')
30
39
  s.add_development_dependency('rspec')
31
- s.add_development_dependency('rubocop', '~>0.76.0')
32
- s.add_development_dependency('simplecov', '~>0.17.0')
33
- s.add_development_dependency('simplecov-console', '~>0.5.0')
40
+ s.add_development_dependency('rubocop', '~> 1')
41
+ s.add_development_dependency('rubocop-rake')
42
+ s.add_development_dependency('rubocop-rspec')
43
+ s.add_development_dependency('simplecov', '~>0.19.0')
44
+ s.add_development_dependency('simplecov-console', '~>0.7.0')
34
45
  end
data/exe/.gitkeep ADDED
File without changes
data/lib/dbee.rb CHANGED
@@ -12,11 +12,16 @@ require 'dry/inflector'
12
12
  require 'forwardable'
13
13
 
14
14
  require_relative 'dbee/base'
15
+ require_relative 'dbee/constant_resolver'
16
+ require_relative 'dbee/dsl_schema_builder'
15
17
  require_relative 'dbee/key_chain'
16
18
  require_relative 'dbee/key_path'
17
19
  require_relative 'dbee/model'
18
- require_relative 'dbee/query'
19
20
  require_relative 'dbee/providers'
21
+ require_relative 'dbee/query'
22
+ require_relative 'dbee/schema'
23
+ require_relative 'dbee/schema_creator'
24
+ require_relative 'dbee/schema_from_tree_based_model'
20
25
 
21
26
  # Top-level namespace that provides the main public API.
22
27
  module Dbee
@@ -30,16 +35,11 @@ module Dbee
30
35
  @inflector ||= Dry::Inflector.new
31
36
  end
32
37
 
33
- def sql(model, query, provider)
34
- query = Query.make(query)
35
- model =
36
- if model.is_a?(Hash) || model.is_a?(Model)
37
- Model.make(model)
38
- else
39
- model.to_model(query.key_chain)
40
- end
38
+ def sql(schema_or_model, query_input, provider)
39
+ raise ArgumentError, 'a provider is required' unless provider
41
40
 
42
- provider.sql(model, query)
41
+ schema_compat = SchemaCreator.new(schema_or_model, query_input)
42
+ provider.sql(schema_compat.schema, schema_compat.query)
43
43
  end
44
44
  end
45
45
  end
data/lib/dbee/base.rb CHANGED
@@ -7,8 +7,8 @@
7
7
  # LICENSE file in the root directory of this source tree.
8
8
  #
9
9
 
10
- require_relative 'dsl/association'
11
10
  require_relative 'dsl/association_builder'
11
+ require_relative 'dsl/association'
12
12
  require_relative 'dsl/methods'
13
13
  require_relative 'dsl/reflectable'
14
14
 
@@ -22,23 +22,9 @@ module Dbee
22
22
  BASE_CLASS_CONSTANT = Dbee::Base
23
23
 
24
24
  class << self
25
- # This method is cycle-resistant due to the fact that it is a requirement to send in a
26
- # key_chain. That means each model produced using to_model is specific to a set of desired
27
- # fields. Basically, you cannot derive a Model from a Base subclass without the context
28
- # of a Query. This is not true for configuration-first Model definitions because, in that
29
- # case, cycles do not exist since the nature of the configuration is flat.
30
- def to_model(key_chain, name = nil, constraints = [], path_parts = [])
31
- derived_name = name.to_s.empty? ? inflected_class_name(self.name) : name.to_s
32
- key = [key_chain, derived_name, constraints, path_parts]
33
-
34
- to_models[key] ||= Model.make(
35
- model_config(
36
- key_chain,
37
- derived_name,
38
- constraints,
39
- path_parts + [name]
40
- )
41
- )
25
+ # Returns the smallest needed Dbee::Schema for the provided key_chain.
26
+ def to_schema(key_chain)
27
+ DslSchemaBuilder.new(self, key_chain).to_schema
42
28
  end
43
29
 
44
30
  def inherited_table_name
@@ -58,43 +44,15 @@ module Dbee
58
44
  end
59
45
  end
60
46
 
61
- private
62
-
63
- def model_config(key_chain, name, constraints, path_parts)
64
- {
65
- constraints: constraints,
66
- models: associations(key_chain, path_parts),
67
- name: name,
68
- partitioners: inherited_partitioners,
69
- table: inherited_table_name
70
- }
71
- end
72
-
73
- def associations(key_chain, path_parts)
74
- inherited_associations.select { |c| key_chain.ancestor_path?(path_parts, c.name) }
75
- .map do |association|
76
- model_constant = association.model_constant
77
-
78
- model_constant.to_model(
79
- key_chain,
80
- association.name,
81
- association.constraints,
82
- path_parts
83
- )
84
- end
47
+ def inflected_class_name
48
+ inflector.underscore(inflector.demodulize(name))
85
49
  end
86
50
 
87
- def to_models
88
- @to_models ||= {}
89
- end
51
+ private
90
52
 
91
53
  def inflected_table_name(name)
92
54
  inflector.pluralize(inflector.underscore(inflector.demodulize(name)))
93
55
  end
94
-
95
- def inflected_class_name(name)
96
- inflector.underscore(inflector.demodulize(name))
97
- end
98
56
  end
99
57
  end
100
58
  end
@@ -0,0 +1,34 @@
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
+ # This class is responsible for turning strings and symbols into constants.
12
+ # It does not deal with inflection, simply just constant resolution.
13
+ class ConstantResolver
14
+ # Only use Module constant resolution if a string or symbol was passed in.
15
+ # Any other type is defined as an acceptable constant and is simply returned.
16
+ def constantize(value)
17
+ value.is_a?(String) || value.is_a?(Symbol) ? object_constant(value) : value
18
+ end
19
+
20
+ private
21
+
22
+ # If the constant has been loaded, we can safely use it through const_get.
23
+ # If the constant has not been loaded, we need to defer to const_missing to resolve it.
24
+ # If we blindly call const_get, it may return false positives for namespaced constants
25
+ # or anything nested.
26
+ def object_constant(value)
27
+ if Object.const_defined?(value, false)
28
+ Object.const_get(value, false)
29
+ else
30
+ Object.const_missing(value)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -19,16 +19,17 @@ module Dbee
19
19
  raise ArgumentError, 'inflector is required' unless inflector
20
20
  raise ArgumentError, 'name is required' if name.to_s.empty?
21
21
 
22
- @on_class_name = on_class_name
23
- @inflector = inflector
24
- @name = name.to_s
25
- @opts = opts || {}
22
+ @on_class_name = on_class_name
23
+ @inflector = inflector
24
+ @name = name.to_s
25
+ @opts = opts || {}
26
+ @constant_resolver = ConstantResolver.new
26
27
 
27
28
  freeze
28
29
  end
29
30
 
30
31
  def model_constant
31
- constantize(class_name)
32
+ constant_resolver.constantize(class_name)
32
33
  end
33
34
 
34
35
  def constraints
@@ -37,6 +38,8 @@ module Dbee
37
38
 
38
39
  private
39
40
 
41
+ attr_reader :constant_resolver
42
+
40
43
  def class_name
41
44
  opts[:model] || relative_class_name
42
45
  end
@@ -47,20 +50,6 @@ module Dbee
47
50
  def relative_class_name
48
51
  (on_class_name.split('::')[0...-1] + [inflector.classify(name)]).join('::')
49
52
  end
50
-
51
- # Only use Module constant resolution if a string or symbol was passed in.
52
- # Any other type is defined as an acceptable constant and is simply returned.
53
- def constantize(value)
54
- value.is_a?(String) || value.is_a?(Symbol) ? object_constant(value) : value
55
- end
56
-
57
- # If the constant has been loaded, we can safely use it through const_get.
58
- # If the constant has not been loaded, we need to defer to const_missing to resolve it.
59
- # If we blindly call const_get, it may return false positives for namespaced constants
60
- # or anything nested.
61
- def object_constant(value)
62
- Object.const_defined?(value) ? Object.const_get(value) : Object.const_missing(value)
63
- end
64
53
  end
65
54
  end
66
55
  end
@@ -0,0 +1,86 @@
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
+ # Builds a Dbee::Schema given a Dbee::Base (DSL) model. Note that this class
12
+ # does not exist in the "Dsl" module as it is not used for defining the DSL
13
+ # itself.
14
+ class DslSchemaBuilder # :nodoc:
15
+ attr_reader :dsl_model, :key_chain
16
+
17
+ def initialize(dsl_model, key_chain)
18
+ @dsl_model = dsl_model || ArgumentError('dsl_model is required')
19
+ @key_chain = key_chain || ArgumentError('key_chain is required')
20
+
21
+ freeze
22
+ end
23
+
24
+ def to_schema
25
+ schema_spec = { dsl_model.inflected_class_name => model_config(dsl_model) }
26
+
27
+ ancestor_paths(key_chain).each do |key_path|
28
+ start_model = dsl_model
29
+
30
+ key_path.ancestor_names.each do |association_name|
31
+ start_model = append_model_and_relationship(schema_spec, start_model, association_name)
32
+ end
33
+ end
34
+
35
+ Schema.new(schema_spec)
36
+ end
37
+
38
+ private
39
+
40
+ def model_config(dsl_model) #:nodoc:
41
+ {
42
+ name: dsl_model.inflected_class_name,
43
+ partitioners: dsl_model.inherited_partitioners,
44
+ table: dsl_model.inherited_table_name,
45
+ relationships: {}
46
+ }
47
+ end
48
+
49
+ def append_model_and_relationship(schema_spec, base_model, association_name)
50
+ association = find_association!(base_model, association_name)
51
+ target_model = association.model_constant
52
+
53
+ schema_spec[target_model.inflected_class_name] ||= model_config(target_model)
54
+
55
+ schema_spec[base_model.inflected_class_name][:relationships][association.name] = {
56
+ constraints: association.constraints,
57
+ model: relationship_model_name(association, target_model)
58
+ }
59
+
60
+ target_model
61
+ end
62
+
63
+ # Returns a unique list of ancestor paths from a key_chain. Omits any
64
+ # fields on the base model.
65
+ def ancestor_paths(key_chain)
66
+ key_chain.to_unique_ancestors.key_path_set.select do |key_path|
67
+ key_path.ancestor_names.any?
68
+ end
69
+ end
70
+
71
+ def find_association!(base_model, association_name)
72
+ base_model.inherited_associations.find { |assoc| assoc.name == association_name } \
73
+ ||
74
+ raise(
75
+ ArgumentError,
76
+ "no association #{association_name} exists on model #{base_model.name}"
77
+ )
78
+ end
79
+
80
+ def relationship_model_name(association, target_model)
81
+ return nil if association.name == target_model.inflected_class_name
82
+
83
+ target_model.inflected_class_name
84
+ end
85
+ end
86
+ end
@@ -38,5 +38,16 @@ module Dbee
38
38
 
39
39
  ancestor_path_set.include?(path)
40
40
  end
41
+
42
+ # Returns a unique set of ancestors by considering all column names to be the same.
43
+ def to_unique_ancestors # :nodoc:
44
+ normalized_paths = key_path_set.map do |kp|
45
+ KeyPath.new((kp.ancestor_names + COLUMN_PLACEHOLDER).join(KeyPath::SPLIT_CHAR))
46
+ end
47
+ self.class.new(normalized_paths.uniq)
48
+ end
49
+
50
+ COLUMN_PLACEHOLDER = ['any_column'].freeze
51
+ private_constant :COLUMN_PLACEHOLDER
41
52
  end
42
53
  end
data/lib/dbee/model.rb CHANGED
@@ -9,65 +9,53 @@
9
9
 
10
10
  require_relative 'model/constraints'
11
11
  require_relative 'model/partitioner'
12
+ require_relative 'model/relationships'
13
+ require_relative 'util/make_keyed_by'
12
14
 
13
15
  module Dbee
14
16
  # In DB terms, a Model is usually a table, but it does not have to be. You can also re-model
15
17
  # your DB schema using Dbee::Models.
16
18
  class Model
17
- extend Forwardable
18
19
  acts_as_hashable
20
+ extend Dbee::Util::MakeKeyedBy
21
+ extend Forwardable
19
22
 
20
23
  class ModelNotFoundError < StandardError; end
21
24
 
22
- attr_reader :constraints, :filters, :name, :partitioners, :table
25
+ attr_reader :constraints, :filters, :name, :partitioners, :relationships, :table
23
26
 
24
27
  def_delegator :models_by_name, :values, :models
25
28
  def_delegator :models, :sort, :sorted_models
26
29
  def_delegator :constraints, :sort, :sorted_constraints
27
30
  def_delegator :partitioners, :sort, :sorted_partitioners
28
31
 
29
- def initialize(name:, constraints: [], models: [], partitioners: [], table: '')
30
- raise ArgumentError, 'name is required' if name.to_s.empty?
31
-
32
- @name = name.to_s
33
- @constraints = Constraints.array(constraints).uniq
32
+ def initialize(
33
+ name:,
34
+ constraints: [], # Exists here for tree based model backward compatibility.
35
+ relationships: [],
36
+ models: [], # Exists here for tree based model backward compatibility.
37
+ partitioners: [],
38
+ table: ''
39
+ )
40
+ @name = name
41
+ @constraints = Constraints.array(constraints || []).uniq
42
+ @relationships = Relationships.make_keyed_by(:name, relationships)
34
43
  @models_by_name = name_hash(Model.array(models))
35
44
  @partitioners = Partitioner.array(partitioners).uniq
36
45
  @table = table.to_s.empty? ? @name : table.to_s
37
46
 
47
+ ensure_input_is_valid
48
+
38
49
  freeze
39
50
  end
40
51
 
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 = {})
46
- return found if Array(parts).empty?
47
-
48
- # Take the first entry in parts
49
- model_name = parts.first.to_s
50
-
51
- # Ensure we have it registered as a child, or raise error
52
- model = assert_model(model_name, visited_parts)
53
-
54
- # Push onto visited list
55
- visited_parts += [model_name]
56
-
57
- # Add found model to flattened structure
58
- found[visited_parts] = model
59
-
60
- # Recursively call for next parts in the chain
61
- model.ancestors!(parts[1..-1], visited_parts, found)
52
+ def relationship_for_name(relationship_name)
53
+ relationships[relationship_name]
62
54
  end
63
55
 
64
56
  def ==(other)
65
57
  other.instance_of?(self.class) &&
66
- other.name == name &&
67
- other.table == table &&
68
- other.sorted_constraints == sorted_constraints &&
69
- other.sorted_partitioners == sorted_partitioners &&
70
- other.sorted_models == sorted_models
58
+ other.name == name && other.table == table && children_are_equal(other)
71
59
  end
72
60
  alias eql? ==
73
61
 
@@ -75,17 +63,41 @@ module Dbee
75
63
  name <=> other.name
76
64
  end
77
65
 
66
+ def hash
67
+ [
68
+ name.hash,
69
+ table.hash,
70
+ relationships.hash,
71
+ sorted_constraints.hash,
72
+ sorted_partitioners.hash,
73
+ sorted_models.hash
74
+ ].hash
75
+ end
76
+
77
+ def to_s
78
+ name
79
+ end
80
+
78
81
  private
79
82
 
80
83
  attr_reader :models_by_name
81
84
 
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
85
  def name_hash(array)
88
86
  array.map { |a| [a.name, a] }.to_h
89
87
  end
88
+
89
+ def children_are_equal(other)
90
+ other.relationships == relationships &&
91
+ other.sorted_constraints == sorted_constraints &&
92
+ other.sorted_partitioners == sorted_partitioners &&
93
+ other.sorted_models == sorted_models
94
+ end
95
+
96
+ def ensure_input_is_valid
97
+ raise ArgumentError, 'name is required' if name.to_s.empty?
98
+
99
+ constraints&.any? && relationships&.any? && \
100
+ raise(ArgumentError, 'constraints and relationships are mutually exclusive')
101
+ end
90
102
  end
91
103
  end