rails-query-command 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: 04c9d9094284ac4f93952cfcdd3ecd4649d430ef7c187a74b5056cd845c90997
4
+ data.tar.gz: 54c59107e0b483c4be257be924202c5971b0c361bf3caba7dd4b4f852d6dd16a
5
+ SHA512:
6
+ metadata.gz: 2339fe0e1cdb277aa9b13c0c3a575a18b15c9eb6754fe44b373851161c88f80f1623f0f2110f8785ab948fa2ef41fc6fa59667b13bb19fe1ce9f30ed42f7e9ec
7
+ data.tar.gz: f97379b216efa767e874c5de05deb27bccdfd436ecbae7633a5ef497608aec4b1f65fdc2e20bef272218f17a9e2d59b54b5fdd3597d86a159685ed7b02ad49ca
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial release. Backports `bin/rails query` from rails/rails `main` at
6
+ [`e9aab74b13`](https://github.com/rails/rails/commit/e9aab74b13) (originally
7
+ [#57156](https://github.com/rails/rails/pull/57156)) for Rails 7.2, 8.0 and 8.1.
data/LICENSE.txt ADDED
@@ -0,0 +1,25 @@
1
+ Copyright (c) 2026 Lázaro Nixon Amaral de Almeida
2
+
3
+ The query command and its tests are taken from Ruby on Rails
4
+ (https://github.com/rails/rails), which is released under the MIT License:
5
+
6
+ Copyright (c) David Heinemeier Hansson
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining
9
+ a copy of this software and associated documentation files (the
10
+ "Software"), to deal in the Software without restriction, including
11
+ without limitation the rights to use, copy, modify, merge, publish,
12
+ distribute, sublicense, and/or sell copies of the Software, and to
13
+ permit persons to whom the Software is furnished to do so, subject to
14
+ the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be
17
+ included in all copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
23
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
24
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
25
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # rails-query-command
2
+
3
+ A backport of `bin/rails query` for **Rails 7.2, 8.0 and 8.1**.
4
+
5
+ `rails query` was added to Rails in [rails/rails#57156](https://github.com/rails/rails/pull/57156) and is planned for Rails 8.2. It runs read-only database queries and prints structured JSON, so agents and scripts can query a database (for example with `kamal app exec`) without risking writes or parsing free-form `rails runner` output.
6
+
7
+ This gem ships the command exactly as it is on Rails `main` (commit [`e9aab74b13`](https://github.com/rails/rails/commit/e9aab74b13)), including the fixes merged after the original PR.
8
+
9
+ ## Installation
10
+
11
+ ```ruby
12
+ # Gemfile
13
+ gem "rails-query-command"
14
+ ```
15
+
16
+ You don't need any setup. `bin/rails` finds the command on the load path, and it shows up in `bin/rails --help`.
17
+
18
+ **Upgrading to Rails 8.2:** the command is built into Rails 8.2, so remove this gem when you upgrade. The gemspec requires `railties < 8.2`, so Bundler will remind you.
19
+
20
+ ## Usage
21
+
22
+ ```bash
23
+ # Active Record expressions
24
+ bin/rails query "Account.where(plan: 'premium').limit(10)"
25
+ bin/rails query "Account.count"
26
+
27
+ # Raw SQL
28
+ bin/rails query --sql "SELECT id, name FROM accounts"
29
+
30
+ # Read from stdin
31
+ echo "Account.first" | bin/rails query -
32
+
33
+ # Pagination (default: --page 1 --per 100, max per page 10000)
34
+ bin/rails query "Account.all" --page 2 --per 50
35
+
36
+ # Introspection
37
+ bin/rails query schema # list tables
38
+ bin/rails query schema accounts # columns, indexes, enums, associations
39
+ bin/rails query models # models with table names and associations
40
+ bin/rails query explain "Account.where(plan: 'premium')"
41
+
42
+ # Pick a database config in a multi-database app
43
+ bin/rails query "Account.count" --database primary_replica
44
+
45
+ # Pretty-print / CSV with jq
46
+ bin/rails query "Account.first" | jq
47
+ bin/rails query "Account.limit(5)" | jq -r '.rows[] | @csv'
48
+ ```
49
+
50
+ The output looks like this:
51
+
52
+ ```json
53
+ {
54
+ "columns": ["id", "name", "plan"],
55
+ "rows": [[1, "Acme", "premium"], [2, "Widgets Co", "premium"]],
56
+ "meta": {
57
+ "row_count": 2,
58
+ "query_time_ms": 4.2,
59
+ "page": 1,
60
+ "per_page": 100,
61
+ "has_more": false,
62
+ "sql": "SELECT \"accounts\".* FROM \"accounts\" WHERE \"accounts\".\"plan\" = 'premium' LIMIT 101"
63
+ }
64
+ }
65
+ ```
66
+
67
+ Errors are also JSON, written to stderr, and the command exits with status 1:
68
+
69
+ ```json
70
+ {"error": "Write query attempted while in readonly mode: INSERT INTO posts (title) VALUES ('x') LIMIT 101", "meta": {"query_time_ms": 0}}
71
+ ```
72
+
73
+ ## Safety
74
+
75
+ - If a `reading` role is configured, queries run against it. Otherwise they run inside `while_preventing_writes`, so `INSERT`, `UPDATE`, `DELETE`, `Post.delete_all` and `record.update!` are rejected.
76
+ - Active Record expressions are evaluated as Ruby, so they're as trusted as `rails runner` and `rails console`. Write protection covers the database, not arbitrary Ruby. Use `--sql` when you only want SQL to run.
77
+ - Every query is wrapped in a `query.rails` `ActiveSupport::Notifications` event with an `expression` payload, which you can use for audit logging:
78
+
79
+ ```ruby
80
+ ActiveSupport::Notifications.subscribe("query.rails") do |event|
81
+ Rails.logger.info("rails query: #{event.payload[:expression]}")
82
+ end
83
+ ```
84
+
85
+ ## Development
86
+
87
+ ```bash
88
+ bundle install
89
+ bundle exec rake test # Rails 8.1
90
+ RAILS_VERSION=8.0 bundle update && RAILS_VERSION=8.0 bundle exec rake test
91
+ RAILS_VERSION=7.2 bundle update && RAILS_VERSION=7.2 bundle exec rake test
92
+ ```
93
+
94
+ The tests are ported from `railties/test/commands/query_test.rb`. Each test runs the real `bin/rails query` against a fresh copy of the app in `test/dummy`.
95
+
96
+ ## License
97
+
98
+ MIT. The command and its tests come from [Ruby on Rails](https://github.com/rails/rails) and are © David Heinemeier Hansson. See `LICENSE.txt`.
@@ -0,0 +1,51 @@
1
+ Description:
2
+ Run read-only queries against your database. Connects to the
3
+ reading replica role by default and prevents database writes.
4
+
5
+ The --sql flag restricts execution to SQL only. ActiveRecord
6
+ expressions are evaluated as Ruby, with the same trust model
7
+ as rails runner and rails console.
8
+
9
+ Output is JSON. Pipe through jq for human-readable formatting.
10
+
11
+ Subcommands:
12
+ <%= executable %> schema [TABLE] Show tables or table detail with associations
13
+ <%= executable %> models List ActiveRecord models with associations
14
+ <%= executable %> explain EXPRESSION Show EXPLAIN output
15
+
16
+ Examples:
17
+ Run an ActiveRecord query:
18
+
19
+ <%= executable %> "Account.where(plan: 'premium').limit(10)"
20
+
21
+ Pretty-print with jq:
22
+
23
+ <%= executable %> "Account.first" | jq
24
+
25
+ Extract just the rows:
26
+
27
+ <%= executable %> "Account.limit(5)" | jq -r '.rows[] | @csv'
28
+
29
+ Run raw SQL:
30
+
31
+ <%= executable %> --sql "SELECT * FROM accounts LIMIT 10"
32
+
33
+ Paginate results:
34
+
35
+ <%= executable %> "Account.all" --page 2 --per 50
36
+
37
+ Use a specific database config:
38
+
39
+ <%= executable %> "Account.count" --database primary_replica
40
+
41
+ Schema introspection:
42
+
43
+ <%= executable %> schema accounts
44
+
45
+ List models:
46
+
47
+ <%= executable %> models
48
+
49
+ Explain a query:
50
+
51
+ <%= executable %> explain "Account.where(plan: 'premium')"
@@ -0,0 +1,309 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ # Backported from rails/rails main (railties/lib/rails/commands/query/query_command.rb
6
+ # at e9aab74b13). Only change: `usage_path` points at this gem's USAGE file.
7
+
8
+ require "json"
9
+ require "rails/command/environment_argument"
10
+
11
+ module Rails
12
+ module Command
13
+ class QueryCommand < Base # :nodoc:
14
+ include EnvironmentArgument
15
+
16
+ class_option :sql, type: :boolean,
17
+ desc: "Treat input as raw SQL instead of an Active Record expression"
18
+
19
+ class_option :database, aliases: "--db", type: :string,
20
+ desc: "Database configuration to use (e.g. primary_replica)"
21
+
22
+ class_option :page, type: :numeric, default: 1,
23
+ desc: "Page number for paginated results"
24
+
25
+ class_option :per, type: :numeric, default: 100,
26
+ desc: "Results per page (max 10000)"
27
+
28
+ def self.usage_path
29
+ File.expand_path("USAGE", __dir__)
30
+ end
31
+
32
+ desc "query [EXPRESSION]", "Run a read-only query against the database"
33
+ def perform(expression = nil, *args)
34
+ boot_application!
35
+ Rails.application.load_runner
36
+
37
+ ActiveSupport::Notifications.instrument("query.rails", expression: expression) do
38
+ case expression
39
+ when "schema"
40
+ run_schema(args.first)
41
+ when "models"
42
+ run_models
43
+ when "explain"
44
+ run_explain(args.first)
45
+ else
46
+ run_query(expression)
47
+ end
48
+ end
49
+ rescue StandardError, SyntaxError, NotImplementedError => e
50
+ output_error(e.message)
51
+ exit 1
52
+ end
53
+
54
+ private
55
+ def run_schema(table = nil)
56
+ with_readonly_connection do |connection|
57
+ if table
58
+ say format_table_detail(connection, table)
59
+ else
60
+ say format_table_list(connection)
61
+ end
62
+ end
63
+ end
64
+
65
+ def run_models
66
+ Rails.application.eager_load!
67
+
68
+ models = ActiveRecord::Base.descendants
69
+ .reject(&:abstract_class?)
70
+ .select { |model| model.table_name.present? }
71
+ .sort_by(&:name)
72
+
73
+ data = models.map do |model|
74
+ {
75
+ model: model.name,
76
+ table_name: model.table_name,
77
+ associations: format_associations(model)
78
+ }
79
+ end
80
+
81
+ say JSON.generate(data)
82
+ end
83
+
84
+ def run_explain(expression = nil)
85
+ expression = resolve_expression(expression)
86
+
87
+ if options[:sql]
88
+ with_readonly_connection do |connection|
89
+ result = connection.select_all("EXPLAIN #{expression}")
90
+ say format_result(columns: result.columns, rows: result.rows, sql: "EXPLAIN #{expression}")
91
+ end
92
+ else
93
+ relation = with_readonly_connection { eval(expression, TOPLEVEL_BINDING, "(query)", 1) }
94
+ sql = relation.to_sql
95
+ with_readonly_connection_for(relation.model.connection_class_for_self) do |connection|
96
+ result = connection.select_all("EXPLAIN #{sql}")
97
+ say format_result(columns: result.columns, rows: result.rows, sql: "EXPLAIN #{sql}")
98
+ end
99
+ end
100
+ end
101
+
102
+ def run_query(expression)
103
+ expression = resolve_expression(expression)
104
+ page = [ options[:page], 1 ].max
105
+ per = [ [ options[:per], 1 ].max, 10_000 ].min
106
+
107
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
108
+
109
+ result = if options[:sql]
110
+ with_readonly_connection do |connection|
111
+ execute_sql(connection: connection, sql: expression, page: page, per: per)
112
+ end
113
+ else
114
+ execute_expression(expression: expression, page: page, per: per)
115
+ end
116
+
117
+ elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(1)
118
+
119
+ say format_result(**result, elapsed_ms: elapsed_ms, page: page, per: per)
120
+ end
121
+
122
+ def with_readonly_connection(&block)
123
+ with_readonly_connection_for(ActiveRecord::Base, &block)
124
+ end
125
+
126
+ def with_readonly_connection_for(connection_class, &block)
127
+ if options[:database]
128
+ with_explicit_database(options[:database], &block)
129
+ elsif reading_role_available?(connection_class)
130
+ connection_class.connected_to(role: :reading) do
131
+ connection_class.with_connection(&block)
132
+ end
133
+ else
134
+ connection_class.while_preventing_writes do
135
+ connection_class.with_connection(&block)
136
+ end
137
+ end
138
+ end
139
+
140
+ def with_explicit_database(database)
141
+ original = ActiveRecord::Base.connection_db_config
142
+ begin
143
+ ActiveRecord::Base.establish_connection(database.to_sym)
144
+ ActiveRecord::Base.while_preventing_writes do
145
+ yield ActiveRecord::Base.lease_connection
146
+ end
147
+ ensure
148
+ ActiveRecord::Base.establish_connection(original)
149
+ end
150
+ end
151
+
152
+ def reading_role_available?(connection_class)
153
+ connection_class.connected_to(role: :reading) do
154
+ connection_class.lease_connection
155
+ end
156
+ true
157
+ rescue ActiveRecord::ConnectionNotEstablished
158
+ false
159
+ end
160
+
161
+ def execute_sql(connection:, sql:, page:, per:)
162
+ unless sql.gsub(/--[^\n]*|\/\*.*?\*\//m, "").match?(/\bLIMIT\b/i)
163
+ offset = (page - 1) * per
164
+ sql = "#{sql.rstrip.chomp(';')} LIMIT #{per + 1}"
165
+ sql += " OFFSET #{offset}" if offset > 0
166
+ end
167
+
168
+ active_record_result = connection.select_all(sql)
169
+ rows = active_record_result.rows
170
+
171
+ tabular_result(columns: active_record_result.columns, rows: rows.first(per), sql: sql, truncated: rows.length > per)
172
+ end
173
+
174
+ def execute_expression(expression:, page:, per:)
175
+ result = with_readonly_connection { eval(expression, TOPLEVEL_BINDING, "(query)", 1) }
176
+ columns, rows, sql, truncated = tabular_result_parts_for(result, expression: expression, page: page, per: per)
177
+
178
+ tabular_result(columns: columns, rows: rows, sql: sql, truncated: truncated)
179
+ end
180
+
181
+ def tabular_result_parts_for(result, expression:, page:, per:)
182
+ case result
183
+ when ActiveRecord::Relation
184
+ relation = if result.limit_value
185
+ result
186
+ else
187
+ result.offset((page - 1) * per).limit(per + 1)
188
+ end
189
+ relation_sql = relation.to_sql
190
+
191
+ with_readonly_connection_for(relation.model.connection_class_for_self) do |connection|
192
+ active_record_result = connection.select_all(relation_sql)
193
+ rows = active_record_result.rows
194
+
195
+ [ active_record_result.columns, rows.first(per), relation_sql, rows.length > per ]
196
+ end
197
+ when ActiveRecord::Result
198
+ [ result.columns, result.rows, expression, false ]
199
+ when ActiveRecord::Base
200
+ attributes = result.attributes
201
+
202
+ [ attributes.keys, [ attributes.values ], expression, false ]
203
+ when Hash
204
+ [ [ "key", "value" ], result.map { |key, val| [ key, val ] }, expression, false ]
205
+ when Array
206
+ peek_on_result = result.first
207
+
208
+ if peek_on_result.is_a?(ActiveRecord::Base)
209
+ columns = peek_on_result.attribute_names
210
+ rows = result.map { |record| record.attributes.values }
211
+ else
212
+ rows = result.map { |value| Array(value) }
213
+ columns = Array.new(rows.first&.length.to_i) { |index| "column_#{index}" }
214
+ end
215
+
216
+ [ columns, rows, expression, false ]
217
+ else
218
+ [ [ "result" ], [ [ result ] ], expression, false ]
219
+ end
220
+ end
221
+
222
+ def tabular_result(columns:, rows:, sql:, truncated: false)
223
+ { columns: columns, rows: rows, sql: sql, truncated: truncated }
224
+ end
225
+
226
+ def format_result(columns:, rows:, sql:, elapsed_ms: 0, page: 1, per: rows.length, truncated: false)
227
+ JSON.generate({
228
+ columns: columns,
229
+ rows: rows,
230
+ meta: {
231
+ row_count: rows.length,
232
+ query_time_ms: elapsed_ms,
233
+ page: page,
234
+ per_page: per,
235
+ has_more: truncated,
236
+ sql: sql
237
+ }
238
+ })
239
+ end
240
+
241
+ def format_table_list(connection)
242
+ tables = connection.tables.sort
243
+ rows = tables.map { |table| [ table ] }
244
+
245
+ format_result(columns: [ "table_name" ], rows: rows, sql: "")
246
+ end
247
+
248
+ def format_table_detail(connection, table)
249
+ raise ArgumentError, "Table '#{table}' does not exist" unless connection.table_exists?(table)
250
+
251
+ columns = connection.columns(table)
252
+ indexes = connection.indexes(table)
253
+ model = model_for_table(table)
254
+
255
+ JSON.generate({
256
+ table: table,
257
+ columns: columns.map do |col|
258
+ { name: col.name, type: col.sql_type, null: col.null, default: col.default }
259
+ end,
260
+ indexes: indexes.map do |idx|
261
+ { name: idx.name, columns: idx.columns, unique: idx.unique }
262
+ end,
263
+ enums: model&.defined_enums.presence,
264
+ associations: format_associations(model)
265
+ }.compact)
266
+ end
267
+
268
+ def model_for_table(table)
269
+ Rails.application.eager_load!
270
+
271
+ ActiveRecord::Base.descendants.find do |klass|
272
+ !klass.abstract_class? && klass.table_name == table
273
+ end
274
+ end
275
+
276
+ def format_associations(model)
277
+ return unless model
278
+
279
+ model.reflect_on_all_associations.map do |assoc|
280
+ hash = {
281
+ type: assoc.macro,
282
+ name: assoc.name,
283
+ class_name: assoc.class_name
284
+ }
285
+ hash[:foreign_key] = assoc.foreign_key if assoc.respond_to?(:foreign_key)
286
+ hash[:through] = assoc.through_reflection.name if assoc.through_reflection?
287
+ hash
288
+ end
289
+ end
290
+
291
+ def resolve_expression(expression)
292
+ if expression == "-" || (expression.nil? && !$stdin.tty?)
293
+ $stdin.read.strip
294
+ elsif expression
295
+ expression
296
+ else
297
+ raise ArgumentError, "No query expression provided. Run '#{self.class.executable} -h' for help."
298
+ end
299
+ end
300
+
301
+ def output_error(message)
302
+ error JSON.generate({
303
+ error: message,
304
+ meta: { query_time_ms: 0 }
305
+ })
306
+ end
307
+ end
308
+ end
309
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsQueryCommand
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The command itself lives at lib/rails/commands/query/query_command.rb and is
4
+ # discovered by `bin/rails` through the load path; nothing needs to be required here.
5
+ require "rails_query_command/version"
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails-query-command
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Lázaro Nixon Amaral de Almeida
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-10-22 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: railties
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.2'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '8.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '7.2'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '8.2'
32
+ - !ruby/object:Gem::Dependency
33
+ name: activerecord
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '7.2'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '8.2'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '7.2'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '8.2'
52
+ description: |
53
+ Adds the `bin/rails query` command from Rails main (rails/rails#57156) to apps running
54
+ Rails 7.2, 8.0 and 8.1. Runs Active Record expressions or raw SQL against a read-only
55
+ connection and prints structured JSON, with pagination and schema/model introspection.
56
+ email:
57
+ - lazaronixon@hotmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - CHANGELOG.md
63
+ - LICENSE.txt
64
+ - README.md
65
+ - lib/rails/commands/query/USAGE
66
+ - lib/rails/commands/query/query_command.rb
67
+ - lib/rails_query_command.rb
68
+ - lib/rails_query_command/version.rb
69
+ homepage: https://github.com/lazaronixon/rails-query-command
70
+ licenses:
71
+ - MIT
72
+ metadata:
73
+ source_code_uri: https://github.com/lazaronixon/rails-query-command
74
+ changelog_uri: https://github.com/lazaronixon/rails-query-command/blob/main/CHANGELOG.md
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '3.1'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubygems_version: 3.6.2
90
+ specification_version: 4
91
+ summary: Backport of `bin/rails query` (read-only JSON database queries) for Rails
92
+ 7.2, 8.0 and 8.1.
93
+ test_files: []