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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69052877b8fd9f2684f63a0bc7a917812fd771f793d55e7f5182149fc1346643
4
- data.tar.gz: 33dbe632e862a31bb4ac95ab8015834947bd1207df3bbdfb921b742274d06517
3
+ metadata.gz: '035182f731c727beff4562127d0ec36aaa7ad56871f11121940f05b0b72819c2'
4
+ data.tar.gz: aa14e68a3ff7fded92a16171842dff3a7329cb78c93946af9475255d25bba1ae
5
5
  SHA512:
6
- metadata.gz: 102682b8e844b1fd574b3d29f386923bb4dfaee33e88e7da6c5615b4901d20023267cbd4e698bec34a94b77718ca04f251f8aebdfd8f74ef5837c637931dd206
7
- data.tar.gz: 3cb7abcc47fd15d71abac788e7c5844229d30f532c01000d177d7c5b718848b48d2150cda32a1725ed7d0b68adbb775f4461805d9c2f4768104ba1320eb95d52
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.42.0...HEAD
128
- [0.41.1]: https://github.com/nobrainerorm/nobrainer/compare/v0.41.1...v0.42.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
@@ -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
- :app_name => { :default => ->{ default_app_name } },
6
- :environment => { :default => ->{ default_environment } },
7
- :rethinkdb_urls => { :default => ->{ [default_rethinkdb_url] } },
8
- :ssl_options => { :default => ->{ nil } },
9
- :driver => { :default => ->{ :regular }, :valid_values => [:regular, :em] },
10
- :logger => { :default => ->{ default_logger } },
11
- :colorize_logger => { :default => ->{ true }, :valid_values => [true, false] },
12
- :warn_on_active_record => { :default => ->{ true }, :valid_values => [true, false] },
13
- :durability => { :default => ->{ nil } }, # legacy
14
- :table_options => { :default => ->{ {:shards => 1, :replicas => 1, :write_acks => :majority} },
15
- :valid_keys => [:durability, :shards, :replicas, :primary_replica_tag, :nonvoting_replica_tags, :write_acks] },
16
- :run_options => { :default => ->{ {:durability => default_durability} } },
17
- :max_string_length => { :default => ->{ 255 } },
18
- :user_timezone => { :default => ->{ :local }, :valid_values => [:unchanged, :utc, :local] },
19
- :db_timezone => { :default => ->{ :utc }, :valid_values => [:unchanged, :utc, :local] },
20
- :geo_options => { :default => ->{ {:geo_system => 'WGS84', :unit => 'm'} } },
21
- :distributed_lock_class => { :default => ->{ "NoBrainer::Lock" } },
22
- :lock_options => { :default => ->{ { :expire => 60, :timeout => 10 } }, :valid_keys => [:expire, :timeout] },
23
- :per_thread_connection => { :default => ->{ false }, :valid_values => [true, false] },
24
- :machine_id => { :default => ->{ default_machine_id } },
25
- :criteria_cache_max_entries => { :default => -> { 10_000 } },
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
- BinaryOperator.new(new_key_path, new_key_modifier, new_op, new_value, model, true)
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
- k, _v = model.association_user_to_model_cast(key_path.first, nil)
201
- key_path = [k]
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 = [:primary_key, :foreign_key, :class_name, :foreign_key_store_as,
6
- :index, :validates, :required, :uniq, :unique]
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
- get_model_by_name(options[:class_name] || target_name.to_s.camelize)
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.without_ordering
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
- owner_model.field(foreign_key, :store_as => options[:foreign_key_store_as], :index => options[:index])
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(k, v)
78
- case v
79
- when target_model then [foreign_key, v.__send__(primary_key)]
80
- when nil then [foreign_key, nil]
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
- opts = { :model => owner_model, :attr_name => k, :type => target_model, :value => v }
83
- raise NoBrainer::Error::InvalidType.new(opts)
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, :target_name, :target_model, :base_criteria, :to => :metadata
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
- criteria = base_criteria
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) || :"#{owner_model.name.split('::').last.underscore}_#{primary_key}"
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 == self.foreign_key and
34
- assoc.primary_key == self.primary_key and
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 ||= base_criteria.where(foreign_key => owner.__send__(primary_key))
62
- .after_find(set_inverse_proc)
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(k,v)
25
- association = association_metadata[k]
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 then association.cast_attr(k,v)
28
- else [k,v]
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!(key)
158
- return if has_field?(key) || has_index?(key)
159
- raise NoBrainer::Error::UnknownAttribute, "`#{key}' is not a valid attribute of #{self}"
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
@@ -1,19 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NoBrainer::Error
2
- class Connection < RuntimeError; end
3
- class DocumentNotFound < RuntimeError; end
4
- class DocumentNotPersisted < RuntimeError; end
5
- class ChildrenExist < RuntimeError; end
6
- class CannotUseIndex < RuntimeError; end
7
- class MissingIndex < RuntimeError; end
8
- class AssociationNotPersisted < RuntimeError; end
9
- class ReadonlyField < RuntimeError; end
10
- class MissingAttribute < RuntimeError; end
11
- class UnknownAttribute < RuntimeError; end
12
- class AtomicBlock < RuntimeError; end
13
- class LostLock < RuntimeError; end
14
- class LockInvalidOp < RuntimeError; end
15
- class LockUnavailable < RuntimeError; end
16
- class InvalidPolymorphicType < RuntimeError; end
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
- class NoBrainer::Profiler::Logger
2
- def on_query(env)
3
- not_indexed = env[:criteria] && env[:criteria].where_present? &&
4
- !env[:criteria].where_indexed? &&
5
- !env[:criteria].model.try(:perf_warnings_disabled)
6
-
7
- level = env[:exception] ? Logger::ERROR :
8
- not_indexed ? Logger::INFO : Logger::DEBUG
9
- return if NoBrainer.logger.level > level
10
-
11
- msg_duration = (env[:duration] * 1000.0).round(1).to_s
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
- msg = [msg_duration, msg_db, msg_query, msg_exception, msg_last].join
41
- NoBrainer.logger.add(level, msg)
42
- end
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
- NoBrainer::Profiler.register(self.new)
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 Exception => e
32
- STDERR.puts "[NoBrainer] Profiling error: #{e.class} #{e.message}"
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.42.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: 2022-06-15 00:00:00.000000000 Z
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: []