nobrainer 0.22.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/autoload.rb +1 -1
  3. data/lib/no_brainer/config.rb +54 -14
  4. data/lib/no_brainer/connection.rb +24 -21
  5. data/lib/no_brainer/connection_manager.rb +29 -9
  6. data/lib/no_brainer/criteria/cache.rb +8 -0
  7. data/lib/no_brainer/criteria/changes.rb +16 -0
  8. data/lib/no_brainer/criteria/core.rb +4 -5
  9. data/lib/no_brainer/criteria/eager_load.rb +2 -2
  10. data/lib/no_brainer/criteria/enumerable.rb +3 -1
  11. data/lib/no_brainer/criteria/find.rb +6 -9
  12. data/lib/no_brainer/criteria/first.rb +3 -3
  13. data/lib/no_brainer/criteria/first_or_create.rb +114 -0
  14. data/lib/no_brainer/criteria/join.rb +62 -0
  15. data/lib/no_brainer/criteria/order_by.rb +19 -16
  16. data/lib/no_brainer/criteria/run.rb +26 -0
  17. data/lib/no_brainer/criteria/scope.rb +8 -8
  18. data/lib/no_brainer/criteria/where.rb +130 -107
  19. data/lib/no_brainer/criteria.rb +4 -4
  20. data/lib/no_brainer/document/association/belongs_to.rb +44 -17
  21. data/lib/no_brainer/document/association/core.rb +11 -0
  22. data/lib/no_brainer/document/association/eager_loader.rb +26 -26
  23. data/lib/no_brainer/document/association/has_many.rb +7 -9
  24. data/lib/no_brainer/document/association/has_many_through.rb +1 -2
  25. data/lib/no_brainer/document/association/has_one.rb +5 -1
  26. data/lib/no_brainer/document/association.rb +5 -5
  27. data/lib/no_brainer/document/atomic_ops.rb +40 -7
  28. data/lib/no_brainer/document/attributes.rb +24 -15
  29. data/lib/no_brainer/document/callbacks.rb +1 -1
  30. data/lib/no_brainer/document/core.rb +18 -18
  31. data/lib/no_brainer/document/criteria.rb +9 -5
  32. data/lib/no_brainer/document/dirty.rb +15 -12
  33. data/lib/no_brainer/document/dynamic_attributes.rb +6 -0
  34. data/lib/no_brainer/document/index/index.rb +5 -9
  35. data/lib/no_brainer/document/index/meta_store.rb +1 -10
  36. data/lib/no_brainer/document/index/synchronizer.rb +16 -20
  37. data/lib/no_brainer/document/index.rb +3 -3
  38. data/lib/no_brainer/document/lazy_fetch.rb +3 -3
  39. data/lib/no_brainer/document/missing_attributes.rb +7 -2
  40. data/lib/no_brainer/document/persistance.rb +14 -30
  41. data/lib/no_brainer/document/polymorphic.rb +8 -4
  42. data/lib/no_brainer/document/primary_key/generator.rb +6 -1
  43. data/lib/no_brainer/document/primary_key.rb +19 -4
  44. data/lib/no_brainer/document/serialization.rb +0 -2
  45. data/lib/no_brainer/document/table_config/synchronizer.rb +21 -0
  46. data/lib/no_brainer/document/table_config.rb +118 -0
  47. data/lib/no_brainer/document/timestamps.rb +8 -0
  48. data/lib/no_brainer/document/validation/core.rb +63 -0
  49. data/lib/no_brainer/document/validation/uniqueness.rb +30 -36
  50. data/lib/no_brainer/document/validation.rb +1 -58
  51. data/lib/no_brainer/document.rb +2 -2
  52. data/lib/no_brainer/error.rb +1 -0
  53. data/lib/no_brainer/geo/base.rb +0 -1
  54. data/lib/no_brainer/locale/en.yml +1 -0
  55. data/lib/no_brainer/lock.rb +12 -8
  56. data/lib/no_brainer/profiler/controller_runtime.rb +76 -0
  57. data/lib/no_brainer/{query_runner → profiler}/logger.rb +11 -27
  58. data/lib/no_brainer/profiler.rb +11 -0
  59. data/lib/no_brainer/query_runner/database_on_demand.rb +8 -8
  60. data/lib/no_brainer/query_runner/driver.rb +3 -1
  61. data/lib/no_brainer/query_runner/missing_index.rb +3 -3
  62. data/lib/no_brainer/query_runner/profiler.rb +43 -0
  63. data/lib/no_brainer/query_runner/reconnect.rb +38 -23
  64. data/lib/no_brainer/query_runner/run_options.rb +35 -15
  65. data/lib/no_brainer/query_runner/table_on_demand.rb +18 -11
  66. data/lib/no_brainer/query_runner.rb +3 -3
  67. data/lib/no_brainer/railtie/database.rake +14 -4
  68. data/lib/no_brainer/railtie.rb +5 -12
  69. data/lib/no_brainer/rql.rb +11 -8
  70. data/lib/no_brainer/symbol_decoration.rb +11 -0
  71. data/lib/no_brainer/system/cluster_config.rb +5 -0
  72. data/lib/no_brainer/system/db_config.rb +5 -0
  73. data/lib/no_brainer/system/document.rb +24 -0
  74. data/lib/no_brainer/system/issue.rb +10 -0
  75. data/lib/no_brainer/system/job.rb +10 -0
  76. data/lib/no_brainer/system/log.rb +11 -0
  77. data/lib/no_brainer/system/server_config.rb +7 -0
  78. data/lib/no_brainer/system/server_status.rb +9 -0
  79. data/lib/no_brainer/system/stat.rb +11 -0
  80. data/lib/no_brainer/system/table_config.rb +10 -0
  81. data/lib/no_brainer/system/table_status.rb +8 -0
  82. data/lib/no_brainer/system.rb +17 -0
  83. data/lib/nobrainer.rb +16 -11
  84. data/lib/rails/generators/nobrainer/install_generator.rb +48 -0
  85. data/lib/rails/generators/nobrainer/{model/model_generator.rb → model_generator.rb} +7 -3
  86. data/lib/rails/generators/nobrainer/namespace_fix.rb +15 -0
  87. data/lib/rails/generators/{nobrainer/model/templates/model.rb.tt → templates/model.rb} +1 -1
  88. data/lib/rails/generators/templates/nobrainer.rb +101 -0
  89. metadata +34 -10
  90. data/lib/no_brainer/document/store_in.rb +0 -35
  91. data/lib/rails/generators/nobrainer.rb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c53755e5f9ec902612c417fcf053404cf114ca0b
