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 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: []