nobrainer 0.42.0 → 0.44.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -2
- data/lib/no_brainer/config.rb +29 -22
- data/lib/no_brainer/criteria/join.rb +1 -0
- data/lib/no_brainer/criteria/where.rb +47 -4
- data/lib/no_brainer/document/association/belongs_to.rb +70 -13
- data/lib/no_brainer/document/association/core.rb +4 -3
- data/lib/no_brainer/document/association/eager_loader.rb +15 -2
- data/lib/no_brainer/document/association/has_many.rb +26 -8
- data/lib/no_brainer/document/association.rb +5 -4
- data/lib/no_brainer/document/attributes.rb +11 -3
- data/lib/no_brainer/error.rb +17 -15
- data/lib/no_brainer/profiler/logger.rb +58 -41
- data/lib/no_brainer/profiler/slow_queries.rb +23 -0
- data/lib/no_brainer/query_runner/profiler.rb +8 -4
- data/lib/rails/generators/templates/nobrainer.rb +14 -0
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '035182f731c727beff4562127d0ec36aaa7ad56871f11121940f05b0b72819c2'
|
4
|
+
data.tar.gz: aa14e68a3ff7fded92a16171842dff3a7329cb78c93946af9475255d25bba1ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b9ab73b56b9df5df5098d7a769581fe367284fa7200b19b7cfd637982f8bf3afd36e071e3f12370504e87e1799bc0d7f1f5ee03ca62617f9f3bfbbb62c6e51df
|
7
|
+
data.tar.gz: 42a7831651d35f55bd39c68c6baaec3b20e845c82e6d5ce562d9e5c2b4184cfcd3daf754ab097bcc3bffe91c05ab873e2d083d37d21c5e44bbfc6afb81197f22
|
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
|
+
## [0.44.0] - 2023-07-31
|
10
|
+
### Added
|
11
|
+
- Slow Queries Logger feature logging slow queries in a dedicated log file
|
12
|
+
|
13
|
+
### Fixed
|
14
|
+
- `first_or_create` with a polymorphic association [#288](https://github.com/NoBrainerORM/nobrainer/issues/288)
|
15
|
+
|
16
|
+
## [0.43.0] - 2022-06-16
|
17
|
+
### Added
|
18
|
+
- Implements polymorphic associations
|
9
19
|
|
10
20
|
## [0.42.0] - 2022-06-15
|
11
21
|
### Added
|
@@ -124,8 +134,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
124
134
|
- Locks: bug fix: allow small timeouts in lock()
|
125
135
|
- Fix reentrant lock counter on steals
|
126
136
|
|
127
|
-
[Unreleased]: https://github.com/nobrainerorm/nobrainer/compare/v0.
|
128
|
-
[0.
|
137
|
+
[Unreleased]: https://github.com/nobrainerorm/nobrainer/compare/v0.44.0...HEAD
|
138
|
+
[0.44.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.43.0...v0.44.0
|
139
|
+
[0.43.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.42.0...v0.43.0
|
140
|
+
[0.42.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.41.1...v0.42.0
|
129
141
|
[0.41.1]: https://github.com/nobrainerorm/nobrainer/compare/v0.41.0...v0.41.1
|
130
142
|
[0.41.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.40.0...v0.41.0
|
131
143
|
[0.40.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.36.0...v0.40.0
|
data/lib/no_brainer/config.rb
CHANGED
@@ -1,29 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'logger'
|
2
4
|
|
3
5
|
module NoBrainer::Config
|
4
6
|
SETTINGS = {
|
5
|
-
:
|
6
|
-
:
|
7
|
-
:
|
8
|
-
:
|
9
|
-
:
|
10
|
-
:
|
11
|
-
:
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:
|
15
|
-
|
16
|
-
:
|
17
|
-
:
|
18
|
-
:
|
19
|
-
:
|
20
|
-
:
|
21
|
-
:
|
22
|
-
:
|
23
|
-
:
|
24
|
-
:
|
25
|
-
:
|
26
|
-
|
7
|
+
app_name: { default: -> { default_app_name } },
|
8
|
+
colorize_logger: { default: -> { true }, valid_values: [true, false] },
|
9
|
+
criteria_cache_max_entries: { default: -> { 10_000 } },
|
10
|
+
db_timezone: { default: -> { :utc }, valid_values: %i[unchanged utc local] },
|
11
|
+
distributed_lock_class: { default: -> { 'NoBrainer::Lock' } },
|
12
|
+
driver: { default: -> { :regular }, valid_values: %i[regular em] },
|
13
|
+
durability: { default: -> {} }, # legacy
|
14
|
+
environment: { default: -> { default_environment } },
|
15
|
+
geo_options: { default: -> { { geo_system: 'WGS84', unit: 'm' } } },
|
16
|
+
lock_options: { default: -> { { expire: 60, timeout: 10 } }, valid_keys: %i[expire timeout] },
|
17
|
+
log_slow_queries: { default: -> { false } },
|
18
|
+
logger: { default: -> { default_logger } },
|
19
|
+
long_query_time: { default: -> { 10 } },
|
20
|
+
machine_id: { default: -> { default_machine_id } },
|
21
|
+
max_string_length: { default: -> { 255 } },
|
22
|
+
per_thread_connection: { default: -> { false }, valid_values: [true, false] },
|
23
|
+
rethinkdb_urls: { default: -> { [default_rethinkdb_url] } },
|
24
|
+
run_options: { default: -> { { durability: default_durability } } },
|
25
|
+
slow_query_log_file: { default: -> { File.join('/', 'var', 'log', 'rethinkdb', 'slow_queries.log') } },
|
26
|
+
ssl_options: { default: -> {} },
|
27
|
+
table_options: {
|
28
|
+
default: -> { { shards: 1, replicas: 1, write_acks: :majority } },
|
29
|
+
valid_keys: %i[durability shards replicas primary_replica_tag nonvoting_replica_tags write_acks]
|
30
|
+
},
|
31
|
+
user_timezone: { default: -> { :local }, valid_values: %i[unchanged utc local] },
|
32
|
+
warn_on_active_record: { default: -> { true }, valid_values: [true, false] }
|
33
|
+
}.freeze
|
27
34
|
|
28
35
|
class << self
|
29
36
|
attr_accessor(*SETTINGS.keys)
|
@@ -16,6 +16,7 @@ module NoBrainer::Criteria::Join
|
|
16
16
|
association = model.association_metadata[k.to_sym]
|
17
17
|
raise "`#{k}' must be an association on `#{model}'" unless association
|
18
18
|
raise "join() does not support through associations" if association.options[:through]
|
19
|
+
raise "join() does not support polymorphic associations" if association.options[:polymorphic]
|
19
20
|
|
20
21
|
criteria = association.base_criteria
|
21
22
|
criteria = case v
|
@@ -107,7 +107,22 @@ module NoBrainer::Criteria::Where
|
|
107
107
|
when :during then [key_modifier, op, [cast_value(value.first), cast_value(value.last)]]
|
108
108
|
else [key_modifier, op, cast_value(value)]
|
109
109
|
end
|
110
|
-
|
110
|
+
|
111
|
+
# When key_path relates to a polymorphic associatoin, the new_key_path is
|
112
|
+
# an Array containing the foreign_type and then the foreign_key.
|
113
|
+
if new_key_path.first.is_a?(Array)
|
114
|
+
foreign_type, foreign_key = new_key_path.first
|
115
|
+
|
116
|
+
MultiOperator.new(
|
117
|
+
:and,
|
118
|
+
[
|
119
|
+
BinaryOperator.new([foreign_type], new_key_modifier, new_op, value.class.to_s, model, true),
|
120
|
+
BinaryOperator.new([foreign_key], new_key_modifier, new_op, value.__send__(value.class.pk_name), model, true)
|
121
|
+
]
|
122
|
+
)
|
123
|
+
else
|
124
|
+
BinaryOperator.new(new_key_path, new_key_modifier, new_op, new_value, model, true)
|
125
|
+
end
|
111
126
|
end
|
112
127
|
|
113
128
|
def to_rql(doc)
|
@@ -185,7 +200,7 @@ module NoBrainer::Criteria::Where
|
|
185
200
|
box_value = key_modifier.in?([:any, :all]) || op == :include
|
186
201
|
value = [value] if box_value
|
187
202
|
k_v = key_path.reverse.reduce(value) { |v,k| {k => v} }.first
|
188
|
-
k_v = model.association_user_to_model_cast(*k_v)
|
203
|
+
k_v = model.association_user_to_model_cast(*k_v, value.class)
|
189
204
|
value = model.cast_user_to_db_for(*k_v)
|
190
205
|
value = key_path[1..-1].reduce(value) { |h,k| h[k] }
|
191
206
|
value = value.first if box_value
|
@@ -193,15 +208,43 @@ module NoBrainer::Criteria::Where
|
|
193
208
|
end
|
194
209
|
end
|
195
210
|
|
211
|
+
#
|
212
|
+
# This method is used in order to transform association from the passed
|
213
|
+
# `key_path` in to model's field(s).
|
214
|
+
#
|
215
|
+
# When `key_path` contains a field, this method just ensures the given field
|
216
|
+
# is well defined in the owner model.
|
217
|
+
# When `key_path` contains the name of an association, this method updates
|
218
|
+
# the `key_path` in order to replace the association name with the field(s)
|
219
|
+
# behind that association.
|
220
|
+
#
|
221
|
+
# In the case of :
|
222
|
+
# * a polymorphic association name passed as `key_path`, the association
|
223
|
+
# name will be replaced by the 2 fields representing it (foreign_key and
|
224
|
+
# foreign_type)
|
225
|
+
# * all other association the association name is replaced by
|
226
|
+
# the primary_key or the foreign_key depending on the association type
|
196
227
|
def cast_key_path(key_path)
|
197
228
|
return key_path if casted_values
|
198
229
|
|
230
|
+
# key_path is an Array of symbols representing the path to the key being
|
231
|
+
# queried.
|
232
|
+
#
|
233
|
+
# The Array size can be greater that 1 when quering from a field with,
|
234
|
+
# the type `Hash` and the query is targetting a nested key from the Hash.
|
235
|
+
#
|
236
|
+
# The first Array element is always the field name.
|
199
237
|
if key_path.size == 1
|
200
|
-
|
201
|
-
|
238
|
+
# With fields and non-polymorphic associations `keys` will be a symbol
|
239
|
+
# while with a polymorphic association it will be an Array of symbols
|
240
|
+
# being the two fields used by the association.
|
241
|
+
keys, _v = model.association_user_to_model_cast(key_path.first, nil, value.class)
|
242
|
+
key_path = [keys]
|
202
243
|
end
|
203
244
|
|
245
|
+
# Ensures fields exist on the model
|
204
246
|
model.ensure_valid_key!(key_path.first)
|
247
|
+
|
205
248
|
key_path
|
206
249
|
end
|
207
250
|
end
|
@@ -2,8 +2,11 @@ class NoBrainer::Document::Association::BelongsTo
|
|
2
2
|
include NoBrainer::Document::Association::Core
|
3
3
|
|
4
4
|
class Metadata
|
5
|
-
VALID_OPTIONS = [
|
6
|
-
|
5
|
+
VALID_OPTIONS = %i[
|
6
|
+
primary_key foreign_key foreign_type class_name foreign_key_store_as
|
7
|
+
index validates required uniq unique polymorphic
|
8
|
+
]
|
9
|
+
|
7
10
|
include NoBrainer::Document::Association::Core::Metadata
|
8
11
|
include NoBrainer::Document::Association::EagerLoader::Generic
|
9
12
|
|
@@ -11,6 +14,12 @@ class NoBrainer::Document::Association::BelongsTo
|
|
11
14
|
options[:foreign_key].try(:to_sym) || :"#{target_name}_#{primary_key}"
|
12
15
|
end
|
13
16
|
|
17
|
+
def foreign_type
|
18
|
+
return nil unless options[:polymorphic]
|
19
|
+
|
20
|
+
options[:foreign_type].try(:to_sym) || (:"#{target_name}_type")
|
21
|
+
end
|
22
|
+
|
14
23
|
def primary_key
|
15
24
|
# We default the primary_key to `:id' and not `target_model.pk_name',
|
16
25
|
# because we don't want to require the target_model to be already loaded.
|
@@ -30,12 +39,22 @@ class NoBrainer::Document::Association::BelongsTo
|
|
30
39
|
end
|
31
40
|
end
|
32
41
|
|
33
|
-
def target_model
|
34
|
-
|
42
|
+
def target_model(target_class = nil)
|
43
|
+
return if options[:polymorphic] && target_class.nil?
|
44
|
+
|
45
|
+
model_name = if options[:polymorphic]
|
46
|
+
target_class
|
47
|
+
else
|
48
|
+
options[:class_name] || target_name.to_s.camelize
|
49
|
+
end
|
50
|
+
|
51
|
+
get_model_by_name(model_name)
|
35
52
|
end
|
36
53
|
|
37
|
-
def base_criteria
|
38
|
-
target_model
|
54
|
+
def base_criteria(target_class = nil)
|
55
|
+
model = target_model(target_class)
|
56
|
+
|
57
|
+
model ? model.without_ordering : nil
|
39
58
|
end
|
40
59
|
|
41
60
|
def hook
|
@@ -47,7 +66,20 @@ class NoBrainer::Document::Association::BelongsTo
|
|
47
66
|
raise "Cannot declare `#{target_name}' in #{owner_model}: the foreign_key `#{foreign_key}' is already used"
|
48
67
|
end
|
49
68
|
|
50
|
-
|
69
|
+
if options[:polymorphic] && options[:class_name]
|
70
|
+
raise 'You cannot set class_name on a polymorphic belongs_to'
|
71
|
+
end
|
72
|
+
|
73
|
+
if options[:polymorphic]
|
74
|
+
if options[:uniq] || options[:unique]
|
75
|
+
owner_model.field(foreign_type, uniq: { scope: foreign_key })
|
76
|
+
owner_model.index([foreign_type, foreign_key])
|
77
|
+
else
|
78
|
+
owner_model.field(foreign_type)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
owner_model.field(foreign_key, store_as: options[:foreign_key_store_as], index: options[:index])
|
51
83
|
|
52
84
|
unless options[:validates] == false
|
53
85
|
owner_model.validates(target_name, options[:validates]) if options[:validates]
|
@@ -74,17 +106,25 @@ class NoBrainer::Document::Association::BelongsTo
|
|
74
106
|
add_callback_for(:after_validation)
|
75
107
|
end
|
76
108
|
|
77
|
-
def cast_attr(
|
78
|
-
case
|
79
|
-
when target_model
|
80
|
-
|
109
|
+
def cast_attr(key, value, target_class = nil)
|
110
|
+
case value
|
111
|
+
when target_model(target_class)
|
112
|
+
[foreign_key, value.__send__(primary_key)]
|
113
|
+
when nil
|
114
|
+
if options[:polymorphic]
|
115
|
+
[[foreign_type, foreign_key], nil]
|
116
|
+
else
|
117
|
+
[foreign_key, nil]
|
118
|
+
end
|
81
119
|
else
|
82
|
-
|
83
|
-
|
120
|
+
raise NoBrainer::Error::InvalidType.new(
|
121
|
+
model: owner_model, attr_name: key, type: target_model, value: value
|
122
|
+
)
|
84
123
|
end
|
85
124
|
end
|
86
125
|
|
87
126
|
def eager_load_owner_key; foreign_key; end
|
127
|
+
def eager_load_owner_type; foreign_type; end
|
88
128
|
def eager_load_target_key; primary_key; end
|
89
129
|
end
|
90
130
|
|
@@ -97,6 +137,17 @@ class NoBrainer::Document::Association::BelongsTo
|
|
97
137
|
@target_container = nil
|
98
138
|
end
|
99
139
|
|
140
|
+
def polymorphic_read
|
141
|
+
return target if loaded?
|
142
|
+
|
143
|
+
target_class = owner.read_attribute(foreign_type)
|
144
|
+
fk = owner.read_attribute(foreign_key)
|
145
|
+
|
146
|
+
if target_class && fk
|
147
|
+
preload(base_criteria(target_class).where(primary_key => fk).first)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
100
151
|
def read
|
101
152
|
return target if loaded?
|
102
153
|
|
@@ -105,6 +156,12 @@ class NoBrainer::Document::Association::BelongsTo
|
|
105
156
|
end
|
106
157
|
end
|
107
158
|
|
159
|
+
def polymorphic_write(target)
|
160
|
+
owner.write_attribute(foreign_key, target.try(primary_key))
|
161
|
+
owner.write_attribute(foreign_type, target.root_class.name)
|
162
|
+
preload(target)
|
163
|
+
end
|
164
|
+
|
108
165
|
def write(target)
|
109
166
|
assert_target_type(target)
|
110
167
|
owner.write_attribute(foreign_key, target.try(primary_key))
|
@@ -33,8 +33,8 @@ module NoBrainer::Document::Association::Core
|
|
33
33
|
|
34
34
|
def hook
|
35
35
|
options.assert_valid_keys(*self.class.const_get(:VALID_OPTIONS))
|
36
|
-
delegate("#{target_name}=", :write)
|
37
|
-
delegate("#{target_name}", :read)
|
36
|
+
delegate("#{target_name}=", "#{'polymorphic_' if options[:polymorphic]}write".to_sym)
|
37
|
+
delegate("#{target_name}", "#{'polymorphic_' if options[:polymorphic]}read".to_sym)
|
38
38
|
end
|
39
39
|
|
40
40
|
def add_callback_for(what)
|
@@ -62,7 +62,8 @@ module NoBrainer::Document::Association::Core
|
|
62
62
|
|
63
63
|
included { attr_accessor :metadata, :owner }
|
64
64
|
|
65
|
-
delegate :primary_key, :foreign_key, :
|
65
|
+
delegate :primary_key, :foreign_key, :foreign_type, :target_name,
|
66
|
+
:target_model, :base_criteria, :to => :metadata
|
66
67
|
|
67
68
|
def initialize(metadata, owner)
|
68
69
|
@metadata, @owner = metadata, owner
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module NoBrainer::Document::Association::EagerLoader
|
2
4
|
extend self
|
3
5
|
|
@@ -5,11 +7,22 @@ module NoBrainer::Document::Association::EagerLoader
|
|
5
7
|
# Used in associations to declare generic eager loading capabilities
|
6
8
|
# The association should implement loaded?, preload,
|
7
9
|
# eager_load_owner_key and eager_load_target_key.
|
8
|
-
def eager_load(docs, additional_criteria=nil)
|
10
|
+
def eager_load(docs, additional_criteria = nil)
|
9
11
|
owner_key = eager_load_owner_key
|
12
|
+
owner_type = eager_load_owner_type
|
10
13
|
target_key = eager_load_target_key
|
11
14
|
|
12
|
-
|
15
|
+
if is_a?(NoBrainer::Document::Association::BelongsTo::Metadata) && owner_type
|
16
|
+
target_class = docs.first.__send__(owner_type)
|
17
|
+
|
18
|
+
if docs.detect { |doc| doc.__send__(owner_type) != target_class }
|
19
|
+
raise NoBrainer::Error::PolymorphicAssociationWithDifferentTypes,
|
20
|
+
"The documents to be eager loaded doesn't have the same " \
|
21
|
+
'type, which is not supported'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
criteria = target_class ? base_criteria(target_class) : base_criteria
|
13
26
|
criteria = criteria.merge(additional_criteria) if additional_criteria
|
14
27
|
|
15
28
|
unloaded_docs = docs.reject { |doc| doc.associations[self].loaded? }
|
@@ -2,12 +2,20 @@ class NoBrainer::Document::Association::HasMany
|
|
2
2
|
include NoBrainer::Document::Association::Core
|
3
3
|
|
4
4
|
class Metadata
|
5
|
-
VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :dependent, :scope
|
5
|
+
VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :dependent, :scope,
|
6
|
+
:as]
|
6
7
|
include NoBrainer::Document::Association::Core::Metadata
|
7
8
|
include NoBrainer::Document::Association::EagerLoader::Generic
|
8
9
|
|
9
10
|
def foreign_key
|
10
|
-
options[:foreign_key].try(:to_sym)
|
11
|
+
return options[:foreign_key].try(:to_sym) if options.key?(:foreign_key)
|
12
|
+
return :"#{options[:as]}_#{primary_key}" if options[:as]
|
13
|
+
|
14
|
+
:"#{owner_model.name.split('::').last.underscore}_#{primary_key}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def foreign_type
|
18
|
+
options[:foreign_type].try(:to_sym) || (options[:as] && :"#{options[:as]}_type")
|
11
19
|
end
|
12
20
|
|
13
21
|
def primary_key
|
@@ -30,9 +38,9 @@ class NoBrainer::Document::Association::HasMany
|
|
30
38
|
# caching is hard (rails console reload, etc.).
|
31
39
|
target_model.association_metadata.values.select do |assoc|
|
32
40
|
assoc.is_a?(NoBrainer::Document::Association::BelongsTo::Metadata) and
|
33
|
-
assoc.foreign_key ==
|
34
|
-
assoc.primary_key ==
|
35
|
-
assoc.target_model.root_class == owner_model.root_class
|
41
|
+
assoc.foreign_key == foreign_key and
|
42
|
+
assoc.primary_key == primary_key and
|
43
|
+
assoc.target_model(target_model).root_class == owner_model.root_class
|
36
44
|
end
|
37
45
|
end
|
38
46
|
|
@@ -46,7 +54,7 @@ class NoBrainer::Document::Association::HasMany
|
|
46
54
|
|
47
55
|
if options[:dependent]
|
48
56
|
unless [:destroy, :delete, :nullify, :restrict, nil].include?(options[:dependent])
|
49
|
-
raise "Invalid dependent option: `#{options[:dependent].inspect}'. "
|
57
|
+
raise "Invalid dependent option: `#{options[:dependent].inspect}'. " \
|
50
58
|
"Valid options are: :destroy, :delete, :nullify, or :restrict"
|
51
59
|
end
|
52
60
|
add_callback_for(:before_destroy)
|
@@ -54,12 +62,22 @@ class NoBrainer::Document::Association::HasMany
|
|
54
62
|
end
|
55
63
|
|
56
64
|
def eager_load_owner_key; primary_key; end
|
65
|
+
def eager_load_owner_type; foreign_type; end
|
57
66
|
def eager_load_target_key; foreign_key; end
|
58
67
|
end
|
59
68
|
|
60
69
|
def target_criteria
|
61
|
-
@target_criteria ||=
|
62
|
-
|
70
|
+
@target_criteria ||= begin
|
71
|
+
query_criteria = { foreign_key => owner.__send__(primary_key) }
|
72
|
+
|
73
|
+
if metadata.options[:as]
|
74
|
+
query_criteria = query_criteria.merge(
|
75
|
+
foreign_type => owner.root_class.name
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
base_criteria.where(query_criteria).after_find(set_inverse_proc)
|
80
|
+
end
|
63
81
|
end
|
64
82
|
|
65
83
|
def read
|
@@ -21,11 +21,12 @@ module NoBrainer::Document::Association
|
|
21
21
|
super
|
22
22
|
end
|
23
23
|
|
24
|
-
def association_user_to_model_cast(
|
25
|
-
association = association_metadata[
|
24
|
+
def association_user_to_model_cast(key, value, target_class = nil)
|
25
|
+
association = association_metadata[key]
|
26
26
|
case association
|
27
|
-
when NoBrainer::Document::Association::BelongsTo::Metadata
|
28
|
-
|
27
|
+
when NoBrainer::Document::Association::BelongsTo::Metadata
|
28
|
+
association.cast_attr(key, value, target_class)
|
29
|
+
else [key, value]
|
29
30
|
end
|
30
31
|
end
|
31
32
|
|
@@ -154,9 +154,17 @@ module NoBrainer::Document::Attributes
|
|
154
154
|
!!fields[attr.to_sym]
|
155
155
|
end
|
156
156
|
|
157
|
-
def ensure_valid_key!(
|
158
|
-
|
159
|
-
|
157
|
+
def ensure_valid_key!(keys)
|
158
|
+
missings = Array(keys).select do |key|
|
159
|
+
has_field?(key) == false && has_index?(key) == false
|
160
|
+
end
|
161
|
+
|
162
|
+
return if missings.empty?
|
163
|
+
|
164
|
+
raise NoBrainer::Error::UnknownAttribute,
|
165
|
+
"`#{missings.join('\', `')}' #{missings.size > 1 ? 'are' : 'is'} " \
|
166
|
+
"not #{'a' if missings.size == 1} valid attribute" \
|
167
|
+
"#{'s' if missings.size > 1} of #{self}"
|
160
168
|
end
|
161
169
|
end
|
162
170
|
end
|
data/lib/no_brainer/error.rb
CHANGED
@@ -1,19 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module NoBrainer::Error
|
2
|
-
class
|
3
|
-
class
|
4
|
-
class
|
5
|
-
class
|
6
|
-
class
|
7
|
-
class
|
8
|
-
class
|
9
|
-
class
|
10
|
-
class
|
11
|
-
class
|
12
|
-
class
|
13
|
-
class
|
14
|
-
class
|
15
|
-
class
|
16
|
-
class
|
4
|
+
class AssociationNotPersisted < RuntimeError; end
|
5
|
+
class AtomicBlock < RuntimeError; end
|
6
|
+
class ChildrenExist < RuntimeError; end
|
7
|
+
class Connection < RuntimeError; end
|
8
|
+
class DocumentNotFound < RuntimeError; end
|
9
|
+
class DocumentNotPersisted < RuntimeError; end
|
10
|
+
class InvalidPolymorphicType < RuntimeError; end
|
11
|
+
class LockInvalidOp < RuntimeError; end
|
12
|
+
class LostLock < RuntimeError; end
|
13
|
+
class LockUnavailable < RuntimeError; end
|
14
|
+
class MissingAttribute < RuntimeError; end
|
15
|
+
class MissingIndex < RuntimeError; end
|
16
|
+
class PolymorphicAssociationWithDifferentTypes < RuntimeError; end
|
17
|
+
class ReadonlyField < RuntimeError; end
|
18
|
+
class UnknownAttribute < RuntimeError; end
|
17
19
|
|
18
20
|
class DocumentInvalid < RuntimeError
|
19
21
|
attr_accessor :instance
|
@@ -1,45 +1,62 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
msg_duration = " " * [0, 6 - msg_duration.size].max + msg_duration
|
13
|
-
msg_duration = "[#{msg_duration}ms] "
|
14
|
-
|
15
|
-
env[:query_type] = NoBrainer::RQL.type_of(env[:query])
|
16
|
-
|
17
|
-
msg_db = "[#{env[:options][:db]}] " if env[:options][:db]
|
18
|
-
msg_query = env[:query].inspect.gsub(/\n/, '').gsub(/ +/, ' ')
|
19
|
-
|
20
|
-
msg_exception = "#{env[:exception].class} #{env[:exception].message.split("\n").first}" if env[:exception]
|
21
|
-
msg_exception ||= "perf: filtering without using an index" if not_indexed
|
22
|
-
|
23
|
-
msg_last = nil
|
24
|
-
|
25
|
-
if NoBrainer::Config.colorize_logger
|
26
|
-
query_color = case env[:query_type]
|
27
|
-
when :write then "\e[1;31m" # red
|
28
|
-
when :read then "\e[1;32m" # green
|
29
|
-
when :management then "\e[1;33m" # yellow
|
30
|
-
end
|
31
|
-
msg_duration = [query_color, msg_duration].join
|
32
|
-
msg_db = ["\e[0;34m", msg_db, query_color].join if msg_db
|
33
|
-
if msg_exception
|
34
|
-
exception_color = "\e[0;31m" if level == Logger::ERROR
|
35
|
-
msg_exception = ["\e[0;39m", " -- ", exception_color, msg_exception].compact.join
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoBrainer
|
4
|
+
module Profiler
|
5
|
+
class Logger
|
6
|
+
def on_query(env)
|
7
|
+
level = ::Logger::ERROR if env[:exception]
|
8
|
+
level ||= not_indexed(env) ? ::Logger::INFO : ::Logger::DEBUG
|
9
|
+
return if NoBrainer.logger.level > level
|
10
|
+
|
11
|
+
NoBrainer.logger.add(level, build_message(env))
|
36
12
|
end
|
37
|
-
msg_last = "\e[0m"
|
38
|
-
end
|
39
13
|
|
40
|
-
|
41
|
-
|
42
|
-
|
14
|
+
private
|
15
|
+
|
16
|
+
def build_message(env)
|
17
|
+
msg_duration = (env[:duration] * 1000.0).round(1).to_s
|
18
|
+
msg_duration = (' ' * [0, 6 - msg_duration.size].max) + msg_duration
|
19
|
+
msg_duration = "[#{msg_duration}ms] "
|
20
|
+
|
21
|
+
env[:query_type] = NoBrainer::RQL.type_of(env[:query])
|
22
|
+
|
23
|
+
msg_db = "[#{env[:options][:db]}] " if env[:options][:db]
|
24
|
+
msg_query = env[:query].inspect.gsub(/\n/, '').gsub(/ +/, ' ')
|
25
|
+
|
26
|
+
msg_exception = "#{env[:exception].class} #{env[:exception].message.split("\n").first}" if env[:exception]
|
27
|
+
msg_exception ||= 'perf: filtering without using an index' if not_indexed(env)
|
28
|
+
|
29
|
+
msg_last = nil
|
43
30
|
|
44
|
-
|
31
|
+
if NoBrainer::Config.colorize_logger
|
32
|
+
msg_duration = [query_color(env[:query_type]), msg_duration].join
|
33
|
+
msg_db = ["\e[0;34m", msg_db, query_color(env[:query_type])].join if msg_db
|
34
|
+
if msg_exception
|
35
|
+
exception_color = "\e[0;31m" if level == Logger::ERROR
|
36
|
+
msg_exception = ["\e[0;39m", ' -- ', exception_color, msg_exception].compact.join
|
37
|
+
end
|
38
|
+
msg_last = "\e[0m"
|
39
|
+
end
|
40
|
+
|
41
|
+
[msg_duration, msg_db, msg_query, msg_exception, msg_last].join
|
42
|
+
end
|
43
|
+
|
44
|
+
def not_indexed(env)
|
45
|
+
env[:criteria] &&
|
46
|
+
env[:criteria].where_present? &&
|
47
|
+
!env[:criteria].where_indexed? &&
|
48
|
+
!env[:criteria].model.try(:perf_warnings_disabled)
|
49
|
+
end
|
50
|
+
|
51
|
+
def query_color(query_type)
|
52
|
+
{
|
53
|
+
write: "\e[1;31m", # red
|
54
|
+
read: "\e[1;32m", # green
|
55
|
+
management: "\e[1;33m" # yellow
|
56
|
+
}[query_type]
|
57
|
+
end
|
58
|
+
|
59
|
+
NoBrainer::Profiler.register(new)
|
60
|
+
end
|
61
|
+
end
|
45
62
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoBrainer
|
4
|
+
module Profiler
|
5
|
+
class SlowQueries < Logger
|
6
|
+
def on_query(env)
|
7
|
+
return unless NoBrainer::Config.log_slow_queries
|
8
|
+
|
9
|
+
query_duration = (env[:duration] * 1000.0).round(1)
|
10
|
+
|
11
|
+
return unless query_duration > NoBrainer::Config.long_query_time
|
12
|
+
|
13
|
+
File.write(
|
14
|
+
NoBrainer::Config.slow_query_log_file,
|
15
|
+
build_message(env),
|
16
|
+
mode: 'a'
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
NoBrainer::Profiler.register(new)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class NoBrainer::QueryRunner::Profiler < NoBrainer::QueryRunner::Middleware
|
2
4
|
def call(env)
|
3
5
|
profiler_start(env)
|
@@ -10,12 +12,13 @@ class NoBrainer::QueryRunner::Profiler < NoBrainer::QueryRunner::Middleware
|
|
10
12
|
private
|
11
13
|
|
12
14
|
require 'no_brainer/profiler/logger'
|
15
|
+
require 'no_brainer/profiler/slow_queries'
|
13
16
|
|
14
17
|
def profiler_start(env)
|
15
18
|
env[:start_time] = Time.now
|
16
19
|
end
|
17
20
|
|
18
|
-
def profiler_end(env, exception=nil)
|
21
|
+
def profiler_end(env, exception = nil)
|
19
22
|
return if handle_on_demand_exception?(env, exception)
|
20
23
|
|
21
24
|
env[:end_time] = Time.now
|
@@ -26,10 +29,11 @@ class NoBrainer::QueryRunner::Profiler < NoBrainer::QueryRunner::Middleware
|
|
26
29
|
env[:query_type] = NoBrainer::RQL.type_of(env[:query])
|
27
30
|
|
28
31
|
NoBrainer::Profiler.registered_profilers.each do |profiler|
|
29
|
-
begin
|
32
|
+
begin # rubocop:disable Style/RedundantBegin
|
30
33
|
profiler.on_query(env)
|
31
|
-
rescue
|
32
|
-
STDERR.puts "[NoBrainer]
|
34
|
+
rescue StandardError => error
|
35
|
+
STDERR.puts "[NoBrainer] #{profiler.class.name} profiler error: " \
|
36
|
+
"#{error.class} #{error.message}\n#{error.backtrace.join('\n')}"
|
33
37
|
end
|
34
38
|
end
|
35
39
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
NoBrainer.configure do |config|
|
2
4
|
# app_name is the name of your application in lowercase.
|
3
5
|
# When using Rails, the application name is automatically inferred.
|
@@ -94,4 +96,16 @@ NoBrainer.configure do |config|
|
|
94
96
|
# is cached. The per criteria cache is disabled if it grows too big to avoid
|
95
97
|
# out of memory issues.
|
96
98
|
# config.criteria_cache_max_entries = 10_000
|
99
|
+
|
100
|
+
# Write queries running longer than config.long_query_time seconds.
|
101
|
+
# The slow query log can be used to find queries that take a long time to
|
102
|
+
# execute and are therefore candidates for optimization.
|
103
|
+
# config.log_slow_queries = true
|
104
|
+
|
105
|
+
# Queries running longer than the bellow value will be logged in a log file if
|
106
|
+
# the above `config.log_slow_queries` is `true`.
|
107
|
+
# config.long_query_time = 10 # seconds
|
108
|
+
|
109
|
+
# Path of the slow queries log file
|
110
|
+
# config.slow_query_log_file = File.join('/', 'var', 'log', 'rethinkdb', 'slow_queries.log')
|
97
111
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nobrainer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.44.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nicolas Viennot
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-07-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -205,6 +205,7 @@ files:
|
|
205
205
|
- lib/no_brainer/profiler.rb
|
206
206
|
- lib/no_brainer/profiler/controller_runtime.rb
|
207
207
|
- lib/no_brainer/profiler/logger.rb
|
208
|
+
- lib/no_brainer/profiler/slow_queries.rb
|
208
209
|
- lib/no_brainer/query_runner.rb
|
209
210
|
- lib/no_brainer/query_runner/connection_lock.rb
|
210
211
|
- lib/no_brainer/query_runner/database_on_demand.rb
|
@@ -247,7 +248,7 @@ metadata:
|
|
247
248
|
homepage_uri: http://nobrainer.io
|
248
249
|
source_code_uri: https://github.com/NoBrainerORM/nobrainer
|
249
250
|
changelog_uri: https://github.com/NoBrainerORM/nobrainer/blob/master/CHANGELOG.md
|
250
|
-
post_install_message:
|
251
|
+
post_install_message:
|
251
252
|
rdoc_options: []
|
252
253
|
require_paths:
|
253
254
|
- lib
|
@@ -263,7 +264,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
263
264
|
version: '0'
|
264
265
|
requirements: []
|
265
266
|
rubygems_version: 3.1.6
|
266
|
-
signing_key:
|
267
|
+
signing_key:
|
267
268
|
specification_version: 4
|
268
269
|
summary: A Ruby ORM for RethinkDB
|
269
270
|
test_files: []
|