nobrainer 0.15.0 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/no_brainer/autoload.rb +2 -2
- data/lib/no_brainer/criteria.rb +3 -2
- data/lib/no_brainer/criteria/after_find.rb +1 -1
- data/lib/no_brainer/criteria/aggregate.rb +25 -0
- data/lib/no_brainer/criteria/core.rb +15 -1
- data/lib/no_brainer/criteria/count.rb +1 -1
- data/lib/no_brainer/criteria/delete.rb +2 -2
- data/lib/no_brainer/criteria/index.rb +37 -0
- data/lib/no_brainer/criteria/order_by.rb +53 -13
- data/lib/no_brainer/criteria/pluck.rb +81 -0
- data/lib/no_brainer/criteria/raw.rb +12 -4
- data/lib/no_brainer/criteria/scope.rb +11 -6
- data/lib/no_brainer/criteria/update.rb +12 -4
- data/lib/no_brainer/criteria/where.rb +133 -64
- data/lib/no_brainer/document.rb +4 -3
- data/lib/no_brainer/document/aliases.rb +55 -0
- data/lib/no_brainer/document/association.rb +1 -1
- data/lib/no_brainer/document/association/belongs_to.rb +2 -2
- data/lib/no_brainer/document/attributes.rb +26 -8
- data/lib/no_brainer/document/criteria.rb +7 -4
- data/lib/no_brainer/document/dirty.rb +21 -4
- data/lib/no_brainer/document/dynamic_attributes.rb +4 -1
- data/lib/no_brainer/document/index.rb +47 -12
- data/lib/no_brainer/document/lazy_fetch.rb +72 -0
- data/lib/no_brainer/document/missing_attributes.rb +73 -0
- data/lib/no_brainer/document/persistance.rb +57 -8
- data/lib/no_brainer/document/polymorphic.rb +14 -8
- data/lib/no_brainer/document/types.rb +5 -2
- data/lib/no_brainer/document/types/binary.rb +23 -0
- data/lib/no_brainer/document/uniqueness.rb +1 -1
- data/lib/no_brainer/error.rb +16 -0
- data/lib/no_brainer/index_manager.rb +1 -0
- data/lib/no_brainer/query_runner.rb +3 -1
- data/lib/no_brainer/query_runner/connection_lock.rb +7 -0
- data/lib/no_brainer/query_runner/database_on_demand.rb +6 -3
- data/lib/no_brainer/query_runner/logger.rb +22 -6
- data/lib/no_brainer/query_runner/missing_index.rb +5 -3
- data/lib/no_brainer/query_runner/table_on_demand.rb +6 -3
- data/lib/no_brainer/railtie.rb +1 -1
- metadata +12 -4
@@ -0,0 +1,73 @@
|
|
1
|
+
module NoBrainer::Document::MissingAttributes
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
def write_attribute(attr, value)
|
5
|
+
super.tap { clear_missing_field(attr) }
|
6
|
+
end
|
7
|
+
|
8
|
+
def assign_attributes(attrs, options={})
|
9
|
+
if options[:missing_attributes]
|
10
|
+
# there is one and only one key :pluck or :without to missing_attributes
|
11
|
+
@missing_attributes = options[:missing_attributes]
|
12
|
+
assert_access_field(self.class.pk_name, "The primary key is not accessible. Use .raw or")
|
13
|
+
assert_access_field(:_type, "The subclass type is not accessible. Use .raw or") if self.class.is_polymorphic
|
14
|
+
end
|
15
|
+
|
16
|
+
attrs.keys.each { |attr| clear_missing_field(attr) } if @missing_attributes && options[:from_db]
|
17
|
+
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def missing_field?(name)
|
22
|
+
return false unless @missing_attributes
|
23
|
+
name = name.to_s
|
24
|
+
return false if @cleared_missing_fields.try(:[], name)
|
25
|
+
if @missing_attributes[:pluck]
|
26
|
+
!@missing_attributes[:pluck][name]
|
27
|
+
else
|
28
|
+
!!@missing_attributes[:without][name]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def clear_missing_field(name)
|
33
|
+
return unless @missing_attributes
|
34
|
+
@cleared_missing_fields ||= {}
|
35
|
+
@cleared_missing_fields[name.to_s] = true
|
36
|
+
end
|
37
|
+
|
38
|
+
def assert_access_field(name, msg=nil)
|
39
|
+
if missing_field?(name)
|
40
|
+
method = @missing_attributes.keys.first
|
41
|
+
msg ||= "The attribute `#{name}' is not accessible,"
|
42
|
+
msg += " add `:#{name}' to pluck()" if method == :pluck
|
43
|
+
msg += " remove `:#{name}' from without()" if method == :without
|
44
|
+
raise NoBrainer::Error::MissingAttribute.new(msg)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
module ClassMethods
|
50
|
+
def _field(attr, options={})
|
51
|
+
super
|
52
|
+
|
53
|
+
inject_in_layer :missing_attributes do
|
54
|
+
define_method("#{attr}") do
|
55
|
+
assert_access_field(attr)
|
56
|
+
super()
|
57
|
+
end
|
58
|
+
|
59
|
+
define_method("#{attr}=") do |value|
|
60
|
+
super(value).tap { clear_missing_field(attr) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def _remove_field(attr, options={})
|
66
|
+
super
|
67
|
+
inject_in_layer :missing_attributes do
|
68
|
+
remove_method("#{attr}=")
|
69
|
+
remove_method("#{attr}")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -18,23 +18,57 @@ module NoBrainer::Document::Persistance
|
|
18
18
|
!new_record? && !destroyed?
|
19
19
|
end
|
20
20
|
|
21
|
-
def
|
22
|
-
|
21
|
+
def _reload_selector(options={})
|
22
|
+
rql = selector
|
23
|
+
if opt = options[:missing_attributes]
|
24
|
+
rql = rql.pluck(self.class.with_fields_aliased(opt[:pluck])) if opt[:pluck]
|
25
|
+
rql = rql.without(self.class.with_fields_aliased(opt[:without])) if opt[:without]
|
26
|
+
end
|
27
|
+
rql
|
28
|
+
end
|
29
|
+
|
30
|
+
def _reload(options={})
|
31
|
+
attrs = NoBrainer.run { _reload_selector(options) }
|
23
32
|
raise NoBrainer::Error::DocumentNotFound, "#{self.class} #{self.class.pk_name}: #{pk_value} not found" unless attrs
|
24
|
-
|
25
|
-
|
33
|
+
|
34
|
+
options = options.merge(:pristine => true, :from_db => true)
|
35
|
+
|
36
|
+
if options[:keep_ivars]
|
37
|
+
assign_attributes(attrs, options)
|
38
|
+
else
|
39
|
+
instance_variables.each { |ivar| remove_instance_variable(ivar) }
|
40
|
+
initialize(attrs, options)
|
41
|
+
end
|
42
|
+
|
26
43
|
self
|
27
44
|
end
|
28
45
|
|
46
|
+
def reload(options={})
|
47
|
+
[:without, :pluck].each do |type|
|
48
|
+
if v = options.delete(type)
|
49
|
+
v = Hash[v.flatten.map { |k| [k, true] }] if v.is_a?(Array)
|
50
|
+
v = {v => true} if !v.is_a?(Hash)
|
51
|
+
v = v.select { |k,_v| _v }
|
52
|
+
v = v.with_indifferent_access
|
53
|
+
next unless v.present?
|
54
|
+
|
55
|
+
options[:missing_attributes] ||= {}
|
56
|
+
options[:missing_attributes][type] = v
|
57
|
+
end
|
58
|
+
end
|
59
|
+
_reload(options)
|
60
|
+
end
|
61
|
+
|
29
62
|
def _create(options={})
|
30
63
|
return false if options[:validate] && !valid?
|
31
|
-
keys = self.class.insert_all(
|
64
|
+
keys = self.class.insert_all(@_attributes)
|
32
65
|
self.pk_value ||= keys.first
|
33
66
|
@new_record = false
|
34
67
|
true
|
35
68
|
end
|
36
69
|
|
37
70
|
def _update(attrs)
|
71
|
+
attrs = self.class.persistable_attributes(attrs)
|
38
72
|
NoBrainer.run { selector.update(attrs) }
|
39
73
|
end
|
40
74
|
|
@@ -50,7 +84,7 @@ module NoBrainer::Document::Persistance
|
|
50
84
|
attr = RethinkDB::RQL.new.literal(attr) if attr.is_a?(Hash)
|
51
85
|
[k, attr]
|
52
86
|
end]
|
53
|
-
_update(
|
87
|
+
_update(attrs) if attrs.present?
|
54
88
|
true
|
55
89
|
end
|
56
90
|
|
@@ -94,13 +128,28 @@ module NoBrainer::Document::Persistance
|
|
94
128
|
new(attrs, options).tap { |doc| doc.save!(options) }
|
95
129
|
end
|
96
130
|
|
97
|
-
def insert_all(*
|
98
|
-
|
131
|
+
def insert_all(*args)
|
132
|
+
docs = args.shift
|
133
|
+
docs = [docs] unless docs.is_a?(Array)
|
134
|
+
docs = docs.map { |doc| persistable_attributes(doc) }
|
135
|
+
result = NoBrainer.run(rql_table.insert(docs, *args))
|
99
136
|
result['generated_keys'].to_a
|
100
137
|
end
|
101
138
|
|
102
139
|
def sync
|
103
140
|
NoBrainer.run(rql_table.sync)['synced'] == 1
|
104
141
|
end
|
142
|
+
|
143
|
+
def persistable_key(k)
|
144
|
+
k
|
145
|
+
end
|
146
|
+
|
147
|
+
def persistable_value(k, v)
|
148
|
+
v
|
149
|
+
end
|
150
|
+
|
151
|
+
def persistable_attributes(attrs)
|
152
|
+
Hash[attrs.map { |k,v| [persistable_key(k), persistable_value(k, v)] }]
|
153
|
+
end
|
105
154
|
end
|
106
155
|
end
|
@@ -3,8 +3,9 @@ module NoBrainer::Document::Polymorphic
|
|
3
3
|
include ActiveSupport::DescendantsTracker
|
4
4
|
|
5
5
|
included do
|
6
|
-
cattr_accessor :root_class
|
6
|
+
cattr_accessor :root_class, :is_polymorphic
|
7
7
|
self.root_class = self
|
8
|
+
self.is_polymorphic = false
|
8
9
|
end
|
9
10
|
|
10
11
|
def assign_attributes(*args)
|
@@ -14,6 +15,7 @@ module NoBrainer::Document::Polymorphic
|
|
14
15
|
|
15
16
|
module ClassMethods
|
16
17
|
def inherited(subclass)
|
18
|
+
subclass.is_polymorphic = true
|
17
19
|
super
|
18
20
|
subclass.field :_type if is_root_class?
|
19
21
|
end
|
@@ -22,22 +24,26 @@ module NoBrainer::Document::Polymorphic
|
|
22
24
|
name
|
23
25
|
end
|
24
26
|
|
25
|
-
def descendants_type_values
|
26
|
-
([self] + descendants).map(&:type_value)
|
27
|
-
end
|
28
|
-
|
29
27
|
def is_root_class?
|
30
28
|
self == root_class
|
31
29
|
end
|
32
30
|
|
31
|
+
def for_each_subclass(&block)
|
32
|
+
([self] + self.descendants).each(&block)
|
33
|
+
end
|
34
|
+
|
35
|
+
def descendants_type_values
|
36
|
+
for_each_subclass.map(&:type_value)
|
37
|
+
end
|
38
|
+
|
33
39
|
def klass_from_attrs(attrs)
|
34
40
|
attrs['_type'].try(:constantize) || root_class
|
35
41
|
end
|
36
42
|
|
37
43
|
def all
|
38
|
-
|
39
|
-
|
40
|
-
|
44
|
+
criteria = super
|
45
|
+
criteria = criteria.where(:_type.in => descendants_type_values) unless is_root_class?
|
46
|
+
criteria
|
41
47
|
end
|
42
48
|
end
|
43
49
|
end
|
@@ -53,8 +53,8 @@ module NoBrainer::Document::Types
|
|
53
53
|
cast_model_to_db_for(attr, value)
|
54
54
|
end
|
55
55
|
|
56
|
-
def
|
57
|
-
|
56
|
+
def persistable_value(k, v)
|
57
|
+
cast_model_to_db_for(k, super)
|
58
58
|
end
|
59
59
|
|
60
60
|
def _field(attr, options={})
|
@@ -88,6 +88,7 @@ module NoBrainer::Document::Types
|
|
88
88
|
end
|
89
89
|
|
90
90
|
def field(attr, options={})
|
91
|
+
options[:real_type] = options[:type]
|
91
92
|
if options[:type] == Array || options[:type] == Hash
|
92
93
|
# XXX For the moment, NoBrainer does not support these complex types
|
93
94
|
options.delete(:type)
|
@@ -96,7 +97,9 @@ module NoBrainer::Document::Types
|
|
96
97
|
end
|
97
98
|
end
|
98
99
|
|
100
|
+
require File.join(File.dirname(__FILE__), 'types', 'binary')
|
99
101
|
require File.join(File.dirname(__FILE__), 'types', 'boolean')
|
102
|
+
Binary = NoBrainer::Binary
|
100
103
|
Boolean = NoBrainer::Boolean
|
101
104
|
|
102
105
|
class << self
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class NoBrainer::Binary
|
2
|
+
def initialize; raise; end
|
3
|
+
def self.inspect; 'Binary'; end
|
4
|
+
def self.to_s; inspect; end
|
5
|
+
def self.name; inspect; end
|
6
|
+
|
7
|
+
module NoBrainerExtentions
|
8
|
+
InvalidType = NoBrainer::Error::InvalidType
|
9
|
+
|
10
|
+
def nobrainer_cast_user_to_model(value)
|
11
|
+
case value
|
12
|
+
when String then RethinkDB::Binary.new(value)
|
13
|
+
else raise InvalidType
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def nobrainer_cast_db_to_model(value)
|
18
|
+
value.is_a?(String) ? RethinkDB::Binary.new(value) : value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
extend NoBrainerExtentions
|
22
|
+
end
|
23
|
+
|
data/lib/no_brainer/error.rb
CHANGED
@@ -8,6 +8,7 @@ module NoBrainer::Error
|
|
8
8
|
class InvalidType < RuntimeError; end
|
9
9
|
class AssociationNotPersisted < RuntimeError; end
|
10
10
|
class ReadonlyField < RuntimeError; end
|
11
|
+
class MissingAttribute < RuntimeError; end
|
11
12
|
|
12
13
|
class DocumentInvalid < RuntimeError
|
13
14
|
attr_accessor :instance
|
@@ -36,4 +37,19 @@ module NoBrainer::Error
|
|
36
37
|
"#{attr_name} should be used with a #{human_type_name}. Got `#{value}` (#{value.class})"
|
37
38
|
end
|
38
39
|
end
|
40
|
+
|
41
|
+
class CannotUseIndex < RuntimeError
|
42
|
+
attr_accessor :index_name
|
43
|
+
def initialize(index_name)
|
44
|
+
@index_name = index_name
|
45
|
+
end
|
46
|
+
|
47
|
+
def message
|
48
|
+
if index_name == true
|
49
|
+
"Cannot use any indexes"
|
50
|
+
else
|
51
|
+
"Cannot use index #{index_name}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
39
55
|
end
|
@@ -11,7 +11,8 @@ module NoBrainer::QueryRunner
|
|
11
11
|
end
|
12
12
|
|
13
13
|
autoload :Driver, :DatabaseOnDemand, :TableOnDemand, :WriteError,
|
14
|
-
:Reconnect, :Selection, :RunOptions, :Logger, :MissingIndex
|
14
|
+
:Reconnect, :Selection, :RunOptions, :Logger, :MissingIndex,
|
15
|
+
:ConnectionLock
|
15
16
|
|
16
17
|
class << self
|
17
18
|
attr_accessor :stack
|
@@ -32,6 +33,7 @@ module NoBrainer::QueryRunner
|
|
32
33
|
use DatabaseOnDemand
|
33
34
|
use TableOnDemand
|
34
35
|
use Logger
|
36
|
+
use ConnectionLock
|
35
37
|
use Reconnect
|
36
38
|
use Driver
|
37
39
|
end
|
@@ -2,14 +2,17 @@ class NoBrainer::QueryRunner::DatabaseOnDemand < NoBrainer::QueryRunner::Middlew
|
|
2
2
|
def call(env)
|
3
3
|
@runner.call(env)
|
4
4
|
rescue RuntimeError => e
|
5
|
-
if
|
6
|
-
|
7
|
-
auto_create_database(env, $1)
|
5
|
+
if database_name = database_on_demand_exception?(e)
|
6
|
+
auto_create_database(env, database_name)
|
8
7
|
retry
|
9
8
|
end
|
10
9
|
raise
|
11
10
|
end
|
12
11
|
|
12
|
+
def database_on_demand_exception?(e)
|
13
|
+
NoBrainer::Config.auto_create_databases && e.message =~ /^Database `(.+)` does not exist\.$/ && $1
|
14
|
+
end
|
15
|
+
|
13
16
|
private
|
14
17
|
|
15
18
|
def auto_create_database(env, database_name)
|
@@ -3,14 +3,18 @@ class NoBrainer::QueryRunner::Logger < NoBrainer::QueryRunner::Middleware
|
|
3
3
|
start_time = Time.now
|
4
4
|
@runner.call(env).tap { log_query(env, start_time) }
|
5
5
|
rescue Exception => e
|
6
|
-
log_query(env, start_time, e)
|
6
|
+
log_query(env, start_time, e)
|
7
7
|
raise e
|
8
8
|
end
|
9
9
|
|
10
10
|
private
|
11
11
|
|
12
12
|
def log_query(env, start_time, exception=nil)
|
13
|
-
return
|
13
|
+
return if on_demand_exception?(exception)
|
14
|
+
not_indexed = env[:criteria] && env[:criteria].where_present? && !env[:criteria].where_indexed?
|
15
|
+
level = exception ? Logger::ERROR :
|
16
|
+
not_indexed ? Logger::INFO : Logger::DEBUG
|
17
|
+
return if NoBrainer.logger.nil? || NoBrainer.logger.level > level
|
14
18
|
|
15
19
|
duration = Time.now - start_time
|
16
20
|
|
@@ -18,9 +22,12 @@ class NoBrainer::QueryRunner::Logger < NoBrainer::QueryRunner::Middleware
|
|
18
22
|
msg_duration = " " * [0, 5 - msg_duration.size].max + msg_duration
|
19
23
|
msg_duration = "[#{msg_duration}ms] "
|
20
24
|
|
21
|
-
msg_db = "[#{env[:db_name]}] " if env[:db_name]
|
25
|
+
msg_db = "[#{env[:db_name]}] " if env[:db_name] && env[:db_name].to_s != NoBrainer.connection.parsed_uri[:db]
|
22
26
|
msg_query = env[:query].inspect.gsub(/\n/, '').gsub(/ +/, ' ')
|
23
|
-
|
27
|
+
|
28
|
+
msg_exception = "#{exception.class} #{exception.message.split("\n").first}" if exception
|
29
|
+
msg_exception ||= "perf: filtering without using an index" if not_indexed
|
30
|
+
|
24
31
|
msg_last = nil
|
25
32
|
|
26
33
|
if NoBrainer::Config.colorize_logger
|
@@ -31,11 +38,20 @@ class NoBrainer::QueryRunner::Logger < NoBrainer::QueryRunner::Middleware
|
|
31
38
|
end
|
32
39
|
msg_duration = [query_color, msg_duration].join
|
33
40
|
msg_db = ["\e[0;34m", msg_db, query_color].join if msg_db
|
34
|
-
|
41
|
+
if msg_exception
|
42
|
+
exception_color = "\e[0;31m" if level == Logger::ERROR
|
43
|
+
msg_exception = ["\e[0;39m", " -- ", exception_color, msg_exception].compact.join
|
44
|
+
end
|
35
45
|
msg_last = "\e[0m"
|
36
46
|
end
|
37
47
|
|
38
48
|
msg = [msg_duration, msg_db, msg_query, msg_exception, msg_last].join
|
39
|
-
NoBrainer.logger.
|
49
|
+
NoBrainer.logger.add(level, msg)
|
50
|
+
end
|
51
|
+
|
52
|
+
def on_demand_exception?(e)
|
53
|
+
# pretty gross I must say.
|
54
|
+
e && (NoBrainer::QueryRunner::DatabaseOnDemand.new(nil).database_on_demand_exception?(e) ||
|
55
|
+
NoBrainer::QueryRunner::TableOnDemand.new(nil).table_on_demand_exception?(e))
|
40
56
|
end
|
41
57
|
end
|
@@ -8,12 +8,14 @@ class NoBrainer::QueryRunner::MissingIndex < NoBrainer::QueryRunner::Middleware
|
|
8
8
|
table_name = $3
|
9
9
|
|
10
10
|
klass = NoBrainer::Document.all.select { |m| m.table_name == table_name }.first
|
11
|
+
index_name = klass.get_index_alias_reverse_map[index_name.to_sym]
|
12
|
+
|
11
13
|
if klass && klass.pk_name.to_s == index_name
|
12
|
-
err_msg = "Please
|
14
|
+
err_msg = "Please update the primary key `#{index_name}` in the table `#{database_name}.#{table_name}`."
|
13
15
|
else
|
14
|
-
err_msg = "Please run
|
16
|
+
err_msg = "Please run `NoBrainer.update_indexes' or `rake db:update_indexes' to create the index `#{index_name}`"
|
15
17
|
err_msg += " in the table `#{database_name}.#{table_name}`."
|
16
|
-
err_msg += "
|
18
|
+
err_msg += " Read http://nobrainer.io/docs/indexes for more information."
|
17
19
|
end
|
18
20
|
|
19
21
|
raise NoBrainer::Error::MissingIndex.new(err_msg)
|