nobrainer 0.42.0 → 0.44.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/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: []
|