nobrainer 0.15.0 → 0.16.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 +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)
|