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 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: