tobias 0.3.0 → 0.4.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: 4b50326ed51ec5388f5f63528cbfe0cda2f27db4e505b0cdf1349f1cc9313311
4
- data.tar.gz: e76ddcd08800005f89ddb772953df045ad623fcfc13e114090bdfcdd49c0dabb
3
+ metadata.gz: 6e8024f2a5efa0e4ffe41d2973817f1906fccfb32193782b86e55f822a82f5e7
4
+ data.tar.gz: 5b6b27888ed7341c76fea8503bc8e577662949903089970930fced41367583e0
5
5
  SHA512:
6
- metadata.gz: 456a7a52310fcac42a5755ad02aeb464bb429742888a195569fb5d5df7f33416d583cef442eca92b035af9807e7ec5d17c626b0eda20b9d43998fc4c99ccb6f3
7
- data.tar.gz: e8c5d112db7cd2e87f37455d90b25a6dfe53eba27e60a492c4fbc4798a941bdc9b69532b813eb39f4c69b68c34812e09112ec19a80485f90e526869e2d0c1794
6
+ metadata.gz: 2e289f868dad239629020799f64c05be1f5658f2facd905d917ce162bcccbefc957e979c19bdb36ec37e1c21eb89ae26d7b2f2a458f11b8d1d335eb5b9a9a37c
7
+ data.tar.gz: fece84c5f4c2d65961c091ee4bb7e2879b43646e60a8c470079d20529541757e0e61d9f4a2163e0615660c38b62c758504bdde9009a35e426f0578d598089468
data/bin/tobias CHANGED
@@ -3,5 +3,8 @@
3
3
 
4
4
  require_relative "../lib/tobias"
5
5
 
6
- RubyVM::YJIT.enable
6
+ if defined?(RubyVM::YJIT)
7
+ RubyVM::YJIT.enable
8
+ end
9
+
7
10
  Tobias::CLI.start(ARGV, exit_on_failure: true)
data/lib/tobias/cli.rb CHANGED
@@ -8,14 +8,46 @@ module Tobias
8
8
  true
9
9
  end
10
10
 
11
+ desc "recommend", "recommend a work_mem setting for a database"
12
+ option :database_url, type: :string, required: true
13
+ option :debug, type: :boolean, default: false
14
+ def recommend
15
+ database = Sequel.connect(options[:database_url])
16
+ database.extension :pgvector
17
+ database.loggers << Logger.new(STDERR) if options[:debug]
18
+
19
+ parsed = TTY::Markdown.parse(<<~MARKDOWN)
20
+ # @tobias is thinking...
21
+ MARKDOWN
22
+ puts parsed
23
+
24
+ work_mem = Tobias::WorkMem.valid_for(database).sort_by(&:amount).reverse.first
25
+
26
+ parsed = TTY::Markdown.parse(<<~MARKDOWN)
27
+ # @tobias has sent you a new message
28
+
29
+ I've reviewed your database by analyzing your shared buffers and connection limits and
30
+ recommend setting `work_mem` to `#{work_mem.to_sql}`. To apply my recommendation, run the following SQL:
31
+
32
+ ```sql
33
+ ALTER SYSTEM SET work_mem = '#{work_mem.to_sql}';
34
+ SELECT pg_reload_conf();
35
+ ```
36
+
37
+ Regards,
38
+ ~ Tobias
39
+ MARKDOWN
40
+ puts parsed
41
+ end
42
+
11
43
  desc "profile SCRIPT", "profile"
12
44
  option :database_url, type: :string, required: true
13
- option :iterations, type: :numeric, default: 10
14
45
  option :debug, type: :boolean, default: false
15
46
  def profile(script)
16
- database = Sequel.connect(options[:database_url])
47
+ database = Sequel.connect(options[:database_url], max_connections: Etc.nprocessors + 2)
17
48
  database.loggers << Logger.new(STDERR) if options[:debug]
18
49
  database.extension :pg_json
50
+ database.extension :pgvector
19
51
 
20
52
  if File.exist?(script)
21
53
  code = File.read(script)
@@ -23,7 +55,7 @@ module Tobias
23
55
  raise "Script not found at: #{script}"
24
56
  end
25
57
 
26
- container = Container.new(code)
58
+ container = Container.new(code, database)
27
59
  results = {}
28
60
 
29
61
  parsed = TTY::Markdown.parse(<<~MARKDOWN)
@@ -1,20 +1,95 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "open3"
4
+
3
5
  module Tobias
4
6
  class Container
5
- def initialize(code)
7
+ def initialize(code, database)
6
8
  @code = code
7
- @queries = {}
9
+ @database = database
10
+ @queries = Concurrent::Hash.new
11
+ @sql = Concurrent::Hash.new
12
+ @options = Concurrent::Hash.new
13
+ @setup = Proc.new { }
14
+ @teardown = Proc.new { }
15
+ @load_data = Proc.new { }
16
+ @helpers = Module.new
8
17
 
9
18
  eval(code, binding, __FILE__, __LINE__)
10
19
  end
11
20
 
21
+ module DefaultHelpers
22
+ def db
23
+ @database
24
+ end
25
+
26
+ def run_parallel(list = Etc.nprocessors.times, &block)
27
+ db.disconnect
28
+
29
+ Parallel.each(list, in_processes: Etc.nprocessors) do |item|
30
+ instance_exec(item, &block)
31
+ end
32
+ end
33
+ end
34
+
35
+ def run_setup
36
+ @database.run("CREATE EXTENSION IF NOT EXISTS pg_stat_statements")
37
+ run_action(@setup)
38
+ end
39
+
40
+ def run_query(query)
41
+ @database.run(run_action(query).sql)
42
+ end
43
+
44
+ def run_teardown
45
+ run_action(@teardown)
46
+ end
47
+
48
+ def options
49
+ Struct.new(*@options.keys).new(*@options.values)
50
+ end
51
+
52
+ def run_action(action)
53
+ helpers = @helpers
54
+
55
+ class_eval do
56
+ include DefaultHelpers
57
+ include helpers
58
+ end
59
+
60
+ instance_eval(&action)
61
+ end
62
+
12
63
  def queries
13
64
  @queries
14
65
  end
15
66
 
16
- def query(name, &block)
17
- @queries[name] = block
67
+ def option(name, default = nil, &block)
68
+ @options[name] = block || default
69
+ end
70
+
71
+ def helpers(&block)
72
+ @helpers.class_eval(&block) if block_given?
73
+ end
74
+
75
+ def setup(&block)
76
+ @setup = block
77
+ end
78
+
79
+ def teardown(&block)
80
+ @teardown = block
81
+ end
82
+
83
+ def load_data(&block)
84
+ @load_data = block
85
+ end
86
+
87
+ def query(name, sql = nil, &block)
88
+ if sql.is_a?(String)
89
+ @queries[name] = Proc.new { sql }
90
+ else
91
+ @queries[name] = block || Proc.new { raise "No SQL provided for query '#{name}'" }
92
+ end
18
93
  end
19
94
  end
20
95
  end
@@ -1,32 +1,64 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ class MarkdownTableBorder < TTY::Table::Border
4
+ def_border do
5
+ left "|"
6
+ center "|"
7
+ right "|"
8
+ bottom " "
9
+ bottom_mid " "
10
+ bottom_left " "
11
+ bottom_right " "
12
+ end
13
+ end
14
+
3
15
  module Tobias
4
16
  module Evaluations
17
+ Result = Struct.new(:name, :value, keyword_init: true) do
18
+ def <=>(other)
19
+ value <=> other.value
20
+ end
21
+ end
22
+
5
23
  class Base
6
- attr_reader :database, :container
24
+ attr_reader :database, :container, :options
7
25
 
8
- def initialize(database, container)
26
+ def initialize(database, container, options)
9
27
  @database = database
10
28
  @container = container
29
+ @options = options
11
30
  end
12
31
 
13
- def run(options, &block)
14
- results = {}
32
+ def run(&block)
33
+ results = Concurrent::Array.new
15
34
 
35
+ container.run_setup
16
36
  container.queries.each do |name, query|
17
- results.merge!(run_each(name, query, options))
37
+ result = run_each(name, query)
38
+ results << result if result
18
39
  end
19
40
 
20
41
  to_markdown(results)
42
+ ensure
43
+ container.run_teardown
21
44
  end
22
45
 
23
- def run_each(query, options)
46
+ def run_each(query)
24
47
  raise NotImplementedError
25
48
  end
26
49
 
27
50
  def to_markdown(results)
28
51
  raise NotImplementedError
29
52
  end
53
+
54
+ def render_table(headers:, body:)
55
+ table = TTY::Table.new(header: headers)
56
+ body.each do |row|
57
+ table << row
58
+ end
59
+
60
+ table.render_with(MarkdownTableBorder)
61
+ end
30
62
  end
31
63
  end
32
64
  end
@@ -3,57 +3,55 @@
3
3
  module Tobias
4
4
  module Evaluations
5
5
  class WorkMem < Base
6
-
7
6
  def work_mems
8
- Tobias::WorkMem.valid_for(database)
7
+ @work_mems ||= Tobias::WorkMem.valid_for(database)
8
+ end
9
+
10
+ def current_work_mem
11
+ @current_work_mem ||= Tobias::WorkMem.from_sql(database.fetch("SHOW work_mem").first[:work_mem])
9
12
  end
10
13
 
11
14
  def description
12
- "Optional work_mem settings"
15
+ "Optimal work_mem settings"
13
16
  end
14
17
 
15
- def run_each(name, query, options)
18
+ def run_each(name, query)
19
+ database.run("CREATE EXTENSION IF NOT EXISTS pg_stat_statements")
20
+
16
21
  work_mems.each do |value|
17
22
  database.transaction do
18
- database.run("CREATE EXTENSION IF NOT EXISTS pg_stat_statements")
19
23
  database.run("SET LOCAL work_mem = '#{value.to_sql}'")
20
24
  database.select(Sequel.function(:pg_stat_reset)).first
25
+ container.run_query(query)
21
26
 
22
- query_result = database.instance_eval(&query)
23
- options[:iterations].to_i.times do
24
- database.run(query_result.sql)
25
- end
26
-
27
- stats = database[:pg_stat_database].where(datname: Sequel.function(:current_database)).first
27
+ stats = database[:pg_stat_database].
28
+ where(datname: Sequel.function(:current_database)).
29
+ first
28
30
 
29
31
  if stats[:temp_files] == 0 && stats[:temp_bytes] == 0
30
- return { name => value }
32
+ return Result.new(name: name, value: value)
31
33
  end
32
34
  end
33
35
  end
34
36
 
35
- # TODO: Add a warning message or something.
36
- return { name => nil }
37
+ # Fallback to the highest work_mem setting if no results are found.
38
+ Result.new(name: name, value: work_mems.last)
37
39
  end
38
40
 
39
41
  def to_markdown(results)
40
- current_work_mem = Tobias::WorkMem.from_sql(database.fetch("SHOW work_mem").first[:work_mem])
41
-
42
42
  <<~MARKDOWN
43
43
  ## #{description}
44
44
 
45
- | Query | Required work_mem |
46
- |-------|-------------------|
47
- #{results.map { |name, work_mem| "| #{name} | #{work_mem.to_sql} |" }.join("\n")}
45
+ #{render_table(headers: ["Query", "Required work_mem"], body: results.map { |r| [r.name, r.value.to_sql] })}
48
46
 
49
- Your application will need to run with at least #{results.values.max.to_sql} of work_mem.
47
+ I see that your current `work_mem` setting is `#{current_work_mem.to_sql}`.
50
48
 
51
- I see that your current work_mem setting is #{current_work_mem.to_sql}.
49
+ Your application will need to run with at least `#{results.max.value.to_sql}` of `work_mem`.
52
50
 
53
51
  To apply my recommendations, run the following SQL:
54
52
 
55
53
  ```sql
56
- ALTER SYSTEM SET work_mem = '#{results.values.max.to_sql}';
54
+ ALTER SYSTEM SET work_mem = '#{results.max.value.to_sql}';
57
55
  SELECT pg_reload_conf();
58
56
  ```
59
57
  MARKDOWN
@@ -4,7 +4,7 @@ module Tobias
4
4
  module Evaluations
5
5
  def self.run(database, container, options)
6
6
  results = []
7
- results << WorkMem.new(database, container).run(options)
7
+ results << WorkMem.new(database, container, options).run
8
8
  results
9
9
  end
10
10
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tobias
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  SUMMARY = "Tobias is a tool to help you find the optimal work_mem for your queries."
6
6
  DESCRIPTION = "Tobias is a tool to help you find the optimal work_mem for your queries."
7
7
  end
data/lib/tobias.rb CHANGED
@@ -11,15 +11,22 @@ if RUBY_ENGINE == "ruby"
11
11
  end
12
12
  end
13
13
 
14
+ # See: https://github.com/ged/ruby-pg/issues/538#issuecomment-1591629049
15
+ ENV["PGGSSENCMODE"] = "disable"
16
+
14
17
  require "bundler/setup"
15
18
  Bundler.require(:default)
16
19
 
17
20
  require "thor"
18
21
  require "active_support/all"
19
22
  require "sequel"
23
+ require "pgvector"
20
24
  require "enumerable-stats"