4
- data.tar.gz: 529fe4b627d366ba4813fd6ff750192caf0df316
3
+ metadata.gz: 8406e0c14de6dc8379b7047aa6d7254675e1b1fb
4
+ data.tar.gz: 0d9906ef55e4f74e5317a6d2377d4c1f19ac9799
5
5
  SHA512:
6
- metadata.gz: 920d6b1c7bec9acd28b4a0e7f54a9967d068a01d31174396ca961eaf8cbfec31e3b8397ab2242ac87c73b9ca3789e80b47c8ca4b809d7a71b5fa80aeedb81f4b
7
- data.tar.gz: 92144b91eb03ac14d2a01ea5244480b215286228691e5406a9f7bcf218499853ecfb0d1313f1380f62d509d97dc2bb1dac65ae8e97c0c2c3cc2b7b4c3835e1c2
6
+ metadata.gz: fdcbf5021c15bad13546e7b346eb73fb7f6d0b62ab861578110b0ab81aafda8c364a4b6fe9b282d0f8f8c6d732ebd8dcc3f5be9aada980b79d34d93b0fed6fea
7
+ data.tar.gz: afa546845837919bb06b01c7ba4168626b74263244986d4d9d3a7428b9b68b24faa968572f110546fb5afb9c4b57dbb236d5c48556b9ae691d271deea62ff816
@@ -14,7 +14,7 @@ module NoBrainer::Autoload
14
14
  end
15
15
 
16
16
  def autoload_and_include(*constants)
17
- constants.each { |constant| autoload constant }
17
+ autoload(*constants)
18
18
  constants.each { |constant| include const_get(constant) }
19
19
  end
20
20
  end
@@ -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
- :distributed_lock_class => { :default => ->{ NoBrainer::Lock } },
18
- :lock_options => { :default => ->{ { :expire => 60, :timeout => 10 } } },
20
+ :distributed_lock_class => { :default => ->{ "NoBrainer::Lock" } },
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,22 +60,31 @@ 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
59
69
  @configured = true
60
70
 
61
- NoBrainer::ConnectionManager.disconnect_if_url_changed
71
+ NoBrainer::ConnectionManager.notify_url_change
62
72
  end
63
73
 
64
74
  def configured?
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
@@ -107,18 +135,23 @@ module NoBrainer::Config
107
135
  dev_mode? ? 1 : 15
108
136
  end
109
137
 
138
+ # XXX Not referencing NoBrainer::Document::PrimaryKey::Generator::MACHINE_ID_MASK
139
+ # because we don't want to load all the document code to speedup boot time.
140
+ MACHINE_ID_BITS = 24
141
+ MACHINE_ID_MASK = (1 << MACHINE_ID_BITS)-1
142
+
110
143
  def default_machine_id
144
+ return ENV['MACHINE_ID'] if ENV['MACHINE_ID']
145
+
111
146
  require 'socket'
112
147
  require 'digest/md5'
113
148
 
114
- return ENV['MACHINE_ID'] if ENV['MACHINE_ID']
115
-
116
149
  host = Socket.gethostname
117
150
  if host.in? %w(127.0.0.1 localhost)
118
151
  raise "Please configure NoBrainer::Config.machine_id due to lack of appropriate hostname (Socket.gethostname = #{host})"
119
152
  end
120
153
 
121
- Digest::MD5.digest(host).unpack("N")[0] & NoBrainer::Document::PrimaryKey::Generator::MACHINE_ID_MASK
154
+ Digest::MD5.digest(host).unpack("N")[0] & MACHINE_ID_MASK
122
155
  end
123
156
 
124
157
  def machine_id=(machine_id)
@@ -127,9 +160,16 @@ module NoBrainer::Config
127
160
  when /^[0-9]+$/ then machine_id.to_i
128
161
  else raise "Invalid machine_id"
129
162
  end
130
- max_id = NoBrainer::Document::PrimaryKey::Generator::MACHINE_ID_MASK
163
+ max_id = MACHINE_ID_MASK
131
164
  raise "Invalid machine_id (must be between 0 and #{max_id})" unless machine_id.in?(0..max_id)
132
165
  @machine_id = machine_id
133
166
  end
167
+
168
+ def distributed_lock_class
169
+ if @distributed_lock_class.is_a?(String)
170
+ @distributed_lock_class = @distributed_lock_class.constantize
171
+ end
172
+ @distributed_lock_class
173
+ end
134
174
  end
135
175
  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,33 +25,37 @@ 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
42
+ def default_db
43
+ parsed_uri[:db]
44
+ end
45
+
46
+ def current_db
47
+ NoBrainer.current_run_options.try(:[], :db) || default_db
43
48
  end
44
49
 
45
50
  def drop!
46
- # XXX Thread.current[:nobrainer_options] is set by NoBrainer::QueryRunner::RunOptions
47
- db = (Thread.current[:nobrainer_options] || parsed_uri)[:db]
48
- db_drop(db)['dropped'] == 1
51
+ NoBrainer.run { |r| r.db_drop(current_db) }
49
52
  end
50
53
 
51
- # Note that truncating each table (purge) is much faster than dropping the
52
- # database (drop)
54
+ # Note that truncating each table (purge!) is much faster than dropping the database (drop!)
53
55
  def purge!
