nobrainer 0.22.0 → 0.28.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/autoload.rb +1 -1
  3. data/lib/no_brainer/config.rb +54 -14
  4. data/lib/no_brainer/connection.rb +24 -21
  5. data/lib/no_brainer/connection_manager.rb +29 -9
  6. data/lib/no_brainer/criteria/cache.rb +8 -0
  7. data/lib/no_brainer/criteria/changes.rb +16 -0
  8. data/lib/no_brainer/criteria/core.rb +4 -5
  9. data/lib/no_brainer/criteria/eager_load.rb +2 -2
  10. data/lib/no_brainer/criteria/enumerable.rb +3 -1
  11. data/lib/no_brainer/criteria/find.rb +6 -9
  12. data/lib/no_brainer/criteria/first.rb +3 -3
  13. data/lib/no_brainer/criteria/first_or_create.rb +114 -0
  14. data/lib/no_brainer/criteria/join.rb +62 -0
  15. data/lib/no_brainer/criteria/order_by.rb +19 -16
  16. data/lib/no_brainer/criteria/run.rb +26 -0
  17. data/lib/no_brainer/criteria/scope.rb +8 -8
  18. data/lib/no_brainer/criteria/where.rb +130 -107
  19. data/lib/no_brainer/criteria.rb +4 -4
  20. data/lib/no_brainer/document/association/belongs_to.rb +44 -17
  21. data/lib/no_brainer/document/association/core.rb +11 -0
  22. data/lib/no_brainer/document/association/eager_loader.rb +26 -26
  23. data/lib/no_brainer/document/association/has_many.rb +7 -9
  24. data/lib/no_brainer/document/association/has_many_through.rb +1 -2
  25. data/lib/no_brainer/document/association/has_one.rb +5 -1
  26. data/lib/no_brainer/document/association.rb +5 -5
  27. data/lib/no_brainer/document/atomic_ops.rb +40 -7
  28. data/lib/no_brainer/document/attributes.rb +24 -15
  29. data/lib/no_brainer/document/callbacks.rb +1 -1
  30. data/lib/no_brainer/document/core.rb +18 -18
  31. data/lib/no_brainer/document/criteria.rb +9 -5
  32. data/lib/no_brainer/document/dirty.rb +15 -12
  33. data/lib/no_brainer/document/dynamic_attributes.rb +6 -0
  34. data/lib/no_brainer/document/index/index.rb +5 -9
  35. data/lib/no_brainer/document/index/meta_store.rb +1 -10
  36. data/lib/no_brainer/document/index/synchronizer.rb +16 -20
  37. data/lib/no_brainer/document/index.rb +3 -3
  38. data/lib/no_brainer/document/lazy_fetch.rb +3 -3
  39. data/lib/no_brainer/document/missing_attributes.rb +7 -2
  40. data/lib/no_brainer/document/persistance.rb +14 -30
  41. data/lib/no_brainer/document/polymorphic.rb +8 -4
  42. data/lib/no_brainer/document/primary_key/generator.rb +6 -1
  43. data/lib/no_brainer/document/primary_key.rb +19 -4
  44. data/lib/no_brainer/document/serialization.rb +0 -2
  45. data/lib/no_brainer/document/table_config/synchronizer.rb +21 -0
  46. data/lib/no_brainer/document/table_config.rb +118 -0
  47. data/lib/no_brainer/document/timestamps.rb +8 -0
  48. data/lib/no_brainer/document/validation/core.rb +63 -0
  49. data/lib/no_brainer/document/validation/uniqueness.rb +30 -36
  50. data/lib/no_brainer/document/validation.rb +1 -58
  51. data/lib/no_brainer/document.rb +2 -2
  52. data/lib/no_brainer/error.rb +1 -0
  53. data/lib/no_brainer/geo/base.rb +0 -1
  54. data/lib/no_brainer/locale/en.yml +1 -0
  55. data/lib/no_brainer/lock.rb +12 -8
  56. data/lib/no_brainer/profiler/controller_runtime.rb +76 -0
  57. data/lib/no_brainer/{query_runner → profiler}/logger.rb +11 -27
  58. data/lib/no_brainer/profiler.rb +11 -0
  59. data/lib/no_brainer/query_runner/database_on_demand.rb +8 -8
  60. data/lib/no_brainer/query_runner/driver.rb +3 -1
  61. data/lib/no_brainer/query_runner/missing_index.rb +3 -3
  62. data/lib/no_brainer/query_runner/profiler.rb +43 -0
  63. data/lib/no_brainer/query_runner/reconnect.rb +38 -23
  64. data/lib/no_brainer/query_runner/run_options.rb +35 -15
  65. data/lib/no_brainer/query_runner/table_on_demand.rb +18 -11
  66. data/lib/no_brainer/query_runner.rb +3 -3
  67. data/lib/no_brainer/railtie/database.rake +14 -4
  68. data/lib/no_brainer/railtie.rb +5 -12
  69. data/lib/no_brainer/rql.rb +11 -8
  70. data/lib/no_brainer/symbol_decoration.rb +11 -0
  71. data/lib/no_brainer/system/cluster_config.rb +5 -0
  72. data/lib/no_brainer/system/db_config.rb +5 -0
  73. data/lib/no_brainer/system/document.rb +24 -0
  74. data/lib/no_brainer/system/issue.rb +10 -0
  75. data/lib/no_brainer/system/job.rb +10 -0
  76. data/lib/no_brainer/system/log.rb +11 -0
  77. data/lib/no_brainer/system/server_config.rb +7 -0
  78. data/lib/no_brainer/system/server_status.rb +9 -0
  79. data/lib/no_brainer/system/stat.rb +11 -0
  80. data/lib/no_brainer/system/table_config.rb +10 -0
  81. data/lib/no_brainer/system/table_status.rb +8 -0
  82. data/lib/no_brainer/system.rb +17 -0
  83. data/lib/nobrainer.rb +16 -11
  84. data/lib/rails/generators/nobrainer/install_generator.rb +48 -0
  85. data/lib/rails/generators/nobrainer/{model/model_generator.rb → model_generator.rb} +7 -3
  86. data/lib/rails/generators/nobrainer/namespace_fix.rb +15 -0
  87. data/lib/rails/generators/{nobrainer/model/templates/model.rb.tt → templates/model.rb} +1 -1
  88. data/lib/rails/generators/templates/nobrainer.rb +101 -0
  89. metadata +34 -10
  90. data/lib/no_brainer/document/store_in.rb +0 -35
  91. data/lib/rails/generators/nobrainer.rb +0 -18
@@ -1,8 +1,6 @@
1
1
  class NoBrainer::Document::Index::Index < Struct.new(
2
2
  :model, :name, :aliased_name, :kind, :what, :external, :geo, :multi, :meta)
3
3
 
4
- MetaStore = NoBrainer::Document::Index::MetaStore
5
-
6
4
  def initialize(*args)
7
5
  super
8
6
 
@@ -59,10 +57,9 @@ class NoBrainer::Document::Index::Index < Struct.new(
59
57
  NoBrainer::RQL.reset_lambda_var_counter
60
58
  NoBrainer.run(model.rql_table.index_create(aliased_name, opt, &rql_proc))
61
59
 
62
- MetaStore.on(model.database_name) do
63
- MetaStore.create(:table_name => model.table_name, :index_name => aliased_name,
64
- :rql_function => serialized_rql_proc)
65
- end
60
+ NoBrainer::Document::Index::MetaStore.create(
61
+ :table_name => model.table_name, :index_name => aliased_name,
62
+ :rql_function => serialized_rql_proc)
66
63
  end
67
64
 
68
65
  def delete(options={})
@@ -70,9 +67,8 @@ class NoBrainer::Document::Index::Index < Struct.new(
70
67
 
71
68
  NoBrainer.run(model.rql_table.index_drop(aliased_name))
72
69
 
73
- MetaStore.on(model.database_name) do
74
- MetaStore.where(:table_name => model.table_name, :index_name => aliased_name).delete_all
75
- end
70
+ NoBrainer::Document::Index::MetaStore.where(
71
+ :table_name => model.table_name, :index_name => aliased_name).delete_all
76
72
  end
77
73
 
78
74
  def update(wanted_index, options={})
@@ -6,8 +6,7 @@ class NoBrainer::Document::Index::MetaStore
6
6
 
7
7
  default_scope ->{ order_by(:created_at) }
8
8
 
9
- store_in :database => ->{ Thread.current[:nobrainer_meta_store_db] },
10
- :table => 'nobrainer_index_meta'
9
+ table_config :name => 'nobrainer_index_meta'
11
10
 
12
11
  field :table_name, :type => String, :required => true
13
12
  field :index_name, :type => String, :required => true
@@ -20,12 +19,4 @@ class NoBrainer::Document::Index::MetaStore
20
19
  def rql_function
21
20
  JSON.load(super)
22
21
  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
- block.call
28
- ensure
29
- Thread.current[:nobrainer_meta_store_db] = old_db_name
30
- end
31
22
  end
@@ -1,16 +1,12 @@
1
1
  class NoBrainer::Document::Index::Synchronizer
2
- Index = NoBrainer::Document::Index::Index
3
- MetaStore = NoBrainer::Document::Index::MetaStore
4
-
5
2
  def initialize(models)
6
3
  @models_indexes_map = Hash[models.map do |model|
7
4
  [model, model.indexes.values.reject { |index| index.name == model.pk_name }]
8
5
  end]
9
6
  end
10
7
 
11
- def meta_store_on(db_name)
12
- @meta_store ||= {}
13
- @meta_store[db_name] ||= MetaStore.on(db_name) { MetaStore.all.to_a }
8
+ def meta_store
9
+ @meta_store ||= NoBrainer::Document::Index::MetaStore.to_a
14
10
  end
15
11
 
16
12
  class Op < Struct.new(:index, :op, :args)
@@ -21,9 +17,9 @@ class NoBrainer::Document::Index::Synchronizer
21
17
 
22
18
  def _generate_plan_for(model, wanted_indexes)
23
19
  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)
20
+ meta = meta_store.select { |i| i.table_name == model.table_name && i.index_name == s['index'] }.last
21
+ NoBrainer::Document::Index::Index.new(
22
+ model, s['index'], s['index'], nil, nil, nil, s['geo'], s['multi'], meta)
27
23
  end
28
24
 
29
25
  all_aliased_names = (wanted_indexes + current_indexes).map(&:aliased_name).uniq
@@ -46,23 +42,23 @@ class NoBrainer::Document::Index::Synchronizer
46
42
  end
47
43
 
48
44
  def generate_plan
49
- @models_indexes_map.map { |model, indexes| _generate_plan_for(model, indexes) }.flatten(1)
45
+ @models_indexes_map.flat_map { |model, indexes| _generate_plan_for(model, indexes) }
50
46
  end
51
47
 
52
48
  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()) }
49
+ lock = NoBrainer::Lock.new('nobrainer:sync_indexes')
50
+
51
+ lock.synchronize do
52
+ generate_plan.each { |op| op.run(options) }
58
53
  end
59
- end
60
54
 
61
- class << self
62
- def instance
63
- new(NoBrainer::Document.all)
55
+ unless options[:wait] == false
56
+ # Waiting on all models due to possible races
57
+ @models_indexes_map.each_key do |model|
58
+ NoBrainer.run(model.rql_table.index_wait())
59
+ end
64
60
  end
65
61
 
66
- delegate :sync_indexes, :to => :instance
62
+ true
67
63
  end
68
64
  end
@@ -26,11 +26,11 @@ module NoBrainer::Document::Index
26
26
  end
27
27
 
28
28
  if name.in?(NoBrainer::Document::Attributes::RESERVED_FIELD_NAMES)
29
- raise "Cannot use a reserved field name: #{name}"
29
+ raise "The index name `:#{name}' is reserved. Please use another one."
30
30
  end
31
31
 
32
32
  if has_field?(name) && kind != :single
33
- raise "Cannot reuse field name #{name}"
33
+ raise "The field `#{name}' is already declared. Please remove its definition first."
34
34
  end
35
35
 
36
36
  if kind == :compound && what.size < 2
@@ -56,7 +56,7 @@ module NoBrainer::Document::Index
56
56
 
57
57
  def _field(attr, options={})
58
58
  if has_index?(attr) && indexes[attr].kind != :single
59
- raise "Cannot reuse index attr #{attr}"
59
+ raise "The index `#{attr}' is already declared. Please remove its definition first."
60
60
  end
61
61
 
62
62
  super
@@ -34,9 +34,9 @@ module NoBrainer::Document::LazyFetch
34
34
  model = self
35
35
  inject_in_layer :lazy_fetch do
36
36
  if options[:lazy_fetch]
37
- model.for_each_subclass { |_model| _model.fields_to_lazy_fetch << attr }
37
+ model.subclass_tree.each { |subclass| subclass.fields_to_lazy_fetch << attr }
38
38
  else
39
- model.for_each_subclass { |_model| _model.fields_to_lazy_fetch.delete(attr) }
39
+ model.subclass_tree.each { |subclass| subclass.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 { |model| model.fields_to_lazy_fetch.delete(attr) }
60
+ subclass_tree.each { |subclass| subclass.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
@@ -6,9 +6,14 @@ module NoBrainer::Document::MissingAttributes
6
6
  end
7
7
 
8
8
  def assign_attributes(attrs, options={})
9
+ # there is one and only one key :pluck or :without to missing_attributes
9
10
  if options[:missing_attributes]
10
- # there is one and only one key :pluck or :without to missing_attributes
11
- @missing_attributes = options[:missing_attributes]
11
+ # TODO XXX this whole thing is gross.
12
+ # if @missing_attributes is already there, it's because we are doing a
13
+ # incremental reload. clear_missing_field will do the work of recognizing
14
+ # which fields are here or not.
15
+ @missing_attributes ||= options[:missing_attributes]
16
+
12
17
  assert_access_field(self.class.pk_name, "The primary key is not accessible. Use .raw or")
13
18
  assert_access_field(:_type, "The subclass type is not accessible. Use .raw or") if self.class.is_polymorphic
14
19
  end
@@ -60,12 +60,11 @@ module NoBrainer::Document::Persistance
60
60
  end
61
61
 
62
62
  def _create(options={})
63
- return false if options[:validate] != false && !valid?(nil, :clear_errors => false)
64
-
65
63
  attrs = self.class.persistable_attributes(@_attributes, :instance => self)
66
64
  result = NoBrainer.run(self.class.rql_table.insert(attrs))
67
65
  self.pk_value ||= result['generated_keys'].to_a.first
68
66
  @new_record = false
67
+ unlock_unique_fields # just an optimization for the uniquness validation
69
68
  true
70
69
  end
71
70
 
@@ -75,8 +74,6 @@ module NoBrainer::Document::Persistance
75
74
  end
76
75
 
77
76
  def _update_only_changed_attrs(options={})
78
- return false if options[:validate] != false && !valid?(nil, :clear_errors => false)
79
-
80
77
  # We won't be using the `changes` values, because they went through
81
78
  # read_attribute(), and we want the raw values.
82
79
  attrs = Hash[self.changed.map do |k|
@@ -87,26 +84,24 @@ module NoBrainer::Document::Persistance
87
84
  [k, attr]
88
85
  end]
89
86
  _update(attrs) if attrs.present?
87
+ unlock_unique_fields # just an optimization for the uniquness validation
90
88
  true
91
89
  end
92
90
 
93
- def _save?(options)
91
+ def _save?(options={})
94
92
  new_record? ? _create(options) : _update_only_changed_attrs(options)
95
93
  end
96
94
 
97
95
  def save?(options={})
98
- errors.clear
99
96
  _save?(options)
100
97
  end
101
98
 
102
- def save(*args)
99
+ def save!(*args)
103
100
  save?(*args) or raise NoBrainer::Error::DocumentInvalid, self
104
- nil
105
101
  end
106
102
 
107
- def save!(*args)
108
- save(*args)
109
- :you_should_be_using_the_non_bang_version_of_save
103
+ def save(*args)
104
+ save?(*args)
110
105
  end
111
106
 
112
107
  def update?(attrs, options={})
@@ -114,27 +109,13 @@ module NoBrainer::Document::Persistance
114
109
  save?(options)
115
110
  end
116
111
 
117
- def update(*args)
118
- update?(*args) or raise NoBrainer::Error::DocumentInvalid, self
119
- nil
120
- end
121
-
122
112
  def update!(*args)
123
- update(*args)
124
- :you_should_be_using_the_non_bang_version_of_update
113
+ update?(*args) or raise NoBrainer::Error::DocumentInvalid, self
125
114
  end
126
115
  alias_method :update_attributes!, :update!
127
116
 
128
- def update_attributes?(*args)
129
- update?(*args).tap { STDERR.puts "[NoBrainer] update_attributes?() is deprecated. Please use update?() instead" }
130
- end
131
-
132
- def update_attributes(*args)
133
- update(*args).tap { STDERR.puts "[NoBrainer] update_attributes() is deprecated. Please use update() instead" }
134
- end
135
-
136
- def update_attributes!(*args)
137
- update!(*args).tap { STDERR.puts "[NoBrainer] update_attributes!() is deprecated. Please use update() instead" }
117
+ def update(*args)
118
+ update?(*args)
138
119
  end
139
120
 
140
121
  def delete
@@ -152,9 +133,12 @@ module NoBrainer::Document::Persistance
152
133
 
153
134
  module ClassMethods
154
135
  def create(attrs={}, options={})
155
- new(attrs, options).tap { |doc| doc.save(options) }
136
+ new(attrs, options).tap { |doc| doc.save?(options) }
137
+ end
138
+
139
+ def create!(attrs={}, options={})
140
+ new(attrs, options).tap { |doc| doc.save!(options) }
156
141
  end
157
- alias_method :create!, :create
158
142
 
159
143
  def insert_all(*args)
160
144
  docs = args.shift
@@ -28,16 +28,20 @@ module NoBrainer::Document::Polymorphic
28
28
  self == root_class
29
29
  end
30
30
 
31
- def for_each_subclass(&block)
32
- ([self] + self.descendants).each(&block)
31
+ def subclass_tree
32
+ [self] + self.descendants
33
33
  end
34
34
 
35
35
  def descendants_type_values
36
- for_each_subclass.map(&:type_value)
36
+ subclass_tree.map(&:type_value)
37
37
  end
38
38
 
39
39
  def model_from_attrs(attrs)
40
- attrs['_type'].try(:constantize) || root_class
40
+ class_name = attrs['_type'] || attrs[:_type]
41
+ return root_class unless class_name
42
+ class_name.to_s.constantize.tap { |cls| raise NameError unless cls <= self }
43
+ rescue NameError
44
+ raise NoBrainer::Error::InvalidPolymorphicType, "Invalid polymorphic class: `#{class_name}' is not a `#{self}'"
41
45
  end
42
46
 
43
47
  def all
@@ -20,6 +20,7 @@ module NoBrainer::Document::PrimaryKey::Generator
20
20
  # 1% of chance to have a collision with ~580 servers.
21
21
  # When using more than 500 machines, it's therefore a good
22
22
  # idea to set the machine_id manually to avoid collisions.
23
+ # XXX This is referenced in nobrainer/config.rb#default_machine_id
23
24
  MACHINE_ID_BITS = 24
24
25
 
25
26
  # 15 bits for the current pid. We wouldn't need it if the sequence number was
@@ -27,7 +28,7 @@ module NoBrainer::Document::PrimaryKey::Generator
27
28
  PID_BITS = 15
28
29
 
29
30
  # Total: 83 bits
30
- # We need at most 14 digits in [A-Za-z0-9] to represent 83 bits:
31
+ # With 14 digits in [A-Za-z0-9], we can represent 83 bits:
31
32
  # Math.log(62**14)/Math.log(2) = 83.35
32
33
  ID_STR_LENGTH = 14
33
34
 
@@ -80,4 +81,8 @@ module NoBrainer::Document::PrimaryKey::Generator
80
81
  def self.generate
81
82
  convert_to_alphanum(@lock.synchronize { _generate })
82
83
  end
84
+
85
+ def self.field_type
86
+ String
87
+ end
83
88
  end
@@ -3,6 +3,7 @@ module NoBrainer::Document::PrimaryKey
3
3
  autoload :Generator
4
4
 
5
5
  extend ActiveSupport::Concern
6
+ include ActiveModel::Conversion
6
7
 
7
8
  DEFAULT_PK_NAME = :id
8
9
 
@@ -23,6 +24,15 @@ module NoBrainer::Document::PrimaryKey
23
24
 
24
25
  delegate :hash, :to => :pk_value
25
26
 
27
+ def cache_key
28
+ "#{self.class.table_name}/#{pk_value}"
29
+ end
30
+
31
+ def to_key
32
+ # ActiveModel::Conversion
33
+ [pk_value]
34
+ end
35
+
26
36
  module ClassMethods
27
37
  def define_default_pk
28
38
  class_variable_set(:@@pk_name, nil)
@@ -30,6 +40,7 @@ module NoBrainer::Document::PrimaryKey
30
40
  end
31
41
 
32
42
  def define_pk(attr)
43
+ return if pk_name == attr
33
44
  if fields[pk_name].try(:[], :primary_key) == :default
34
45
  remove_field(pk_name, :set_default_pk => false)
35
46
  end
@@ -46,15 +57,19 @@ module NoBrainer::Document::PrimaryKey
46
57
  end
47
58
 
48
59
  def field(attr, options={})
49
- if options[:primary_key]
60
+ if attr.to_sym == pk_name || options[:primary_key]
50
61
  options = options.merge(:readonly => true) if options[:readonly].nil?
51
62
  options = options.merge(:index => true)
52
63
 
53
- if options[:type].in?([String, nil]) && options[:default].nil?
54
- options[:type] = String
55
- options[:default] = ->{ NoBrainer::Document::PrimaryKey::Generator.generate }
64
+ # TODO Maybe we should let the user configure the pk generator
65
+ pk_generator = NoBrainer::Document::PrimaryKey::Generator
66
+
67
+ if options[:type].in?([pk_generator.field_type, nil]) && !options.key?(:default)
68
+ options[:type] = pk_generator.field_type
69
+ options[:default] = ->{ pk_generator.generate }
56
70
  end
57
71
  end
72
+
58
73
  super
59
74
  end
60
75
 
@@ -4,8 +4,6 @@ module NoBrainer::Document::Serialization
4
4
  include ActiveModel::Serialization
5
5
  include ActiveModel::Serializers::JSON
6
6
 
7
- included { self.include_root_in_json = false }
8
-
9
7
  def read_attribute_for_serialization(*a, &b)
10
8
  read_attribute(*a, &b)
11
9
  end
@@ -0,0 +1,21 @@
1
+ class NoBrainer::Document::TableConfig::Synchronizer
2
+ def initialize(models)
3
+ @models = models
4
+ end
5
+
6
+ def sync_table_config(options={})
7
+ # XXX A bit funny since we might touch the lock table...
8
+ lock = NoBrainer::Lock.new('nobrainer:sync_table_config')
9
+
10
+ lock.synchronize do
11
+ @models.each(&:sync_table_config)
12
+ end
13
+
14
+ unless options[:wait] == false
15
+ # Waiting on all models due to possible races
16
+ @models.each(&:table_wait)
17
+ end
18
+
19
+ true
20
+ end
21
+ end
@@ -0,0 +1,118 @@
1
+ require 'rethinkdb'
2
+
3
+ module NoBrainer::Document::TableConfig
4
+ extend ActiveSupport::Concern
5
+ extend NoBrainer::Autoload
6
+
7
+ autoload :Synchronizer
8
+
9
+ VALID_TABLE_CONFIG_OPTIONS = [:name, :durability, :shards, :replicas, :primary_replica_tag, :write_acks]
10
+
11
+ included do
12
+ cattr_accessor :table_config_options, :instance_accessor => false
13
+ self.table_config_options = {}
14
+ end
15
+
16
+ module ClassMethods
17
+ def store_in(options)
18
+ if options[:table]
19
+ STDERR.puts "[NoBrainer] `store_in(table: ...)' has been removed. Use `table_config(name: ...)' instead."
20
+ options[:name] = options.delete(:table)
21
+ end
22
+
23
+ if options[:database] || options[:db]
24
+ raise "`store_in(db: ...)' has been removed. Use `run_with(db: ...)' instead."
25
+ end
26
+
27
+ table_config(options)
28
+ end
29
+
30
+ def _set_table_config(options)
31
+ raise "table_config() must be used at the parent class, not a subclass" unless is_root_class?
32
+
33
+ options.assert_valid_keys(*VALID_TABLE_CONFIG_OPTIONS)
34
+ self.table_config_options.merge!(options)
35
+ end
36
+
37
+ def table_name
38
+ name = table_config_options[:name]
39
+ name = name.call if name.is_a?(Proc)
40
+ (name || root_class.name.tableize.gsub('/', '__')).to_s
41
+ end
42
+
43
+ def rql_table
44
+ RethinkDB::RQL.new.table(table_name)
45
+ end
46
+
47
+ def table_config(options={})
48
+ return _set_table_config(options) unless options.empty?
49
+ NoBrainer::System::TableConfig.new_from_db(NoBrainer.run { rql_table.config })
50
+ end
51
+
52
+ def table_status
53
+ NoBrainer::System::TableConfig.new_from_db(NoBrainer.run { rql_table.status })
54
+ end
55
+
56
+ def table_stats
57
+ NoBrainer::System::Stats.where(:db => NoBrainer.current_db, :table => table_name).to_a
58
+ end
59
+
60
+ def rebalance
61
+ NoBrainer.run { rql_table.rebalance }
62
+ true
63
+ end
64
+
65
+ def table_wait
66
+ NoBrainer.run { rql_table.wait }
67
+ end
68
+
69
+ def table_create_options
70
+ NoBrainer::Config.table_options
71
+ .merge(table_config_options)
72
+ .merge(:name => table_name)
73
+ .merge(:primary_key => lookup_field_alias(pk_name))
74
+ .reverse_merge(:durability => 'hard')
75
+ .reduce({}) { |h,(k,v)| h[k] = v.is_a?(Symbol) ? v.to_s : v; h } # symbols -> strings
76
+ end
77
+
78
+ def sync_table_config(options={})
79
+ c = table_create_options
80
+ table_config.update!(c.slice(:durability, :primary_key, :write_acks))
81
+ NoBrainer.run { rql_table.reconfigure(c.slice(:shards, :replicas, :primary_replica_tag)) }
82
+ true
83
+ end
84
+
85
+ def sync_indexes(options={})
86
+ NoBrainer::Document::Index::Synchronizer.new(self).sync_indexes(options)
87
+ end
88
+
89
+ def sync_schema(options={})
90
+ sync_table_config(options)
91
+ sync_indexes(options)
92
+ end
93
+ end
94
+
95
+ class << self
96
+ def sync_table_config(options={})
97
+ models = NoBrainer::Document.all(:types => [:user, :nobrainer])
98
+ NoBrainer::Document::TableConfig::Synchronizer.new(models).sync_table_config(options)
99
+ end
100
+
101
+ def sync_indexes(options={})
102
+ # nobrainer models don't have indexes
103
+ models = NoBrainer::Document.all(:types => [:user])
104
+ NoBrainer::Document::Index::Synchronizer.new(models).sync_indexes(options)
105
+ end
106
+
107
+ def sync_schema(options={})
108
+ sync_table_config(options)
109
+ sync_indexes(options)
110
+ end
111
+
112
+ def rebalance(options={})
113
+ models = NoBrainer::Document.all(:types => [:user])
114
+ models.each(&:rebalance)
115
+ true
116
+ end
117
+ end
118
+ end
@@ -17,4 +17,12 @@ module NoBrainer::Document::Timestamps
17
17
  self.updated_at = Time.now unless updated_at_changed?
18
18
  super(attrs.merge('updated_at' => @_attributes['updated_at']))
19
19
  end
20
+
21
+ def cache_key
22
+ "#{super}#{updated_at.try(:strftime, "-%s%L")}"
23
+ end
24
+
25
+ def touch
26
+ update!(:updated_at => Time.now)
27
+ end
20
28
  end
@@ -0,0 +1,63 @@
1
+ module NoBrainer::Document::Validation::Core
2
+ extend ActiveSupport::Concern
3
+ include ActiveModel::Validations
4
+ include ActiveModel::Validations::Callbacks
5
+
6
+ included do
7
+ # We don't want before_validation returning false to halt the chain.
8
+ define_callbacks :validation, :skip_after_callbacks_if_terminated => true,
9
+ :scope => [:kind, :name], :terminator => proc { false }
10
+ end
11
+
12
+ def valid?(context=nil, options={})
13
+ super(context || (new_record? ? :create : :update))
14
+ end
15
+
16
+ def save?(options={})
17
+ options = { :validate => true }.merge(options)
18
+
19
+ if options[:validate]
20
+ valid? ? super : false
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ SHORTHANDS = { :format => :format, :length => :length, :required => :presence,
27
+ :uniq => :uniqueness, :unique => :uniqueness, :in => :inclusion }
28
+
29
+ module ClassMethods
30
+ def _field(attr, options={})
31
+ super
32
+
33
+ shorthands = SHORTHANDS
34
+ shorthands = shorthands.merge(:required => :not_null) if options[:type] == NoBrainer::Boolean
35
+ shorthands.each { |k,v| validates(attr, v => options[k]) if options.key?(k) }
36
+
37
+ validates(attr, options[:validates]) if options[:validates]
38
+ validates(attr, :length => { :minimum => options[:min_length] }) if options[:min_length]
39
+ validates(attr, :length => { :maximum => options[:max_length] }) if options[:max_length]
40
+ end
41
+ end
42
+ end
43
+
44
+ class ActiveModel::EachValidator
45
+ def should_validate_field?(record, attribute)
46
+ return true unless record.is_a?(NoBrainer::Document)
47
+ return true if record.new_record?
48
+
49
+ attr_changed = "#{attribute}_changed?"
50
+ return record.respond_to?(attr_changed) ? record.__send__(attr_changed) : true
51
+ end
52
+
53
+ # XXX Monkey Patching :(
54
+ def validate(record)
55
+ attributes.each do |attribute|
56
+ next unless should_validate_field?(record, attribute) # <--- Added
57
+ value = record.read_attribute_for_validation(attribute)
58
+ next if value.is_a?(NoBrainer::Document::AtomicOps::PendingAtomic) # <--- Added
59
+ next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
60
+ validate_each(record, attribute, value)
61
+ end
62
+ end
63
+ end