dbee 2.0.2 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +14 -10
- data/.ruby-version +1 -1
- data/.travis.yml +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +257 -43
- data/dbee.gemspec +17 -6
- data/exe/.gitkeep +0 -0
- data/lib/dbee.rb +10 -10
- data/lib/dbee/base.rb +7 -49
- data/lib/dbee/constant_resolver.rb +34 -0
- data/lib/dbee/dsl/association.rb +8 -19
- data/lib/dbee/dsl_schema_builder.rb +86 -0
- data/lib/dbee/key_chain.rb +11 -0
- data/lib/dbee/model.rb +50 -38
- data/lib/dbee/model/relationships.rb +24 -0
- data/lib/dbee/model/relationships/basic.rb +47 -0
- data/lib/dbee/query.rb +30 -24
- data/lib/dbee/query/field.rb +35 -5
- data/lib/dbee/schema.rb +66 -0
- data/lib/dbee/schema_creator.rb +107 -0
- data/lib/dbee/schema_from_tree_based_model.rb +47 -0
- data/lib/dbee/util/make_keyed_by.rb +50 -0
- data/lib/dbee/version.rb +1 -1
- data/spec/dbee/base_spec.rb +9 -72
- data/spec/dbee/constant_resolver_spec.rb +58 -0
- data/spec/dbee/dsl_schema_builder_spec.rb +106 -0
- data/spec/dbee/key_chain_spec.rb +24 -0
- data/spec/dbee/model/constraints_spec.rb +6 -7
- data/spec/dbee/model_spec.rb +62 -59
- data/spec/dbee/query/field_spec.rb +54 -6
- data/spec/dbee/query/filters_spec.rb +16 -17
- data/spec/dbee/query_spec.rb +55 -62
- data/spec/dbee/schema_creator_spec.rb +163 -0
- data/spec/dbee/schema_from_tree_based_model_spec.rb +31 -0
- data/spec/dbee/schema_spec.rb +62 -0
- data/spec/dbee_spec.rb +17 -37
- data/spec/fixtures/models.yaml +254 -56
- data/spec/spec_helper.rb +7 -0
- metadata +83 -18
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.
|
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.
|
30
|
+
s.required_ruby_version = '>= 2.5'
|
23
31
|
|
24
|
-
s.add_dependency('acts_as_hashable', '~>1', '>=1.
|
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', '~>
|
32
|
-
s.add_development_dependency('
|
33
|
-
s.add_development_dependency('
|
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(
|
34
|
-
|
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
|
-
|
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
|
-
#
|
26
|
-
|
27
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/dbee/dsl/association.rb
CHANGED
@@ -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
|
23
|
-
@inflector
|
24
|
-
@name
|
25
|
-
@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
|
data/lib/dbee/key_chain.rb
CHANGED
@@ -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(
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
42
|
-
|
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
|