54
- table_list.each do |table_name|
55
- next if table_name == 'nobrainer_index_meta' # keeping 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
58
+ next if table_name == NoBrainer::Document::Index::MetaStore.table_name
56
59
  NoBrainer.run { |r| r.table(table_name).delete }
57
60
  end
58
61
  true
@@ -4,17 +4,30 @@ 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 }
7
+ @lock.synchronize { block.call }
8
+ end
9
+
10
+ def warn_for_other_orms
11
+ if defined?(ActiveRecord) && NoBrainer::Config.warn_on_active_record
12
+ STDERR.puts "[NoBrainer] ActiveRecord is loaded which is probably not what you want."
13
+ STDERR.puts "[NoBrainer] Follow the instructions on http://nobrainer.io/docs/configuration/#removing_activerecord"
14
+ STDERR.puts "[NoBrainer] Configure NoBrainer with 'config.warn_on_active_record = false' to disable with warning."
15
+ end
16
+
17
+ if defined?(Mongoid)
18
+ STDERR.puts "[NoBrainer] WARNING: Mongoid is loaded, and we conflict on the symbol decorations"
19
+ STDERR.puts "[NoBrainer] They are used in queries such as Model.where(:tags.in => ['fun', 'stuff'])"
20
+ STDERR.puts "[NoBrainer] This is a problem!"
11
21
  end
12
22
  end
13
23
 
14
24
  def get_new_connection
15
- url = NoBrainer::Config.rethinkdb_url
16
- raise "Please specify a database connection to RethinkDB" unless url
17
- NoBrainer::Connection.new(url)
25
+ # We don't want to warn on "rails g nobrainer:install", but because it's
26
+ # hard to check when the generator is running because of spring as it wipes
27
+ # ARGV. So we check for other ORMs during the connection instantiation.
28
+ warn_for_other_orms
29
+
30
+ NoBrainer::Connection.new(get_next_url)
18
31
  end
19
32
 
20
33
  def current_connection
@@ -33,6 +46,12 @@ module NoBrainer::ConnectionManager
33
46
  end
34
47
  end
35
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
+
36
55
  def connection
37
56
  c = self.current_connection
38
57
  return c if c
@@ -51,10 +70,11 @@ module NoBrainer::ConnectionManager
51
70
  synchronize { _disconnect }
52
71
  end
53
72
 
54
- def disconnect_if_url_changed
73
+ def notify_url_change
55
74
  synchronize do
75
+ @urls = nil
56
76
  c = current_connection
57
- _disconnect if c && c.uri != NoBrainer::Config.rethinkdb_url
77
+ _disconnect if c && !NoBrainer::Config.rethinkdb_urls.include?(c.uri)
58
78
  end
59
79
  end
60
80
  end
@@ -77,4 +77,12 @@ module NoBrainer::Criteria::Cache
77
77
 
78
78
  use_cache_for :first, :last, :count, :empty?, :any?
79
79
  reload_on :update_all, :destroy_all, :delete_all
80
+
81
+ private
82
+
83
+ def apply_named_scope(name, args, block)
84
+ return super unless with_cache?
85
+ @scope_cache ||= {}
86
+ @scope_cache[[name, args, block]] ||= super
87
+ end
80
88
  end
@@ -0,0 +1,16 @@
1
+ module NoBrainer::Criteria::Changes
2
+ extend ActiveSupport::Concern
3
+
4
+ def changes(*args)
5
+ return finalized_criteria.changes(*args) unless finalized?
6
+
7
+ # We won't do any instantiations with attributes for now.
8
+ raise 'Please use .raw.changes()' unless raw?
9
+
10
+ # We can't have implicit sorting as eager streams are not
11
+ # supported by r.changes().
12
+ criteria = self
13
+ criteria = criteria.without_ordering if ordering_mode == :implicit
14
+ run { criteria.to_rql.changes(*args) }
15
+ end
16
+ end
@@ -32,11 +32,6 @@ module NoBrainer::Criteria::Core
32
32
  to_rql.inspect rescue super
33
33
  end
34
34
 
35
- def run(&block)
36
- block ||= proc { to_rql }
37
- NoBrainer.run(:criteria => self, &block)
38
- end
39
-
40
35
  def merge!(criteria, options={})
41
36
  criteria.options.each do |k,v|
42
37
  merge_proc = self.class.options_definitions[k]
@@ -105,5 +100,9 @@ module NoBrainer::Criteria::Core
105
100
  def append_array(a, b)
106
101
  a ? a+b : b
107
102
  end
103
+
104
+ def merge_hash(a, b)
105
+ a ? a.merge(b) : b
106
+ end
108
107
  end
109
108
  end
@@ -15,7 +15,7 @@ module NoBrainer::Criteria::EagerLoad
15
15
  def merge!(criteria, options={})
16
16
  super.tap do
17
17
  # If we already have some cached documents, and we need to so some eager
18
- # loading, then we it now. It's easier than doing it lazily.
18
+ # loading, then we do it now. It's easier than doing it lazily.
19
19
  if self.cached? && criteria.options[:eager_load].present?
20
20
  perform_eager_load(@cache)
21
21
  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
@@ -3,7 +3,7 @@ module NoBrainer::Criteria::Enumerable
3
3
 
4
4
  def each(options={}, &block)
5
5
  return enum_for(:each, options) unless block
6
- self.run.each { |attrs| block.call(instantiate_doc(attrs)) }
6
+ run.each { |attrs| block.call(instantiate_doc(attrs)) }
7
7
  self
8
8
  end
9
9
 
@@ -21,4 +21,6 @@ module NoBrainer::Criteria::Enumerable
21
21
  return super unless [].respond_to?(name)
22
22
  to_a.__send__(name, *args, &block)
23
23
  end
24
+
25
+ delegate :as_json, :to => :each
24
26
  end
@@ -1,27 +1,24 @@
1
1
  module NoBrainer::Criteria::Find
2
2
  extend ActiveSupport::Concern
3
3
 
4
- def find_by?(*args, &block)
5
- where(*args, &block).first
6
- end
7
-
8
4
  def find_by(*args, &block)
9
- find_by?(*args, &block).tap { |doc| raise_not_found(args) unless doc }
5
+ raise "find_by() has unclear semantics. Please use where().first instead"
10
6
  end
11
7
  alias_method :find_by!, :find_by
8
+ alias_method :find_by?, :find_by
12
9
 
13
10
  def find?(pk)
14
- without_ordering.find_by?(model.pk_name => pk)
11
+ without_ordering.where(model.pk_name => pk).first
15
12
  end
16
13
 
17
14
  def find(pk)
18
- without_ordering.find_by(model.pk_name => pk)
15
+ find?(pk) || raise_not_found(pk)
19
16
  end
20
17
  alias_method :find!, :find
21
18
 
22
19
  private
23
20
 
24
- def raise_not_found(args)
25
- raise NoBrainer::Error::DocumentNotFound, "#{model} #{args.inspect.gsub(/\[{(.*)}\]/, '\1')} not found"
21
+ def raise_not_found(pk)
22
+ raise NoBrainer::Error::DocumentNotFound, "#{model} :#{model.pk_name}=>#{pk.inspect} not found"
26
23
  end
27
24
  end
@@ -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)
@@ -26,6 +26,6 @@ module NoBrainer::Criteria::First
26
26
  private
27
27
 
28
28
  def get_one(criteria)
29
- instantiate_doc(criteria.limit(1).run.first)
29
+ instantiate_doc(criteria.limit(1).__send__(:run).first)
30
30
  end
31
31
  end
