nobrainer 0.40.0 → 0.42.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: 2be88d279ca6fb8c9c8698d25efe539c635949fa5d328034f9a30430e084d396
4
- data.tar.gz: 4c5d8c072503e15d84198c9e1c33c293efdf3d4789784bbfe8420c7755cbc968
3
+ metadata.gz: 69052877b8fd9f2684f63a0bc7a917812fd771f793d55e7f5182149fc1346643
4
+ data.tar.gz: 33dbe632e862a31bb4ac95ab8015834947bd1207df3bbdfb921b742274d06517
5
5
  SHA512:
6
- metadata.gz: 552bc01e727596b8060026e28dcd825cdce2152e3fbf7aa6ec128379bea7dc38b945938bfa79d82917e186d22477fded04127d6c565e4d2bc6e569b775bafc88
7
- data.tar.gz: 9e52d7ffea00b58b0f80102e8d31b43c32d397082e30ccf3bb82e140565c0aec9ce853bf84d2ba204241076cd2e14523ac48670d9b55a8b12b918a08422f8072
6
+ metadata.gz: 102682b8e844b1fd574b3d29f386923bb4dfaee33e88e7da6c5615b4901d20023267cbd4e698bec34a94b77718ca04f251f8aebdfd8f74ef5837c637931dd206
7
+ data.tar.gz: 3cb7abcc47fd15d71abac788e7c5844229d30f532c01000d177d7c5b718848b48d2150cda32a1725ed7d0b68adbb775f4461805d9c2f4768104ba1320eb95d52
data/CHANGELOG.md CHANGED
@@ -6,6 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+
10
+ ## [0.42.0] - 2022-06-15
11
+ ### Added
12
+ - Add support for partial compound index queries
13
+
14
+ ## [0.41.1] - 2022-03-21
15
+ ### Fixed
16
+ - Removing table_config duplicates after a runtime exception (caspiano)
17
+
18
+ ## [0.41.0] - 2021-10-17
19
+ ### Added
20
+ - ActiveRecord `store_accessor` helper method
21
+
22
+ ### Fixed
23
+ - gemspec dependencies on activemodel and activesupport
24
+
9
25
  ## [0.40.0] - 2021-10-16
10
26
  ### Fixed
11
27
  - Ruby 3 compatibility
@@ -108,7 +124,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
108
124
  - Locks: bug fix: allow small timeouts in lock()
109
125
  - Fix reentrant lock counter on steals
110
126
 
