| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
PostgreSQL silently truncates prepared statement names to NAMEDATALEN - 1
(63 bytes). The default names `ecto_<op>_<source>_<n>` and
`ecto_insert_all_<source>` exceed that for long table names, so two sources
that only differ after the 63rd byte (e.g. `..._leave_comp_rest_minutes` and
`..._leave_comp_rest_minutes_event_logs`) map to a single server-side
statement while Postgrex caches them under distinct client names. Once the
second table has been prepared on a connection, the next cached execution of
the first binds against the wrong statement:
ERROR 08P01 (protocol_violation) bind message supplies 8 parameters,
but prepared statement "ecto_insert_..." requires 5
and, since that error does not evict the client cache, the connection keeps
failing until it reconnects.
Cap generated names at 63 bytes: names that already fit are unchanged; longer
ones keep a prefix of the source and add a hash of the full source so they
stay distinct within the limit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| if byte_size(name) <= @max_cache_statement_name_size do | ||
| name | ||
| else | ||
| hash = source |> :erlang.phash2(4_294_967_296) |> Integer.to_string(36) |
There was a problem hiding this comment.
Why the phash2 explicit limit?
Sorry, something went wrong.
| budget = @max_cache_statement_name_size - byte_size(prefix <> suffix <> hash) - 1 | ||
| kept = source |> truncate_utf8(max(budget, 0)) |> String.trim_trailing("_") | ||
| prefix <> kept <> "_" <> hash <> suffix |
There was a problem hiding this comment.
No need to trim or add _, it is more important to be faster than have pretty names:
| budget = @max_cache_statement_name_size - byte_size(prefix <> suffix <> hash) - 1 | |
| kept = source |> truncate_utf8(max(budget, 0)) |> String.trim_trailing("_") | |
| prefix <> kept <> "_" <> hash <> suffix | |
| budget = @max_cache_statement_name_size - byte_size(prefix <> suffix <> hash) | |
| kept = source |> truncate_utf8(max(budget, 0)) | |
| prefix <> kept <> hash <> suffix |
Sorry, something went wrong.
| end | ||
| end | ||
|
|
||
| defp truncate_utf8(string, size) when byte_size(string) <= size, do: string |
There was a problem hiding this comment.
Instead of truncating utf8, you can traverse the string and collect the first n-valid ascii bytes. It should be cheaper to implement.
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
PostgreSQL silently truncates prepared statement names to NAMEDATALEN - 1 (63 bytes) — the statement hash table is keyed with HASH_STRINGS at NAMEDATALEN, so only the first 63 bytes of a name are compared (pgsql-bugs discussion; the application is expected to avoid it).
The default cache_statement names (ecto_<op>_<source>_<n>, ecto_insert_all_<source>) exceed that for long table names, so two sources that only differ after the 63rd byte map to one server-side statement while Postgrex caches them under distinct client names. On one connection:
This is what we hit in production; the new integration test (integration_test/pg/prepare_test.exs) reproduces it on master and passes with this change.
Change
Ecto.Adapters.SQL.cache_statement_name/3 caps the generated name at 63 bytes. Names that already fit are returned byte-for-byte unchanged, so the vast majority of applications see no difference. Longer names keep a (UTF-8 safe) prefix of the source and append :erlang.phash2/2 of the full source in base 36, e.g.
The cap is applied in the shared SQL layer rather than only in the Postgres adapter: MySQL/TDS don't have the limit, but shorter names are harmless there and it keeps one code path.
Tests
🤖 Generated with Claude Code