nobrainer 0.41.0 → 0.43.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -1
- data/lib/no_brainer/criteria/join.rb +1 -0
- data/lib/no_brainer/criteria/where.rb +35 -2
- data/lib/no_brainer/document/association/belongs_to.rb +48 -6
- data/lib/no_brainer/document/association/core.rb +4 -3
- data/lib/no_brainer/document/association/eager_loader.rb +15 -2
- data/lib/no_brainer/document/association/has_many.rb +26 -8
- data/lib/no_brainer/error.rb +17 -15
- data/lib/no_brainer/query_runner/table_on_demand.rb +7 -6
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ea5237da296873c106bd564c6ca6e62674dd63bf3e1214e8542c3097bb978c82
|
4
|
+
data.tar.gz: bc02529b83bc6cb8fac1d1895fc3ae927220f6f497a4c4c921e41786b41d3a1a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8aa09bfe2bfc6948258d43d6e9ffc16261ea718da79aabfe2b012ead4b69a42fb0fc5d8d30e6a26a0a1880210ae7f0d342ef0911f3193fa91235e07edb87f509
|
7
|
+
data.tar.gz: 999c029ba2e70dd0fd4f458f091c25bbc493d4be0a181e09ae66871a0f6d43bfab005088d509c509cd6cf58bc3528daf72b296a7e02e589d1d23a59b1502c09d
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
9
|
|
10
|
+
## [0.43.0] - 2022-06-16
|
11
|
+
### Added
|
12
|
+
- Implements polymorphic associations
|
13
|
+
|
14
|
+
## [0.42.0] - 2022-06-15
|
15
|
+
### Added
|
16
|
+
- Add support for partial compound index queries
|
17
|
+
|
18
|
+
## [0.41.1] - 2022-03-21
|
19
|
+
### Fixed
|
20
|
+
- Removing table_config duplicates after a runtime exception (caspiano)
|
21
|
+
|
10
22
|
## [0.41.0] - 2021-10-17
|
11
23
|
### Added
|
12
24
|
- ActiveRecord `store_accessor` helper method
|
@@ -116,7 +128,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
116
128
|
- Locks: bug fix: allow small timeouts in lock()
|
117
129
|
- Fix reentrant lock counter on steals
|
118
130
|
|
119
|
-
[Unreleased]: https://github.com/nobrainerorm/nobrainer/compare/v0.
|
131
|
+
[Unreleased]: https://github.com/nobrainerorm/nobrainer/compare/v0.43.0...HEAD
|
132
|
+
[0.43.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.42.0...v0.43.0
|
133
|
+
[0.42.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.41.1...v0.42.0
|
134
|
+
[0.41.1]: https://github.com/nobrainerorm/nobrainer/compare/v0.41.0...v0.41.1
|
120
135
|
[0.41.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.40.0...v0.41.0
|
121
136
|
[0.40.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.36.0...v0.40.0
|
122
137
|
[0.36.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.35.0...v0.36.0
|
@@ -16,6 +16,7 @@ module NoBrainer::Criteria::Join
|
|
16
16
|
association = model.association_metadata[k.to_sym]
|
17
17
|
raise "`#{k}' must be an association on `#{model}'" unless association
|
18
18
|
raise "join() does not support through associations" if association.options[:through]
|
19
|
+
raise "join() does not support polymorphic associations" if association.options[:polymorphic]
|
19
20
|
|
20
21
|
criteria = association.base_criteria
|
21
22
|
criteria = case v
|
@@ -399,12 +399,45 @@ module NoBrainer::Criteria::Where
|
|
399
399
|
get_usable_indexes(:kind => :compound, :geo => false, :multi => false).each do |index|
|
400
400
|
indexed_clauses = index.what.map { |field| clauses[[field]] }
|
401
401
|
next unless indexed_clauses.all? { |c| c.try(:compatible_with_index?, index) }
|
402
|
-
|
403
402
|
return IndexStrategy.new(self, ast, indexed_clauses, index, :get_all, [indexed_clauses.map(&:value)])
|
404
403
|
end
|
405
404
|
return nil
|
406
405
|
end
|
407
406
|
|
407
|
+
def find_strategy_compound_partial
|
408
|
+
clauses = get_candidate_clauses(:eq, :between).map { |c| [c.key_path, c] }.to_h
|
409
|
+
return nil unless clauses.present?
|
410
|
+
|
411
|
+
get_usable_indexes(:kind => :compound, :geo => false, :multi => false).each do |index|
|
412
|
+
indexed_clauses = index.what.map { |field| clauses[[field]] }
|
413
|
+
partial_clauses = indexed_clauses.compact
|
414
|
+
pad = indexed_clauses.length - partial_clauses.length
|
415
|
+
if partial_clauses.any? && partial_clauses.all? { |c| c.try(:compatible_with_index?, index) }
|
416
|
+
# can only use partial compound index if:
|
417
|
+
# * index contains all clause fields
|
418
|
+
next unless (clauses.values & partial_clauses) == clauses.values
|
419
|
+
# * all clause fields come first in the indexed clauses (unused indexed fields are at the end)
|
420
|
+
next unless indexed_clauses.last(pad).all?(&:nil?)
|
421
|
+
# * all clause fields are :eq, except the last (which may be :between)
|
422
|
+
next unless partial_clauses[0..-2].all? { |c| c.op == :eq }
|
423
|
+
|
424
|
+
# use range query to cover unused index fields
|
425
|
+
left_bound = partial_clauses.map(&:value)
|
426
|
+
right_bound = partial_clauses.map(&:value)
|
427
|
+
if (clause = partial_clauses[-1]).op == :between
|
428
|
+
left_bound[-1] = clause.value.min
|
429
|
+
right_bound[-1] = clause.value.max
|
430
|
+
end
|
431
|
+
if pad > 0
|
432
|
+
left_bound.append *Array.new(pad, RethinkDB::RQL.new.minval)
|
433
|
+
right_bound.append *Array.new(pad, RethinkDB::RQL.new.maxval)
|
434
|
+
end
|
435
|
+
return IndexStrategy.new(self, ast, partial_clauses, index, :between, [left_bound, right_bound], :left_bound => :closed, :right_bound => :closed)
|
436
|
+
end
|
437
|
+
end
|
438
|
+
nil
|
439
|
+
end
|
440
|
+
|
408
441
|
def find_strategy_hidden_between
|
409
442
|
clauses = get_candidate_clauses(:gt, :ge, :lt, :le).group_by(&:key_path)
|
410
443
|
return nil unless clauses.present?
|
@@ -454,7 +487,7 @@ module NoBrainer::Criteria::Where
|
|
454
487
|
def find_strategy
|
455
488
|
return nil unless ast.try(:clauses).present? && !criteria.without_index?
|
456
489
|
case ast.op
|
457
|
-
when :and then find_strategy_compound || find_strategy_canonical || find_strategy_hidden_between
|
490
|
+
when :and then find_strategy_compound || find_strategy_compound_partial || find_strategy_canonical || find_strategy_hidden_between
|
458
491
|
when :or then find_strategy_union
|
459
492
|
end
|
460
493
|
end
|
@@ -2,8 +2,11 @@ class NoBrainer::Document::Association::BelongsTo
|
|
2
2
|
include NoBrainer::Document::Association::Core
|
3
3
|
|
4
4
|
class Metadata
|
5
|
-
VALID_OPTIONS = [
|
6
|
-
|
5
|
+
VALID_OPTIONS = %i[
|
6
|
+
primary_key foreign_key foreign_type class_name foreign_key_store_as
|
7
|
+
index validates required uniq unique polymorphic
|
8
|
+
]
|
9
|
+
|
7
10
|
include NoBrainer::Document::Association::Core::Metadata
|
8
11
|
include NoBrainer::Document::Association::EagerLoader::Generic
|
9
12
|
|
@@ -11,6 +14,12 @@ class NoBrainer::Document::Association::BelongsTo
|
|
11
14
|
options[:foreign_key].try(:to_sym) || :"#{target_name}_#{primary_key}"
|
12
15
|
end
|
13
16
|
|
17
|
+
def foreign_type
|
18
|
+
return nil unless options[:polymorphic]
|
19
|
+
|
20
|
+
options[:foreign_type].try(:to_sym) || (:"#{target_name}_type")
|
21
|
+
end
|
22
|
+
|
14
23
|
def primary_key
|
15
24
|
# We default the primary_key to `:id' and not `target_model.pk_name',
|
16
25
|
# because we don't want to require the target_model to be already loaded.
|
@@ -30,12 +39,22 @@ class NoBrainer::Document::Association::BelongsTo
|
|
30
39
|
end
|
31
40
|
end
|
32
41
|
|
33
|
-
def target_model
|
34
|
-
|
42
|
+
def target_model(target_class = nil)
|
43
|
+
return if options[:polymorphic] && target_class.nil?
|
44
|
+
|
45
|
+
model_name = if options[:polymorphic]
|
46
|
+
target_class
|
47
|
+
else
|
48
|
+
options[:class_name] || target_name.to_s.camelize
|
49
|
+
end
|
50
|
+
|
51
|
+
get_model_by_name(model_name)
|
35
52
|
end
|
36
53
|
|
37
|
-
def base_criteria
|
38
|
-
target_model
|
54
|
+
def base_criteria(target_class = nil)
|
55
|
+
model = target_model(target_class)
|
56
|
+
|
57
|
+
model ? model.without_ordering : nil
|
39
58
|
end
|
40
59
|
|
41
60
|
def hook
|
@@ -47,6 +66,11 @@ class NoBrainer::Document::Association::BelongsTo
|
|
47
66
|
raise "Cannot declare `#{target_name}' in #{owner_model}: the foreign_key `#{foreign_key}' is already used"
|
48
67
|
end
|
49
68
|
|
69
|
+
if options[:polymorphic] && options[:class_name]
|
70
|
+
raise 'You cannot set class_name on a polymorphic belongs_to'
|
71
|
+
end
|
72
|
+
|
73
|
+
owner_model.field(foreign_type) if options[:polymorphic]
|
50
74
|
owner_model.field(foreign_key, :store_as => options[:foreign_key_store_as], :index => options[:index])
|
51
75
|
|
52
76
|
unless options[:validates] == false
|
@@ -85,6 +109,7 @@ class NoBrainer::Document::Association::BelongsTo
|
|
85
109
|
end
|
86
110
|
|
87
111
|
def eager_load_owner_key; foreign_key; end
|
112
|
+
def eager_load_owner_type; foreign_type; end
|
88
113
|
def eager_load_target_key; primary_key; end
|
89
114
|
end
|
90
115
|
|
@@ -97,6 +122,17 @@ class NoBrainer::Document::Association::BelongsTo
|
|
97
122
|
@target_container = nil
|
98
123
|
end
|
99
124
|
|
125
|
+
def polymorphic_read
|
126
|
+
return target if loaded?
|
127
|
+
|
128
|
+
target_class = owner.read_attribute(foreign_type)
|
129
|
+
fk = owner.read_attribute(foreign_key)
|
130
|
+
|
131
|
+
if target_class && fk
|
132
|
+
preload(base_criteria(target_class).where(primary_key => fk).first)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
100
136
|
def read
|
101
137
|
return target if loaded?
|
102
138
|
|
@@ -105,6 +141,12 @@ class NoBrainer::Document::Association::BelongsTo
|
|
105
141
|
end
|
106
142
|
end
|
107
143
|
|
144
|
+
def polymorphic_write(target)
|
145
|
+
owner.write_attribute(foreign_key, target.try(primary_key))
|
146
|
+
owner.write_attribute(foreign_type, target.root_class.name)
|
147
|
+
preload(target)
|
148
|
+
end
|
149
|
+
|
108
150
|
def write(target)
|
109
151
|
assert_target_type(target)
|
110
152
|
owner.write_attribute(foreign_key, target.try(primary_key))
|
@@ -33,8 +33,8 @@ module NoBrainer::Document::Association::Core
|
|
33
33
|
|
34
34
|
def hook
|
35
35
|
options.assert_valid_keys(*self.class.const_get(:VALID_OPTIONS))
|
36
|
-
delegate("#{target_name}=", :write)
|
37
|
-
delegate("#{target_name}", :read)
|
36
|
+
delegate("#{target_name}=", "#{'polymorphic_' if options[:polymorphic]}write".to_sym)
|
37
|
+
delegate("#{target_name}", "#{'polymorphic_' if options[:polymorphic]}read".to_sym)
|
38
38
|
end
|
39
39
|
|
40
40
|
def add_callback_for(what)
|
@@ -62,7 +62,8 @@ module NoBrainer::Document::Association::Core
|
|
62
62
|
|
63
63
|
included { attr_accessor :metadata, :owner }
|
64
64
|
|
65
|
-
delegate :primary_key, :foreign_key, :
|
65
|
+
delegate :primary_key, :foreign_key, :foreign_type, :target_name,
|
66
|
+
:target_model, :base_criteria, :to => :metadata
|
66
67
|
|
67
68
|
def initialize(metadata, owner)
|
68
69
|
@metadata, @owner = metadata, owner
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module NoBrainer::Document::Association::EagerLoader
|
2
4
|
extend self
|
3
5
|
|
@@ -5,11 +7,22 @@ module NoBrainer::Document::Association::EagerLoader
|
|
5
7
|
# Used in associations to declare generic eager loading capabilities
|
6
8
|
# The association should implement loaded?, preload,
|
7
9
|
# eager_load_owner_key and eager_load_target_key.
|
8
|
-
def eager_load(docs, additional_criteria=nil)
|
10
|
+
def eager_load(docs, additional_criteria = nil)
|
9
11
|
owner_key = eager_load_owner_key
|
12
|
+
owner_type = eager_load_owner_type
|
10
13
|
target_key = eager_load_target_key
|
11
14
|
|
12
|
-
|
15
|
+
if is_a?(NoBrainer::Document::Association::BelongsTo::Metadata) && owner_type
|
16
|
+
target_class = docs.first.__send__(owner_type)
|
17
|
+
|
18
|
+
if docs.detect { |doc| doc.__send__(owner_type) != target_class }
|
19
|
+
raise NoBrainer::Error::PolymorphicAssociationWithDifferentTypes,
|
20
|
+
"The documents to be eager loaded doesn't have the same " \
|
21
|
+
'type, which is not supported'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
criteria = target_class ? base_criteria(target_class) : base_criteria
|
13
26
|
criteria = criteria.merge(additional_criteria) if additional_criteria
|
14
27
|
|
15
28
|
unloaded_docs = docs.reject { |doc| doc.associations[self].loaded? }
|
@@ -2,12 +2,20 @@ class NoBrainer::Document::Association::HasMany
|
|
2
2
|
include NoBrainer::Document::Association::Core
|
3
3
|
|
4
4
|
class Metadata
|
5
|
-
VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :dependent, :scope
|
5
|
+
VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :dependent, :scope,
|
6
|
+
:as]
|
6
7
|
include NoBrainer::Document::Association::Core::Metadata
|
7
8
|
include NoBrainer::Document::Association::EagerLoader::Generic
|
8
9
|
|
9
10
|
def foreign_key
|
10
|
-
options[:foreign_key].try(:to_sym)
|
11
|
+
return options[:foreign_key].try(:to_sym) if options.key?(:foreign_key)
|
12
|
+
return :"#{options[:as]}_#{primary_key}" if options[:as]
|
13
|
+
|
14
|
+
:"#{owner_model.name.split('::').last.underscore}_#{primary_key}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def foreign_type
|
18
|
+
options[:foreign_type].try(:to_sym) || (options[:as] && :"#{options[:as]}_type")
|
11
19
|
end
|
12
20
|
|
13
21
|
def primary_key
|
@@ -30,9 +38,9 @@ class NoBrainer::Document::Association::HasMany
|
|
30
38
|
# caching is hard (rails console reload, etc.).
|
31
39
|
target_model.association_metadata.values.select do |assoc|
|
32
40
|
assoc.is_a?(NoBrainer::Document::Association::BelongsTo::Metadata) and
|
33
|
-
assoc.foreign_key ==
|
34
|
-
assoc.primary_key ==
|
35
|
-
assoc.target_model.root_class == owner_model.root_class
|
41
|
+
assoc.foreign_key == foreign_key and
|
42
|
+
assoc.primary_key == primary_key and
|
43
|
+
assoc.target_model(target_model).root_class == owner_model.root_class
|
36
44
|
end
|
37
45
|
end
|
38
46
|
|
@@ -46,7 +54,7 @@ class NoBrainer::Document::Association::HasMany
|
|
46
54
|
|
47
55
|
if options[:dependent]
|
48
56
|
unless [:destroy, :delete, :nullify, :restrict, nil].include?(options[:dependent])
|
49
|
-
raise "Invalid dependent option: `#{options[:dependent].inspect}'. "
|
57
|
+
raise "Invalid dependent option: `#{options[:dependent].inspect}'. " \
|
50
58
|
"Valid options are: :destroy, :delete, :nullify, or :restrict"
|
51
59
|
end
|
52
60
|
add_callback_for(:before_destroy)
|
@@ -54,12 +62,22 @@ class NoBrainer::Document::Association::HasMany
|
|
54
62
|
end
|
55
63
|
|
56
64
|
def eager_load_owner_key; primary_key; end
|
65
|
+
def eager_load_owner_type; foreign_type; end
|
57
66
|
def eager_load_target_key; foreign_key; end
|
58
67
|
end
|
59
68
|
|
60
69
|
def target_criteria
|
61
|
-
@target_criteria ||=
|
62
|
-
|
70
|
+
@target_criteria ||= begin
|
71
|
+
query_criteria = { foreign_key => owner.__send__(primary_key) }
|
72
|
+
|
73
|
+
if metadata.options[:as]
|
74
|
+
query_criteria = query_criteria.merge(
|
75
|
+
foreign_type => owner.root_class.name
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
base_criteria.where(query_criteria).after_find(set_inverse_proc)
|
80
|
+
end
|
63
81
|
end
|
64
82
|
|
65
83
|
def read
|
data/lib/no_brainer/error.rb
CHANGED
@@ -1,19 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module NoBrainer::Error
|
2
|
-
class
|
3
|
-
class
|
4
|
-
class
|
5
|
-
class
|
6
|
-
class
|
7
|
-
class
|
8
|
-
class
|
9
|
-
class
|
10
|
-
class
|
11
|
-
class
|
12
|
-
class
|
13
|
-
class
|
14
|
-
class
|
15
|
-
class
|
16
|
-
class
|
4
|
+
class AssociationNotPersisted < RuntimeError; end
|
5
|
+
class AtomicBlock < RuntimeError; end
|
6
|
+
class ChildrenExist < RuntimeError; end
|
7
|
+
class Connection < RuntimeError; end
|
8
|
+
class DocumentNotFound < RuntimeError; end
|
9
|
+
class DocumentNotPersisted < RuntimeError; end
|
10
|
+
class InvalidPolymorphicType < RuntimeError; end
|
11
|
+
class LockInvalidOp < RuntimeError; end
|
12
|
+
class LostLock < RuntimeError; end
|
13
|
+
class LockUnavailable < RuntimeError; end
|
14
|
+
class MissingAttribute < RuntimeError; end
|
15
|
+
class MissingIndex < RuntimeError; end
|
16
|
+
class PolymorphicAssociationWithDifferentTypes < RuntimeError; end
|
17
|
+
class ReadonlyField < RuntimeError; end
|
18
|
+
class UnknownAttribute < RuntimeError; end
|
17
19
|
|
18
20
|
class DocumentInvalid < RuntimeError
|
19
21
|
attr_accessor :instance
|
@@ -29,9 +29,13 @@ class NoBrainer::QueryRunner::TableOnDemand < NoBrainer::QueryRunner::Middleware
|
|
29
29
|
env[:last_auto_create_table] = [db_name, table_name]
|
30
30
|
|
31
31
|
create_options = model.table_create_options
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
begin
|
33
|
+
NoBrainer.run(:db => db_name) do |r|
|
34
|
+
r.table_create(table_name, create_options.reject { |k,_| k.in? [:name, :write_acks] })
|
35
|
+
end
|
36
|
+
rescue RuntimeError => e
|
37
|
+
# We might have raced with another table create
|
38
|
+
raise unless e.message =~ /Table `#{db_name}\.#{table_name}` already exists/
|
35
39
|
end
|
36
40
|
|
37
41
|
# Prevent duplicate table errors on a cluster.
|
@@ -49,8 +53,5 @@ class NoBrainer::QueryRunner::TableOnDemand < NoBrainer::QueryRunner::Middleware
|
|
49
53
|
r.table(table_name).config().update(:write_acks => create_options[:write_acks])
|
50
54
|
end
|
51
55
|
end
|
52
|
-
rescue RuntimeError => e
|
53
|
-
# We might have raced with another table create
|
54
|
-
raise unless e.message =~ /Table `#{db_name}\.#{table_name}` already exists/
|
55
56
|
end
|
56
57
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nobrainer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.43.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nicolas Viennot
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-06-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -242,7 +242,11 @@ files:
|
|
242
242
|
homepage: http://nobrainer.io
|
243
243
|
licenses:
|
244
244
|
- LGPL-3.0-only
|
245
|
-
metadata:
|
245
|
+
metadata:
|
246
|
+
allowed_push_host: https://rubygems.org
|
247
|
+
homepage_uri: http://nobrainer.io
|
248
|
+
source_code_uri: https://github.com/NoBrainerORM/nobrainer
|
249
|
+
changelog_uri: https://github.com/NoBrainerORM/nobrainer/blob/master/CHANGELOG.md
|
246
250
|
post_install_message:
|
247
251
|
rdoc_options: []
|
248
252
|
require_paths:
|