111
- [Unreleased]: https://github.com/nobrainerorm/nobrainer/compare/v0.40.0...HEAD
127
+ [Unreleased]: https://github.com/nobrainerorm/nobrainer/compare/v0.42.0...HEAD
128
+ [0.41.1]: https://github.com/nobrainerorm/nobrainer/compare/v0.41.1...v0.42.0
129
+ [0.41.1]: https://github.com/nobrainerorm/nobrainer/compare/v0.41.0...v0.41.1
130
+ [0.41.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.40.0...v0.41.0
112
131
  [0.40.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.36.0...v0.40.0
113
132
  [0.36.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.35.0...v0.36.0
114
133
  [0.35.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.34.1...v0.35.0
@@ -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
@@ -0,0 +1,304 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/hash/indifferent_access'
4
+
5
+ module NoBrainer
6
+ module Document
7
+ # Store gives you a way for storing hashes in a single field with accessors
8
+ # to the Hash keys.
9
+ # It is a portage of the ActiveRecord::Store which make gems using it
10
+ # compatible with NoBrainer.
11
+ #
12
+ # You can then declare accessors to this store that are then accessible just
13
+ # like any other attribute of the model. This is very helpful for easily
14
+ # exposing store keys to a form or elsewhere that's already built around
15
+ # just accessing attributes on the model.
16
+ #
17
+ # Every accessor comes with dirty tracking methods (+key_changed?+,
18
+ # +key_was+ and +key_change+).
19
+ #
20
+ # You can set custom coder to encode/decode your serialized attributes
21
+ # to/from different formats.
22
+ # JSON, YAML, Marshal are supported out of the box.
23
+ # Generally it can be any wrapper that provides +load+ and +dump+.
24
+ #
25
+ # NOTE: The {.store}[rdoc-ref:rdoc-ref:ClassMethods#store] method is here
26
+ # for compatibility reason, but you should use
27
+ # {.store_accessor}[rdoc-ref:ClassMethods#store_accessor] instead
28
+ # to generate the accessor methods.
29
+ # Be aware that these columns use a string keyed hash and do not allow
30
+ # access using a symbol.
31
+ #
32
+ # NOTE: The default validations with the exception of +uniqueness+ will work.
33
+ #
34
+ # Examples:
35
+ #
36
+ # class User
37
+ # include NoBrainer::Document
38
+ #
39
+ # store :settings, accessors: [ :color, :homepage ], coder: JSON
40
+ # store :parent, accessors: [ :name ], coder: JSON, prefix: true
41
+ # store :spouse, accessors: [ :name ], coder: JSON, prefix: :partner
42
+ # store :settings, accessors: [ :two_factor_auth ], suffix: true
43
+ # store :settings, accessors: [ :login_retry ], suffix: :config
44
+ # end
45
+ #
46
+ # u = User.new(color: 'black', homepage: '37signals.com',
47
+ # parent_name: 'Mary', partner_name: 'Lily')
48
+ # u.color # Accessor stored attribute
49
+ # u.parent_name # Accessor stored attribute with prefix
50
+ # u.partner_name # Accessor stored attribute with custom prefix
51
+ # u.two_factor_auth_settings # Accessor stored attribute with suffix
52
+ # u.login_retry_config # Accessor stored attribute with custom suffix
53
+ # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
54
+ #
55
+ # # There is no difference between strings and symbols for accessing
56
+ # # custom attributes
57
+ # u.settings[:country] # => 'Denmark'
58
+ # u.settings['country'] # => 'Denmark'
59
+ #
60
+ # # Dirty tracking
61
+ # u.color = 'green'
62
+ # u.color_changed? # => true
63
+ # u.color_was # => 'black'
64
+ # u.color_change # => ['black', 'red']
65
+ #
66
+ # # Add additional accessors to an existing store through store_accessor
67
+ # class SuperUser < User
68
+ # store_accessor :settings, :privileges, :servants
69
+ # store_accessor :parent, :birthday, prefix: true
70
+ # store_accessor :settings, :secret_question, suffix: :config
71
+ # end
72
+ #
73
+ # The stored attribute names can be retrieved using
74
+ # {.stored_attributes}[rdoc-ref:rdoc-ref:ClassMethods#stored_attributes].
75
+ #
76
+ # User.stored_attributes[:settings]
77
+ # #=> [:color, :homepage, :two_factor_auth, :login_retry]
78
+ #
79
+ # == Overwriting default accessors
80
+ #
81
+ # All stored values are automatically available through accessors on
82
+ # the NoBrainer Document object, but sometimes you want to specialize
83
+ # this behavior. This can be done by overwriting the default accessors
84
+ # (using the same name as the attribute) and calling <tt>super</tt>
85
+ # to actually change things.
86
+ #
87
+ # class Song
88
+ # include NoBrainer::Document
89
+ #
90
+ # # Uses a stored integer to hold the volume adjustment of the song
91
+ # store :settings, accessors: [:volume_adjustment]
92
+ #
93
+ # def volume_adjustment=(decibels)
94
+ # super(decibels.to_i)
95
+ # end
96
+ #
97
+ # def volume_adjustment
98
+ # super.to_i
99
+ # end
100
+ # end
101
+ module Store
102
+ extend ActiveSupport::Concern
103
+
104
+ included do
105
+ class << self
106
+ attr_accessor :local_stored_attributes
107
+ end
108
+ end
109
+
110
+ module ClassMethods
111
+ def store(store_attribute, options = {})
112
+ store_accessor(store_attribute, options[:accessors], **options.slice(:prefix, :suffix)) if options.has_key? :accessors
113
+ end
114
+
115
+ def store_accessor(store_attribute, *keys, prefix: nil, suffix: nil)
116
+ keys = keys.flatten
117
+
118
+ accessor_prefix =
119
+ case prefix
120
+ when String, Symbol
121
+ "#{prefix}_"
122
+ when TrueClass
123
+ "#{store_attribute}_"
124
+ else
125
+ ""
126
+ end
127
+ accessor_suffix =
128
+ case suffix
129
+ when String, Symbol
130
+ "_#{suffix}"
131
+ when TrueClass
132
+ "_#{store_attribute}"
133
+ else
134
+ ""
135
+ end
136
+
137
+ field store_attribute, type: Hash, default: {} unless has_field?(store_attribute)
138
+
139
+ define_method("#{store_attribute}=") do |value|
140
+ super(value) if value.is_a?(Hash) || value.nil?
141
+ end
142
+
143
+ _store_accessors_module.module_eval do
144
+ keys.each do |key|
145
+ accessor_key = "#{accessor_prefix}#{key}#{accessor_suffix}"
146
+
147
+ define_method("#{accessor_key}=") do |value|
148
+ write_store_attribute(store_attribute, key, value)
149
+ end
150
+
151
+ define_method(accessor_key) do
152
+ read_store_attribute(store_attribute, key)
153
+ end
154
+
155
+ define_method("#{accessor_key}_changed?") do
156
+ return false unless __send__("#{store_attribute}_changed?")
157
+ prev_store, new_store = changes[store_attribute]
158
+ if NoBrainer.rails4?
159
+ (prev_store && prev_store[key.to_s]) != (new_store && new_store[key.to_s])
160
+ else
161
+ (prev_store && prev_store.dig(key)) != (new_store && new_store.dig(key))
162
+ end
163
+ end
164
+
165
+ define_method("#{accessor_key}_change") do
166
+ return unless __send__("#{store_attribute}_changed?")
167
+ prev_store, new_store = changes[store_attribute]
168
+ if NoBrainer.rails4?
169
+ [(prev_store && prev_store[key.to_s]), (new_store && new_store[key.to_s])]
170
+ else
171
+ [(prev_store && prev_store.dig(key)), (new_store && new_store.dig(key))]
172
+ end
173
+ end
174
+
175
+ define_method("#{accessor_key}_was") do
176
+ return unless __send__("#{store_attribute}_changed?")
177
+ prev_store, _new_store = changes[store_attribute]
178
+ if NoBrainer.rails4?
179
+ (prev_store && prev_store[key.to_s])
180
+ else
181
+ (prev_store && prev_store.dig(key))
182
+ end
183
+ end
184
+
185
+ # NoBrainer doesn't have `attribute_will_change!` so those methods
186
+ # can't be implemented yet.
187
+ # See https://github.com/NoBrainerORM/nobrainer/pull/190
188
+ #
189
+ # define_method("saved_change_to_#{accessor_key}?") do
190
+ # return false unless __send__("saved_change_to_#{store_attribute}?")
191
+ # prev_store, new_store = __send__("saved_change_to_#{store_attribute}")
192
+ # prev_store&.dig(key) != new_store&.dig(key)
193
+ # end
194
+
195
+ # define_method("saved_change_to_#{accessor_key}") do
196
+ # return unless __send__("saved_change_to_#{store_attribute}?")
197
+ # prev_store, new_store = __send__("saved_change_to_#{store_attribute}")
198
+ # [prev_store&.dig(key), new_store&.dig(key)]
199
+ # end
200
+
201
+ # define_method("#{accessor_key}_before_last_save") do
202
+ # return unless __send__("saved_change_to_#{store_attribute}?")
203
+ # prev_store, _new_store = __send__("saved_change_to_#{store_attribute}")
204
+ # prev_store&.dig(key)
205
+ # end
206
+ end
207
+ end
208
+
209
+ # assign new store attribute and create new hash to ensure that each class in the hierarchy
210
+ # has its own hash of stored attributes.
211
+ self.local_stored_attributes ||= {}
212
+ self.local_stored_attributes[store_attribute] ||= []
213
+ self.local_stored_attributes[store_attribute] |= keys
214
+ end
215
+
216
+ def _store_accessors_module # :nodoc:
217
+ @_store_accessors_module ||= begin
218
+ mod = Module.new
219
+ include mod
220
+ mod
221
+ end
222
+ end
223
+
224
+ def stored_attributes
225
+ parent = superclass.respond_to?(:stored_attributes) ? superclass.stored_attributes : {}
226
+ if local_stored_attributes
227
+ parent.merge!(local_stored_attributes) { |k, a, b| a | b }
228
+ end
229
+ parent
230
+ end
231
+ end
232
+
233
+ private
234
+
235
+ def read_store_attribute(store_attribute, key) # :doc:
236
+ StringKeyedHashAccessor.read(self, store_attribute, key)
237
+ end
238
+
239
+ def write_store_attribute(store_attribute, key, value) # :doc:
240
+ StringKeyedHashAccessor.write(self, store_attribute, key, value)
241
+ end
242
+
243
+ class HashAccessor # :nodoc:
244
+ def self.read(object, attribute, key)
245
+ prepare(object, attribute)
246
+ object.public_send(attribute)[key]
247
+ end
248
+
249
+ def self.write(object, attribute, key, value)
250
+ prepare(object, attribute)
251
+ if value != read(object, attribute, key)
252
+ # "#{attribute}_will_change!" is not implemented in NoBrainer. See issue #190
253
+ # object.public_send :"#{attribute}_will_change!"
254
+ object.public_send(attribute)[key] = value
255
+ end
256
+ end
257
+
258
+ def self.prepare(object, attribute)
259
+ object.public_send :"#{attribute}=", {} unless object.send(attribute)
260
+ end
261
+ end
262
+
263
+ class StringKeyedHashAccessor < HashAccessor # :nodoc:
264
+ def self.read(object, attribute, key)
265
+ super object, attribute, key.to_s
266
+ end
267
+
268
+ def self.write(object, attribute, key, value)
269
+ super object, attribute, key.to_s, value
270
+ end
271
+ end
272
+
273
+ class IndifferentCoder # :nodoc:
274
+ def initialize(attr_name, coder_or_class_name)
275
+ @coder =
276
+ if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump)
277
+ coder_or_class_name
278
+ else
279
+ NoBrainer::Document::Coders::YAMLColumn.new(attr_name, coder_or_class_name || Object)
280
+ end
281
+ end
282
+
283
+ def dump(obj)
284
+ @coder.dump self.class.as_indifferent_hash(obj)
285
+ end
286
+
287
+ def load(yaml)
288
+ self.class.as_indifferent_hash(@coder.load(yaml || ""))
289
+ end
290
+
291
+ def self.as_indifferent_hash(obj)
292
+ case obj
293
+ when ActiveSupport::HashWithIndifferentAccess
294
+ obj
295
+ when Hash
296
+ obj.with_indifferent_access
297
+ else
298
+ ActiveSupport::HashWithIndifferentAccess.new
299
+ end
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end
@@ -7,7 +7,7 @@ module NoBrainer::Document
7
7
  autoload_and_include :Core, :TableConfig, :InjectionLayer, :Attributes, :Readonly,
8
8
  :Persistance, :Callbacks, :Validation, :Types, :Dirty, :PrimaryKey,
9
9
  :Association, :Serialization, :Criteria, :Polymorphic, :Index, :Aliases,
10
- :MissingAttributes, :LazyFetch, :AtomicOps, :VirtualAttributes
10
+ :MissingAttributes, :LazyFetch, :AtomicOps, :VirtualAttributes, :Store
11
11
 
12
12
  autoload :DynamicAttributes, :Timestamps
13
13
 
@@ -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
data/lib/nobrainer.rb CHANGED
@@ -37,6 +37,10 @@ module NoBrainer
37
37
  RUBY_PLATFORM == 'java'
38
38
  end
39
39
 
40
+ def rails4?
41
+ Gem.loaded_specs['activesupport'].version >= Gem::Version.new('4.0.0')
42
+ end
43
+
40
44
  def rails5?
41
45
  Gem.loaded_specs['activesupport'].version >= Gem::Version.new('5.0.0.beta')
42
46
  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.40.0
4
+ version: 0.42.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-16 00:00:00.000000000 Z
11
+ date: 2022-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: 4.1.0
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '6.2'
22
+ version: '8'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,7 +29,7 @@ dependencies:
29
29
  version: 4.1.0
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '6.2'
32
+ version: '8'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: activesupport
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -39,7 +39,7 @@ dependencies:
39
39
  version: 4.1.0
40
40
  - - "<"
41
41
  - !ruby/object:Gem::Version
42
- version: '6.2'
42
+ version: '8'
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
@@ -49,7 +49,7 @@ dependencies:
49
49
  version: 4.1.0
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
- version: '6.2'
52
+ version: '8'
53
53
  - !ruby/object:Gem::Dependency
54
54
  name: middleware
55
55
  requirement: !ruby/object:Gem::Requirement
@@ -168,6 +168,7 @@ files:
168
168
  - lib/no_brainer/document/primary_key/generator.rb
169
169
  - lib/no_brainer/document/readonly.rb
170
170
  - lib/no_brainer/document/serialization.rb
171
+ - lib/no_brainer/document/store.rb
171
172
  - lib/no_brainer/document/table_config.rb
172
173
  - lib/no_brainer/document/table_config/synchronizer.rb
173
174
  - lib/no_brainer/document/timestamps.rb
@@ -241,7 +242,11 @@ files:
241
242
  homepage: http://nobrainer.io
242
243
  licenses:
243
244
  - LGPL-3.0-only
244
- 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
245
250
  post_install_message:
246
251
  rdoc_options: []
247
252
  require_paths: