duel-activerecord 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 56141f41f03d09cf171aeaa7097d59341a1b6b101c855de77ffc472ca0be405a
4
+ data.tar.gz: 2f8a62b553cfd1bd9432311b301ee93bb37fcdac462f2dd7cca83623d978d0c1
5
+ SHA512:
6
+ metadata.gz: 652b932685d313d3a9a345fe2044e5c0ffd6c16b2a30491cac55cf498bb82591a5cb7d71d48e6f8969fced24a5fc5140e4e961dfa7a2e977a6109b4a244bc1dd
7
+ data.tar.gz: 695268da1a1371ff6a78e2021c29f338857321217f05dfe463f74de587242ffde9ba1365c4f44103741949871fae050de6afe9072d5e41f0c25eff4872170cad
data/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-10-24
11
+
12
+ The first release. duel-activerecord is a Duel plugin, open source
13
+ under the MIT license. It needs Duel 0.1, Active Record 7.2 or later,
14
+ and Ruby 3.4 or later (or JRuby 10, or TruffleRuby 40).
15
+
16
+ ### Added
17
+
18
+ - `c.use :activerecord`: the `active_record_queries`,
19
+ `active_record_cached_queries`, `active_record_transactions` and
20
+ `active_record_instantiations` metrics, from Active Record's
21
+ notifications, and `active_record_query_time` when asked for
22
+ (`query_time: true`), with `ignore:` to leave queries out. They're
23
+ running totals (Duel's `c.metric`), subscribed to in workers only, as
24
+ each worker opens the metric, and unsubscribed from as it closes it,
25
+ when the worker ends. Where the queries regressed, Duel shows the
26
+ statements each version sent most, and how many times an iteration
27
+ (Duel's `details`). `active_record_query_time` is in nanoseconds
28
+ (`unit: "ns"`), shown as time. The counts are in queries,
29
+ transactions and records (`unit:`, each with its plural), so reports
30
+ read "1 query → 2 queries".
31
+ - Mutation tested: mutant kills every change to the plugin's code, in
32
+ CI.
33
+ - RBS signatures in sig/, checked with Steep.
34
+
35
+ [Unreleased]: https://github.com/sferik/duel-ruby/compare/duel-activerecord-v0.1.0...HEAD
36
+ [0.1.0]: https://github.com/sferik/duel-ruby/releases/tag/duel-activerecord-v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Erik Berlin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # duel-activerecord
2
+
3
+ [![Duel](https://raw.githubusercontent.com/sferik/duel-ruby/main/assets/badge.svg)](https://github.com/sferik/duel-ruby)
4
+ [![duel-activerecord](https://github.com/sferik/duel-ruby/actions/workflows/plugin-activerecord.yml/badge.svg)](https://github.com/sferik/duel-ruby/actions/workflows/plugin-activerecord.yml)
5
+ [![Gem Version](https://badge.fury.io/rb/duel-activerecord.svg)](https://rubygems.org/gems/duel-activerecord)
6
+
7
+ A [Duel](https://github.com/sferik/duel-ruby) plugin for code that uses
8
+ Active Record: every benchmark is judged on what Active Record did for it
9
+ too, whatever the database.
10
+
11
+ | Metric | What it is | A regression |
12
+ |---|---|---|
13
+ | `active_record_queries` | the queries it sent the database: not schema queries, transactions' own statements (`BEGIN`, `COMMIT`...), or those the query cache answered | any increase |
14
+ | `active_record_cached_queries` | the queries the query cache answered | any increase |
15
+ | `active_record_transactions` | the transactions it opened, and savepoints | any increase |
16
+ | `active_record_instantiations` | the records it instantiated | any increase |
17
+ | `active_record_query_time` (when asked for) | how long its queries took, in elapsed time | > 5% slower, 99% confident |
18
+
19
+ So a change that adds an N+1 query, or saves records one transaction at
20
+ a time, fails on its counts, exactly, whatever its timings say. And on a
21
+ database server, whose work the benchmark's CPU time doesn't include,
22
+ the counts are the sign of it.
23
+
24
+ ## Installation
25
+
26
+ ```ruby
27
+ # Gemfile
28
+ group :development do
29
+ gem "duel-activerecord"
30
+ end
31
+ ```
32
+
33
+ > [!NOTE]
34
+ > It needs Active Record 7.2 or later.
35
+
36
+ ## Usage
37
+
38
+ ```ruby
39
+ # .duel.rb
40
+ Duel.configure { |c| c.use :activerecord }
41
+ ```
42
+
43
+ `c.use` requires the plugin, so `.duel.rb` doesn't need to.
44
+
45
+ The plugin doesn't give workers a database: they connect as your
46
+ benchmarks do (in `duel/duel_helper.rb`, say), to one of their own (an
47
+ in-memory SQLite database, as here, or a file in the directory that
48
+ `c.prepare(:worker)` gives each worker) or to one all of them share. Then any benchmark is judged on
49
+ its queries:
50
+
51
+ ```ruby
52
+ # duel/duel_helper.rb
53
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
54
+ load "db/schema.rb"
55
+ 10.times { |i| Post.create!(title: "post #{i}").comments.create! }
56
+
57
+ # duel/posts_duel.rb
58
+ require_relative "duel_helper"
59
+
60
+ duel "posts: the index page" do
61
+ PostsPresenter.new(Post.all).rows # one query for posts, one for their comments
62
+ end
63
+ ```
64
+
65
+ A change that loads each post's comments on its own fails, and says
66
+ which statements each version sent most, and how many times an
67
+ iteration:
68
+
69
+ ```
70
+ ! posts: the index page 1.2s n=2 ... active_record_queries 4→22 active_record_cached_queries 0 active_record_transactions 0 active_record_instantiations 40 records ...
71
+ REGRESSION posts: the index page: worse instructions, cpu, active_record_queries, allocations in every measurement
72
+ ...
73
+ active_record_queries: 2 queries → 11 queries an iteration
74
+ active_record_queries (candidate): an iteration: 10× SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?; 1× SELECT "posts".* FROM "posts"
75
+ active_record_queries (baseline): an iteration: 1× SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); 1× SELECT "posts".* FROM "posts"
76
+ ```
77
+
78
+ > [!TIP]
79
+ > Queries a benchmark makes to set itself up each time go in `untimed`,
80
+ > which pauses every metric, these too.
81
+
82
+ Options:
83
+
84
+ ```ruby
85
+ c.use :activerecord,
86
+ query_time: true, # active_record_query_time too (default: false)
87
+ ignore: ->(payload) { payload[:sql].start_with?("PRAGMA") } # queries not to count, nor time
88
+ ```
89
+
90
+ `ignore` gets each query's `sql.active_record` payload (`:sql`, `:name`,
91
+ `:binds`...). Duel's `c.measure` changes a measure's settings, or leaves
92
+ it out (and the plugin doesn't subscribe to what no measure left needs):
93
+
94
+ ```ruby
95
+ c.measure("active_record_query_time", limit: 1.1)
96
+ c.measure("active_record_instantiations", skip: true)
97
+ ```
98
+
99
+ ## How it measures
100
+
101
+ From Active Record's own notifications (`sql.active_record`,
102
+ `transaction.active_record` and `instantiation.active_record`), in every
103
+ thread of the worker, as running totals (Duel's `c.metric`). Reading
104
+ them queries nothing, so there's nothing for Duel to take away
105
+ (`calibrate: false`). Counts are shown in their units (queries,
106
+ transactions, records: `1 query`, `2 queries`, though the summary line
107
+ leaves out the one a name ends in), and
108
+ `active_record_query_time` is in nanoseconds (`unit: "ns"`), shown as
109
+ time.
110
+
111
+ It subscribes in each worker as Duel opens its metric (the metric's
112
+ `open`), once the benchmark files have loaded, so queries made while
113
+ they load (a schema, seeds) aren't counted, and the runner, which loads
114
+ `.duel.rb` too, never loads Active Support for it. It unsubscribes when
115
+ the worker ends (the metric's `close`).
116
+
117
+ It also keeps track of the statement each of the last 1,000 queries
118
+ sent, by its SQL (with binds, `WHERE id = ?`, the same statement for
119
+ every id). That's a hash of the SQL for each query, and no copy of it
120
+ (nothing a benchmark's allocations would count), but it holds on to the
121
+ SQL of the first eight statements sent since a sample was last
122
+ explained (with [duel-retained](https://github.com/sferik/duel-ruby/tree/main/plugins/retained),
123
+ that's within its allowance), and the rest are told as others. A regression
124
+ shows the three a sample sent most, and how many times an iteration
125
+ (and others'), of its last 1,000 queries, but those in `untimed` blocks,
126
+ with at most 100 characters of each.
127
+
128
+ `active_record_queries` counts what Rails' log counts as queries, but
129
+ leaves out cached ones, which never reach the database: they're
130
+ `active_record_cached_queries`. Transactions are counted on their own, once each
131
+ (not their `BEGIN` and `COMMIT`), and only those Active Record opened
132
+ (an empty `transaction` block opens none).
133
+
134
+ ### Why `active_record_query_time` is off unless asked for
135
+
136
+ It's elapsed time, which Duel otherwise never judges: a busy machine
137
+ adds to it, and so does anything else the database is doing. With
138
+ SQLite, it's time the benchmark's CPU time already counts. With a
139
+ database server, it's mostly waiting: on the network, and on a server
140
+ that may be serving other workers too. Paired side by side, the two
141
+ versions see much the same of that, so it can settle a large
142
+ difference, and it's judged like CPU time but with a looser limit (5%),
143
+ and only for samples with 0.3 ms of it. But for what the server did, its
144
+ own statistics are the better measure (as
145
+ [duel-redis](https://github.com/sferik/duel-ruby/tree/main/plugins/redis) reads Redis's), and
146
+ the query counts catch most of what makes it slower.
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Duel
4
+ module ActiveRecord
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "duel"
4
+ require_relative "activerecord/version"
5
+
6
+ module Duel
7
+ # What Active Record did for each benchmark, from its notifications,
8
+ # whatever the database:
9
+ #
10
+ # - active_record_queries: the queries it sent the database (counted),
11
+ # but schema queries, transactions' own statements (BEGIN, COMMIT...)
12
+ # and those the query cache answered
13
+ # - active_record_cached_queries: the queries the query cache answered
14
+ # (counted)
15
+ # - active_record_transactions: the transactions, and savepoints, it
16
+ # opened (counted)
17
+ # - active_record_instantiations: the records it instantiated (counted)
18
+ # - active_record_query_time, when asked for: how long the queries
19
+ # took, in elapsed time (a ratio metric, in nanoseconds, like CPU
20
+ # time)
21
+ #
22
+ # # .duel.rb
23
+ # Duel.configure { |c| c.use :activerecord }
24
+ #
25
+ # It subscribes to the notifications in workers only, as its metric is
26
+ # opened, once the benchmarks have loaded (so the runner never loads
27
+ # Active Support for it), and unsubscribes when the worker ends. Where the queries
28
+ # regressed, it says which statements a sample sent most, on each
29
+ # version (Counter#details).
30
+ module ActiveRecord
31
+ Duel.plugin(:activerecord, self)
32
+
33
+ # Counts. Reading them queries nothing, so there's nothing to take
34
+ # away.
35
+ MEASURES = {
36
+ "active_record_queries" => {kind: :counted, unit: %w[query queries], calibrate: false},
37
+ "active_record_cached_queries" => {kind: :counted, unit: %w[query queries], calibrate: false},
38
+ "active_record_transactions" => {kind: :counted, unit: %w[transaction transactions], calibrate: false},
39
+ "active_record_instantiations" => {kind: :counted, unit: %w[record records], calibrate: false}
40
+ }
41
+
42
+ # Elapsed time, measured when asked for (see the README).
43
+ QUERY_TIME = {"active_record_query_time" => {kind: :ratio, unit: "ns", floor: 300_000, limit: 1.05}}
44
+
45
+ # Configures Duel for the plugin (c.use :activerecord): its metrics,
46
+ # and +query_time+ too if true. +ignore+, if given, is called with
47
+ # each query's notification payload, and a query it's true for isn't
48
+ # counted, nor timed. Returns the Counter.
49
+ def self.configure(config, query_time: false, ignore: nil)
50
+ counter = Counter.new(query_time:, ignore:)
51
+ measures = query_time ? MEASURES.merge(QUERY_TIME) : MEASURES
52
+ config.metric(measures) { counter.totals }
53
+ .open { counter.subscribe(measures.keys.reject { |name| config.skip?(name) }) }
54
+ .close { counter.unsubscribe }
55
+ .details { |stretches, sample| counter.details(stretches, sample.n) }
56
+ counter
57
+ end
58
+
59
+ # Running totals, from the notifications it subscribes to. One object
60
+ # (Active Support calls its #start and #finish, the cheapest kind of
61
+ # subscriber) for every event.
62
+ class Counter
63
+ SQL = "sql.active_record"
64
+ TRANSACTION = "transaction.active_record"
65
+ INSTANTIATION = "instantiation.active_record"
66
+
67
+ # The events each measure needs.
68
+ EVENTS = {
69
+ "active_record_queries" => SQL, "active_record_cached_queries" => SQL, "active_record_query_time" => SQL,
70
+ "active_record_transactions" => TRANSACTION, "active_record_instantiations" => INSTANTIATION
71
+ }.freeze
72
+
73
+ # Where a query's start is kept until it finishes (queries in
74
+ # several threads or fibers at once each have their own).
75
+ STARTED = :duel_activerecord_started
76
+
77
+ # The queries counted last that it keeps track of (which statement
78
+ # each was), for #details.
79
+ KEPT = 1000
80
+
81
+ # The most statements whose SQL it keeps between two #details
82
+ # (the first sent: the rest are told as others).
83
+ STATEMENTS = 8
84
+
85
+ # The most statements shown, and the most shown of each one's SQL.
86
+ SHOWN = 3
87
+ LENGTH = 100
88
+
89
+ def initialize(query_time:, ignore:)
90
+ @query_time = query_time
91
+ @ignore = ignore
92
+ @queries = @cached = @transactions = @instantiations = @time = 0
93
+ @subscriptions = [] #: Array[untyped]
94
+ # Each query's SQL's hash, round (the first KEPT make it).
95
+ @recent = [] #: Array[Integer]
96
+ @sql = {} #: Hash[Integer, String]
97
+ end
98
+
99
+ # Subscribes to the events +measures+ need (see EVENTS).
100
+ def subscribe(measures)
101
+ @timing = measures.include?("active_record_query_time")
102
+ @subscriptions = measures.map { |measure| EVENTS.fetch(measure) }.uniq.map do |event|
103
+ notifications.subscribe(event, self)
104
+ end
105
+ end
106
+
107
+ # The totals so far, by name.
108
+ def totals
109
+ totals = {"active_record_queries" => @queries, "active_record_cached_queries" => @cached,
110
+ "active_record_transactions" => @transactions, "active_record_instantiations" => @instantiations}
111
+ @query_time ? totals.merge("active_record_query_time" => @time) : totals
112
+ end
113
+
114
+ # What explains the queries a sample sent, in its stretches (pairs
115
+ # of readings of the totals: see Snapshot#details) of +n+
116
+ # iterations: the statements sent most (of the last KEPT), and how
117
+ # many times an iteration, the SHOWN most and others: "an
118
+ # iteration: 3× SELECT ...; 1× others". Nothing where none were. It
119
+ # forgets their SQL.
120
+ def details(stretches, n)
121
+ oldest = @queries - KEPT
122
+ keys = stretches.flat_map do |started, stopped|
123
+ ([queries(started), oldest].max...queries(stopped)).map { |i| @recent.fetch(i % KEPT) }
124
+ end
125
+ return if keys.empty?
126
+
127
+ sent = keys.tally.filter_map { |key, count| [count, @sql.fetch(key)] if @sql.key?(key) } #: Array[[Integer, String]]
128
+ shown = sent.sort_by { |count, sql| [-count, sql] }.first(SHOWN)
129
+ others = keys.size - shown.sum { |count, _| count }
130
+ shown << [others, "others"] if others.positive?
131
+ {"active_record_queries" => "an iteration: #{shown.map { |count, sql| "#{format("%.4g", count.fdiv(n))}× #{shorten(sql)}" }.join("; ")}"}
132
+ ensure
133
+ @sql.clear
134
+ end
135
+
136
+ # Stops counting.
137
+ def unsubscribe = @subscriptions.each { |subscription| notifications.unsubscribe(subscription) }
138
+
139
+ # (Active Support's, for every event subscribed to.)
140
+ def start(name, _id, _payload)
141
+ (Thread.current[STARTED] ||= []) << monotonic if @timing && name == SQL
142
+ end
143
+
144
+ def finish(name, _id, payload)
145
+ case name
146
+ when SQL then query(payload)
147
+ when TRANSACTION then @transactions += 1
148
+ else @instantiations += payload.fetch(:record_count)
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ def query(payload)
155
+ took = monotonic - Thread.current.fetch(STARTED).pop if @timing
156
+ name = payload[:name]
157
+ return if name == "SCHEMA" || @ignore&.call(payload)
158
+
159
+ @time += took if took
160
+ return if name == "TRANSACTION"
161
+ return @cached += 1 if payload[:cached]
162
+
163
+ sent(payload.fetch(:sql))
164
+ @queries += 1
165
+ end
166
+
167
+ # Keeps track of the statement a query sent, by its SQL's hash (no
168
+ # copy of it, and nothing allocated: what a benchmark allocates is
169
+ # measured), and its SQL, for the first STATEMENTS.
170
+ def sent(sql)
171
+ key = sql.hash
172
+ @recent[@queries % KEPT] = key
173
+ @sql[key] = sql if @sql.size < STATEMENTS
174
+ end
175
+
176
+ # The queries counted by a reading of the totals.
177
+ def queries(reading) = reading.total("active_record_queries")
178
+
179
+ # +sql+ on one line, and at most LENGTH long.
180
+ def shorten(sql)
181
+ line = sql.gsub(/\s+/, " ").strip
182
+ (line.length > LENGTH) ? "#{line[0, LENGTH - 3]}..." : line
183
+ end
184
+
185
+ def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
186
+
187
+ # Active Support's notifications, loaded when it first subscribes
188
+ # (in a worker: never in the runner).
189
+ # mutant:disable -- the tests have loaded Active Support, so leaving
190
+ # out the require can't fail them in their process
191
+ # (test_subscribes_in_the_worker checks it, in a process of its own)
192
+ def notifications
193
+ require "active_support/notifications"
194
+ ::ActiveSupport::Notifications
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,99 @@
1
+ module Duel
2
+ # What Active Record did for each benchmark, from its notifications
3
+ # (active_record_queries, active_record_cached_queries,
4
+ # active_record_transactions, active_record_instantiations and, when
5
+ # asked for, active_record_query_time).
6
+ module ActiveRecord
7
+ # A notification's payload, as Active Record gives it.
8
+ type payload = Hash[Symbol, untyped]
9
+
10
+ VERSION: String
11
+
12
+ # Counts.
13
+ MEASURES: Hash[String, Metric::settings]
14
+
15
+ # Elapsed time, measured when asked for.
16
+ QUERY_TIME: Hash[String, Metric::settings]
17
+
18
+ # Configures Duel for the plugin (c.use :activerecord): its metrics,
19
+ # and +query_time+ too if true. A query +ignore+ is true for isn't
20
+ # counted, nor timed. Returns the Counter.
21
+ def self.configure: (Config config, ?query_time: bool, ?ignore: (^(payload payload) -> boolish)?) -> Counter
22
+
23
+ # Running totals, from the notifications it subscribes to.
24
+ class Counter
25
+ SQL: String
26
+ TRANSACTION: String
27
+ INSTANTIATION: String
28
+
29
+ # The events each measure needs.
30
+ EVENTS: Hash[String, String]
31
+
32
+ # Where a query's start is kept until it finishes.
33
+ STARTED: Symbol
34
+
35
+ # The queries counted last that it keeps track of.
36
+ KEPT: Integer
37
+
38
+ # The most statements whose SQL it keeps between two #details.
39
+ STATEMENTS: Integer
40
+
41
+ # The most statements shown, and the most shown of each one's SQL.
42
+ SHOWN: Integer
43
+ LENGTH: Integer
44
+
45
+ @query_time: bool
46
+ @ignore: (^(payload payload) -> boolish)?
47
+ @timing: bool
48
+ @queries: Integer
49
+ @cached: Integer
50
+ @transactions: Integer
51
+ @instantiations: Integer
52
+ @time: Integer
53
+ @subscriptions: Array[untyped]
54
+ @recent: Array[Integer]
55
+ @sql: Hash[Integer, String]
56
+
57
+ def initialize: (query_time: bool, ignore: (^(payload payload) -> boolish)?) -> void
58
+
59
+ # Subscribes to the events +measures+ need.
60
+ def subscribe: (Array[String] measures) -> void
61
+
62
+ # The totals so far, by name.
63
+ def totals: () -> Hash[String, Integer]
64
+
65
+ # What explains the queries a sample of +n+ iterations sent, in its
66
+ # stretches: the statements sent most, and how many times an
67
+ # iteration. It forgets their SQL.
68
+ def details: (Snapshot::stretches[Integer, nil] stretches, Integer n) -> Hash[String, String]?
69
+
70
+ # Stops counting.
71
+ def unsubscribe: () -> void
72
+
73
+ # (Active Support's, for every event subscribed to.)
74
+ def start: (String name, String id, payload payload) -> void
75
+
76
+ def finish: (String name, String id, payload payload) -> void
77
+
78
+ private
79
+
80
+ def query: (payload payload) -> void
81
+
82
+ # Keeps track of the statement a query sent, and its SQL, for the
83
+ # first STATEMENTS.
84
+ def sent: (String sql) -> void
85
+
86
+ # The queries counted by a reading of the totals.
87
+ def queries: (Snapshot::Reading[Integer, nil] reading) -> Integer
88
+
89
+ # +sql+ on one line, and at most LENGTH long.
90
+ def shorten: (String sql) -> String
91
+
92
+ def monotonic: () -> Integer
93
+
94
+ # Active Support's notifications, loaded when it first subscribes
95
+ # (untyped: Active Support has no signatures of its own).
96
+ def notifications: () -> untyped
97
+ end
98
+ end
99
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: duel-activerecord
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Erik Berlin
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: duel
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activerecord
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.2'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.2'
40
+ description: 'A Duel plugin: every benchmark is judged on the SQL queries, cached
41
+ queries, transactions and record instantiations Active Record did for it too, whatever
42
+ the database.'
43
+ email:
44
+ - sferik@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - CHANGELOG.md
50
+ - LICENSE
51
+ - README.md
52
+ - lib/duel/activerecord.rb
53
+ - lib/duel/activerecord/version.rb
54
+ - sig/duel/activerecord.rbs
55
+ homepage: https://github.com/sferik/duel-ruby/tree/main/plugins/activerecord
56
+ licenses:
57
+ - MIT
58
+ metadata:
59
+ source_code_uri: https://github.com/sferik/duel-ruby/tree/main/plugins/activerecord
60
+ changelog_uri: https://github.com/sferik/duel-ruby/blob/main/plugins/activerecord/CHANGELOG.md
61
+ bug_tracker_uri: https://github.com/sferik/duel-ruby/issues
62
+ rubygems_mfa_required: 'true'
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '3.4'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 4.0.23
78
+ specification_version: 4
79
+ summary: 'What Active Record did for each Duel benchmark: queries, transactions and
80
+ records'
81
+ test_files: []