nobrainer 0.27.0 → 0.28.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/config.rb +36 -8
  3. data/lib/no_brainer/connection.rb +16 -19
  4. data/lib/no_brainer/connection_manager.rb +10 -10
  5. data/lib/no_brainer/criteria.rb +1 -1
  6. data/lib/no_brainer/criteria/eager_load.rb +1 -1
  7. data/lib/no_brainer/criteria/find.rb +1 -1
  8. data/lib/no_brainer/criteria/first.rb +2 -2
  9. data/lib/no_brainer/criteria/first_or_create.rb +32 -19
  10. data/lib/no_brainer/criteria/join.rb +62 -0
  11. data/lib/no_brainer/criteria/where.rb +25 -14
  12. data/lib/no_brainer/document.rb +1 -1
  13. data/lib/no_brainer/document/association/belongs_to.rb +4 -3
  14. data/lib/no_brainer/document/association/eager_loader.rb +26 -25
  15. data/lib/no_brainer/document/association/has_many.rb +3 -2
  16. data/lib/no_brainer/document/association/has_many_through.rb +1 -2
  17. data/lib/no_brainer/document/association/has_one.rb +4 -0
  18. data/lib/no_brainer/document/atomic_ops.rb +31 -4
  19. data/lib/no_brainer/document/attributes.rb +12 -9
  20. data/lib/no_brainer/document/core.rb +18 -18
  21. data/lib/no_brainer/document/criteria.rb +3 -2
  22. data/lib/no_brainer/document/dirty.rb +3 -3
  23. data/lib/no_brainer/document/index.rb +3 -3
  24. data/lib/no_brainer/document/index/index.rb +5 -5
  25. data/lib/no_brainer/document/index/meta_store.rb +1 -1
  26. data/lib/no_brainer/document/index/synchronizer.rb +5 -17
  27. data/lib/no_brainer/document/missing_attributes.rb +7 -2
  28. data/lib/no_brainer/document/primary_key.rb +14 -8
  29. data/lib/no_brainer/document/table_config.rb +118 -0
  30. data/lib/no_brainer/document/table_config/synchronizer.rb +21 -0
  31. data/lib/no_brainer/document/timestamps.rb +4 -0
  32. data/lib/no_brainer/document/validation/core.rb +1 -1
  33. data/lib/no_brainer/document/validation/uniqueness.rb +1 -1
  34. data/lib/no_brainer/lock.rb +4 -4
  35. data/lib/no_brainer/profiler/logger.rb +1 -1
  36. data/lib/no_brainer/query_runner/database_on_demand.rb +1 -1
  37. data/lib/no_brainer/query_runner/reconnect.rb +37 -21
  38. data/lib/no_brainer/query_runner/table_on_demand.rb +12 -5
  39. data/lib/no_brainer/railtie/database.rake +14 -4
  40. data/lib/no_brainer/rql.rb +3 -2
  41. data/lib/no_brainer/symbol_decoration.rb +1 -1
  42. data/lib/no_brainer/system.rb +17 -0
  43. data/lib/no_brainer/system/cluster_config.rb +5 -0
  44. data/lib/no_brainer/system/db_config.rb +5 -0
  45. data/lib/no_brainer/system/document.rb +24 -0
  46. data/lib/no_brainer/system/issue.rb +10 -0
  47. data/lib/no_brainer/system/job.rb +10 -0
  48. data/lib/no_brainer/system/log.rb +11 -0
  49. data/lib/no_brainer/system/server_config.rb +7 -0
  50. data/lib/no_brainer/system/server_status.rb +9 -0
  51. data/lib/no_brainer/system/stat.rb +11 -0
  52. data/lib/no_brainer/system/table_config.rb +10 -0
  53. data/lib/no_brainer/system/table_status.rb +8 -0
  54. data/lib/nobrainer.rb +7 -6
  55. data/lib/rails/generators/templates/nobrainer.rb +11 -2
  56. metadata +17 -3
  57. data/lib/no_brainer/document/store_in.rb +0 -33
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cc50cb3ae605543bb7ed3df448d0ff32d3b984d4
4
- data.tar.gz: b8ae438c48a0d64408f4966f92002be17f6cdff0
3
+ metadata.gz: 8406e0c14de6dc8379b7047aa6d7254675e1b1fb
4
+ data.tar.gz: 0d9906ef55e4f74e5317a6d2377d4c1f19ac9799
5
5
  SHA512:
