rails-ai-context 5.35.0 → 5.35.2
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 +4 -4
- data/CHANGELOG.md +1024 -0
- data/CONTEXT.md +37 -7
- data/CONTRIBUTING.md +30 -1
- data/README.md +9 -9
- data/SECURITY.md +2 -2
- data/docs/ARCHITECTURE.md +12 -4
- data/docs/CLI.md +44 -15
- data/docs/COMPATIBILITY.md +34 -8
- data/docs/CONFIGURATION.md +28 -5
- data/docs/CUSTOM_TOOLS.md +8 -2
- data/docs/FAQ.md +5 -5
- data/docs/GUIDE.md +33 -65
- data/docs/INTROSPECTORS.md +4 -4
- data/docs/QUICKSTART.md +1 -1
- data/docs/RECIPES.md +5 -5
- data/docs/RELEASING.md +3 -1
- data/docs/SECURITY.md +255 -74
- data/docs/SETUP.md +1 -1
- data/docs/STANDALONE.md +2 -2
- data/docs/TOOLS.md +151 -90
- data/docs/TROUBLESHOOTING.md +123 -8
- data/docs/_config.yml +1 -1
- data/docs/adr/0008-query-runs-only-where-it-can-guard.md +38 -0
- data/docs/adr/0009-dbhub-shapes-in-ruby.md +26 -0
- data/docs/adr/0010-one-config-tool-with-sections.md +32 -0
- data/docs/index.md +2 -2
- data/exe/rails-ai-context +24 -9
- data/lib/generators/rails_ai_context/install/install_generator.rb +2 -0
- data/lib/rails_ai_context/app_databases.rb +377 -0
- data/lib/rails_ai_context/app_kind.rb +2 -0
- data/lib/rails_ai_context/cli/app_root.rb +1 -0
- data/lib/rails_ai_context/cli/tool_runner.rb +110 -99
- data/lib/rails_ai_context/configuration.rb +15 -8
- data/lib/rails_ai_context/database_family/mysql_runtime.rb +170 -0
- data/lib/rails_ai_context/database_family/postgresql_plan.rb +124 -0
- data/lib/rails_ai_context/database_family/postgresql_runtime.rb +211 -0
- data/lib/rails_ai_context/database_family/sqlite_runtime.rb +391 -0
- data/lib/rails_ai_context/database_family.rb +282 -0
- data/lib/rails_ai_context/database_file.rb +61 -7
- data/lib/rails_ai_context/database_yml.rb +7 -29
- data/lib/rails_ai_context/doctor.rb +15 -16
- data/lib/rails_ai_context/instrumentation.rb +1 -1
- data/lib/rails_ai_context/introspectors/connection_pool_introspector.rb +8 -23
- data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +6 -70
- data/lib/rails_ai_context/introspectors/declared_constant.rb +19 -7
- data/lib/rails_ai_context/introspectors/env_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/env_references.rb +1 -1
- data/lib/rails_ai_context/introspectors/job_introspector.rb +7 -7
- data/lib/rails_ai_context/introspectors/migration_introspector.rb +8 -11
- data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +9 -9
- data/lib/rails_ai_context/introspectors/model_introspector.rb +33 -39
- data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +33 -30
- data/lib/rails_ai_context/introspectors/node_source.rb +2 -0
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +80 -24
- data/lib/rails_ai_context/introspectors/pg_partitions.rb +1 -4
- data/lib/rails_ai_context/introspectors/schema_conventions.rb +6 -8
- data/lib/rails_ai_context/introspectors/schema_dump_path.rb +12 -14
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +47 -27
- data/lib/rails_ai_context/introspectors/schema_reader.rb +1 -1
- data/lib/rails_ai_context/introspectors/source_scan.rb +39 -4
- data/lib/rails_ai_context/migration_status.rb +18 -46
- data/lib/rails_ai_context/payload.rb +48 -1
- data/lib/rails_ai_context/pending_migrations.rb +3 -4
- data/lib/rails_ai_context/run_cache.rb +5 -0
- data/lib/rails_ai_context/schema_adapter.rb +13 -18
- data/lib/rails_ai_context/secret_columns.rb +506 -0
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +2 -2
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +14 -10
- data/lib/rails_ai_context/serializers/section_facts.rb +12 -2
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +5 -2
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +35 -7
- data/lib/rails_ai_context/server.rb +143 -29
- data/lib/rails_ai_context/sql_gate.rb +1007 -0
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +1 -2
- data/lib/rails_ai_context/test_helper.rb +3 -1
- data/lib/rails_ai_context/tools/analyze_feature.rb +11 -7
- data/lib/rails_ai_context/tools/base_tool.rb +71 -48
- data/lib/rails_ai_context/tools/dependency_graph.rb +1 -1
- data/lib/rails_ai_context/tools/diagnose.rb +105 -42
- data/lib/rails_ai_context/tools/generate_test.rb +4 -6
- data/lib/rails_ai_context/tools/get_active_support.rb +1 -6
- data/lib/rails_ai_context/tools/get_api.rb +1 -6
- data/lib/rails_ai_context/tools/get_autoload.rb +1 -6
- data/lib/rails_ai_context/tools/get_callbacks.rb +5 -5
- data/lib/rails_ai_context/tools/get_component_catalog.rb +6 -7
- data/lib/rails_ai_context/tools/get_concern.rb +6 -6
- data/lib/rails_ai_context/tools/get_config.rb +189 -12
- data/lib/rails_ai_context/tools/get_context.rb +111 -105
- data/lib/rails_ai_context/tools/get_controllers.rb +8 -5
- data/lib/rails_ai_context/tools/get_conventions.rb +1 -6
- data/lib/rails_ai_context/tools/get_engines.rb +1 -6
- data/lib/rails_ai_context/tools/get_env.rb +1 -6
- data/lib/rails_ai_context/tools/get_env_config.rb +2 -8
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +1 -6
- data/lib/rails_ai_context/tools/get_gems.rb +1 -6
- data/lib/rails_ai_context/tools/get_helper_methods.rb +4 -5
- data/lib/rails_ai_context/tools/get_i18n.rb +1 -7
- data/lib/rails_ai_context/tools/get_job_pattern.rb +1 -1
- data/lib/rails_ai_context/tools/get_mailers.rb +1 -1
- data/lib/rails_ai_context/tools/get_model_details.rb +5 -8
- data/lib/rails_ai_context/tools/get_partial_interface.rb +3 -4
- data/lib/rails_ai_context/tools/get_schema.rb +10 -8
- data/lib/rails_ai_context/tools/get_service_pattern.rb +60 -24
- data/lib/rails_ai_context/tools/get_turbo_map.rb +1 -1
- data/lib/rails_ai_context/tools/migration_advisor.rb +47 -32
- data/lib/rails_ai_context/tools/onboard.rb +24 -24
- data/lib/rails_ai_context/tools/performance_check.rb +15 -2
- data/lib/rails_ai_context/tools/query.rb +142 -1356
- data/lib/rails_ai_context/tools/read_logs.rb +1 -1
- data/lib/rails_ai_context/tools/review_changes.rb +6 -7
- data/lib/rails_ai_context/tools/runtime_info.rb +125 -88
- data/lib/rails_ai_context/tools/safe_call.rb +70 -28
- data/lib/rails_ai_context/tools/search_code.rb +0 -4
- data/lib/rails_ai_context/tools/search_docs.rb +3 -1
- data/lib/rails_ai_context/tools/security_scan.rb +9 -5
- data/lib/rails_ai_context/tools/session_context.rb +18 -12
- data/lib/rails_ai_context/tools/validate.rb +1 -0
- data/lib/rails_ai_context/tools/validate_semantics.rb +5 -5
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context.rb +4 -7
- data/server.json +2 -2
- metadata +14 -3
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,1030 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [5.35.2] - 2026-10-14
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
|
|
12
|
+
- **On MySQL and MariaDB a quote character inside a string does not change
|
|
13
|
+
which databases a statement is read to name.** The secret column rule reads
|
|
14
|
+
the columns of every database a statement names. In 5.35.1, the first release
|
|
15
|
+
with that read, a quote character inside a string could keep the rule from
|
|
16
|
+
seeing that the statement named another database. A secret column of a table
|
|
17
|
+
or of a view in that database then came back. Each name before a dot is now
|
|
18
|
+
read backward from its dot, so nothing written before the name changes how it
|
|
19
|
+
is read. The refusals are the ones 5.35.1 gives. This gap is in 5.35.1
|
|
20
|
+
alone: earlier versions read no other database, which the 5.35.1 notes cover.
|
|
21
|
+
- **On MySQL and MariaDB a `[` outside a string or a quoted name is refused.**
|
|
22
|
+
MariaDB reads `[name]` as a quoted name when the session's sql_mode holds
|
|
23
|
+
`MSSQL`, and the statement checks read a bracket as an ordinary character. In
|
|
24
|
+
a session with that mode the checks and the server could read one statement
|
|
25
|
+
two ways. `rails_query` does not read the session's mode, so it now refuses
|
|
26
|
+
the bracket on both servers. 5.35.1 and every earlier version had the gap,
|
|
27
|
+
on an app whose connection runs in that mode.
|
|
28
|
+
- **On MySQL and MariaDB a query leaves no value behind on the app's
|
|
29
|
+
connection.** A user variable assignment (`:=`) and `LAST_INSERT_ID` called
|
|
30
|
+
with an argument each set a value that stayed on the pooled connection after
|
|
31
|
+
the query returned, which a read-only transaction does not stop. Both are now
|
|
32
|
+
refused. 5.35.1 and every earlier version ran them.
|
|
33
|
+
- **On PostgreSQL what a foreign scan sends to the remote server is read like
|
|
34
|
+
what the plan runs itself.** A condition, a sort or a group on a secret
|
|
35
|
+
column of a `postgres_fdw` foreign table ran on the remote server, where the
|
|
36
|
+
plan checks did not read it. So a statement could test the column's value,
|
|
37
|
+
also through a view that renames the column and under a `column_name`
|
|
38
|
+
option. The remote statement the plan prints is now read, and such a
|
|
39
|
+
statement answers "Blocked: the query's plan uses the secret column
|
|
40
|
+
`<column>`". Fetching the column returns `[FILTERED]` as before. A statement
|
|
41
|
+
whose plan holds a foreign scan costs one more catalog read. Only the remote
|
|
42
|
+
statement of `postgres_fdw` is read: another foreign data wrapper prints
|
|
43
|
+
keys of its own. 5.35.1 and every earlier version had the gap.
|
|
44
|
+
- **On PostgreSQL a join that the remote server runs is traced to its
|
|
45
|
+
tables.** When `postgres_fdw` sends a whole join to the remote server, the
|
|
46
|
+
plan names the tables with their aliases in one line of the scan, which the
|
|
47
|
+
plan checks did not read, so the secret column rule did not reach those
|
|
48
|
+
tables. The tables and aliases of that line are now read like any scan's: a
|
|
49
|
+
secret column is `[FILTERED]` in place and a whole row is refused, as for a
|
|
50
|
+
local table. 5.35.1 and every earlier version had the gap.
|
|
51
|
+
- **On PostgreSQL the check for a secret column passed through a subquery
|
|
52
|
+
reads a name with two spaces in a row or a dotted capital I.** In 5.35.1
|
|
53
|
+
that check missed a secret column whose name holds either one. A statement
|
|
54
|
+
that passes such a column through a subquery, a CTE or a set operation is
|
|
55
|
+
now refused as for any other secret column. 5.35.0 refused it, so the gap is
|
|
56
|
+
in 5.35.1 alone.
|
|
57
|
+
- **On SQLite a virtual table built on a relation that is not read is refused
|
|
58
|
+
however its definition is written.** 5.35.1 read a relation's name in the
|
|
59
|
+
`content=` option of a full-text table and in the table a vocabulary lists.
|
|
60
|
+
A virtual table that named the relation another way ran, and returned the
|
|
61
|
+
rows or words of a refused view, a shadow table or the page modules. The
|
|
62
|
+
module and every argument can now name a relation, except the columns a
|
|
63
|
+
full-text table (fts3, fts4, fts5) declares and the type an `fts5vocab`
|
|
64
|
+
table ends with (`row`, `col` or `instance`). An argument with `=` is read
|
|
65
|
+
whole, whatever its name, and comments are taken out first. The module is
|
|
66
|
+
read after the table's own name, so a name that spells a module changes
|
|
67
|
+
nothing: 5.35.1 read such a name as the module, and the rule for a virtual
|
|
68
|
+
table that holds a secret column did not reach that table. The answers are
|
|
69
|
+
the ones 5.35.1 gives. The gap needs a virtual table the app itself created.
|
|
70
|
+
5.35.0 and earlier returned such a table's rows too.
|
|
71
|
+
- **On SQLite a relation whose name holds an apostrophe is matched where it
|
|
72
|
+
is written in single quotes.** SQLite reads a name in single quotes where a
|
|
73
|
+
table is expected, with the apostrophe doubled inside. The secret column
|
|
74
|
+
rule spelled a name in double quotes and in backticks and missed that
|
|
75
|
+
spelling, so a statement or a definition that used it was not held to the
|
|
76
|
+
rule for that relation. 5.35.1 and every earlier version had the gap, on an
|
|
77
|
+
app with a relation so named.
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
|
|
81
|
+
The three refusals below go out under a patch number. Each closes a guard gap,
|
|
82
|
+
and the maintainer chose 5.35.2 for them, outside step 1 of `docs/RELEASING.md`.
|
|
83
|
+
|
|
84
|
+
- **On MySQL and MariaDB a statement with a user variable assignment (`:=`) is
|
|
85
|
+
refused.** The answer is "Blocked: a user variable assignment (`:=`) is not
|
|
86
|
+
run. It leaves a value on the pooled connection after the query returns,
|
|
87
|
+
which a read-only transaction does not stop." A statement that numbers its
|
|
88
|
+
rows with `@n := @n + 1` ran before and is refused now. Use a window
|
|
89
|
+
function, `ROW_NUMBER() OVER (...)`, which MySQL 8 and MariaDB 10.2 and
|
|
90
|
+
later have. A `:=` inside a string or a quoted name is data and runs, with
|
|
91
|
+
one case apart: a statement with a backslash before a quote inside a string.
|
|
92
|
+
The second reading of a backslash ends the string at that quote, and where
|
|
93
|
+
that leaves the `:=` outside a string the statement is refused. Write the
|
|
94
|
+
quote doubled.
|
|
95
|
+
- **On MySQL and MariaDB `LAST_INSERT_ID` with an argument is refused.** The
|
|
96
|
+
answer is "Blocked: LAST_INSERT_ID with an argument is not run. It sets a
|
|
97
|
+
value that stays on the pooled connection after the query returns, which a
|
|
98
|
+
read-only transaction does not stop. LAST_INSERT_ID() with no argument
|
|
99
|
+
runs." The call is matched on the raw text, like the session functions, so
|
|
100
|
+
it is refused inside a string too.
|
|
101
|
+
- **On MySQL and MariaDB a statement with a `[` outside a string or a quoted
|
|
102
|
+
name is refused.** The answer is "Blocked: a `[` outside a string or a
|
|
103
|
+
quoted name is not run. MariaDB reads it as the start of a quoted name when
|
|
104
|
+
the session's sql_mode holds MSSQL, and rails_query does not read the
|
|
105
|
+
session's mode. Quote a name in backticks." MySQL, and MariaDB outside that
|
|
106
|
+
mode, run no statement that holds one. In a session with that mode, write
|
|
107
|
+
`` `name` `` for `[name]`. A `[` inside a string or a backticked name runs,
|
|
108
|
+
with one case apart: a statement with a backslash before a quote inside a
|
|
109
|
+
string, as in `'O\'Brien'` beside `REGEXP '[a-z]'`. The second reading of a
|
|
110
|
+
backslash ends the string at that quote, and where that leaves the `[`
|
|
111
|
+
outside a string the statement is refused. Such a statement ran before.
|
|
112
|
+
Write the quote doubled.
|
|
113
|
+
|
|
114
|
+
### Fixed
|
|
115
|
+
|
|
116
|
+
- **A write that follows a `WITH` clause is refused as the write it is.**
|
|
117
|
+
`WITH ... INSERT INTO ...` answered "Blocked: SELECT INTO creates a table".
|
|
118
|
+
It now answers "Blocked: contains INSERT", and `REPLACE` and `MERGE` after a
|
|
119
|
+
`WITH` clause are named the same way. A real `SELECT ... INTO` keeps its
|
|
120
|
+
sentence.
|
|
121
|
+
- **A refusal names a view, a table and a relation as the database spells
|
|
122
|
+
them.** It named them case folded, with a run of spaces as one space, so a
|
|
123
|
+
view called `Hq Odd View` was named `hq odd view`, which no relation is
|
|
124
|
+
called. Two names that differ by a run of spaces alone are one name to the
|
|
125
|
+
rule, and the refusal names the first of them it read.
|
|
126
|
+
- **A view refused over a table that is not read is said to name it.** The
|
|
127
|
+
answer ended "its definition reads `<table>`, which is not read", and the
|
|
128
|
+
rule tests whether the definition names the table. It now ends "its
|
|
129
|
+
definition names `<table>`, which is not read".
|
|
130
|
+
- **The answer for a view that leads too deep drops its count.** It reads "The
|
|
131
|
+
view `<view>` leads through more databases than rails_query follows, so the
|
|
132
|
+
secret columns it could read cannot be told and no statement that names it
|
|
133
|
+
runs." The "(3 rounds)" after "follows" told a user nothing.
|
|
134
|
+
- **The note for a row limit of one says "1 row shown".** It said "1 rows
|
|
135
|
+
shown; the query returned more".
|
|
136
|
+
- **On PostgreSQL a sequence can be queried.** `SELECT last_value FROM
|
|
137
|
+
<sequence>` and `SELECT * FROM <sequence>` ran on 5.35.0, and 5.35.1 answered
|
|
138
|
+
that the sequence's columns could not be read. A sequence is now read like a
|
|
139
|
+
table that holds no secret column.
|
|
140
|
+
- **On PostgreSQL a whole row of a table with no secret column runs when
|
|
141
|
+
another table shares its alias or its name with a number after it.** 5.35.1
|
|
142
|
+
refused such a statement when a subquery used the same alias for a table
|
|
143
|
+
that holds a secret column, or when a table named like `name_2` stood beside
|
|
144
|
+
it, and the refusal named the other table. 5.35.0 ran it. The number the
|
|
145
|
+
planner adds to an alias is now taken off for the members of an Append or a
|
|
146
|
+
Merge Append alone, and a refusal over a whole row names the relation whose
|
|
147
|
+
row the statement reads.
|
|
148
|
+
- **On PostgreSQL a block comment inside a block comment is read as the server
|
|
149
|
+
reads it.** PostgreSQL nests block comments, and the gate ended one at the
|
|
150
|
+
first `*/`, so `SELECT 1 /* a /* b */ c */ AS n` answered PostgreSQL's
|
|
151
|
+
syntax error. It now runs.
|
|
152
|
+
- **On SQLite a statement that runs past `query_timeout` answers the timeout
|
|
153
|
+
when a writer takes the lock while it runs.** It answered that the database
|
|
154
|
+
was locked and the query did not run. The query process now takes its read
|
|
155
|
+
lock before it prepares the statement, so no lock stops the statement from
|
|
156
|
+
there on. The locked answer is given for a lock the query process is still
|
|
157
|
+
waiting on when it gives up or when the deadline passes.
|
|
158
|
+
- **On a SQLite file database a statement that names something the app's
|
|
159
|
+
connection alone holds runs on that connection.** While that connection
|
|
160
|
+
holds a temp object or an attachment, a statement runs there when it names a
|
|
161
|
+
temp object, `sqlite_temp_master` or `sqlite_temp_schema`, the `temp` schema
|
|
162
|
+
or an attached database, or calls a pragma function (`pragma_table_info(...)`,
|
|
163
|
+
`pragma_table_list` and the rest) or `dbstat`, since an argument can name any
|
|
164
|
+
of them.
|
|
165
|
+
The query process has none of them, so it answered an empty list, the rows of
|
|
166
|
+
a main table of the same name, or "unknown database". The answer now ends
|
|
167
|
+
with the note that the query ran without a time limit. A name is matched
|
|
168
|
+
anywhere in the statement, in any letter case and inside a string too.
|
|
169
|
+
Finding this out takes two more reads before a statement on a file database
|
|
170
|
+
where a query process can run: eight where there were six.
|
|
171
|
+
- **The full generated file lists the first database's recent migrations
|
|
172
|
+
under its own name.** With a first database not named `primary` and another
|
|
173
|
+
one that is, the heading read `### Recent Migrations: primary`. It now names
|
|
174
|
+
the first database, as in `### Recent Migrations: animals`.
|
|
175
|
+
- **The database marks and the first database's own name do not depend on the
|
|
176
|
+
preset.** On `preset: standard`, or any `introspectors` list without
|
|
177
|
+
`multi_database`, `rails_get_schema`, `rails_onboard`,
|
|
178
|
+
`rails_performance_check` and the generated files lost the mark of a
|
|
179
|
+
database another environment configures, such as `cache (production only)`,
|
|
180
|
+
and called a first database with another name `primary`. They now read the
|
|
181
|
+
app databases list there, as on `full`, and `rails_onboard` names a
|
|
182
|
+
secondary's adapter.
|
|
183
|
+
- **The model, controller, job, mailer and channel lists hold a class only
|
|
184
|
+
while its own constant names it.** A class whose constant was removed stays
|
|
185
|
+
loaded until the garbage collector takes it, so a list could hold it in one
|
|
186
|
+
run and lose it in the next. Such a class is now left out. A constant still
|
|
187
|
+
waiting to autoload counts as naming its class. The lists of a class's
|
|
188
|
+
children follow the same rule: a model's STI children, the classes a base job
|
|
189
|
+
or mailer is inherited by, and the abstract bases.
|
|
190
|
+
- **A model file that raises before its class opens costs that model alone.**
|
|
191
|
+
A failing `require` at the top of the file, or a raise above the class line,
|
|
192
|
+
made the whole models section answer an error. The file is now named with
|
|
193
|
+
what it raised, and the other models stay listed.
|
|
194
|
+
- **The refusal over a secret column says where `query_allowed_columns` is
|
|
195
|
+
set on either install.** It said to add the name "in an initializer", which
|
|
196
|
+
does nothing on a standalone install. It now ends "add "x" to
|
|
197
|
+
query_allowed_columns in .rails-ai-context.yml (or in
|
|
198
|
+
config/initializers/rails_ai_context.rb where the app bundles the gem)."
|
|
199
|
+
- **`rails_runtime_info` says when table sizes or index usage could not be
|
|
200
|
+
read.** A read that failed or ran out of time left its part of the database
|
|
201
|
+
section out with no word. The section now says "_Table sizes were not read:
|
|
202
|
+
the read failed or took longer than query_timeout._", and at `detail:
|
|
203
|
+
"full"` "_Index usage was not read: the read failed or took longer than
|
|
204
|
+
query_timeout._"
|
|
205
|
+
|
|
206
|
+
## [5.35.1] - 2026-10-13
|
|
207
|
+
|
|
208
|
+
### Security
|
|
209
|
+
|
|
210
|
+
- **`rails_query` sends no statement to an adapter with no database family.**
|
|
211
|
+
On SQL Server, Oracle, CockroachDB and any other adapter outside PostgreSQL,
|
|
212
|
+
MySQL and SQLite it ran the statement with the text checks alone, with no
|
|
213
|
+
read-only transaction and no timeout. It now answers with the adapter's
|
|
214
|
+
name, and it opens no connection to do so.
|
|
215
|
+
- **MySQL bounds a statement that starts with WITH or EXPLAIN ANALYZE.** The
|
|
216
|
+
`MAX_EXECUTION_TIME` hint bound only a statement that starts with SELECT, so
|
|
217
|
+
one that starts with WITH or EXPLAIN ANALYZE ran with no time limit. The
|
|
218
|
+
session's `max_execution_time` now bounds those too and is put back after the
|
|
219
|
+
statement, also when the statement fails. It does not cover `EXPLAIN` or
|
|
220
|
+
`SHOW`, where a wait on a lock ends at the session's `lock_wait_timeout`.
|
|
221
|
+
- **`rails_query` keeps a column secret when `Redaction`'s secret words name
|
|
222
|
+
it.** Columns such as `passwd`, `salt`, `credentials` and `pepper` were
|
|
223
|
+
returned by `rails_query` while log and config redaction filtered those
|
|
224
|
+
words. They are now refused by name and filtered in results, so a column
|
|
225
|
+
name is secret in every tool or in none.
|
|
226
|
+
- **An `encrypts` column stays secret when `query_allowed_columns` lists it.**
|
|
227
|
+
The statement check let an allowed name through, and on MySQL and SQLite a
|
|
228
|
+
query that gave the column an alias returned the stored value. The check now
|
|
229
|
+
refuses the name on every database family, and the refusal says the allowed
|
|
230
|
+
list does not release it.
|
|
231
|
+
- **On PostgreSQL a statement whose plan cannot be read does not run.** The
|
|
232
|
+
checks that keep a secret column out of a result read the statement's plan,
|
|
233
|
+
and a statement ran without them when planning timed out or failed. Every
|
|
234
|
+
statement except `SHOW` now runs only with a plan. An `EXPLAIN` is planned
|
|
235
|
+
as the statement it explains. A planning timeout is answered as the timeout,
|
|
236
|
+
and a planning error as the SQL error.
|
|
237
|
+
- **`rails_query` refuses `LOCK IN SHARE MODE` and `FOR KEY SHARE`.** MySQL
|
|
238
|
+
and MariaDB ran `LOCK IN SHARE MODE` inside the read-only transaction, where
|
|
239
|
+
it locks the rows it reads. The gate now refuses it wherever it refuses
|
|
240
|
+
`FOR SHARE`, with `NOWAIT`, `SKIP LOCKED`, `OF` or `WAIT` after it too.
|
|
241
|
+
- **A view cannot rename a secret column past `rails_query`.** On MySQL,
|
|
242
|
+
MariaDB and SQLite a view that selects a secret column under another name
|
|
243
|
+
returned its values. A statement that names such a view is now refused with
|
|
244
|
+
the view and the column. So is a view built on it, a view whose definition
|
|
245
|
+
has a wildcard and names a table that holds a secret column, and a view
|
|
246
|
+
whose definition cannot be read on a database that holds secret columns. A
|
|
247
|
+
definition is read with its comments taken out, and a view's name is matched
|
|
248
|
+
whatever characters it holds. `query_allowed_columns` releases a column by
|
|
249
|
+
name as before, and an `encrypts` column stays secret. On PostgreSQL a plain
|
|
250
|
+
view keeps the filter on the one column. A materialized view stores its
|
|
251
|
+
rows, so no result column of it is traced to a table column, and one whose
|
|
252
|
+
definition names a secret column returned it under another name. A
|
|
253
|
+
statement that reads such a materialized view is now refused, through a
|
|
254
|
+
view over it too.
|
|
255
|
+
- **A generated column that reads a secret column is secret.** On every database
|
|
256
|
+
family its values were returned under its own name. A statement that names it
|
|
257
|
+
is now refused and its values come back `[FILTERED]`. Two tables can each have
|
|
258
|
+
a generated column of one name. That name is secret when either column reads a
|
|
259
|
+
secret column, and it is held as an `encrypts` column when either reads one.
|
|
260
|
+
- **The secret column rule knows every relation a statement reads.** It read the
|
|
261
|
+
columns of the connection's own database, or of the search path, and no
|
|
262
|
+
others. So a secret column of a table anywhere else came back under an alias
|
|
263
|
+
or through an expression. The rule now follows what a statement reaches. On
|
|
264
|
+
MySQL and MariaDB it reads every database the statement's text names. On
|
|
265
|
+
PostgreSQL it reads materialized views and foreign tables, every schema the
|
|
266
|
+
statement's text names, and every relation the plan names. On SQLite it reads
|
|
267
|
+
the temp schema and every attached database. Then it reads the databases or
|
|
268
|
+
schemas that the definitions of the views in the statement name, and follows
|
|
269
|
+
those views three reads deep. On PostgreSQL only a materialized view's
|
|
270
|
+
definition is followed, since the plan reads through a plain view. A statement
|
|
271
|
+
that names a view that leads deeper is refused with the view's name. A
|
|
272
|
+
relation whose columns cannot be read is not queried.
|
|
273
|
+
- **On PostgreSQL two aliases that differ only by case are two relations.**
|
|
274
|
+
The plan checks folded an alias to lower case, so a whole-row read of one
|
|
275
|
+
relation was taken for a read of the other. An alias is now read as the plan
|
|
276
|
+
prints it.
|
|
277
|
+
- **On SQLite the database file's raw pages are not read.** `sqlite_dbpage`,
|
|
278
|
+
`sqlite_dbdata` and `sqlite_dbptr` return the file's pages and cells, and
|
|
279
|
+
every stored value is in them, secret columns included. A statement that
|
|
280
|
+
names one is now refused.
|
|
281
|
+
- **On SQLite a shadow table of a virtual table is not read.** A virtual
|
|
282
|
+
table, such as a full-text one, keeps what it stores in shadow tables under
|
|
283
|
+
other column names, so a secret column's values came back from there. A
|
|
284
|
+
statement that names a shadow table is now refused, and the refusal names
|
|
285
|
+
the virtual table to query. Before SQLite 3.37 a table whose name starts
|
|
286
|
+
with a virtual table's name and an underscore is read as a shadow table.
|
|
287
|
+
- **On SQLite a virtual table that holds a secret column is refused whole.** A
|
|
288
|
+
search or a function over a virtual table reads a column by its position, with
|
|
289
|
+
no name written, so it returned or confirmed a secret column's values. A
|
|
290
|
+
statement that names such a table is now refused. So is one that names an
|
|
291
|
+
`fts5vocab` or `fts4aux` table over it, which lists its words, and one that
|
|
292
|
+
names a view over any of these tables. Only the columns a virtual table
|
|
293
|
+
declares decide this, so a full-text table whose own name reads like a secret
|
|
294
|
+
name runs. A virtual table whose definition names a refused view, or a table
|
|
295
|
+
that is not read, is refused with its `fts5vocab` or `fts4aux` table too.
|
|
296
|
+
- **`rails_query` also keeps nonce and initialization vector columns secret.**
|
|
297
|
+
A column whose name holds `nonce`, or is `iv` or ends in `_iv`, is refused
|
|
298
|
+
by name and filtered in results. `query_allowed_columns` releases one by
|
|
299
|
+
name, as for any name the heuristic flags.
|
|
300
|
+
- **`authentication_string` is a built-in secret name.** MySQL keeps password
|
|
301
|
+
hashes in a column of that name, and `rails_query` returned it. A statement
|
|
302
|
+
that names it is now refused, and its values come back `[FILTERED]`.
|
|
303
|
+
- **A column's name is compared the way the database compares it.** MySQL
|
|
304
|
+
folds a letter that Ruby's `downcase` leaves as it is, so a secret column's
|
|
305
|
+
name written with that letter passed the name check. Names in a statement
|
|
306
|
+
and in the schema are now folded the same way before they are compared.
|
|
307
|
+
- **A column-alias list is refused in every spelling.** `rails_query` refuses
|
|
308
|
+
a list such as `t(a, b, c)`, since it renames the columns a wildcard
|
|
309
|
+
returns. The check read one spelling of the list, so a list written another
|
|
310
|
+
way passed it and returned secret columns under other names. The gate now
|
|
311
|
+
reads the list by its shape on every family, whatever the quoting of its
|
|
312
|
+
names, with or without `AS`, and with `MATERIALIZED` or `NOT MATERIALIZED`
|
|
313
|
+
before its body. One name after a function call, such as
|
|
314
|
+
`generate_series(1, 5) AS g(n)`, still runs, and so does a list over
|
|
315
|
+
`VALUES` rows.
|
|
316
|
+
- **On MySQL, MariaDB and SQLite a `*` inside a subquery is refused when the
|
|
317
|
+
statement names a table that holds a secret column.** Such a statement names
|
|
318
|
+
no secret column, so it ran, and a comparison with the subquery or a new name
|
|
319
|
+
around it could tell or return a secret value. It is now refused with the
|
|
320
|
+
table and the column, and the refusal says to name the columns in the
|
|
321
|
+
subquery. `EXISTS (SELECT * ...)` and `count(*)` still run, and MySQL's
|
|
322
|
+
`(TABLE t)` counts as a `*`. PostgreSQL keeps its plan checks. SQLite reads a
|
|
323
|
+
string as a table's name wherever a name can stand, so there a string equal to
|
|
324
|
+
such a table's name counts as that name. SQLite's `x IN t` is its shorthand
|
|
325
|
+
for `x IN (SELECT * FROM t)`. It ran with no `*` written, and it is now held
|
|
326
|
+
to the same rule.
|
|
327
|
+
- **On MySQL and MariaDB the statement text checks hold under both readings of a
|
|
328
|
+
backslash in a string.** MySQL reads a backslash in a string as an escape,
|
|
329
|
+
unless the session's sql_mode holds `NO_BACKSLASH_ESCAPES`. The checks read it
|
|
330
|
+
as an escape alone. In a session with that mode a string ended earlier than
|
|
331
|
+
the checks took it to, and the text after it ran unchecked. Each check now
|
|
332
|
+
reads the statement both ways, and a statement that either reading refuses is
|
|
333
|
+
refused. A refusal that the second reading alone gives says so. `rails_query`
|
|
334
|
+
does not read the session's mode.
|
|
335
|
+
- **On SQLite a result column named `name:1` is filtered.** SQLite gives a
|
|
336
|
+
second result column of the same name the name `name:1`. The result filter
|
|
337
|
+
read that whole name, so a secret column that came back twice was returned
|
|
338
|
+
once in clear. The filter now reads the name without the suffix.
|
|
339
|
+
- **On SQLite a dollar-quoted span is read as code.** SQLite has no dollar
|
|
340
|
+
quoting, and the checks skipped the text between two `$tag$` marks as if it
|
|
341
|
+
were a string. They now read it.
|
|
342
|
+
- **`SHOW` with a `WHERE` clause is refused.** MySQL's statement timeout holds
|
|
343
|
+
`SELECT` only, so a `SHOW ... WHERE` filter ran past `query_timeout`. It is
|
|
344
|
+
now refused on every family and `SHOW ... LIKE` still runs. The refusal
|
|
345
|
+
names the `information_schema` query to use.
|
|
346
|
+
- **More `SHOW` forms are refused.** `SHOW FULL PROCESSLIST`,
|
|
347
|
+
`SHOW REPLICA STATUS`, `SHOW ALL SLAVES STATUS`, `SHOW ALL REPLICAS STATUS`,
|
|
348
|
+
`SHOW BINARY LOGS`, `SHOW BINARY LOG STATUS`, `SHOW RELAYLOG EVENTS`,
|
|
349
|
+
`SHOW ENGINE ... STATUS`, `SHOW PROFILE`, `SHOW PROFILES`,
|
|
350
|
+
`SHOW OPEN TABLES`, `SHOW EXPLAIN FOR`, `SHOW ANALYZE FOR`,
|
|
351
|
+
`SHOW USER_STATISTICS`, `SHOW CLIENT_STATISTICS` and the `SHOW WSREP_` forms
|
|
352
|
+
ran on MySQL and MariaDB. Most of them tell of other sessions' statements,
|
|
353
|
+
of replication hosts and users, or of the rows a log holds. They now get the
|
|
354
|
+
refusal `SHOW PROCESSLIST` gets.
|
|
355
|
+
- **A function that returns a whole table as one value is refused.**
|
|
356
|
+
PostgreSQL's `table_to_xml`, `schema_to_xml`, `database_to_xml` and the rest
|
|
357
|
+
of the `_to_xml` family returned a table, its secret columns included, as
|
|
358
|
+
one XML value. A call to one is now refused by name.
|
|
359
|
+
- **Relations that hold sampled values, other sessions' statements or stored
|
|
360
|
+
credentials are not read.** No check on a column's name applies to what they
|
|
361
|
+
hold, so a query on one ran and returned it. A statement that names one is now
|
|
362
|
+
refused. On PostgreSQL they include `pg_stats`, `pg_stats_ext`,
|
|
363
|
+
`pg_statistic`, `pg_statistic_ext`, `pg_largeobject`, `pg_stat_activity` and
|
|
364
|
+
the functions it reads through, `pg_stat_statements`,
|
|
365
|
+
`pg_prepared_statements`, `pg_cursors`, `pg_authid`, `pg_shadow`,
|
|
366
|
+
`pg_db_role_setting`, `pg_user_mapping`, `pg_user_mappings`,
|
|
367
|
+
`pg_subscription`, `pg_foreign_server`, `pg_foreign_data_wrapper`,
|
|
368
|
+
`pg_foreign_table`, `pg_settings`, `pg_show_all_settings()`,
|
|
369
|
+
`pg_show_all_file_settings()`, `pg_hba_file_rules` and
|
|
370
|
+
`pg_ident_file_mappings`. There the plan is checked too, so a view over one of
|
|
371
|
+
them is refused, the `information_schema` views of user mappings and foreign
|
|
372
|
+
servers among them. On MySQL and MariaDB they include the whole `mysql`
|
|
373
|
+
schema, `performance_schema`, `sys`, `processlist`, `general_log`, `slow_log`,
|
|
374
|
+
`column_statistics`, `column_stats`, `global_priv`, and in
|
|
375
|
+
`information_schema` `INNODB_TRX`, `INNODB_LOCKS`, `INNODB_LOCK_WAITS`, the
|
|
376
|
+
`INNODB_FT_` tables, `USER_STATISTICS`, `CLIENT_STATISTICS`, `PROFILING` and
|
|
377
|
+
`OPTIMIZER_TRACE`. On SQLite they include `sqlite_stat1`, `sqlite_stat3`,
|
|
378
|
+
`sqlite_stat4` and `sqlite_stmt`, which lists the connection's own
|
|
379
|
+
statements with their literals. docs/SECURITY.md lists every name. Each name
|
|
380
|
+
is refused on the database family that has it, wherever it stands in the
|
|
381
|
+
statement. So an app table called `slow_log` or `processlist` is refused on
|
|
382
|
+
MySQL and MariaDB.
|
|
383
|
+
- **On PostgreSQL a role's password hash and its stored settings are secret
|
|
384
|
+
columns.** `pg_authid.rolpassword` and `pg_db_role_setting.setconfig` were
|
|
385
|
+
returned, also through the views PostgreSQL builds on them. Both names are
|
|
386
|
+
now built-in secret names. A view that reads neither column still runs, so
|
|
387
|
+
`pg_roles` and `pg_user` answer, and `pg_roles.rolconfig` comes back
|
|
388
|
+
`[FILTERED]`.
|
|
389
|
+
- **PostgreSQL settings that can hold a password are not read.**
|
|
390
|
+
`SHOW primary_conninfo` and `current_setting('primary_conninfo')` returned a
|
|
391
|
+
connection string with its password, and `SHOW ALL` listed it.
|
|
392
|
+
`primary_conninfo`, `ssl_passphrase_command`, `archive_command`,
|
|
393
|
+
`restore_command`, `archive_cleanup_command` and `recovery_end_command` are
|
|
394
|
+
now refused, and so is a setting whose own name reads as a secret, such as
|
|
395
|
+
`app.jwt_secret`. On PostgreSQL `SHOW ALL` is refused and `SHOW` takes a
|
|
396
|
+
setting's plain name. `current_setting` runs only with the name written as
|
|
397
|
+
one plain string, so the gate can read which setting it returns.
|
|
398
|
+
- **`EXPLAIN FOR CONNECTION` is refused.** On MySQL and MariaDB it showed the
|
|
399
|
+
plan of the statement another session was running. It is now refused there,
|
|
400
|
+
also when it is written with `DESCRIBE` or `DESC`.
|
|
401
|
+
- **A function called by a quoted name is refused.** The checks that read a
|
|
402
|
+
function's name read it bare, so a function whose name was written in
|
|
403
|
+
quotes, backticks or brackets passed them. A quoted name followed by a
|
|
404
|
+
parenthesis is now refused on every family. On PostgreSQL the functions a
|
|
405
|
+
statement's text names are checked beside those its plan prints, since a
|
|
406
|
+
plan prints no row of a `VALUES` list and no `LIMIT` expression.
|
|
407
|
+
- **A database whose columns cannot be read is not queried.** When the schema
|
|
408
|
+
read failed, `rails_query` ran the statement with no column of the schema to
|
|
409
|
+
check its text against. It now answers "The columns of the <name> database
|
|
410
|
+
could not be read, so its secret columns cannot be told and no query runs on
|
|
411
|
+
it." The primary alone falls back to the schema in the cached context.
|
|
412
|
+
- **On PostgreSQL a result column is filtered when its table's columns cannot
|
|
413
|
+
be read and that table holds a secret column.** The trace from a result
|
|
414
|
+
column to its table column skipped a table whose column names it could not
|
|
415
|
+
read, so a secret column of that table could come back under another name.
|
|
416
|
+
Such a column is now filtered when its table holds a secret column. When the
|
|
417
|
+
table cannot be told either, it is filtered when any table does.
|
|
418
|
+
- **The reads a `rails_query` call makes beside the query have a time limit on
|
|
419
|
+
PostgreSQL, MySQL and MariaDB.** Those reads, such as the schema read behind
|
|
420
|
+
the secret column rule, had no time limit, so a call against a table another
|
|
421
|
+
session had locked waited until the lock went. Each family now bounds them the
|
|
422
|
+
way it bounds the query, a wait on a lock included. The query has
|
|
423
|
+
`query_timeout` to itself. The schema read, the PostgreSQL plan with its
|
|
424
|
+
function lookup, and the column trace share a second `query_timeout`, and each
|
|
425
|
+
is charged the time it ran. A read that runs out of time ends the call with an
|
|
426
|
+
error that names those reads, so the statements of a call take about twice
|
|
427
|
+
`query_timeout` at most.
|
|
428
|
+
On MySQL and MariaDB a wait on a lock ends at `query_timeout` rounded up to a
|
|
429
|
+
whole second, and `explain: true` is bounded while it waits on a lock too.
|
|
430
|
+
`SHOW`, `DESCRIBE` and a plain `EXPLAIN` have that lock wait limit alone
|
|
431
|
+
there, since the statement limit does not cover them. On SQLite those reads
|
|
432
|
+
run on the app's own connection with no statement limit, and a wait on another
|
|
433
|
+
writer ends at that connection's busy timeout.
|
|
434
|
+
- **`rails_runtime_info` reads table sizes, index usage and pending migrations
|
|
435
|
+
under `query_timeout` on PostgreSQL, MySQL and MariaDB.** They had no time
|
|
436
|
+
limit, so a locked table held the answer until the lock went. Each read now
|
|
437
|
+
ends at `query_timeout`, a wait on a lock included.
|
|
438
|
+
- **A `query_timeout` under a millisecond still bounds the statement.** On
|
|
439
|
+
PostgreSQL and MySQL it rounded to zero, which turns the statement limit
|
|
440
|
+
off. PostgreSQL, MySQL and MariaDB now bound the statement at one
|
|
441
|
+
millisecond.
|
|
442
|
+
- **SQLite no longer chooses the app's own connection from an error message.**
|
|
443
|
+
`rails_query` runs a SQLite statement in a child process that it ends at
|
|
444
|
+
`query_timeout`. It ran the statement again on the app's own connection, with
|
|
445
|
+
no time limit, when the child's error message read like a missing function,
|
|
446
|
+
module or collation, and text from the statement could reach that message. The
|
|
447
|
+
choice is now made from which step failed in the child. A statement runs on
|
|
448
|
+
the app's own connection in four cases: an in-memory database, a platform
|
|
449
|
+
without `fork`, a statement the child could not prepare, and a transaction
|
|
450
|
+
open on the app's connection. A statement that fails while it runs in the
|
|
451
|
+
child is answered with its SQL error and is not run again. On
|
|
452
|
+
the app's own connection a statement that returns rows slowly now ends a few
|
|
453
|
+
rows after `query_timeout` passes, since an interrupt lands only between two
|
|
454
|
+
rows. One long step still cannot be ended there, so the answer keeps its
|
|
455
|
+
"without a time limit" note.
|
|
456
|
+
- **On MySQL and MariaDB no statement runs on a connection Rails replaced.**
|
|
457
|
+
Rails reconnects a connection it lost before the transaction begins, and the
|
|
458
|
+
new session has no read-only transaction and no time limit. The statement is
|
|
459
|
+
now sent only on the session that holds both. On any other the answer is an
|
|
460
|
+
error that starts "The <name> database stopped answering during the query:"
|
|
461
|
+
and says the statement was not sent.
|
|
462
|
+
- **`rails_query` holds `LIMIT ALL`, `LIMIT NULL` and a negative limit to the
|
|
463
|
+
row cap.** The three mean no limit, and the query fetched every row before
|
|
464
|
+
the cap cut the answer. They now read as no limit of the query's own and
|
|
465
|
+
fetch one row past the cap, with the note that rows were held back.
|
|
466
|
+
- **A SQLite database whose file is not there is refused by name, and no file
|
|
467
|
+
is created.** In 5.35.0 the connection created an empty database file.
|
|
468
|
+
`rails_query` now answers with an error that names the database and
|
|
469
|
+
`bin/rails db:prepare`, with or without `database`. `rails_runtime_info`
|
|
470
|
+
does the same for a named database. With none, its Database section says the
|
|
471
|
+
primary does not exist yet and the other sections still answer. A database
|
|
472
|
+
written as a `file:` URI is checked at the path inside the URI, or at the
|
|
473
|
+
whole name on a SQLite build that reads no URIs.
|
|
474
|
+
- **`rails_runtime_info` creates no Solid Cache database.** Under an
|
|
475
|
+
environment whose Solid Cache SQLite file was not there, the cache section's
|
|
476
|
+
read created an empty one. The section now says the database does not exist
|
|
477
|
+
yet.
|
|
478
|
+
- **`skip_tools` holds inside composite answers.**
|
|
479
|
+
`rails_get_context(include:)` ran `rails_security_scan` and `rails_get_env`
|
|
480
|
+
when `skip_tools` listed them. An include whose tool is turned off is now
|
|
481
|
+
answered by saying so, and that tool does not run. `rails_get_context`,
|
|
482
|
+
`rails_diagnose` and `rails_review_changes` build every other section the
|
|
483
|
+
same way: from a built-in tool only when the app serves it. A section left
|
|
484
|
+
out is named in one line at the end of the answer.
|
|
485
|
+
|
|
486
|
+
### Removed
|
|
487
|
+
|
|
488
|
+
Ten tools that each answered one section of the app's configuration leave
|
|
489
|
+
`tools/list`, and `rails_get_config` answers all of them through a new
|
|
490
|
+
`section` parameter (ADR-0010). The tool count is now **35** (was 45).
|
|
491
|
+
|
|
492
|
+
- **Ten tools are now sections of `rails_get_config`.** They are
|
|
493
|
+
`rails_get_gems` (`section:"gems"`), `rails_get_frontend_stack`
|
|
494
|
+
(`"frontend"`), `rails_get_api` (`"api"`), `rails_get_engines`
|
|
495
|
+
(`"engines"`), `rails_get_conventions` (`"conventions"`),
|
|
496
|
+
`rails_get_autoload` (`"autoload"`), `rails_get_active_support`
|
|
497
|
+
(`"active_support"`), `rails_get_env` (`"env"`), `rails_get_env_config`
|
|
498
|
+
(`"env_config"`) and `rails_get_i18n` (`"i18n"`). `rails_get_config()` with
|
|
499
|
+
no `section` answers `app`, what it answered before.
|
|
500
|
+
- **The sections' parameters ride on `rails_get_config`.** They are `detail`
|
|
501
|
+
(read by `frontend`, `api` and `env`), `category` (`gems`), `environment`
|
|
502
|
+
(`env_config`), `locale` (`i18n`), and `offset` and `limit` (`gems`,
|
|
503
|
+
`env_config`, `i18n`). A parameter the asked section does not read is named
|
|
504
|
+
in the answer as ignored.
|
|
505
|
+
- **`rails_onboard` drops `quick`.** It takes `summary`, `standard` and
|
|
506
|
+
`full`, the levels every other tool with `detail` takes, and `summary` is
|
|
507
|
+
the one-paragraph overview `quick` gave.
|
|
508
|
+
- **`tools/list` is smaller.** Measured as the JSON of every tool's
|
|
509
|
+
definition, it was 47,802 characters (about 11,950 tokens at four characters
|
|
510
|
+
per token) and is now 41,275 characters (about 10,320 tokens).
|
|
511
|
+
|
|
512
|
+
### Migration notes
|
|
513
|
+
|
|
514
|
+
- **Old names keep working where a name is typed.** The CLI resolves `gems`,
|
|
515
|
+
`rails_get_gems`, `frontend_stack` and every other old short and full name to
|
|
516
|
+
its section, and `--json` reports `rails_get_config`. A call to a retired
|
|
517
|
+
name, from the CLI or over MCP, is answered from its section and ends with a
|
|
518
|
+
line naming the call to make instead. That line is the command under
|
|
519
|
+
`tool_mode: :cli` and the MCP call under `:mcp`, and it carries only the
|
|
520
|
+
arguments the section reads. A value goes into the call when it is a number, a
|
|
521
|
+
boolean, or at most 80 characters of letters, digits, `_`, `.` and `-`. Any
|
|
522
|
+
other value is named after the call, as in "_rails_get_i18n is now
|
|
523
|
+
`rails_get_config(section:"i18n")`, with the same `locale`; call that
|
|
524
|
+
instead._" `rails_session_context(mark:)` takes a retired name too, and
|
|
525
|
+
records it as `rails_get_config` with its section. Update saved prompts to the
|
|
526
|
+
new call: `rails_get_gems(category:"auth")` becomes
|
|
527
|
+
`rails_get_config(section:"gems", category:"auth")`.
|
|
528
|
+
- **`skip_tools` keeps its meaning.** An old tool name turns its section off,
|
|
529
|
+
so an app that skipped `rails_get_env` still keeps the `env` section off.
|
|
530
|
+
`rails_get_config` turns off the `app` section only. A section that is off
|
|
531
|
+
leaves the published `section` list, the tool description, the parameters
|
|
532
|
+
only it reads and the guide row. A call that names it anyway is told it is
|
|
533
|
+
turned off. The tool leaves `tools/list` only when all eleven names are
|
|
534
|
+
listed.
|
|
535
|
+
- **With the `app` section off, `section` is required.** `app` is the default,
|
|
536
|
+
so once `skip_tools` lists `rails_get_config` the published schema marks
|
|
537
|
+
`section` required and the description says so. A call with no `section` is
|
|
538
|
+
answered with the sections that are on, and the guide row shows a call to
|
|
539
|
+
the first of them.
|
|
540
|
+
- **A custom `rails_get_config` still replaces the built-in.** An app that
|
|
541
|
+
lists `rails_get_config` in `skip_tools` and registers a custom tool with
|
|
542
|
+
that name gets the custom tool in the built-in's place. The ten sections go
|
|
543
|
+
with the built-in, so a retired name such as `rails_get_gems` is then an
|
|
544
|
+
unknown tool, and no answer points at a section.
|
|
545
|
+
- **A custom tool that reuses a retired name is served as its own tool.** A
|
|
546
|
+
custom `rails_get_gems` answers a call to that name. The gem's own hints
|
|
547
|
+
still mean the built-in section, and they print it as
|
|
548
|
+
`rails_get_config(section:"gems")`.
|
|
549
|
+
- **Ruby callers are unchanged.** `RailsAiContext::Tools::GetGems.call` and
|
|
550
|
+
the other nine classes still answer. They are out of the MCP registry, so
|
|
551
|
+
`Tools::BaseTool.registered_tools` no longer lists them. The test helper's
|
|
552
|
+
`execute_tool("rails_get_gems")` still finds the class.
|
|
553
|
+
- **`rails_get_context(include:)` takes the same keys.** `config`, `gems`,
|
|
554
|
+
`env` and `conventions` now run as `rails_get_config` sections.
|
|
555
|
+
- **The session context names `rails_get_config`.** A section call is recorded
|
|
556
|
+
as `rails_get_config` with its section, also when a retired name made the
|
|
557
|
+
call. A section call answered with an error is not recorded.
|
|
558
|
+
- **The CLI's `--environment` is a section's filter only after `env_config`.**
|
|
559
|
+
After `config --section env_config`, in any capitalisation, it is the
|
|
560
|
+
section's filter and leaves `RAILS_ENV` alone, as it does after
|
|
561
|
+
`env_config`. After any other section it sets `RAILS_ENV`.
|
|
562
|
+
- **`rails_onboard(detail:"quick")` becomes `detail:"summary"`.** Over MCP a
|
|
563
|
+
`quick` call is refused with the three valid levels. The CLI's
|
|
564
|
+
`--detail quick` warns with them and answers at `standard`.
|
|
565
|
+
- **Apps on SQL Server, Oracle or CockroachDB lose `rails_query`.** It answers
|
|
566
|
+
with the adapter's name and runs nothing (ADR-0008). It ran there with no
|
|
567
|
+
read-only transaction and no timeout. On SQL Server and Oracle the `LIMIT`
|
|
568
|
+
its row cap appended is a syntax error, so a query without its own row limit
|
|
569
|
+
failed. Add `rails_query` to `skip_tools` to take it out of `tools/list`.
|
|
570
|
+
`rails_runtime_info` still answers, and says table sizes and index usage are
|
|
571
|
+
not available for the adapter.
|
|
572
|
+
- **`query_allowed_columns` no longer releases an `encrypts` column.** A query
|
|
573
|
+
that names one is refused, and the refusal says the allowed list does not
|
|
574
|
+
release it. Its values come back `[FILTERED]` under `SELECT *`. Take the
|
|
575
|
+
column out of the query.
|
|
576
|
+
- **`rails_query` refuses more column names.** A column named `salt`, `passwd`,
|
|
577
|
+
`credentials`, `pepper`, `authentication_string`, `rolpassword` or
|
|
578
|
+
`setconfig`, one whose name holds `nonce`, and one named `iv` or ending in
|
|
579
|
+
`_iv` is refused by name and comes back `[FILTERED]` under `SELECT *`. Each of
|
|
580
|
+
`salt`, `passwd`, `credentials` and `pepper` counts as a whole name and as one
|
|
581
|
+
part of a name split at its underscores. So `salt_rounds`, `aws_credentials`
|
|
582
|
+
and `pepper_spray` are refused, and `salty`, `basalt` and `peppermint` are
|
|
583
|
+
not. To read one that holds no secret, list its name in
|
|
584
|
+
`query_allowed_columns`.
|
|
585
|
+
- **On MySQL, MariaDB and SQLite a query over a view that reads a secret
|
|
586
|
+
column is refused.** It ran on 5.35.0 and returned what the view selected.
|
|
587
|
+
The refusal names the view and the column. Query the tables the view reads,
|
|
588
|
+
or list the column in `query_allowed_columns` when it holds no secret. An
|
|
589
|
+
`encrypts` column is not released that way. On PostgreSQL the same holds for
|
|
590
|
+
a materialized view.
|
|
591
|
+
- **A secret column in another database or schema is refused by name.** A
|
|
592
|
+
statement that names a schema or database beside the connection's own, such as
|
|
593
|
+
`SELECT api_token AS t FROM other_db.users`, ran on 5.35.0. The rule now reads
|
|
594
|
+
the columns there, so the statement is refused. Name the other columns, or
|
|
595
|
+
list the name in `query_allowed_columns` when it holds no secret. A statement
|
|
596
|
+
that names a view that leads through more than three reads of further
|
|
597
|
+
databases or schemas is not run, and the answer names the view.
|
|
598
|
+
- **On SQLite a virtual table with a secret column, a shadow table and the
|
|
599
|
+
page modules are refused.** A query on a full-text table that holds a secret
|
|
600
|
+
column ran on 5.35.0. So did one on a shadow table of a virtual table, on an
|
|
601
|
+
`fts5vocab` table over it and on `sqlite_dbpage`. Keep the secret column out
|
|
602
|
+
of the virtual table, or query the table it was copied from. A shadow
|
|
603
|
+
table's refusal names the virtual table to query.
|
|
604
|
+
- **On SQLite only the pragma functions that describe the schema or the file
|
|
605
|
+
run.** They are `pragma_table_info`, `pragma_table_xinfo`,
|
|
606
|
+
`pragma_table_list`, `pragma_index_list`, `pragma_index_info`,
|
|
607
|
+
`pragma_index_xinfo`, `pragma_foreign_key_list`, `pragma_foreign_key_check`,
|
|
608
|
+
`pragma_integrity_check`, `pragma_quick_check`, `pragma_user_version`,
|
|
609
|
+
`pragma_application_id`, `pragma_schema_version`, `pragma_page_size`,
|
|
610
|
+
`pragma_page_count`, `pragma_freelist_count`, `pragma_encoding`,
|
|
611
|
+
`pragma_auto_vacuum`, `pragma_journal_mode`, `pragma_compile_options` and
|
|
612
|
+
`pragma_pragma_list`. Every other `pragma_*` function ran on 5.35.0 and is
|
|
613
|
+
refused now, and so are `changes()`, `total_changes()` and
|
|
614
|
+
`last_insert_rowid()`. Read those values on the app's own connection, in
|
|
615
|
+
`bin/rails runner` or the console.
|
|
616
|
+
- **A statement over 1,000,000 bytes is refused.** 5.35.0 took a statement of
|
|
617
|
+
any length. Every text check reads the whole statement, so its length is
|
|
618
|
+
checked before any of them. Send a shorter statement: 100,000 ids in an `IN`
|
|
619
|
+
list are about 700,000 bytes. The text checks also give up when they have run
|
|
620
|
+
longer than `query_timeout`, with a refusal that says so.
|
|
621
|
+
- **A generated column that reads a secret column is refused by name.** A
|
|
622
|
+
query that names it ran on 5.35.0. Take it out of the query, or list its
|
|
623
|
+
name in `query_allowed_columns` when it holds no secret.
|
|
624
|
+
- **On MySQL, MariaDB and SQLite `SELECT *` inside a subquery is refused over a
|
|
625
|
+
table with a secret column.**
|
|
626
|
+
`WITH x AS (SELECT * FROM users) SELECT * FROM x` ran on 5.35.0, and is
|
|
627
|
+
refused now when `users` holds a secret column. Name the columns in the
|
|
628
|
+
subquery. On SQLite `WHERE id IN users` ran too, and is
|
|
629
|
+
refused the same way: write `IN (SELECT id FROM users)`. There a string equal
|
|
630
|
+
to the name of such a table, as in `WHERE role = 'users'`, refuses a statement
|
|
631
|
+
with a `*` inside a subquery or with that `IN` form.
|
|
632
|
+
- **On MySQL and MariaDB a statement has to pass under both readings of a
|
|
633
|
+
backslash.** A statement with a backslash before a quote inside a string, such
|
|
634
|
+
as `'it\'s'`, ran on 5.35.0 when the checks passed it with the backslash read
|
|
635
|
+
as an escape. It is refused now when the checks refuse it with the backslash
|
|
636
|
+
read as an ordinary character, as MySQL reads it under `NO_BACKSLASH_ESCAPES`.
|
|
637
|
+
Write a quote inside a string doubled, as in `'it''s'`, so that both readings
|
|
638
|
+
agree.
|
|
639
|
+
- **`SHOW ... WHERE` is refused.** Use `SHOW ... LIKE`, or select from
|
|
640
|
+
`information_schema`: `SELECT table_name FROM information_schema.tables
|
|
641
|
+
WHERE table_schema = DATABASE()`.
|
|
642
|
+
- **`rails_query` no longer reads statistics, session and credential
|
|
643
|
+
relations.** A query on one ran on 5.35.0 and is refused now. Each name is
|
|
644
|
+
refused on the database family that has it. On PostgreSQL they are
|
|
645
|
+
`pg_stat_activity`, `pg_stats`, `pg_settings` and the other catalog names
|
|
646
|
+
docs/SECURITY.md lists. On MySQL and MariaDB they are `performance_schema`,
|
|
647
|
+
`processlist`, `general_log`, `slow_log`, `column_statistics`,
|
|
648
|
+
`column_stats`, `global_priv`, the listed `information_schema` tables and
|
|
649
|
+
anything under `mysql.` or `sys.`. On SQLite they are `sqlite_stat1`,
|
|
650
|
+
`sqlite_stat3`, `sqlite_stat4` and `sqlite_stmt`. A name is matched as a
|
|
651
|
+
whole word wherever it stands, inside a string literal too, so an app table
|
|
652
|
+
named `slow_log` is refused on MySQL and MariaDB. The `mysql` and `sys`
|
|
653
|
+
schemas are matched as a name before a dot, with string literals masked, so
|
|
654
|
+
`'mysql.com'` runs. An alias named `sys` or `mysql` before a dot is refused
|
|
655
|
+
there, and so is a double-quoted string that holds `mysql.`. Give such an
|
|
656
|
+
alias another name, and write such a string in single quotes. Read the
|
|
657
|
+
refused relations with the database's own client.
|
|
658
|
+
- **On PostgreSQL `SHOW ALL` and six settings are refused.** `SHOW ALL`,
|
|
659
|
+
`SHOW "work_mem"` with quotes, and `SHOW` or `current_setting` of
|
|
660
|
+
`primary_conninfo`, `ssl_passphrase_command`, `archive_command`,
|
|
661
|
+
`restore_command`, `archive_cleanup_command` or `recovery_end_command` ran
|
|
662
|
+
on 5.35.0. Show one setting by its plain name: `SHOW work_mem`, or
|
|
663
|
+
`current_setting('work_mem')`. On PostgreSQL a statement that holds one of
|
|
664
|
+
the six names anywhere is refused.
|
|
665
|
+
- **A function whose name is written in quotes cannot be called.**
|
|
666
|
+
`SELECT "upper"('x')` ran on 5.35.0 and is refused now. Write the name
|
|
667
|
+
without quotes. A function whose name needs quotes, such as a mixed-case one
|
|
668
|
+
on PostgreSQL, cannot be called through `rails_query`.
|
|
669
|
+
- **The `rails_query` refusals say "secret column".** They said "sensitive
|
|
670
|
+
column", and now use the glossary's word. A script that matches on the old
|
|
671
|
+
words in a refusal has to match on the new ones. The config keys keep their
|
|
672
|
+
names.
|
|
673
|
+
- **`query_timeout` takes a positive number only.** Zero, a negative value or
|
|
674
|
+
a value that is no number raises `ArgumentError` in an initializer, as
|
|
675
|
+
`query_row_limit` does. In `.rails-ai-context.yml` it is reported and the
|
|
676
|
+
default of 5 seconds is kept. In 5.35.0 zero turned the timeout off on
|
|
677
|
+
PostgreSQL and MySQL. Set a larger number for a slow query. A fraction of a
|
|
678
|
+
second is allowed.
|
|
679
|
+
- **A CLI call with a value its parameter cannot take exits 1.** `--format xml`
|
|
680
|
+
on `schema`, `--limit abc` and `--no-limit` exited 0 in 5.35.0, the first two
|
|
681
|
+
with a warning on stderr and the parameter's default. A script that leaned on
|
|
682
|
+
the fallback has to pass a value the parameter takes. The Fixed entry "The CLI
|
|
683
|
+
refuses a value a parameter does not take" gives the answers. `--detail` alone
|
|
684
|
+
still warns and falls back.
|
|
685
|
+
- **Some CLI calls that exited 0 now exit 1.** Beside the refused values above,
|
|
686
|
+
they are a `rails_query` timeout, a `rails_query` call the production guard
|
|
687
|
+
refuses, a database that cannot be reached, a failing `explain: true`, a
|
|
688
|
+
failed Brakeman scan in the bundle or outside it, a crash inside a section
|
|
689
|
+
of `rails_get_context`, `rails_diagnose` or
|
|
690
|
+
`rails_review_changes`, and an unexpected failure in `rails_onboard`,
|
|
691
|
+
`rails_diagnose`, `rails_generate_test`, `rails_review_changes`,
|
|
692
|
+
`rails_runtime_info`, `rails_session_context`, `rails_get_context` or
|
|
693
|
+
`rails_search_code`. Over MCP the same answers carry `isError`.
|
|
694
|
+
- **Regenerate the context files.** Run `rails ai:context` or
|
|
695
|
+
`rails-ai-context context` so the tool guide and tables name the new call.
|
|
696
|
+
|
|
697
|
+
### Added
|
|
698
|
+
|
|
699
|
+
- **`rails_query` and `rails_runtime_info` take `database`.** It is an app
|
|
700
|
+
database's name from `config/database.yml`, checked when the tool runs. With
|
|
701
|
+
none they read the primary, the database `ActiveRecord::Base` is connected
|
|
702
|
+
to. A misspelled name is answered with the closest one. A database only
|
|
703
|
+
other environments configure, a replica and one with `database_tasks` off
|
|
704
|
+
included, is answered with the environments that configure it. One only a
|
|
705
|
+
dump names is refused with the reason. On the static tier both tools are
|
|
706
|
+
unavailable as a whole, with or without `database`.
|
|
707
|
+
- **Another database is reached through a connection handler of its own.** The
|
|
708
|
+
handler lasts for the call, so the app's pools are untouched and two calls
|
|
709
|
+
at once never share a pool. The database's secret columns are read from its
|
|
710
|
+
own schema. It has `query_timeout`, rounded up to whole seconds, to connect
|
|
711
|
+
in unless its entry sets `connect_timeout`. On MySQL and MariaDB a read on
|
|
712
|
+
it ends after twice `query_timeout` plus 5 seconds unless its entry sets
|
|
713
|
+
`read_timeout`, so a server that goes silent ends the call. PostgreSQL's
|
|
714
|
+
driver has no read timeout, so a PostgreSQL server that goes silent after it
|
|
715
|
+
connects is not ended by the client.
|
|
716
|
+
- **A query that misses a table another database holds names that database.**
|
|
717
|
+
The SQL error ends with the database that holds the table and the `database`
|
|
718
|
+
value to use. A failing `explain: true` carries the same pointer.
|
|
719
|
+
- **`rails_runtime_info` checks `database` whatever the `section`.** A name it
|
|
720
|
+
cannot reach is an error for every section, and a section that does not read
|
|
721
|
+
`database` says so.
|
|
722
|
+
- **`rails_runtime_info` names a database no connection reaches.** It says what
|
|
723
|
+
stopped the connection and what to do, as the `rails_query` answer under Fixed
|
|
724
|
+
does. For a named database that is an error for the call. With none, the
|
|
725
|
+
Database section says so about the primary and the other sections still
|
|
726
|
+
answer.
|
|
727
|
+
- **A database whose adapter gem is not in the bundle is named with the gem to
|
|
728
|
+
add.** The answer reads "The <name> database cannot be reached: its
|
|
729
|
+
<adapter> adapter did not load. Add the <gem> gem to the Gemfile and run
|
|
730
|
+
`bundle install`." `rails ai:doctor` gives the same reason and fix, and
|
|
731
|
+
`rails_runtime_info` with no `database` still answers its other sections.
|
|
732
|
+
- **Under a named database `rails_runtime_info` shows that database's own
|
|
733
|
+
pending migrations.** For a replica it says Rails runs no migrations on it.
|
|
734
|
+
For a database whose entry turns `database_tasks` off it says "_Rails runs
|
|
735
|
+
no migrations on <name>: its database.yml entry turns `database_tasks`
|
|
736
|
+
off._"
|
|
737
|
+
- **A SQLite database other than the primary that lives in memory is refused
|
|
738
|
+
by name.** That is `:memory:`, or a `file:` URI with `mode=memory`. It
|
|
739
|
+
exists only inside the process that created it, and a new connection would
|
|
740
|
+
open an empty one. `rails ai:doctor` and `rails_runtime_info` leave such a
|
|
741
|
+
database out of their pending migrations check.
|
|
742
|
+
- **`.ai-context.json` carries `primary_name` where the primary has another
|
|
743
|
+
name.** The key sits in the `multi_database` section and holds the primary's
|
|
744
|
+
`config/database.yml` name. An app whose primary is named `primary` has no
|
|
745
|
+
such key.
|
|
746
|
+
|
|
747
|
+
### Changed
|
|
748
|
+
|
|
749
|
+
- **Statistics say when an adapter has none.** On an adapter with no database
|
|
750
|
+
family, `rails_runtime_info` says table sizes and index usage are not
|
|
751
|
+
available for that adapter, and the database stats say the same of row
|
|
752
|
+
estimates.
|
|
753
|
+
- **`rails_query` reads the schema in a few statements, whatever the number of
|
|
754
|
+
tables.** The secret column rule read each table's columns with a statement of
|
|
755
|
+
its own on every call, about 1.5 seconds on a 300-table PostgreSQL schema and
|
|
756
|
+
2 seconds on MySQL. One catalogue read per family now lists every table's
|
|
757
|
+
columns, and one each reads the view definitions and the generated columns.
|
|
758
|
+
With the lookups beside them that is four statements at most on MySQL and
|
|
759
|
+
MariaDB, five on PostgreSQL and six on SQLite. A statement that names another
|
|
760
|
+
schema or database, or a view that names one, adds the reads of that one. Any
|
|
761
|
+
other statement adds none.
|
|
762
|
+
- **`rails_get_service_pattern` says which files its count leaves out.** A
|
|
763
|
+
listing that counts fewer services than the service directories hold Ruby
|
|
764
|
+
files adds one line with the number left out and the kinds: base classes,
|
|
765
|
+
concerns, modules with no entry point that other code mixes in, mailers and
|
|
766
|
+
files too large to read. It names up to five of each kind, and the base
|
|
767
|
+
classes are named above the listing as before.
|
|
768
|
+
|
|
769
|
+
### Fixed
|
|
770
|
+
|
|
771
|
+
- **Every file requires the standard library it names.** Twelve files used
|
|
772
|
+
`JSON`, `YAML`, `URI`, `Pathname`, `FileUtils`, `RbConfig`, `DidYouMean`,
|
|
773
|
+
`Prism`, `Time.parse` or `iso8601` and worked only when another library had
|
|
774
|
+
loaded it first. On a Ruby that does not load the library by itself that
|
|
775
|
+
showed as "uninitialized constant" or "undefined method".
|
|
776
|
+
- **`rails_onboard` answers on the static tier for an app with Solid
|
|
777
|
+
databases.** On a default Rails 8 app `tool onboard --no-boot` answered
|
|
778
|
+
"Onboard error: undefined method 'to_sentence'". The gem called an
|
|
779
|
+
ActiveSupport extension that only a booted app had loaded. It now loads the
|
|
780
|
+
extension itself.
|
|
781
|
+
- **A tool that fails unexpectedly is an error result.** `rails_onboard`,
|
|
782
|
+
`rails_diagnose`, `rails_generate_test`, `rails_review_changes`,
|
|
783
|
+
`rails_runtime_info`, `rails_session_context`, `rails_get_context` and
|
|
784
|
+
`rails_search_code` answered an exception they did not plan for as ordinary
|
|
785
|
+
text, so the CLI exited 0. They now answer as every other tool does:
|
|
786
|
+
`isError: true` over MCP, exit 1 from the CLI, and a text that starts "Tool
|
|
787
|
+
<name> failed:".
|
|
788
|
+
- **A crash inside a section of a composite answer makes the answer an error.**
|
|
789
|
+
`rails_get_context`, `rails_diagnose` and `rails_review_changes` build their
|
|
790
|
+
sections from other tools. When one of those raised an exception, the answer
|
|
791
|
+
was ordinary text and the CLI exited 0. It is now an error response on both
|
|
792
|
+
surfaces. The text keeps every section, and the crashed one reads "Tool <name>
|
|
793
|
+
failed: <Class>: <message>". A section that answers an error for another
|
|
794
|
+
reason, such as a refusal or a failed Brakeman run, stays a section of an
|
|
795
|
+
ordinary answer.
|
|
796
|
+
- **A stack overflow or a `SecurityError` in a tool is an error response.**
|
|
797
|
+
Either one ended the stdio server and printed a backtrace on the CLI. The
|
|
798
|
+
answer is now an error such as "Tool <name> failed: SystemStackError: stack
|
|
799
|
+
level too deep", and the server keeps running.
|
|
800
|
+
- **A failed Brakeman scan is an error result.** `rails_security_scan` answered
|
|
801
|
+
"Brakeman scan failed: ..." as an ordinary answer, an unknown check name
|
|
802
|
+
included. The text is the same, and it now carries `isError` over MCP and
|
|
803
|
+
exits 1 from the CLI. A brakeman run from outside the bundle that started and
|
|
804
|
+
failed is an error too, with the text it had. One that could not start stays
|
|
805
|
+
an ordinary answer.
|
|
806
|
+
- **`serve --transport http` starts on rack 2 with rackup 1.x.** It stopped
|
|
807
|
+
with "uninitialized constant Rackup::Handler" on an app that pins rack below
|
|
808
|
+
3 and bundles the rackup gem.
|
|
809
|
+
- **The `rails_get_config` app section names the database family.** Its
|
|
810
|
+
Database line showed `mysql2` where every other surface says "MySQL". It now
|
|
811
|
+
reads "MySQL", "PostgreSQL" or "SQLite", and an adapter with no family as
|
|
812
|
+
database.yml writes it. The JSON keeps adapter names as written.
|
|
813
|
+
- **`rails_onboard` words one Solid database in the singular.** For one
|
|
814
|
+
framework with one database it said "keeps their tables in databases of
|
|
815
|
+
their own". It now says "Solid Cache keeps its tables in a database of its
|
|
816
|
+
own: cache."
|
|
817
|
+
- **`rails_runtime_info` names a Solid Cache store "SolidCache::Store".** The
|
|
818
|
+
cache section's Store line read "Store".
|
|
819
|
+
- **`rails_runtime_info` says when a database family has no index usage.** At
|
|
820
|
+
full detail on MySQL, MariaDB and SQLite it printed nothing where PostgreSQL
|
|
821
|
+
lists index usage. It now says "_Index usage is not available for the MySQL
|
|
822
|
+
database family._", or SQLite.
|
|
823
|
+
- **`rails_runtime_info` reads pending migrations in every database configured
|
|
824
|
+
here.** It said "all up to date" while another database had pending
|
|
825
|
+
migrations. The line now names the databases it read, as in "**Migrations:**
|
|
826
|
+
all up to date in primary, queue". A database it could not read gets a line
|
|
827
|
+
of its own with the reason, and the databases it did read are still
|
|
828
|
+
reported.
|
|
829
|
+
- **A booted MySQL app reads "MySQL".** A connection on mysql2 or Trilogy
|
|
830
|
+
showed its driver name in the generated context and in `rails_runtime_info`.
|
|
831
|
+
It now reads "MySQL" there, as the static tier already said. PostGIS reads
|
|
832
|
+
"PostgreSQL".
|
|
833
|
+
- **Migration advice for an undecided database names no database.** When the
|
|
834
|
+
app's database is one of two candidates, `rails_migration_advisor` no longer
|
|
835
|
+
gives PostgreSQL and MySQL advice at once.
|
|
836
|
+
- **Migration advice follows the database that holds the table.** Index and
|
|
837
|
+
lock advice took the primary's database family for a table another database
|
|
838
|
+
holds. It now takes the family of the database that holds the table.
|
|
839
|
+
- **Every surface names the same databases.** On a default Rails 8 app, where
|
|
840
|
+
the running environment configures only the primary and production configures
|
|
841
|
+
queue, cache and cable, the schema section, the multi-database section, the
|
|
842
|
+
stack overview and the model listing now all name the four databases. The
|
|
843
|
+
migrations section names each one whose migrations directories hold files. A
|
|
844
|
+
database only other environments configure is marked with every one of them,
|
|
845
|
+
"(production only)" or "(staging and production)", and one only a committed
|
|
846
|
+
dump names is marked "(committed dump only)". The mark is on every line that
|
|
847
|
+
names such a database: the `rails_get_schema` Secondary databases line, each
|
|
848
|
+
database `rails_onboard` names, the performance check's "Not checked" line,
|
|
849
|
+
the Database, Migrations and Databases lines of the compact generated files,
|
|
850
|
+
and the headings of the full generated file and of the schema rules file, as
|
|
851
|
+
in "## cache (production only, SQLite, 1 table)".
|
|
852
|
+
- **An app whose first database is not named `primary` is read by its own
|
|
853
|
+
names.** Where another database is the one named `primary`, the generated
|
|
854
|
+
files called both of them primary. They now name the first database by its own
|
|
855
|
+
name, as in "## Database Schema: animals (2 tables)". Migration advice for a
|
|
856
|
+
table the database named `primary` holds gave no `--database`, and now carries
|
|
857
|
+
`--database primary`.
|
|
858
|
+
- **A `replica` entry is read as Rails reads it.** `replica: "yes"`, or any
|
|
859
|
+
other value Ruby counts as true, marks a replica. The static tier took `true`
|
|
860
|
+
alone.
|
|
861
|
+
- **The static tier reads an environment whose whole value is a URL.** A
|
|
862
|
+
`config/database.yml` environment written as one URL string was left out. It
|
|
863
|
+
is now read as that environment's one database.
|
|
864
|
+
- **A secondary dump too large to read is named.** It keeps an entry in the
|
|
865
|
+
schema section that gives the reason, and a database whose migrations are
|
|
866
|
+
replayed instead no longer has its existing dump called missing.
|
|
867
|
+
- **`rails_onboard` says when a database's dump was not read.** For a secondary
|
|
868
|
+
whose dump is too large to read it said "(0 tables)" and counted the database
|
|
869
|
+
in its summary. It now says "plus the warehouse database (tables not read)",
|
|
870
|
+
and the summary reads "5 tables in 2 databases, the warehouse database not
|
|
871
|
+
read". The unread database is left out of both counts.
|
|
872
|
+
- **The performance check covers every database.** Missing indexes and counter
|
|
873
|
+
caches are checked in each database's dump, a finding outside the primary
|
|
874
|
+
names its database, and a database it could not read is listed under "Not
|
|
875
|
+
checked" with the reason.
|
|
876
|
+
- **Migration advice counts rows only on the primary.** The row estimate comes
|
|
877
|
+
from the primary database, so a table on another database is no longer given
|
|
878
|
+
the primary's count.
|
|
879
|
+
- **A `.rails-ai-context.yml` that holds no settings is reported and
|
|
880
|
+
ignored.** A file that holds a bare string, a list or a number crashed the
|
|
881
|
+
loader with a `NoMethodError`. It now warns on stderr that the file holds no
|
|
882
|
+
`key: value` settings, and reads nothing from it.
|
|
883
|
+
- **Of two tools under a built-in's name the server keeps the built-in.** With
|
|
884
|
+
no `skip_tools` entry the warning said it kept the built-in, while MCP could
|
|
885
|
+
serve an app's `BaseTool` subclass under that name. The server now keeps the
|
|
886
|
+
gem's own tool, as the warning says and as the CLI runs.
|
|
887
|
+
- **`tool --list` shows the tools the MCP server serves.** A custom tool that
|
|
888
|
+
shares a built-in's name is listed once, and the CLI's single-tool lookup
|
|
889
|
+
picks the tool the server serves while still loading only that tool's class.
|
|
890
|
+
- **No answer points at a tool the app turned off.** Every pointer to another
|
|
891
|
+
tool, in `_Next:` hints, fix steps, recovery lines and lists, goes through
|
|
892
|
+
one helper, and a tool `skip_tools` names is left out with its hint. A
|
|
893
|
+
custom tool in a built-in's place is left out the same way, since a hint
|
|
894
|
+
carries the built-in's arguments.
|
|
895
|
+
- **The CLI refuses a value a parameter does not take.**
|
|
896
|
+
`rails-ai-context tool schema --format xml` warned on stderr and answered in
|
|
897
|
+
the default format with exit 0, and so did `--limit abc` on a parameter that
|
|
898
|
+
takes an integer. Each now exits 1 and names what the parameter takes: the
|
|
899
|
+
listed values, "an integer", "a string", "an array", or "true or false" for a
|
|
900
|
+
boolean. An empty `--limit=` is refused too. So is `--no-<param>` on a
|
|
901
|
+
parameter that is no boolean, which passed `false` to the tool: `--no-limit`
|
|
902
|
+
answers "**Error:** `--no-limit` is not a valid `limit`. Valid: an integer".
|
|
903
|
+
A `--detail` level a tool does not have still falls back to the default with
|
|
904
|
+
a warning, since every level answers the same question.
|
|
905
|
+
- **`tool <name> --help` describes the tool the app serves.** Help read no
|
|
906
|
+
configuration, so `tool config --help` said "Default: app." in an app that
|
|
907
|
+
turned the `app` section off. Inside an app, or with `--app-path`, help now
|
|
908
|
+
reads `.rails-ai-context.yml` without booting. A tool or section `skip_tools`
|
|
909
|
+
turned off is answered as turned off at exit 1, and `tool config --help` says
|
|
910
|
+
"Required: the app section is turned off in this app." when `app` is off.
|
|
911
|
+
Outside an app the output is unchanged. A `skip_tools` set in an initializer
|
|
912
|
+
is not seen without a boot.
|
|
913
|
+
- **A call to a skipped tool shows in instrumentation.** Over MCP it left the
|
|
914
|
+
`rails_ai_context.tools.call` payload with no tool name and no error. The
|
|
915
|
+
payload now carries `tool_name` and `error: :tool_skipped`.
|
|
916
|
+
- **A model class in a `concerns` directory is listed.** A file in a `concerns`
|
|
917
|
+
directory of a model root holds a model when the class its path names has a
|
|
918
|
+
model superclass. Such a class was missing from the static tier's models, and
|
|
919
|
+
from the booted listing until something loaded it. It is now listed under the
|
|
920
|
+
constant its file declares. A concern module that holds an inner class stays a
|
|
921
|
+
concern, the mixins beside it stay out, and `rails_validate` gives the model's
|
|
922
|
+
file the model checks. On the static tier a class whose superclass is a gem's
|
|
923
|
+
own record base is not listed.
|
|
924
|
+
- **`rails_get_context(include:)` lists all twelve keys it takes.** It also
|
|
925
|
+
names a key it does not take, and a key given twice, in any capitalisation,
|
|
926
|
+
runs once.
|
|
927
|
+
- **`rails_get_context` says what is not there with a tool turned off.** With
|
|
928
|
+
`rails_get_controllers` in `skip_tools`, an `action` the controller does not
|
|
929
|
+
have is answered "Action 'nope' not found in PostsController. Available:
|
|
930
|
+
create, destroy, ...". With `rails_analyze_feature` in `skip_tools`, a
|
|
931
|
+
`feature` keyword that only model names hold is answered "The model names that
|
|
932
|
+
hold 'comment': Comment."
|
|
933
|
+
- **The performance check finds a `has_paper_trail` model's unindexed versions
|
|
934
|
+
table on the static tier.** paper_trail adds `has_many :versions, as: :item`
|
|
935
|
+
when the app boots, so only the booted tier reported
|
|
936
|
+
`add_index :versions, [:item_type, :item_id]`. The static tier now reads the
|
|
937
|
+
macro, and the versions class its options name (`versions: { class_name: ...
|
|
938
|
+
}`), so both tiers give the same finding.
|
|
939
|
+
- **A database `rails_query` cannot reach is an error that names it.** A server
|
|
940
|
+
that was down, or a database its server does not hold, came back as an
|
|
941
|
+
ordinary answer headed "Database unavailable" or "Database not found", with no
|
|
942
|
+
database named and advice to try `RAILS_ENV=test`. With or without `database`,
|
|
943
|
+
the answer is now an error that names the database, says what stopped the
|
|
944
|
+
connection and says what to do: start its server or fix its
|
|
945
|
+
`config/database.yml` entry, or run `bin/rails db:create`.
|
|
946
|
+
- **A connection lost while a statement runs is answered as one.** On Rails
|
|
947
|
+
7.1 and later the answer is an error that starts "The <name> database
|
|
948
|
+
stopped answering during the query:".
|
|
949
|
+
- **`rails_query` refused in production is an error.** With
|
|
950
|
+
`allow_query_in_production` off, the refusal came back as an ordinary answer
|
|
951
|
+
and the CLI exited 0. The text is the same, and it is now an error response
|
|
952
|
+
like the tool's other refusals.
|
|
953
|
+
- **A locked SQLite database is answered as locked.** With another connection
|
|
954
|
+
holding the database, `rails_query` answered "Query exceeded 5 second timeout"
|
|
955
|
+
although nothing ran. It now answers "The database is locked by another
|
|
956
|
+
connection, so the query did not run. Run it again when the other connection
|
|
957
|
+
has finished." The query process waits on a lock for 80% of `query_timeout`
|
|
958
|
+
first. The process tells its parent the moment it meets a lock, so the answer
|
|
959
|
+
is the locked one however slow the machine is. A lock met while the connection
|
|
960
|
+
opens, as on the CLI or on a server's first call, gets the same answer.
|
|
961
|
+
- **On SQLite a statement sent inside an open transaction reads that
|
|
962
|
+
transaction's rows.** It ran in the query process, which opens the file
|
|
963
|
+
itself, so it answered from committed rows alone. With a transaction open on
|
|
964
|
+
the app's connection the statement now runs on that connection, and the
|
|
965
|
+
answer carries the "ran without a time limit" note.
|
|
966
|
+
- **On SQLite a pragma function no longer answers for the wrong connection.**
|
|
967
|
+
`pragma_foreign_keys`, `changes()` and their like answered for the
|
|
968
|
+
connection `rails_query` runs on, which is its own and has values the tool
|
|
969
|
+
set. They are now refused, and the Migration note lists the pragma functions
|
|
970
|
+
that run.
|
|
971
|
+
- **On PostgreSQL a wildcard over a partitioned table is filtered in place.**
|
|
972
|
+
`SELECT *` over a partitioned table, or over an inheritance parent, was
|
|
973
|
+
refused with a reason about a CTE or subquery when the table holds a secret
|
|
974
|
+
column. It now runs with that column `[FILTERED]`, also with an expression
|
|
975
|
+
beside the wildcard, as in `SELECT *, id + 1 AS next FROM events`. A whole-row
|
|
976
|
+
read of such a table stays refused when any of its partitions holds a secret
|
|
977
|
+
column.
|
|
978
|
+
- **A `rails_query` timeout is an error.** A query that ran past
|
|
979
|
+
`query_timeout` came back as an ordinary answer. With the same text it is
|
|
980
|
+
now an error response, for a query and for `explain: true`: the CLI exits
|
|
981
|
+
non-zero and an MCP client sees `isError`.
|
|
982
|
+
- **A guarded read leaves an open transaction as it found it.** On a
|
|
983
|
+
connection already inside a transaction, as in a transactional test,
|
|
984
|
+
PostgreSQL's `SET TRANSACTION READ ONLY` left the caller's transaction
|
|
985
|
+
read-only. PostgreSQL now guards inside a savepoint, and the outer
|
|
986
|
+
transaction stays writable. MySQL and MariaDB cannot make an open
|
|
987
|
+
transaction read-only, so there the statement is not sent: "Query failed:
|
|
988
|
+
MySQL cannot make a transaction that is already open read-only, so the
|
|
989
|
+
statement was not sent. Run it outside the transaction."
|
|
990
|
+
- **`rails_query` runs `JOIN ... USING (x, y)` and a row comparison.** The
|
|
991
|
+
column-alias check read `JOIN b USING (x, y)` and `WHERE (a, b) IN (...)` as
|
|
992
|
+
an alias list and refused them.
|
|
993
|
+
- **On PostgreSQL a subquery or join that reads no secret column runs.**
|
|
994
|
+
`SELECT (SELECT count(*) FROM users) AS n` was refused when `users` holds a
|
|
995
|
+
secret column, although `count(*)` reads none. It runs now, and so do
|
|
996
|
+
`count(id)`, `max(id)` and `EXISTS (SELECT 1 ...)` in its place. An
|
|
997
|
+
aggregate over a secret column, a whole row and `count(users)` stay refused.
|
|
998
|
+
- **On PostgreSQL a quoted mixed-case secret column is filtered in place.**
|
|
999
|
+
`SELECT *` over a table with a secret column such as `"ApiToken"` was refused
|
|
1000
|
+
whole, where a lowercase one is filtered.
|
|
1001
|
+
- **CSV output with no rows is the header row.** `format: "csv"` answered a
|
|
1002
|
+
query that returned no rows with the sentence the table format prints.
|
|
1003
|
+
- **EXPLAIN on MySQL leaves a row with no table out of the scan summary.**
|
|
1004
|
+
`explain: true` on a statement that reads no table, such as `SELECT 1`,
|
|
1005
|
+
listed a scan of `?`.
|
|
1006
|
+
- **EXPLAIN on SQLite reads a constant row and an alias for what they are.**
|
|
1007
|
+
`explain: true` on `SELECT 1` listed a full table scan of `CONSTANT` with a
|
|
1008
|
+
warning, and now shows no scan summary. A table the statement gives another
|
|
1009
|
+
name was listed as a table named by its alias, and now reads
|
|
1010
|
+
`u (a name the statement gives)`.
|
|
1011
|
+
- **Only a timeout is reported as a timeout.** `rails_query` read any SQL
|
|
1012
|
+
error that mentioned "timeout" as a timeout, so an unknown column named
|
|
1013
|
+
`timeout` came back as one. A timeout is now read from the error's class or
|
|
1014
|
+
the database's native code, and MariaDB's 1969 counts.
|
|
1015
|
+
- **MySQL table sizes count base tables only.** The row estimates already did.
|
|
1016
|
+
- **An `explain: true` failure is an error.** A failing EXPLAIN, or one asked
|
|
1017
|
+
of a statement other than SELECT or WITH, came back as an ordinary answer.
|
|
1018
|
+
The CLI now exits non-zero and an MCP client sees `isError`. Asked of
|
|
1019
|
+
another statement it answered "EXPLAIN only supports SELECT queries." and
|
|
1020
|
+
now answers "`explain: true` takes a SELECT or WITH statement and explains
|
|
1021
|
+
it.", with "The statement is already an EXPLAIN." in front when it is one.
|
|
1022
|
+
- **An SQL error no longer ends in a colon.** Rails ends an error's first line
|
|
1023
|
+
with a colon where the statement follows, and `rails_query` printed it.
|
|
1024
|
+
- **`rails_query` runs a string literal that only reads like a refused
|
|
1025
|
+
phrase.** The `OR 1=1` check is gone: the caller writes the whole statement,
|
|
1026
|
+
so it protected nothing and refused `LIKE '%OR 1=1%'`. On PostgreSQL a
|
|
1027
|
+
locking clause is read with literals masked, since its read-only transaction
|
|
1028
|
+
refuses a locking read, so `'%ready for update%'` runs there. MySQL's
|
|
1029
|
+
read-only transaction allows `FOR SHARE`, so there and on SQLite the raw
|
|
1030
|
+
text still decides.
|
|
1031
|
+
|
|
8
1032
|
## [5.35.0] - 2026-10-12
|
|
9
1033
|
|
|
10
1034
|
### Added
|