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.
- checksums.yaml +4 -4
- data/lib/no_brainer/autoload.rb +1 -1
- data/lib/no_brainer/config.rb +54 -14
- data/lib/no_brainer/connection.rb +24 -21
- data/lib/no_brainer/connection_manager.rb +29 -9
- data/lib/no_brainer/criteria/cache.rb +8 -0
- data/lib/no_brainer/criteria/changes.rb +16 -0
- data/lib/no_brainer/criteria/core.rb +4 -5
- data/lib/no_brainer/criteria/eager_load.rb +2 -2
- data/lib/no_brainer/criteria/enumerable.rb +3 -1
- data/lib/no_brainer/criteria/find.rb +6 -9
- data/lib/no_brainer/criteria/first.rb +3 -3
- data/lib/no_brainer/criteria/first_or_create.rb +114 -0
- data/lib/no_brainer/criteria/join.rb +62 -0
- data/lib/no_brainer/criteria/order_by.rb +19 -16
- data/lib/no_brainer/criteria/run.rb +26 -0
- data/lib/no_brainer/criteria/scope.rb +8 -8
- data/lib/no_brainer/criteria/where.rb +130 -107
- data/lib/no_brainer/criteria.rb +4 -4
- data/lib/no_brainer/document/association/belongs_to.rb +44 -17
- data/lib/no_brainer/document/association/core.rb +11 -0
- data/lib/no_brainer/document/association/eager_loader.rb +26 -26
- data/lib/no_brainer/document/association/has_many.rb +7 -9
- data/lib/no_brainer/document/association/has_many_through.rb +1 -2
- data/lib/no_brainer/document/association/has_one.rb +5 -1
- data/lib/no_brainer/document/association.rb +5 -5
- data/lib/no_brainer/document/atomic_ops.rb +40 -7
- data/lib/no_brainer/document/attributes.rb +24 -15
- data/lib/no_brainer/document/callbacks.rb +1 -1
- data/lib/no_brainer/document/core.rb +18 -18
- data/lib/no_brainer/document/criteria.rb +9 -5
- data/lib/no_brainer/document/dirty.rb +15 -12
- data/lib/no_brainer/document/dynamic_attributes.rb +6 -0
- data/lib/no_brainer/document/index/index.rb +5 -9
- data/lib/no_brainer/document/index/meta_store.rb +1 -10
- data/lib/no_brainer/document/index/synchronizer.rb +16 -20
- data/lib/no_brainer/document/index.rb +3 -3
- data/lib/no_brainer/document/lazy_fetch.rb +3 -3
- data/lib/no_brainer/document/missing_attributes.rb +7 -2
- data/lib/no_brainer/document/persistance.rb +14 -30
- data/lib/no_brainer/document/polymorphic.rb +8 -4
- data/lib/no_brainer/document/primary_key/generator.rb +6 -1
- data/lib/no_brainer/document/primary_key.rb +19 -4
- data/lib/no_brainer/document/serialization.rb +0 -2
- data/lib/no_brainer/document/table_config/synchronizer.rb +21 -0
- data/lib/no_brainer/document/table_config.rb +118 -0
- data/lib/no_brainer/document/timestamps.rb +8 -0
- data/lib/no_brainer/document/validation/core.rb +63 -0
- data/lib/no_brainer/document/validation/uniqueness.rb +30 -36
- data/lib/no_brainer/document/validation.rb +1 -58
- data/lib/no_brainer/document.rb +2 -2
- data/lib/no_brainer/error.rb +1 -0
- data/lib/no_brainer/geo/base.rb +0 -1
- data/lib/no_brainer/locale/en.yml +1 -0
- data/lib/no_brainer/lock.rb +12 -8
- data/lib/no_brainer/profiler/controller_runtime.rb +76 -0
- data/lib/no_brainer/{query_runner → profiler}/logger.rb +11 -27
- data/lib/no_brainer/profiler.rb +11 -0
- data/lib/no_brainer/query_runner/database_on_demand.rb +8 -8
- data/lib/no_brainer/query_runner/driver.rb +3 -1
- data/lib/no_brainer/query_runner/missing_index.rb +3 -3
- data/lib/no_brainer/query_runner/profiler.rb +43 -0
- data/lib/no_brainer/query_runner/reconnect.rb +38 -23
- data/lib/no_brainer/query_runner/run_options.rb +35 -15
- data/lib/no_brainer/query_runner/table_on_demand.rb +18 -11
- data/lib/no_brainer/query_runner.rb +3 -3
- data/lib/no_brainer/railtie/database.rake +14 -4
- data/lib/no_brainer/railtie.rb +5 -12
- data/lib/no_brainer/rql.rb +11 -8
- data/lib/no_brainer/symbol_decoration.rb +11 -0
- data/lib/no_brainer/system/cluster_config.rb +5 -0
- data/lib/no_brainer/system/db_config.rb +5 -0
- data/lib/no_brainer/system/document.rb +24 -0
- data/lib/no_brainer/system/issue.rb +10 -0
- data/lib/no_brainer/system/job.rb +10 -0
- data/lib/no_brainer/system/log.rb +11 -0
- data/lib/no_brainer/system/server_config.rb +7 -0
- data/lib/no_brainer/system/server_status.rb +9 -0
- data/lib/no_brainer/system/stat.rb +11 -0
- data/lib/no_brainer/system/table_config.rb +10 -0
- data/lib/no_brainer/system/table_status.rb +8 -0
- data/lib/no_brainer/system.rb +17 -0
- data/lib/nobrainer.rb +16 -11
- data/lib/rails/generators/nobrainer/install_generator.rb +48 -0
- data/lib/rails/generators/nobrainer/{model/model_generator.rb → model_generator.rb} +7 -3
- data/lib/rails/generators/nobrainer/namespace_fix.rb +15 -0
- data/lib/rails/generators/{nobrainer/model/templates/model.rb.tt → templates/model.rb} +1 -1
- data/lib/rails/generators/templates/nobrainer.rb +101 -0
- metadata +34 -10
- data/lib/no_brainer/document/store_in.rb +0 -35
- data/lib/rails/generators/nobrainer.rb +0 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8406e0c14de6dc8379b7047aa6d7254675e1b1fb
|
|
4
|
+
data.tar.gz: 0d9906ef55e4f74e5317a6d2377d4c1f19ac9799
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fdcbf5021c15bad13546e7b346eb73fb7f6d0b62ab861578110b0ab81aafda8c364a4b6fe9b282d0f8f8c6d732ebd8dcc3f5be9aada980b79d34d93b0fed6fea
|
|
7
|
+
data.tar.gz: afa546845837919bb06b01c7ba4168626b74263244986d4d9d3a7428b9b68b24faa968572f110546fb5afb9c4b57dbb236d5c48556b9ae691d271deea62ff816
|
data/lib/no_brainer/autoload.rb
CHANGED
data/lib/no_brainer/config.rb
CHANGED
|
@@ -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
|
-
:
|
|
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
|
-
:
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
69
|
-
unless __send__(name).in?(
|
|
70
|
-
raise ArgumentError.new("
|
|
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] &
|
|
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 =
|
|
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 :
|
|
5
|
+
attr_accessor :parsed_uri
|
|
5
6
|
|
|
6
7
|
def initialize(uri)
|
|
7
|
-
|
|
8
|
-
parsed_uri # just to raise an exception if there is a problem.
|
|
8
|
+
parse_uri(uri)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def
|
|
12
|
-
@parsed_uri
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
73
|
+
def notify_url_change
|
|
55
74
|
synchronize do
|
|
75
|
+
@urls = nil
|
|
56
76
|
c = current_connection
|
|
57
|
-
_disconnect if c &&
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
11
|
+
without_ordering.where(model.pk_name => pk).first
|
|
15
12
|
end
|
|
16
13
|
|
|
17
14
|
def find(pk)
|
|
18
|
-
|
|
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(
|
|
25
|
-
raise NoBrainer::Error::DocumentNotFound, "#{model}
|
|
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
|
|
13
|
+
first || (raise NoBrainer::Error::DocumentNotFound)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def last!
|
|
17
|
-
last
|
|
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
|