6
- metadata.gz: 118724ce72e1c29432aa208b062f9e107b56dc0401d921f5194708a2c3f2b7a95160572cf2cd83cb22f322cad0fb7cfe3939fdccf8eb6ef401da99f975df275b
7
- data.tar.gz: 2d5737b1035583abd7f888f61341b0922d85e654b919aee2c282be2fcd07bb144f3654ef76ad2d016289b5692766d72d7d0e7620f8d13a6923d054acab49de91
6
+ metadata.gz: fdcbf5021c15bad13546e7b346eb73fb7f6d0b62ab861578110b0ab81aafda8c364a4b6fe9b282d0f8f8c6d732ebd8dcc3f5be9aada980b79d34d93b0fed6fea
7
+ data.tar.gz: afa546845837919bb06b01c7ba4168626b74263244986d4d9d3a7428b9b68b24faa968572f110546fb5afb9c4b57dbb236d5c48556b9ae691d271deea62ff816
@@ -4,18 +4,21 @@ module NoBrainer::Config
4
4
  SETTINGS = {
5
5
  :app_name => { :default => ->{ default_app_name } },
6
6
  :environment => { :default => ->{ default_environment } },
7
- :rethinkdb_url => { :default => ->{ default_rethinkdb_url } },
7
+ :rethinkdb_urls => { :default => ->{ [default_rethinkdb_url] } },
8
+ :ssl_options => { :default => ->{ nil } },
8
9
  :logger => { :default => ->{ default_logger } },
9
10
  :colorize_logger => { :default => ->{ true }, :valid_values => [true, false] },
10
11
  :warn_on_active_record => { :default => ->{ true }, :valid_values => [true, false] },
11
12
  :max_retries_on_connection_failure => { :default => ->{ default_max_retries_on_connection_failure } },
12
13
  :durability => { :default => ->{ default_durability }, :valid_values => [:hard, :soft] },
13
- :max_string_length => { :default => -> { 255 } },
14
+ :table_options => { :default => ->{ {:shards => 1, :replicas => 1, :write_acks => :majority} },
15
+ :valid_keys => [:shards, :replicas, :primary_replica_tag, :write_acks, :durability] },
16
+ :max_string_length => { :default => ->{ 255 } },
14
17
  :user_timezone => { :default => ->{ :local }, :valid_values => [:unchanged, :utc, :local] },
15
18
  :db_timezone => { :default => ->{ :utc }, :valid_values => [:unchanged, :utc, :local] },
16
19
  :geo_options => { :default => ->{ {:geo_system => 'WGS84', :unit => 'm'} } },
17
20
  :distributed_lock_class => { :default => ->{ "NoBrainer::Lock" } },
18
- :lock_options => { :default => ->{ { :expire => 60, :timeout => 10 } } },
21
+ :lock_options => { :default => ->{ { :expire => 60, :timeout => 10 } }, :valid_keys => [:expire, :timeout] },
19
22
  :per_thread_connection => { :default => ->{ false }, :valid_values => [true, false] },
20
23
  :machine_id => { :default => ->{ default_machine_id } },
21
24
  :criteria_cache_max_entries => { :default => -> { 10_000 } },
@@ -44,7 +47,12 @@ module NoBrainer::Config
44
47
  end
45
48
 
46
49
  def assert_valid_options
47
- SETTINGS.each { |k,v| assert_array_in(k, v[:valid_values]) if v[:valid_values] }
50
+ SETTINGS.each do |k,v|
51
+ assert_value_in(k, v[:valid_values]) if v[:valid_values]
52
+ assert_hash_keys_in(k, v[:valid_keys]) if v[:valid_keys]
53
+ end
54
+
55
+ validate_urls
48
56
  end
49
57
 
50
58
  def reset!
@@ -52,7 +60,9 @@ module NoBrainer::Config
52
60
  end
53
61
 
54
62
  def configure(&block)
55
- @applied_defaults_for.to_a.each { |k| remove_instance_variable("@#{k}") }
63
+ @applied_defaults_for.to_a.each do |k|
64
+ remove_instance_variable("@#{k}") if instance_variable_defined?("@#{k}")
65
+ end
56
66
  block.call(self) if block
57
67
  apply_defaults
58
68
  assert_valid_options
@@ -65,9 +75,16 @@ module NoBrainer::Config
65
75
  !!@configured
66
76
  end
67
77
 
68
- def assert_array_in(name, values)
69
- unless __send__(name).in?(values)
70
- raise ArgumentError.new("Unknown configuration for #{name}: #{__send__(name)}. Valid values are: #{values.inspect}")
78
+ def assert_value_in(name, valid_values)
79
+ unless __send__(name).in?(valid_values)
80
+ raise ArgumentError.new("Invalid configuration for #{name}: #{__send__(name)}. Valid values are: #{valid_values.inspect}")
81
+ end
82
+ end
83
+
84
+ def assert_hash_keys_in(name, valid_keys)
85
+ extra_keys = __send__(name).keys - valid_keys
86
+ unless extra_keys.empty?
87
+ raise ArgumentError.new("Invalid configuration for #{name}: #{__send__(name)}. Valid keys are: #{valid_keys.inspect}")
71
88
  end
72
89
  end
73
90
 
@@ -84,6 +101,10 @@ module NoBrainer::Config
84
101
  ENV['RUBY_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || :production
85
102
  end
86
103
 
104
+ def rethinkdb_url=(value)
105
+ self.rethinkdb_urls = [*value]
106
+ end
107
+
87
108
  def default_rethinkdb_url
88
109
  db = ENV['RETHINKDB_DB'] || ENV['RDB_DB']
89
110
  db ||= "#{self.app_name}_#{self.environment}" if self.app_name && self.environment
@@ -95,6 +116,13 @@ module NoBrainer::Config
95
116
  url
96
117
  end
97
118
 
119
+ def validate_urls
120
+ # This is not connecting, just validating the format.
121
+ dbs = rethinkdb_urls.compact.map { |url| NoBrainer::Connection.new(url).parsed_uri[:db] }.uniq
122
+ raise "Please specify at least one rethinkdb_url" if dbs.size == 0
123
+ raise "All the rethinkdb_urls must specify the same db name (instead of #{dbs.inspect})" if dbs.size != 1
124
+ end
125
+
98
126
  def default_logger
99
127
  defined?(Rails.logger) ? Rails.logger : Logger.new(STDERR).tap { |l| l.level = Logger::WARN }
100
128
  end
@@ -1,17 +1,16 @@
1
1
  require 'rethinkdb'
2
+ require 'uri'
2
3
 
3
4
  class NoBrainer::Connection
4
- attr_accessor :uri, :parsed_uri
5
+ attr_accessor :parsed_uri
5
6
 
6
7
  def initialize(uri)
7
- self.uri = uri
8
- parsed_uri # just to raise an exception if there is a problem.
8
+ parse_uri(uri)
9
9
  end
10
10
 
11
- def parsed_uri
12
- @parsed_uri ||= begin
13
- require 'uri'
14
- uri = URI.parse(self.uri)
11
+ def parse_uri(uri)
12
+ @parsed_uri = begin
13
+ uri = URI.parse(uri)
15
14
 
16
15
  if uri.scheme != 'rethinkdb'
17
16
  raise NoBrainer::Error::Connection,
@@ -26,22 +25,20 @@ class NoBrainer::Connection
26
25
  end
27
26
  end
28
27
 
28
+ def uri
29
+ "rethinkdb://#{'****@' if parsed_uri[:auth_key]}#{parsed_uri[:host]}:#{parsed_uri[:port]}/#{parsed_uri[:db]}"
30
+ end
31
+
29
32
  def raw
30
- @raw ||= RethinkDB::Connection.new(parsed_uri)
33
+ options = parsed_uri
34
+ options = options.merge(:ssl => NoBrainer::Config.ssl_options) if NoBrainer::Config.ssl_options
35
+ @raw ||= RethinkDB::Connection.new(options).tap { NoBrainer.logger.info("Connected to #{uri}") }
31
36
  end
32
37
 
33
38
  delegate :reconnect, :close, :run, :to => :raw
34
39
  alias_method :connect, :raw
35
40
  alias_method :disconnect, :close
36
41
 
37
- [:db_create, :db_drop, :db_list, :table_create, :table_drop, :table_list].each do |cmd|
38
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
39
- def #{cmd}(*args)
40
- NoBrainer.run { |r| r.#{cmd}(*args) }
41
- end
42
- RUBY
43
- end
44
-
45
42
  def default_db
46
43
  parsed_uri[:db]
47
44
  end
@@ -51,13 +48,13 @@ class NoBrainer::Connection
51
48
  end
52
49
 
53
50
  def drop!
54
- db_drop(current_db)['dropped'] == 1
51
+ NoBrainer.run { |r| r.db_drop(current_db) }
55
52
  end
56
53
 
57
54
  # Note that truncating each table (purge!) is much faster than dropping the database (drop!)
58
55
  def purge!
59
- table_list.each do |table_name|
60
- # keeping the index meta store because indexes are not going away
56
+ NoBrainer.run { |r| r.table_list }.each do |table_name|
57
+ # keeping the index meta store because indexes are not going away when purging
61
58
  next if table_name == NoBrainer::Document::Index::MetaStore.table_name
62
59
  NoBrainer.run { |r| r.table(table_name).delete }
63
60
  end
@@ -4,11 +4,7 @@ module NoBrainer::ConnectionManager
4
4
  @lock = Mutex.new
5
5
 
6
6
  def synchronize(&block)
7
- if NoBrainer::Config.per_thread_connection
8
- block.call
9
- else
10
- @lock.synchronize { block.call }
11
- end
7
+ @lock.synchronize { block.call }
12
8
  end
13
9
 
14
10
  def warn_for_other_orms
@@ -26,15 +22,12 @@ module NoBrainer::ConnectionManager
26
22
  end
27
23
 
28
24
  def get_new_connection
29
- url = NoBrainer::Config.rethinkdb_url
30
- raise "Please specify a database connection to RethinkDB" unless url
31
-
32
25
  # We don't want to warn on "rails g nobrainer:install", but because it's
33
26
  # hard to check when the generator is running because of spring as it wipes
34
27
  # ARGV. So we check for other ORMs during the connection instantiation.
35
28
  warn_for_other_orms
36
29
 
37
- NoBrainer::Connection.new(url)
30
+ NoBrainer::Connection.new(get_next_url)
38
31
  end
39
32
 
40
33
  def current_connection
@@ -53,6 +46,12 @@ module NoBrainer::ConnectionManager
53
46
  end
54
47
  end
55
48
 
49
+ def get_next_url
50
+ @urls ||= NoBrainer::Config.rethinkdb_urls.shuffle
51
+ @cycle_index = (@cycle_index || 0) + 1
52
+ @urls[@cycle_index % @urls.size] # not using .cycle due to threading issues
53
+ end
54
+
56
55
  def connection
57
56
  c = self.current_connection
58
57
  return c if c
@@ -73,8 +72,9 @@ module NoBrainer::ConnectionManager
73
72
 
74
73
  def notify_url_change
75
74
  synchronize do
75
+ @urls = nil
76
76
  c = current_connection
77
- _disconnect if c && c.uri != NoBrainer::Config.rethinkdb_url
77
+ _disconnect if c && !NoBrainer::Config.rethinkdb_urls.include?(c.uri)
78
78
  end
79
79
  end
80
80
  end
@@ -5,5 +5,5 @@ class NoBrainer::Criteria
5
5
  autoload_and_include :Core, :Run, :Raw, :Scope, :AfterFind, :Where, :OrderBy,
6
6
  :Limit, :Pluck, :Count, :Delete, :Enumerable, :Find,
7
7
  :First, :FirstOrCreate, :Changes, :Aggregate, :EagerLoad,
8
- :Update, :Cache, :Index, :Extend
8
+ :Update, :Cache, :Index, :Extend, :Join
9
9
  end
@@ -45,7 +45,7 @@ module NoBrainer::Criteria::EagerLoad
45
45
 
46
46
  def perform_eager_load(docs)
47
47
  if should_eager_load? && docs.present?
48
- NoBrainer::Document::Association::EagerLoader.new.eager_load(docs, @options[:eager_load])
48
+ NoBrainer.eager_load(docs, @options[:eager_load])
49
49
  end
50
50
  end
51
51
  end
@@ -12,7 +12,7 @@ module NoBrainer::Criteria::Find
12
12
  end
13
13
 
14
14
  def find(pk)
15
- find?(pk).tap { |doc| raise_not_found(pk) unless doc }
15
+ find?(pk) || raise_not_found(pk)
16
16
  end
17
17
  alias_method :find!, :find
18
18
 
@@ -10,11 +10,11 @@ module NoBrainer::Criteria::First
10
10
  end
11
11
 
12
12
  def first!
13
- first.tap { |doc| raise NoBrainer::Error::DocumentNotFound unless doc }
13
+ first || (raise NoBrainer::Error::DocumentNotFound)
14
14
  end
15
15
 
16
16
  def last!
17
- last.tap { |doc| raise NoBrainer::Error::DocumentNotFound unless doc }
17
+ last || (raise NoBrainer::Error::DocumentNotFound)
18
18
  end
19
19
 
20
20
  def sample(n=nil)
@@ -1,31 +1,42 @@
1
1
  module NoBrainer::Criteria::FirstOrCreate
2
2
  extend ActiveSupport::Concern
3
3
 
4
- def first_or_create(create_params={}, &block)
5
- _first_or_create(create_params, :save_method => :save?, &block)
4
+ def first_or_create(create_params={}, save_options={}, &block)
5
+ _first_or_create(create_params, save_options.merge(:save_method => :save?), &block)
6
6
  end
7
7
 
8
- def first_or_create!(create_params={}, &block)
9
- _first_or_create(create_params, :save_method => :save!, &block)
8
+ def first_or_create!(create_params={}, save_options={}, &block)
9
+ _first_or_create(create_params, save_options.merge(:save_method => :save!), &block)
10
+ end
11
+
12
+ def upsert(attrs, save_options={})
13
+ _upsert(attrs, save_options.merge(:save_method => :save?))
14
+ end
15
+
16
+ def upsert!(attrs, save_options={})
17
+ _upsert(attrs, save_options.merge(:save_method => :save!))
10
18
  end
11
19
 
12
20
  private
13
21
 
14
- def _first_or_create(create_params={}, options={}, &block)
22
+ def _upsert(attrs, save_options)
23
+ attrs = attrs.symbolize_keys
24
+ unique_keys = get_model_unique_fields.detect { |keys| keys & attrs.keys == keys }
25
+ raise "Could not find a uniqueness validator within `#{attrs.keys.inspect}'.\n" +
26
+ "Please add a corresponding uniqueness validator" unless unique_keys
27
+ where(attrs.slice(*unique_keys)).__send__(:_first_or_create, attrs, save_options)
28
+ end
29
+
30
+ def _first_or_create(create_params, save_options, &block)
15
31
  raise "Cannot use .raw() with .first_or_create()" if raw?
16
32
  raise "Use first_or_create() on the root class `#{model.root_class}'" unless model.is_root_class?
17
33
 
18
34
  where_params = extract_where_params()
19
- keys = where_params.keys
20
35
 
21
- # When matching on the primary key, we'll just pretend that we have a
22
- # uniqueness validator on it. We will be racy against other create(),
23
- # but not on first_or_create().
24
- # And if we get caught in a race with another create(), we'll just have a
25
- # duplicate primary key exception.
26
- matched_validator = true if keys == [model.pk_name]
27
- matched_validator ||= !!get_uniqueness_validators_map[keys.sort]
28
- unless matched_validator
36
+ # Note that we are not matching a subset of the keys on the uniqueness
37
+ # validators; we need an exact match on the keys.
38
+ keys = where_params.keys
39
+ unless get_model_unique_fields.include?(keys.sort)
29
40
  # We could do without a uniqueness validator, but it's much preferable to
30
41
  # have it, so that we don't conflict with others create(), not just others
31
42
  # first_or_create().
@@ -53,7 +64,7 @@ module NoBrainer::Criteria::FirstOrCreate
53
64
  create_params = create_params.symbolize_keys
54
65
 
55
66
  keys_in_conflict = create_params.keys & where_params.keys
56
- keys_in_conflict = keys_in_conflict.select { |k| create_params[k] == where_params[k] }
67
+ keys_in_conflict = keys_in_conflict.reject { |k| create_params[k] == where_params[k] }
57
68
  unless keys_in_conflict.empty?
58
69
  raise "where() and first_or_create() were given conflicting values " +
59
70
  "on the following keys: #{keys_in_conflict.inspect}"
@@ -69,7 +80,8 @@ module NoBrainer::Criteria::FirstOrCreate
69
80
  end
70
81
 
71
82
  new_instance.assign_attributes(create_params)
72
- new_instance.__send__(options[:save_method])
83
+ save_method = save_options.delete(:save_method)
84
+ new_instance.__send__(save_method, save_options)
73
85
  return new_instance
74
86
  ensure
75
87
  new_instance.try(:unlock_unique_fields)
@@ -93,9 +105,10 @@ module NoBrainer::Criteria::FirstOrCreate
93
105
  end]
94
106
  end
95
107
 
96
- def get_uniqueness_validators_map
97
- Hash[model.unique_validators
108
+ def get_model_unique_fields
109
+ [[model.pk_name]] +
110
+ model.unique_validators
98
111
  .flat_map { |validator| validator.attributes.map { |attr| [attr, validator] } }
99
- .map { |f, validator| [[f, *validator.scope].map(&:to_sym).sort, validator] }]
112
+ .map { |f, validator| [f, *validator.scope].map(&:to_sym).sort }
100
113
  end
101
114
  end
@@ -0,0 +1,62 @@
1
+ module NoBrainer::Criteria::Join
2
+ extend ActiveSupport::Concern
3
+
4
+ included { criteria_option :join, :merge_with => :append_array }
5
+
6
+ def join(*values)
7
+ chain(:join => values)
8
+ end
9
+
10
+ private
11
+
12
+ def _compile_join_ast(value)
13
+ case value
14
+ when Hash then
15
+ value.reduce({}) do |h, (k,v)|
16
+ association = model.association_metadata[k.to_sym]
17
+ raise "`#{k}' must be an association on `#{model}'" unless association
18
+ raise "join() does not support through associations" if association.options[:through]
19
+
20
+ criteria = association.base_criteria
21
+ criteria = case v
22
+ when NoBrainer::Criteria then criteria.merge(v)
23
+ when true then criteria
24
+ else criteria.join(v)
25
+ end
26
+ h.merge(association => criteria)
27
+ end
28
+ when Array then value.map { |v| _compile_join_ast(v) }.reduce({}, :merge)
29
+ else _compile_join_ast(value => true)
30
+ end
31
+ end
32
+
33
+ def join_ast
34
+ @join_ast ||= _compile_join_ast(@options[:join])
35
+ end
36
+
37
+ def _instantiate_model(attrs, options={})
38
+ return super unless @options[:join] && !raw?
39
+
40
+ associated_instances = join_ast.map do |association, criteria|
41
+ [association, criteria.send(:_instantiate_model, attrs.delete(association.target_name.to_s))]
42
+ end
43
+ super(attrs, options).tap do |instance|
44
+ associated_instances.each do |association, assoc_instance|
45
+ instance.associations[association].preload([assoc_instance])
46
+ end
47
+ end
48
+ end
49
+
50
+ def compile_rql_pass2
51
+ return super unless @options[:join]
52
+
53
+ join_ast.reduce(super) do |rql, (association, criteria)|
54
+ rql.concat_map do |doc|
55
+ key = doc[association.eager_load_owner_key]
56
+ criteria.where(association.eager_load_target_key => key).to_rql.map do |assoc_doc|
57
+ doc.merge(association.target_name => assoc_doc)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -11,6 +11,10 @@ module NoBrainer::Criteria::Where
11
11
  chain(:where_ast => parse_clause([*args, block].compact))
12
12
  end
13
13
 
14
+ def _where(*args, &block)
15
+ chain(:where_ast => parse_clause([*args, block].compact, :unsafe => true))
16
+ end
17
+
14
18
  def self.merge_where_ast(a, b)
15
19
  (a ? MultiOperator.new(:and, [a, b]) : b).simplify
16
20
  end
@@ -90,20 +94,19 @@ module NoBrainer::Criteria::Where
90
94
 
91
95
  def simplify
92
96
  new_key_path = cast_key_path(key_path)
93
- new_op, new_value = case op
97
+ new_key_modifier, new_op, new_value = case op
94
98
  when :in then
95
99
  case value
96
- when Range then [:between, (cast_value(value.min)..cast_value(value.max))]
97
- when Array then [:in, value.map(&method(:cast_value)).uniq]
100
+ when Range then [key_modifier, :between, (cast_value(value.min)..cast_value(value.max))]
101
+ when Array then [key_modifier, :in, value.map(&method(:cast_value)).uniq]
98
102
  else raise ArgumentError.new "`in' takes an array/range, not #{value}"
99
103
  end
100
- when :between then [op, (cast_value(value.min)..cast_value(value.max))]
101
- when :defined
102
- raise "Incorrect use of `#{op}' and `#{key_modifier}'" if key_modifier != :scalar
103
- [op, cast_value(value)]
104
- else [op, cast_value(value)]
104
+ when :between then [key_modifier, op, (cast_value(value.min)..cast_value(value.max))]
105
+ when :include then ensure_scalar_for(op); [:any, :eq, cast_value(value)]
106
+ when :defined, :undefined then ensure_scalar_for(op); [key_modifier, op, cast_value(value)]
107
+ else [key_modifier, op, cast_value(value)]
105
108
  end
106
- BinaryOperator.new(new_key_path, key_modifier, new_op, new_value, model, true)
109
+ BinaryOperator.new(new_key_path, new_key_modifier, new_op, new_value, model, true)
107
110
  end
108
111
 
109
112
  def to_rql(doc)
@@ -115,7 +118,8 @@ module NoBrainer::Criteria::Where
115
118
  case key_modifier
116
119
  when :scalar then
117
120
  case op
118
- when :defined then value ? doc.has_fields(key) : doc.has_fields(key).not
121
+ when :defined then value ? doc.has_fields(key) : doc.has_fields(key).not
122
+ when :undefined then !value ? doc.has_fields(key) : doc.has_fields(key).not
119
123
  else to_rql_scalar(doc[key])
120
124
  end
121
125
  when :any then doc[key].map { |lvalue| to_rql_scalar(lvalue) }.contains(true)
@@ -142,6 +146,10 @@ module NoBrainer::Criteria::Where
142
146
 
143
147
  private
144
148
 
149
+ def ensure_scalar_for(op)
150
+ raise "Incorrect use of `#{op}' and `#{key_modifier}'" if key_modifier != :scalar
151
+ end
152
+
145
153
  def association
146
154
  return nil if key_path.size > 1
147
155
  @association ||= [model.association_metadata[key_path.first]]
@@ -161,7 +169,7 @@ module NoBrainer::Criteria::Where
161
169
  value.pk_value
162
170
  else
163
171
  case op
164
- when :defined then NoBrainer::Boolean.nobrainer_cast_user_to_model(value)
172
+ when :defined, :undefined then NoBrainer::Boolean.nobrainer_cast_user_to_model(value)
165
173
  when :intersects
166
174
  raise "Use a geo object with `intersects`" unless value.is_a?(NoBrainer::Geo::Base)
167
175
  value
@@ -308,14 +316,17 @@ module NoBrainer::Criteria::Where
308
316
  end if op == :eq
309
317
 
310
318
  nested_prefix = options[:nested_prefix] || []
319
+
320
+ tail_args = [op, value, self.model, !!options[:unsafe]]
321
+
311
322
  case key
312
323
  when Symbol::Decoration
313
324
  raise "Use only one .not, .all or .any modifiers in the query" if key.symbol.is_a?(Symbol::Decoration)
314
325
  case key.decorator
315
- when :any, :all then BinaryOperator.new(nested_prefix + [key.symbol], key.decorator, op, value, self.model)
316
- when :not then UnaryOperator.new(:not, BinaryOperator.new(nested_prefix + [key.symbol], :scalar, op, value, self.model))
326
+ when :any, :all then BinaryOperator.new(nested_prefix + [key.symbol], key.decorator, *tail_args)
327
+ when :not then UnaryOperator.new(:not, BinaryOperator.new(nested_prefix + [key.symbol], :scalar, *tail_args))
317
328
  end
318
- else BinaryOperator.new(nested_prefix + [key], :scalar, op, value, self.model)
329
+ else BinaryOperator.new(nested_prefix + [key], :scalar, *tail_args)
319
330
  end
320
331
  end
321
332