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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/autoload.rb +2 -2
  3. data/lib/no_brainer/criteria.rb +3 -2
  4. data/lib/no_brainer/criteria/after_find.rb +1 -1
  5. data/lib/no_brainer/criteria/aggregate.rb +25 -0
  6. data/lib/no_brainer/criteria/core.rb +15 -1
  7. data/lib/no_brainer/criteria/count.rb +1 -1
  8. data/lib/no_brainer/criteria/delete.rb +2 -2
  9. data/lib/no_brainer/criteria/index.rb +37 -0
  10. data/lib/no_brainer/criteria/order_by.rb +53 -13
  11. data/lib/no_brainer/criteria/pluck.rb +81 -0
  12. data/lib/no_brainer/criteria/raw.rb +12 -4
  13. data/lib/no_brainer/criteria/scope.rb +11 -6
  14. data/lib/no_brainer/criteria/update.rb +12 -4
  15. data/lib/no_brainer/criteria/where.rb +133 -64
  16. data/lib/no_brainer/document.rb +4 -3
  17. data/lib/no_brainer/document/aliases.rb +55 -0
  18. data/lib/no_brainer/document/association.rb +1 -1
  19. data/lib/no_brainer/document/association/belongs_to.rb +2 -2
  20. data/lib/no_brainer/document/attributes.rb +26 -8
  21. data/lib/no_brainer/document/criteria.rb +7 -4
  22. data/lib/no_brainer/document/dirty.rb +21 -4
  23. data/lib/no_brainer/document/dynamic_attributes.rb +4 -1
  24. data/lib/no_brainer/document/index.rb +47 -12
  25. data/lib/no_brainer/document/lazy_fetch.rb +72 -0
  26. data/lib/no_brainer/document/missing_attributes.rb +73 -0
  27. data/lib/no_brainer/document/persistance.rb +57 -8
  28. data/lib/no_brainer/document/polymorphic.rb +14 -8
  29. data/lib/no_brainer/document/types.rb +5 -2
  30. data/lib/no_brainer/document/types/binary.rb +23 -0
  31. data/lib/no_brainer/document/uniqueness.rb +1 -1
  32. data/lib/no_brainer/error.rb +16 -0
  33. data/lib/no_brainer/index_manager.rb +1 -0
  34. data/lib/no_brainer/query_runner.rb +3 -1
  35. data/lib/no_brainer/query_runner/connection_lock.rb +7 -0
  36. data/lib/no_brainer/query_runner/database_on_demand.rb +6 -3
  37. data/lib/no_brainer/query_runner/logger.rb +22 -6
  38. data/lib/no_brainer/query_runner/missing_index.rb +5 -3
  39. data/lib/no_brainer/query_runner/table_on_demand.rb +6 -3
  40. data/lib/no_brainer/railtie.rb +1 -1
  41. 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 reload(options={})
22
- attrs = NoBrainer.run { self.class.selector_for(pk_value) }
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
- instance_variables.each { |ivar| remove_instance_variable(ivar) } unless options[:keep_ivars]
25
- initialize(attrs, :pristine => true, :from_db => true)
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(self.class.persistable_attributes(@_attributes))
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(self.class.persistable_attributes(attrs)) if attrs.present?
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(*attrs)
98
- result = NoBrainer.run(rql_table.insert(*attrs))
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
- criterion = super
39
- criterion = criterion.where(:_type.in => descendants_type_values) unless is_root_class?
40
- criterion
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 persistable_attributes(attrs)
57
- Hash[attrs.map { |k,v| [k, cast_model_to_db_for(k, v)] }]
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
+
@@ -55,8 +55,8 @@ module NoBrainer::Document::Uniqueness
55
55
  end
56
56
 
57
57
  def inherited(subclass)
58
- super
59
58
  subclass.unique_validators = self.unique_validators.dup
59
+ super
60
60
  end
61
61
 
62
62
  end
@@ -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
@@ -4,5 +4,6 @@ module NoBrainer::IndexManager
4
4
  unless options[:wait] == false
5
5
  NoBrainer::Document.all.each { |model| model.wait_for_index(nil) }
6
6
  end
7
+ true
7
8
  end
8
9
  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
@@ -0,0 +1,7 @@
1
+ class NoBrainer::QueryRunner::ConnectionLock < NoBrainer::QueryRunner::Middleware
2
+ @@lock = Mutex.new
3
+
4
+ def call(env)
5
+ @@lock.synchronize { @runner.call(env) }
6
+ end
7
+ 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 NoBrainer::Config.auto_create_databases &&
6
- e.message =~ /^Database `(.+)` does not exist\.$/
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) rescue nil
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 unless NoBrainer.logger.debug?
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
- msg_exception = " #{exception.class} #{exception.message.split("\n").first}" if exception
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
- msg_exception = ["\e[0;31m", msg_exception].join if msg_exception
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.debug(msg)
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 run update the primary key `#{index_name}` in the table `#{database_name}.#{table_name}`."
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 \"rake db:update_indexes\" to create the index `#{index_name}`"
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 += "\n--> Read http://nobrainer.io/docs/indexes for more information."
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)