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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 477e66e4e3380775847ce79e706f50a04134e7e87002b31aa0ca33c9e164d9d7
4
- data.tar.gz: b303bbb3dc6a45df84f39edd2cce6d26af27000939c56be05e8f036c0ad11e76
3
+ metadata.gz: ea5237da296873c106bd564c6ca6e62674dd63bf3e1214e8542c3097bb978c82
4
+ data.tar.gz: bc02529b83bc6cb8fac1d1895fc3ae927220f6f497a4c4c921e41786b41d3a1a
5
5
  SHA512:
6
- metadata.gz: f1f883d15467994b72aabddbb61adcadd7095a5220f89ad1b4d8e58cf6927cfb7a99e0e1443fc24d7edf37bf4a24c24e95d0bc99c47d39281b0e56c73ee549b0
7
- data.tar.gz: '04592d9c64732b25089dccd897ad451cd2f71b12a96a93532604768c9c40c1757e6f2b528792976f8be054e376d0a3a4541ca3a3aa7de60014a7e88d97e10453'
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.41.0...HEAD
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 = [:primary_key, :foreign_key, :class_name, :foreign_key_store_as,
6
- :index, :validates, :required, :uniq, :unique]
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
- get_model_by_name(options[:class_name] || target_name.to_s.camelize)
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.without_ordering
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, :target_name, :target_model, :base_criteria, :to => :metadata
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
- criteria = base_criteria
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) || :"#{owner_model.name.split('::').last.underscore}_#{primary_key}"
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 == self.foreign_key and
34
- assoc.primary_key == self.primary_key and
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 ||= base_criteria.where(foreign_key => owner.__send__(primary_key))
62
- .after_find(set_inverse_proc)
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
@@ -1,19 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NoBrainer::Error
2
- class Connection < RuntimeError; end
3
- class DocumentNotFound < RuntimeError; end
4
- class DocumentNotPersisted < RuntimeError; end
5
- class ChildrenExist < RuntimeError; end
6
- class CannotUseIndex < RuntimeError; end
7
- class MissingIndex < RuntimeError; end
8
- class AssociationNotPersisted < RuntimeError; end
9
- class ReadonlyField < RuntimeError; end
10
- class MissingAttribute < RuntimeError; end
11
- class UnknownAttribute < RuntimeError; end
12
- class AtomicBlock < RuntimeError; end
13
- class LostLock < RuntimeError; end
14
- class LockInvalidOp < RuntimeError; end
15
- class LockUnavailable < RuntimeError; end
16
- class InvalidPolymorphicType < RuntimeError; end
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
- NoBrainer.run(:db => db_name) do |r|
34
- r.table_create(table_name, create_options.reject { |k,_| k.in? [:name, :write_acks] })
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.41.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: 2021-10-17 00:00:00.000000000 Z
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: