nobrainer 0.17.0 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/config.rb +42 -16
  3. data/lib/no_brainer/connection.rb +1 -0
  4. data/lib/no_brainer/connection_manager.rb +1 -1
  5. data/lib/no_brainer/criteria.rb +1 -1
  6. data/lib/no_brainer/criteria/aggregate.rb +8 -5
  7. data/lib/no_brainer/criteria/core.rb +8 -7
  8. data/lib/no_brainer/criteria/count.rb +1 -1
  9. data/lib/no_brainer/criteria/delete.rb +1 -1
  10. data/lib/no_brainer/criteria/extend.rb +31 -0
  11. data/lib/no_brainer/criteria/first.rb +1 -1
  12. data/lib/no_brainer/criteria/index.rb +1 -1
  13. data/lib/no_brainer/criteria/limit.rb +0 -1
  14. data/lib/no_brainer/criteria/order_by.rb +8 -9
  15. data/lib/no_brainer/criteria/pluck.rb +1 -1
  16. data/lib/no_brainer/criteria/raw.rb +1 -1
  17. data/lib/no_brainer/criteria/scope.rb +6 -6
  18. data/lib/no_brainer/criteria/update.rb +3 -3
  19. data/lib/no_brainer/criteria/where.rb +24 -32
  20. data/lib/no_brainer/document/association.rb +5 -5
  21. data/lib/no_brainer/document/association/belongs_to.rb +6 -6
  22. data/lib/no_brainer/document/association/core.rb +11 -11
  23. data/lib/no_brainer/document/association/eager_loader.rb +1 -1
  24. data/lib/no_brainer/document/association/has_many.rb +7 -7
  25. data/lib/no_brainer/document/association/has_many_through.rb +1 -1
  26. data/lib/no_brainer/document/association/has_one.rb +1 -1
  27. data/lib/no_brainer/document/atomic_ops.rb +26 -18
  28. data/lib/no_brainer/document/attributes.rb +9 -8
  29. data/lib/no_brainer/document/callbacks.rb +1 -1
  30. data/lib/no_brainer/document/core.rb +1 -1
  31. data/lib/no_brainer/document/criteria.rb +9 -2
  32. data/lib/no_brainer/document/dirty.rb +1 -3
  33. data/lib/no_brainer/document/index.rb +13 -79
  34. data/lib/no_brainer/document/index/index.rb +83 -0
  35. data/lib/no_brainer/document/index/meta_store.rb +31 -0
  36. data/lib/no_brainer/document/index/synchronizer.rb +68 -0
  37. data/lib/no_brainer/document/lazy_fetch.rb +5 -5
  38. data/lib/no_brainer/document/persistance.rb +27 -7
  39. data/lib/no_brainer/document/polymorphic.rb +1 -1
  40. data/lib/no_brainer/document/types.rb +6 -4
  41. data/lib/no_brainer/document/uniqueness.rb +3 -3
  42. data/lib/no_brainer/document/validation.rb +13 -4
  43. data/lib/no_brainer/fork.rb +1 -0
  44. data/lib/no_brainer/query_runner/database_on_demand.rb +6 -5
  45. data/lib/no_brainer/query_runner/logger.rb +10 -6
  46. data/lib/no_brainer/query_runner/missing_index.rb +5 -4
  47. data/lib/no_brainer/query_runner/reconnect.rb +20 -17
  48. data/lib/no_brainer/query_runner/run_options.rb +3 -0
  49. data/lib/no_brainer/query_runner/table_on_demand.rb +11 -8
  50. data/lib/no_brainer/railtie.rb +5 -5
  51. data/lib/no_brainer/railtie/database.rake +12 -12
  52. data/lib/no_brainer/rql.rb +9 -0
  53. data/lib/nobrainer.rb +5 -3
  54. metadata +8 -8
  55. data/lib/no_brainer/index_manager.rb +0 -9
@@ -0,0 +1,83 @@
1
+ class NoBrainer::Document::Index::Index < Struct.new(
2
+ :model, :name, :aliased_name, :kind, :what, :external, :geo, :multi, :meta)
3
+
4
+ MetaStore = NoBrainer::Document::Index::MetaStore
5
+
6
+ def initialize(*args)
7
+ super
8
+
9
+ self.name = self.name.to_sym
10
+ self.aliased_name = self.aliased_name.to_sym
11
+ self.external = !!self.external
12
+ self.geo = !!self.geo
13
+ self.multi = !!self.multi
14
+ end
15
+
16
+ def same_definition?(other)
17
+ # allow name to change through renames
18
+ self.model == other.model &&
19
+ self.geo == other.geo &&
20
+ self.multi == other.multi &&
21
+ self.serialized_rql_proc == other.serialized_rql_proc
22
+ end
23
+
24
+ def human_name
25
+ index_name = "index #{model}.#{name}"
26
+ index_name += " as #{aliased_name}" unless name == aliased_name
27
+ index_name
28
+ end
29
+
30
+ def rql_proc
31
+ case kind
32
+ when :single then ->(doc) { doc[model.lookup_field_alias(what)] }
33
+ when :compound then ->(doc) { what.map { |field| doc[model.lookup_field_alias(field)] } }
34
+ when :proc then what # TODO XXX not translating the field aliases
35
+ end
36
+ end
37
+
38
+ def serialized_rql_proc
39
+ meta.try(:rql_function) || (rql_proc && NoBrainer::RQL.rql_proc_as_json(rql_proc))
40
+ end
41
+
42
+ def show_op(verb, options={})
43
+ color = case verb
44
+ when :create then "\e[1;32m" # green
45
+ when :delete then "\e[1;31m" # red
46
+ when :update then "\e[1;33m" # yellow
47
+ end
48
+ STDERR.puts "[NoBrainer] #{color}#{verb.to_s.capitalize} #{human_name}\e[0m" if options[:verbose]
49
+ end
50
+
51
+ def create(options={})
52
+ show_op(:create, options)
53
+
54
+ opt = {}
55
+ opt[:multi] = true if multi
56
+ opt[:geo] = true if geo
57
+
58
+ NoBrainer::RQL.reset_lambda_var_counter
59
+ NoBrainer.run(model.rql_table.index_create(aliased_name, opt, &rql_proc))
60
+
61
+ MetaStore.on(model.database_name) do
62
+ MetaStore.create(:table_name => model.table_name, :index_name => aliased_name,
63
+ :rql_function => serialized_rql_proc)
64
+ end
65
+ end
66
+
67
+ def delete(options={})
68
+ show_op(:delete, options)
69
+
70
+ NoBrainer.run(model.rql_table.index_drop(aliased_name))
71
+
72
+ MetaStore.on(model.database_name) do
73
+ MetaStore.where(:table_name => model.table_name, :index_name => aliased_name).delete_all
74
+ end
75
+ end
76
+
77
+ def update(wanted_index, options={})
78
+ wanted_index.show_op(:update, options)
79
+
80
+ self.delete(options.merge(:verbose => false))
81
+ wanted_index.create(options.merge(:verbose => false))
82
+ end
83
+ end
@@ -0,0 +1,31 @@
1
+ class NoBrainer::Document::Index::MetaStore
2
+ include NoBrainer::Document
3
+ include NoBrainer::Document::Timestamps
4
+
5
+ disable_perf_warnings
6
+
7
+ default_scope ->{ order_by(:created_at) }
8
+
9
+ store_in :database => ->{ Thread.current[:nobrainer_meta_store_db] },
10
+ :table => 'nobrainer_index_meta'
11
+
12
+ field :table_name, :type => String, :required => true
13
+ field :index_name, :type => String, :required => true
14
+ field :rql_function, :type => String, :required => true
15
+
16
+ def rql_function=(value)
17
+ super(JSON.dump(value))
18
+ end
19
+
20
+ def rql_function
21
+ JSON.load(super)
22
+ end
23
+
24
+ def self.on(db_name, &block)
25
+ old_db_name = Thread.current[:nobrainer_meta_store_db]
26
+ Thread.current[:nobrainer_meta_store_db] = db_name
27
+ NoBrainer.with(:auto_create_tables => true) { block.call }
28
+ ensure
29
+ Thread.current[:nobrainer_meta_store_db] = old_db_name
30
+ end
31
+ end
@@ -0,0 +1,68 @@
1
+ class NoBrainer::Document::Index::Synchronizer
2
+ Index = NoBrainer::Document::Index::Index
3
+ MetaStore = NoBrainer::Document::Index::MetaStore
4
+
5
+ def initialize(models)
6
+ @models_indexes_map = Hash[models.map do |model|
7
+ [model, model.indexes.values.reject { |index| index.name == model.pk_name }]
8
+ end]
9
+ end
10
+
11
+ def meta_store_on(db_name)
12
+ @meta_store ||= {}
13
+ @meta_store[db_name] ||= MetaStore.on(db_name) { MetaStore.all.to_a }
14
+ end
15
+
16
+ class Op < Struct.new(:index, :op, :args)
17
+ def run(options={})
18
+ index.__send__(op, *args, options)
19
+ end
20
+ end
21
+
22
+ def _generate_plan_for(model, wanted_indexes)
23
+ current_indexes = NoBrainer.run(model.rql_table.index_status).map do |s|
24
+ meta = meta_store_on(model.database_name)
25
+ .select { |i| i.table_name == model.table_name && i.index_name == s['index'] }.last
26
+ Index.new(model, s['index'], s['index'], nil, nil, nil, s['geo'], s['multi'], meta)
27
+ end
28
+
29
+ all_aliased_names = (wanted_indexes + current_indexes).map(&:aliased_name).uniq
30
+ all_aliased_names.map do |aliased_name|
31
+ wanted_index = wanted_indexes.select { |i| i.aliased_name == aliased_name }.first
32
+ current_index = current_indexes.select { |i| i.aliased_name == aliased_name }.first
33
+
34
+ next if wanted_index.try(:external)
35
+
36
+ case [!wanted_index.nil?, !current_index.nil?]
37
+ when [true, false] then Op.new(wanted_index, :create)
38
+ when [false, true] then Op.new(current_index, :delete)
39
+ when [true, true] then
40
+ case wanted_index.same_definition?(current_index)
41
+ when true then nil # up to date
42
+ when false then Op.new(current_index, :update, [wanted_index])
43
+ end
44
+ end
45
+ end.compact
46
+ end
47
+
48
+ def generate_plan
49
+ @models_indexes_map.map { |model, indexes| _generate_plan_for(model, indexes) }.flatten(1)
50
+ end
51
+
52
+ def sync_indexes(options={})
53
+ plan = generate_plan
54
+ plan.each { |op| op.run(options) }
55
+ unless options[:wait] == false
56
+ models = plan.map(&:index).map(&:model).uniq
57
+ models.each { |model| NoBrainer.run(model.rql_table.index_wait()) }
58
+ end
59
+ end
60
+
61
+ class << self
62
+ def instance
63
+ new(NoBrainer::Document.all)
64
+ end
65
+
66
+ delegate :sync_indexes, :to => :instance
67
+ end
68
+ end
@@ -19,7 +19,7 @@ module NoBrainer::Document::LazyFetch
19
19
  lazy_fetch = self.class.fields_to_lazy_fetch.to_a
20
20
  return super unless lazy_fetch.present?
21
21
  return super if options[:pluck]
22
- super(options.merge(:without => lazy_fetch, :lazy_fetch => lazy_fetch))
22
+ super(options.deep_merge(:without => lazy_fetch, :lazy_fetch => lazy_fetch))
23
23
  end
24
24
 
25
25
  module ClassMethods
@@ -31,12 +31,12 @@ module NoBrainer::Document::LazyFetch
31
31
  def _field(attr, options={})
32
32
  super
33
33
  attr = attr.to_s
34
- klass = self
34
+ model = self
35
35
  inject_in_layer :lazy_fetch do
36
36
  if options[:lazy_fetch]
37
- klass.for_each_subclass { |_klass| _klass.fields_to_lazy_fetch << attr }
37
+ model.for_each_subclass { |_model| _model.fields_to_lazy_fetch << attr }
38
38
  else
39
- klass.for_each_subclass { |_klass| _klass.fields_to_lazy_fetch.delete(attr) }
39
+ model.for_each_subclass { |_model| _model.fields_to_lazy_fetch.delete(attr) }
40
40
  end
41
41
 
42
42
  # Lazy loading can also specified through criteria.
@@ -57,7 +57,7 @@ module NoBrainer::Document::LazyFetch
57
57
 
58
58
  def _remove_field(attr, options={})
59
59
  super
60
- for_each_subclass { |klass| klass.fields_to_lazy_fetch.delete(attr) }
60
+ for_each_subclass { |model| model.fields_to_lazy_fetch.delete(attr) }
61
61
  inject_in_layer :lazy_fetch do
62
62
  remove_method("#{attr}") if method_defined?("#{attr}")
63
63
  end
@@ -60,7 +60,8 @@ module NoBrainer::Document::Persistance
60
60
  end
61
61
 
62
62
  def _create(options={})
63
- return false if options[:validate] && !valid?
63
+ return false if options[:validate] != false && !valid?(nil, :clear_errors => false)
64
+
64
65
  attrs = self.class.persistable_attributes(@_attributes, :instance => self)
65
66
  result = NoBrainer.run(self.class.rql_table.insert(attrs))
66
67
  self.pk_value ||= result['generated_keys'].to_a.first
@@ -74,7 +75,7 @@ module NoBrainer::Document::Persistance
74
75
  end
75
76
 
76
77
  def _update_only_changed_attrs(options={})
77
- return false if options[:validate] && !valid?
78
+ return false if options[:validate] != false && !valid?(nil, :clear_errors => false)
78
79
 
79
80
  # We won't be using the `changes` values, because they went through
80
81
  # read_attribute(), and we want the raw values.
@@ -89,23 +90,42 @@ module NoBrainer::Document::Persistance
89
90
  true
90
91
  end
91
92
 
92
- def save?(options={})
93
- options = options.reverse_merge(:validate => true)
93
+ def _save?(options)
94
94
  new_record? ? _create(options) : _update_only_changed_attrs(options)
95
95
  end
96
96
 
97
+ def save?(options={})
98
+ errors.clear
99
+ _save?(options)
100
+ end
101
+
97
102
  def save(*args)
98
103
  save?(*args) or raise NoBrainer::Error::DocumentInvalid, self
104
+ nil
99
105
  end
100
106
 
101
- def update_attributes?(attrs, options={})
107
+ def save!(*args)
108
+ save(*args)
109
+ :you_should_be_using_the_non_bang_version_of_save
110
+ end
111
+
112
+ def update?(attrs, options={})
102
113
  assign_attributes(attrs, options)
103
114
  save?(options)
104
115
  end
116
+ alias_method :update_attributes?, :update?
117
+
118
+ def update(*args)
119
+ update?(*args) or raise NoBrainer::Error::DocumentInvalid, self
120
+ nil
121
+ end
122
+ alias_method :update_attributes, :update
105
123
 
106
- def update_attributes(*args)
107
- update_attributes?(*args) or raise NoBrainer::Error::DocumentInvalid, self
124
+ def update!(*args)
125
+ update(*args)
126
+ :you_should_be_using_the_non_bang_version_of_update
108
127
  end
128
+ alias_method :update_attributes!, :update!
109
129
 
110
130
  def delete
111
131
  unless @destroyed
@@ -36,7 +36,7 @@ module NoBrainer::Document::Polymorphic
36
36
  for_each_subclass.map(&:type_value)
37
37
  end
38
38
 
39
- def klass_from_attrs(attrs)
39
+ def model_from_attrs(attrs)
40
40
  attrs['_type'].try(:constantize) || root_class
41
41
  end
42
42
 
@@ -60,6 +60,8 @@ module NoBrainer::Document::Types
60
60
  def _field(attr, options={})
61
61
  super
62
62
 
63
+ return unless options[:type]
64
+
63
65
  NoBrainer::Document::Types.load_type_extensions(options[:type]) if options[:type]
64
66
 
65
67
  inject_in_layer :types do
@@ -105,13 +107,13 @@ module NoBrainer::Document::Types
105
107
  class << self
106
108
  mattr_accessor :loaded_extensions
107
109
  self.loaded_extensions = Set.new
108
- def load_type_extensions(klass)
109
- unless loaded_extensions.include?(klass)
110
+ def load_type_extensions(model)
111
+ unless loaded_extensions.include?(model)
110
112
  begin
111
- require File.join(File.dirname(__FILE__), 'types', klass.name.underscore)
113
+ require File.join(File.dirname(__FILE__), 'types', model.name.underscore)
112
114
  rescue LoadError
113
115
  end
114
- loaded_extensions << klass
116
+ loaded_extensions << model
115
117
  end
116
118
  end
117
119
  end
@@ -66,10 +66,10 @@ module NoBrainer::Document::Uniqueness
66
66
 
67
67
  def initialize(options={})
68
68
  super
69
- klass = options[:class]
69
+ model = options[:class]
70
70
  self.scope = [*options[:scope]]
71
- ([klass] + klass.descendants).each do |_klass|
72
- _klass.unique_validators << self
71
+ ([model] + model.descendants).each do |_model|
72
+ _model.unique_validators << self
73
73
  end
74
74
  end
75
75
 
@@ -5,19 +5,28 @@ module NoBrainer::Document::Validation
5
5
 
6
6
  included do
7
7
  # We don't want before_validation returning false to halt the chain.
8
- define_callbacks :validation, :skip_after_callbacks_if_terminated => true, :scope => [:kind, :name],
9
- :terminator => proc { false }
8
+ define_callbacks :validation, :skip_after_callbacks_if_terminated => true,
9
+ :scope => [:kind, :name], :terminator => proc { false }
10
10
  end
11
11
 
12
- def valid?(context=nil)
13
- super(context || (new_record? ? :create : :update))
12
+ def valid?(context=nil, options={})
13
+ context ||= new_record? ? :create : :update
14
+
15
+ # copy/pasted, because we need to have control on errors.clear
16
+ current_context, self.validation_context = validation_context, context
17
+ errors.clear unless options[:clear_errors] == false
18
+ run_validations!
19
+ ensure
20
+ self.validation_context = current_context
14
21
  end
15
22
 
16
23
  module ClassMethods
17
24
  def _field(attr, options={})
18
25
  super
26
+ validates(attr, { :format => { :with => options[:format] } }) if options.has_key?(:format)
19
27
  validates(attr, { :presence => options[:required] }) if options.has_key?(:required)
20
28
  validates(attr, { :uniqueness => options[:unique] }) if options.has_key?(:unique)
29
+ validates(attr, { :uniqueness => options[:uniq] }) if options.has_key?(:uniq)
21
30
  validates(attr, { :inclusion => {:in => options[:in]} }) if options.has_key?(:in)
22
31
  validates(attr, options[:validates]) if options[:validates]
23
32
  end
@@ -4,6 +4,7 @@ module NoBrainer::Fork
4
4
  alias_method :fork_without_nobrainer, :fork
5
5
 
6
6
  def fork(&block)
7
+ # Not so safe to disconnect in the child (c.f. driver's code)
7
8
  NoBrainer.disconnect
8
9
  fork_without_nobrainer(&block)
9
10
  end
@@ -2,24 +2,25 @@ class NoBrainer::QueryRunner::DatabaseOnDemand < NoBrainer::QueryRunner::Middlew
2
2
  def call(env)
3
3
  @runner.call(env)
4
4
  rescue RuntimeError => e
5
- if database_name = database_on_demand_exception?(e)
5
+ if database_name = handle_database_on_demand_exception?(env, e)
6
6
  auto_create_database(env, database_name)
7
7
  retry
8
8
  end
9
9
  raise
10
10
  end
11
11
 
12
- def database_on_demand_exception?(e)
13
- NoBrainer::Config.auto_create_databases && e.message =~ /^Database `(.+)` does not exist\.$/ && $1
12
+ def handle_database_on_demand_exception?(env, e)
13
+ (NoBrainer::Config.auto_create_databases || env[:auto_create_databases]) &&
14
+ e.message =~ /^Database `(.+)` does not exist\.$/ && $1
14
15
  end
15
16
 
16
17
  private
17
18
 
18
19
  def auto_create_database(env, database_name)
19
- if env[:auto_create_database] == database_name
20
+ if env[:last_auto_create_database] == database_name
20
21
  raise "Auto database creation is not working with #{database_name}"
21
22
  end
22
- env[:auto_create_database] = database_name
23
+ env[:last_auto_create_database] = database_name
23
24
 
24
25
  NoBrainer.db_create(database_name)
25
26
  rescue RuntimeError => e
@@ -10,8 +10,12 @@ class NoBrainer::QueryRunner::Logger < NoBrainer::QueryRunner::Middleware
10
10
  private
11
11
 
12
12
  def log_query(env, start_time, exception=nil)
13
- return if on_demand_exception?(exception)
14
- not_indexed = env[:criteria] && env[:criteria].where_present? && !env[:criteria].where_indexed?
13
+ return if handle_on_demand_exception?(env, exception)
14
+
15
+ not_indexed = env[:criteria] && env[:criteria].where_present? &&
16
+ !env[:criteria].where_indexed? &&
17
+ !env[:criteria].model.try(:perf_warnings_disabled)
18
+
15
19
  level = exception ? Logger::ERROR :
16
20
  not_indexed ? Logger::INFO : Logger::DEBUG
17
21
  return if NoBrainer.logger.nil? || NoBrainer.logger.level > level
@@ -19,7 +23,7 @@ class NoBrainer::QueryRunner::Logger < NoBrainer::QueryRunner::Middleware
19
23
  duration = Time.now - start_time
20
24
 
21
25
  msg_duration = (duration * 1000.0).round(1).to_s
22
- msg_duration = " " * [0, 5 - msg_duration.size].max + msg_duration
26
+ msg_duration = " " * [0, 6 - msg_duration.size].max + msg_duration
23
27
  msg_duration = "[#{msg_duration}ms] "
24
28
 
25
29
  msg_db = "[#{env[:db_name]}] " if env[:db_name] && env[:db_name].to_s != NoBrainer.connection.parsed_uri[:db]
@@ -49,9 +53,9 @@ class NoBrainer::QueryRunner::Logger < NoBrainer::QueryRunner::Middleware
49
53
  NoBrainer.logger.add(level, msg)
50
54
  end
51
55
 
52
- def on_demand_exception?(e)
56
+ def handle_on_demand_exception?(env, e)
53
57
  # 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))
58
+ e && (NoBrainer::QueryRunner::DatabaseOnDemand.new(nil).handle_database_on_demand_exception?(env, e) ||
59
+ NoBrainer::QueryRunner::TableOnDemand.new(nil).handle_table_on_demand_exception?(env, e))
56
60
  end
57
61
  end