nobrainer 0.27.0 → 0.28.0

Sign up to get free protection for your applications and to get access to all the features.
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