nobrainer 0.43.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: 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: []