Problem
The catalog interceptor decides whether a query is a catalog query by substring-matching the whole query text, including inside SQL string literals (src/catalog/query_interceptor.rs:29 and the lower_query.contains(...) chain below it).
So a query that merely mentions a catalog relation in a literal is misclassified:
CREATE TABLE notes (id INT, msg TEXT);
INSERT INTO notes VALUES (1, 'see pg_catalog.pg_class for details');
SELECT count(*) FROM notes WHERE msg = 'see pg_catalog.pg_class for details';
The row matches and the count is correct, but the result column is typed text instead of int8, so a binary-protocol client doing try_get::<i64> fails.
Any table storing SQL text triggers this — audit logs of executed statements, migration-history tables, documentation or notes tables.
Related: a third blind-replace site
src/session/db_handler.rs:2743-2751 still does blind String::replace of schema qualifiers over the whole query text, the same defect #103 fixed in SchemaPrefixTranslator. It was left alone there as out of scope.
#103 added replace_outside_literals (case-insensitive, skips '...' literals with the doubled-quote escape and "..." quoted identifiers, single left-to-right scan, verified linear and UTF-8 safe). That helper is directly reusable here.
Suggested fix
Make the classification gate literal-aware, and switch the db_handler.rs site to replace_outside_literals. A cheap version of the former: strip string literals into a scratch buffer once, match the gate against that, and execute the original text.
Note the two are the same root cause — "we pattern-match SQL as an opaque string" — and are worth fixing together.
Also worth folding in
count(*) over pg_class is still typed text rather than int8 (src/query/extended.rs:2422). #103 added an Expr::Function arm that types it correctly, but scoped it with query.contains("information_schema"). Review confirmed empirically that the value encoder follows the RowDescription OID, so dropping that scope would fix pg_class too rather than break it — it was left narrow only to limit that change's blast radius.
Severity
Not urgent — this produces a wrong type OID, not a wrong value. Before #103 the same query class produced a genuinely wrong answer (the blind replace mangled the stored literal so the WHERE matched nothing); that part is already fixed.
Split out of #88 / #103.
Problem
The catalog interceptor decides whether a query is a catalog query by substring-matching the whole query text, including inside SQL string literals (
src/catalog/query_interceptor.rs:29and thelower_query.contains(...)chain below it).So a query that merely mentions a catalog relation in a literal is misclassified:
The row matches and the count is correct, but the result column is typed
textinstead ofint8, so a binary-protocol client doingtry_get::<i64>fails.Any table storing SQL text triggers this — audit logs of executed statements, migration-history tables, documentation or notes tables.
Related: a third blind-replace site
src/session/db_handler.rs:2743-2751still does blindString::replaceof schema qualifiers over the whole query text, the same defect #103 fixed inSchemaPrefixTranslator. It was left alone there as out of scope.#103addedreplace_outside_literals(case-insensitive, skips'...'literals with the doubled-quote escape and"..."quoted identifiers, single left-to-right scan, verified linear and UTF-8 safe). That helper is directly reusable here.Suggested fix
Make the classification gate literal-aware, and switch the
db_handler.rssite toreplace_outside_literals. A cheap version of the former: strip string literals into a scratch buffer once, match the gate against that, and execute the original text.Note the two are the same root cause — "we pattern-match SQL as an opaque string" — and are worth fixing together.
Also worth folding in
count(*)overpg_classis still typedtextrather thanint8(src/query/extended.rs:2422). #103 added anExpr::Functionarm that types it correctly, but scoped it withquery.contains("information_schema"). Review confirmed empirically that the value encoder follows theRowDescriptionOID, so dropping that scope would fixpg_classtoo rather than break it — it was left narrow only to limit that change's blast radius.Severity
Not urgent — this produces a wrong type OID, not a wrong value. Before #103 the same query class produced a genuinely wrong answer (the blind replace mangled the stored literal so the
WHEREmatched nothing); that part is already fixed.Split out of #88 / #103.