nobrainer 0.43.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: ea5237da296873c106bd564c6ca6e62674dd63bf3e1214e8542c3097bb978c82
4
- data.tar.gz: bc02529b83bc6cb8fac1d1895fc3ae927220f6f497a4c4c921e41786b41d3a1a
3
+ metadata.gz: '035182f731c727beff4562127d0ec36aaa7ad56871f11121940f05b0b72819c2'
4
+ data.tar.gz: aa14e68a3ff7fded92a16171842dff3a7329cb78c93946af9475255d25bba1ae
5
5
  SHA512:
6
- metadata.gz: 8aa09bfe2bfc6948258d43d6e9ffc16261ea718da79aabfe2b012ead4b69a42fb0fc5d8d30e6a26a0a1880210ae7f0d342ef0911f3193fa91235e07edb87f509
7
- data.tar.gz: 999c029ba2e70dd0fd4f458f091c25bbc493d4be0a181e09ae66871a0f6d43bfab005088d509c509cd6cf58bc3528daf72b296a7e02e589d1d23a59b1502c09d
6
+ metadata.gz: b9ab73b56b9df5df5098d7a769581fe367284fa7200b19b7cfd637982f8bf3afd36e071e3f12370504e87e1799bc0d7f1f5ee03ca62617f9f3bfbbb62c6e51df
7
+ data.tar.gz: 42a7831651d35f55bd39c68c6baaec3b20e845c82e6d5ce562d9e5c2b4184cfcd3daf754ab097bcc3bffe91c05ab873e2d083d37d21c5e44bbfc6afb81197f22
data/CHANGELOG.md CHANGED
@@ -6,6 +6,12 @@ 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)
9
15
 
10
16
  ## [0.43.0] - 2022-06-16
11
17
  ### Added
@@ -128,7 +134,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
128
134
  - Locks: bug fix: allow small timeouts in lock()
129
135
  - Fix reentrant lock counter on steals
130
136
 
131
- [Unreleased]: https://github.com/nobrainerorm/nobrainer/compare/v0.43.0...HEAD
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
132
139
  [0.43.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.42.0...v0.43.0
133
140
  [0.42.0]: https://github.com/nobrainerorm/nobrainer/compare/v0.41.1...v0.42.0
134
141
  [0.41.1]: https://github.com/nobrainerorm/nobrainer/compare/v0.41.0...v0.41.1
@@ -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)
@@ -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
@@ -70,8 +70,16 @@ class NoBrainer::Document::Association::BelongsTo
70
70
  raise 'You cannot set class_name on a polymorphic belongs_to'
71
71
  end
72
72
 
73
- owner_model.field(foreign_type) if options[:polymorphic]
74
- owner_model.field(foreign_key, :store_as => options[:foreign_key_store_as], :index => options[:index])
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])
75
83
 
76
84
  unless options[:validates] == false
77
85
  owner_model.validates(target_name, options[:validates]) if options[:validates]
@@ -98,13 +106,20 @@ class NoBrainer::Document::Association::BelongsTo
98
106
  add_callback_for(:after_validation)
99
107
  end
100
108
 
101
- def cast_attr(k, v)
102
- case v
103
- when target_model then [foreign_key, v.__send__(primary_key)]
104
- 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
105
119
  else
106
- opts = { :model => owner_model, :attr_name => k, :type => target_model, :value => v }
107
- 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
+ )
108
123
  end
109
124
  end
110
125
 
@@ -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,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.43.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-16 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: []