nobrainer 0.41.0 → 0.43.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 +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:
|