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.
- checksums.yaml +4 -4
- data/lib/no_brainer/autoload.rb +1 -1
- data/lib/no_brainer/config.rb +54 -14
- data/lib/no_brainer/connection.rb +24 -21
- data/lib/no_brainer/connection_manager.rb +29 -9
- data/lib/no_brainer/criteria/cache.rb +8 -0
- data/lib/no_brainer/criteria/changes.rb +16 -0
- data/lib/no_brainer/criteria/core.rb +4 -5
- data/lib/no_brainer/criteria/eager_load.rb +2 -2
- data/lib/no_brainer/criteria/enumerable.rb +3 -1
- data/lib/no_brainer/criteria/find.rb +6 -9
- data/lib/no_brainer/criteria/first.rb +3 -3
- data/lib/no_brainer/criteria/first_or_create.rb +114 -0
- data/lib/no_brainer/criteria/join.rb +62 -0
- data/lib/no_brainer/criteria/order_by.rb +19 -16
- data/lib/no_brainer/criteria/run.rb +26 -0
- data/lib/no_brainer/criteria/scope.rb +8 -8
- data/lib/no_brainer/criteria/where.rb +130 -107
- data/lib/no_brainer/criteria.rb +4 -4
- data/lib/no_brainer/document/association/belongs_to.rb +44 -17
- data/lib/no_brainer/document/association/core.rb +11 -0
- data/lib/no_brainer/document/association/eager_loader.rb +26 -26
- data/lib/no_brainer/document/association/has_many.rb +7 -9
- data/lib/no_brainer/document/association/has_many_through.rb +1 -2
- data/lib/no_brainer/document/association/has_one.rb +5 -1
- data/lib/no_brainer/document/association.rb +5 -5
- data/lib/no_brainer/document/atomic_ops.rb +40 -7
- data/lib/no_brainer/document/attributes.rb +24 -15
- data/lib/no_brainer/document/callbacks.rb +1 -1
- data/lib/no_brainer/document/core.rb +18 -18
- data/lib/no_brainer/document/criteria.rb +9 -5
- data/lib/no_brainer/document/dirty.rb +15 -12
- data/lib/no_brainer/document/dynamic_attributes.rb +6 -0
- data/lib/no_brainer/document/index/index.rb +5 -9
- data/lib/no_brainer/document/index/meta_store.rb +1 -10
- data/lib/no_brainer/document/index/synchronizer.rb +16 -20
- data/lib/no_brainer/document/index.rb +3 -3
- data/lib/no_brainer/document/lazy_fetch.rb +3 -3
- data/lib/no_brainer/document/missing_attributes.rb +7 -2
- data/lib/no_brainer/document/persistance.rb +14 -30
- data/lib/no_brainer/document/polymorphic.rb +8 -4
- data/lib/no_brainer/document/primary_key/generator.rb +6 -1
- data/lib/no_brainer/document/primary_key.rb +19 -4
- data/lib/no_brainer/document/serialization.rb +0 -2
- data/lib/no_brainer/document/table_config/synchronizer.rb +21 -0
- data/lib/no_brainer/document/table_config.rb +118 -0
- data/lib/no_brainer/document/timestamps.rb +8 -0
- data/lib/no_brainer/document/validation/core.rb +63 -0
- data/lib/no_brainer/document/validation/uniqueness.rb +30 -36
- data/lib/no_brainer/document/validation.rb +1 -58
- data/lib/no_brainer/document.rb +2 -2
- data/lib/no_brainer/error.rb +1 -0
- data/lib/no_brainer/geo/base.rb +0 -1
- data/lib/no_brainer/locale/en.yml +1 -0
- data/lib/no_brainer/lock.rb +12 -8
- data/lib/no_brainer/profiler/controller_runtime.rb +76 -0
- data/lib/no_brainer/{query_runner → profiler}/logger.rb +11 -27
- data/lib/no_brainer/profiler.rb +11 -0
- data/lib/no_brainer/query_runner/database_on_demand.rb +8 -8
- data/lib/no_brainer/query_runner/driver.rb +3 -1
- data/lib/no_brainer/query_runner/missing_index.rb +3 -3
- data/lib/no_brainer/query_runner/profiler.rb +43 -0
- data/lib/no_brainer/query_runner/reconnect.rb +38 -23
- data/lib/no_brainer/query_runner/run_options.rb +35 -15
- data/lib/no_brainer/query_runner/table_on_demand.rb +18 -11
- data/lib/no_brainer/query_runner.rb +3 -3
- data/lib/no_brainer/railtie/database.rake +14 -4
- data/lib/no_brainer/railtie.rb +5 -12
- data/lib/no_brainer/rql.rb +11 -8
- data/lib/no_brainer/symbol_decoration.rb +11 -0
- data/lib/no_brainer/system/cluster_config.rb +5 -0
- data/lib/no_brainer/system/db_config.rb +5 -0
- data/lib/no_brainer/system/document.rb +24 -0
- data/lib/no_brainer/system/issue.rb +10 -0
- data/lib/no_brainer/system/job.rb +10 -0
- data/lib/no_brainer/system/log.rb +11 -0
- data/lib/no_brainer/system/server_config.rb +7 -0
- data/lib/no_brainer/system/server_status.rb +9 -0
- data/lib/no_brainer/system/stat.rb +11 -0
- data/lib/no_brainer/system/table_config.rb +10 -0
- data/lib/no_brainer/system/table_status.rb +8 -0
- data/lib/no_brainer/system.rb +17 -0
- data/lib/nobrainer.rb +16 -11
- data/lib/rails/generators/nobrainer/install_generator.rb +48 -0
- data/lib/rails/generators/nobrainer/{model/model_generator.rb → model_generator.rb} +7 -3
- data/lib/rails/generators/nobrainer/namespace_fix.rb +15 -0
- data/lib/rails/generators/{nobrainer/model/templates/model.rb.tt → templates/model.rb} +1 -1
- data/lib/rails/generators/templates/nobrainer.rb +101 -0
- metadata +34 -10
- data/lib/no_brainer/document/store_in.rb +0 -35
- 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.
|
|
63
|
-
|
|
64
|
-
|
|
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.
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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.
|
|
37
|
+
model.subclass_tree.each { |subclass| subclass.fields_to_lazy_fetch << attr }
|
|
38
38
|
else
|
|
39
|
-
model.
|
|
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
|
-
|
|
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
|
-
#
|
|
11
|
-
@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
|
|
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
|
|
129
|
-
update?(*args)
|
|
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
|
|
32
|
-
|
|
31
|
+
def subclass_tree
|
|
32
|
+
[self] + self.descendants
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def descendants_type_values
|
|
36
|
-
|
|
36
|
+
subclass_tree.map(&:type_value)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def model_from_attrs(attrs)
|
|
40
|
-
attrs['_type']
|
|
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
|
-
#
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
|
@@ -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
|