@@ -0,0 +1,114 @@
1
+ module NoBrainer::Criteria::FirstOrCreate
2
+ extend ActiveSupport::Concern
3
+
4
+ def first_or_create(create_params={}, save_options={}, &block)
5
+ _first_or_create(create_params, save_options.merge(:save_method => :save?), &block)
6
+ end
7
+
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!))
18
+ end
19
+
20
+ private
21
+
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)
31
+ raise "Cannot use .raw() with .first_or_create()" if raw?
32
+ raise "Use first_or_create() on the root class `#{model.root_class}'" unless model.is_root_class?
33
+
34
+ where_params = extract_where_params()
35
+
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)
40
+ # We could do without a uniqueness validator, but it's much preferable to
41
+ # have it, so that we don't conflict with others create(), not just others
42
+ # first_or_create().
43
+ raise "Please add the following uniqueness validator for first_or_create():\n" +
44
+ "class #{model}\n" +
45
+ case keys.size
46
+ when 1 then " field :#{keys.first}, :uniq => true"
47
+ when 2 then " field :#{keys.first}, :uniq => {:scope => :#{keys.last}}"
48
+ else " field :#{keys.first}, :uniq => {:scope => #{keys[1..-1].inspect}}"
49
+ end +
50
+ "\nend"
51
+ end
52
+
53
+ # We don't want to access create_params yet, because invoking the block
54
+ # might be costly (the user might be doing some API call or w/e), and
55
+ # so we want to invoke the block only if necessary.
56
+ new_instance = model.new(where_params)
57
+ lock_key_name = model._uniqueness_key_name_from_params(where_params)
58
+ new_instance._lock_for_uniqueness_once(lock_key_name)
59
+
60
+ old_instance = self.first
61
+ return old_instance if old_instance
62
+
63
+ create_params = block.call if block
64
+ create_params = create_params.symbolize_keys
65
+
66
+ keys_in_conflict = create_params.keys & where_params.keys
67
+ keys_in_conflict = keys_in_conflict.reject { |k| create_params[k] == where_params[k] }
68
+ unless keys_in_conflict.empty?
69
+ raise "where() and first_or_create() were given conflicting values " +
70
+ "on the following keys: #{keys_in_conflict.inspect}"
71
+ end
72
+
73
+ if create_params[:_type]
74
+ # We have to recreate the instance because we are given a _type in
75
+ # create_params specifying a subclass. We'll have to transfert the lock
76
+ # ownership to that new instance.
77
+ new_instance = model.model_from_attrs(create_params).new(where_params).tap do |i|
78
+ i.locked_keys_for_uniqueness = new_instance.locked_keys_for_uniqueness
79
+ end
80
+ end
81
+
82
+ new_instance.assign_attributes(create_params)
83
+ save_method = save_options.delete(:save_method)
84
+ new_instance.__send__(save_method, save_options)
85
+ return new_instance
86
+ ensure
87
+ new_instance.try(:unlock_unique_fields)
88
+ end
89
+
90
+ def extract_where_params()
91
+ where_clauses = finalized_criteria.options[:where_ast]
92
+
93
+ unless where_clauses.is_a?(NoBrainer::Criteria::Where::MultiOperator) &&
94
+ where_clauses.op == :and && where_clauses.clauses.size > 0 &&
95
+ where_clauses.clauses.all? do |c|
96
+ c.is_a?(NoBrainer::Criteria::Where::BinaryOperator) &&
97
+ c.op == :eq && c.key_modifier == :scalar
98
+ end
99
+ raise "Please use a query of the form `.where(...).first_or_create(...)'"
100
+ end
101
+
102
+ Hash[where_clauses.clauses.map do |c|
103
+ raise "You may not use nested hash queries with first_or.create()" if c.key_path.size > 1
104
+ [c.key_path.first.to_sym, c.value]
105
+ end]
106
+ end
107
+
108
+ def get_model_unique_fields
109
+ [[model.pk_name]] +
110
+ model.unique_validators
111
+ .flat_map { |validator| validator.attributes.map { |attr| [attr, validator] } }
112
+ .map { |f, validator| [f, *validator.scope].map(&:to_sym).sort }
113
+ end
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