nobrainer 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
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)