21
25
  require "benchmark"
26
+ require "parquet"
27
+ require "parallel"
22
28
  require "tty-markdown"
29
+ require "tty-table"
23
30
 
24
31
  $LOAD_PATH.unshift File.dirname(__FILE__)
25
32
 
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tobias
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Daniel
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 2025-08-04 00:00:00.000000000 Z
11
+ date: 2025-08-23 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: activesupport
@@ -189,6 +190,66 @@ dependencies:
189
190
  - - ">="
190
191
  - !ruby/object:Gem::Version
191
192
  version: 5.76.0
193
+ - !ruby/object:Gem::Dependency
194
+ name: parquet
195
+ requirement: !ruby/object:Gem::Requirement
196
+ requirements:
197
+ - - "~>"
198
+ - !ruby/object:Gem::Version
199
+ version: '0.7'
200
+ - - ">="
201
+ - !ruby/object:Gem::Version
202
+ version: 0.7.3
203
+ type: :runtime
204
+ prerelease: false
205
+ version_requirements: !ruby/object:Gem::Requirement
206
+ requirements:
207
+ - - "~>"
208
+ - !ruby/object:Gem::Version
209
+ version: '0.7'
210
+ - - ">="
211
+ - !ruby/object:Gem::Version
212
+ version: 0.7.3
213
+ - !ruby/object:Gem::Dependency
214
+ name: pgvector
215
+ requirement: !ruby/object:Gem::Requirement
216
+ requirements:
217
+ - - "~>"
218
+ - !ruby/object:Gem::Version
219
+ version: '0.3'
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: 0.3.0
223
+ type: :runtime
224
+ prerelease: false
225
+ version_requirements: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - "~>"
228
+ - !ruby/object:Gem::Version
229
+ version: '0.3'
230
+ - - ">="
231
+ - !ruby/object:Gem::Version
232
+ version: 0.3.0
233
+ - !ruby/object:Gem::Dependency
234
+ name: parallel
235
+ requirement: !ruby/object:Gem::Requirement
236
+ requirements:
237
+ - - "~>"
238
+ - !ruby/object:Gem::Version
239
+ version: '1.20'
240
+ - - ">="
241
+ - !ruby/object:Gem::Version
242
+ version: 1.20.0
243
+ type: :runtime
244
+ prerelease: false
245
+ version_requirements: !ruby/object:Gem::Requirement
246
+ requirements:
247
+ - - "~>"
248
+ - !ruby/object:Gem::Version
249
+ version: '1.20'
250
+ - - ">="
251
+ - !ruby/object:Gem::Version
252
+ version: 1.20.0
192
253
  - !ruby/object:Gem::Dependency
193
254
  name: thor
194
255
  requirement: !ruby/object:Gem::Requirement
@@ -229,6 +290,26 @@ dependencies:
229
290
  - - ">="
230
291
  - !ruby/object:Gem::Version
231
292
  version: 0.7.0
293
+ - !ruby/object:Gem::Dependency
294
+ name: tty-table
295
+ requirement: !ruby/object:Gem::Requirement
296
+ requirements:
297
+ - - "~>"
298
+ - !ruby/object:Gem::Version
299
+ version: '0.12'
300
+ - - ">="
301
+ - !ruby/object:Gem::Version
302
+ version: 0.12.0
303
+ type: :runtime
304
+ prerelease: false
305
+ version_requirements: !ruby/object:Gem::Requirement
306
+ requirements:
307
+ - - "~>"
308
+ - !ruby/object:Gem::Version
309
+ version: '0.12'
310
+ - - ">="
311
+ - !ruby/object:Gem::Version
312
+ version: 0.12.0
232
313
  description: Tobias is a tool to help you find the optimal work_mem for your queries.
233
314
  email: binarycleric@gmail.com
234
315
  executables:
@@ -251,6 +332,7 @@ licenses:
251
332
  metadata:
252
333
  source_code_uri: https://github.com/binarycleric/tobias
253
334
  rubygems_mfa_required: 'true'
335
+ post_install_message:
254
336
  rdoc_options: []
255
337
  require_paths:
256
338
  - lib
@@ -265,7 +347,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
265
347
  - !ruby/object:Gem::Version
266
348
  version: '0'
267
349
  requirements: []
268
- rubygems_version: 3.6.2
350
+ rubygems_version: 3.5.22
351
+ signing_key:
269
352
  specification_version: 4
270
353
  summary: Tobias is a tool to help you find the optimal work_mem for your queries.
271
354
  test_files: []