-
Notifications
You must be signed in to change notification settings - Fork 346
Keep cache statement names within PostgreSQL's 63-byte limit #751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -987,7 +987,7 @@ defmodule Ecto.Adapters.SQL do | |||||||||||||
|
|
||||||||||||||
| opts = | ||||||||||||||
| if is_nil(Keyword.get(opts, :cache_statement)) do | ||||||||||||||
| [{:cache_statement, "ecto_insert_all_#{source}"} | opts] | ||||||||||||||
| [{:cache_statement, cache_statement_name("ecto_insert_all_", source)} | opts] | ||||||||||||||
| else | ||||||||||||||
| opts | ||||||||||||||
| end | ||||||||||||||
|
|
@@ -1171,6 +1171,36 @@ defmodule Ecto.Adapters.SQL do | |||||||||||||
| end | ||||||||||||||
| end | ||||||||||||||
|
|
||||||||||||||
| # PostgreSQL silently truncates prepared statement names to NAMEDATALEN - 1 | ||||||||||||||
| # (63 bytes). Two sources that only differ after that point would share one | ||||||||||||||
| # server-side statement while the driver caches them under distinct client | ||||||||||||||
| # names, so the next cached execution binds against the other statement and | ||||||||||||||
| # fails with "bind message supplies N parameters, but prepared statement | ||||||||||||||
| # requires M". Keep names within the limit, using a hash to tell them apart. | ||||||||||||||
| @max_cache_statement_name_size 63 | ||||||||||||||
|
|
||||||||||||||
| @doc false | ||||||||||||||
| def cache_statement_name(prefix, source, suffix \\ "") do | ||||||||||||||
| source = to_string(source) | ||||||||||||||
| name = prefix <> source <> suffix | ||||||||||||||
|
|
||||||||||||||
| if byte_size(name) <= @max_cache_statement_name_size do | ||||||||||||||
| name | ||||||||||||||
| else | ||||||||||||||
| hash = source |> :erlang.phash2(4_294_967_296) |> Integer.to_string(36) | ||||||||||||||
| 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 | ||||||||||||||
|
Comment on lines
+1191
to
+1193
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to trim or add _, it is more important to be faster than have pretty names:
Suggested change
|
||||||||||||||
| end | ||||||||||||||
| end | ||||||||||||||
|
|
||||||||||||||
| defp truncate_utf8(string, size) when byte_size(string) <= size, do: string | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of truncating utf8, you can traverse the string and collect the first n-valid ascii bytes. It should be cheaper to implement. |
||||||||||||||
|
|
||||||||||||||
| defp truncate_utf8(string, size) do | ||||||||||||||
| truncated = binary_part(string, 0, size) | ||||||||||||||
| if String.valid?(truncated), do: truncated, else: truncate_utf8(string, size - 1) | ||||||||||||||
| end | ||||||||||||||
|
|
||||||||||||||
| @doc false | ||||||||||||||
| def struct( | ||||||||||||||
| adapter_meta, | ||||||||||||||
|
|
@@ -1186,7 +1216,8 @@ defmodule Ecto.Adapters.SQL do | |||||||||||||
| ) do | ||||||||||||||
| opts = | ||||||||||||||
| if is_nil(Keyword.get(opts, :cache_statement)) do | ||||||||||||||
| [{:cache_statement, "ecto_#{operation}_#{source}_#{length(params)}"} | opts] | ||||||||||||||
| name = cache_statement_name("ecto_#{operation}_", source, "_#{length(params)}") | ||||||||||||||
| [{:cache_statement, name} | opts] | ||||||||||||||
| else | ||||||||||||||
| opts | ||||||||||||||
| end | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| defmodule Ecto.Adapters.SQLTest do | ||
| use ExUnit.Case, async: true | ||
|
|
||
| import Ecto.Adapters.SQL, only: [cache_statement_name: 2, cache_statement_name: 3] | ||
|
|
||
| describe "cache_statement_name/3" do | ||
| test "keeps names within the limit untouched" do | ||
| assert cache_statement_name("ecto_insert_", "posts", "_0") == "ecto_insert_posts_0" | ||
| assert cache_statement_name("ecto_insert_all_", "posts") == "ecto_insert_all_posts" | ||
| assert cache_statement_name("ecto_insert_", :posts, "_0") == "ecto_insert_posts_0" | ||
|
|
||
| exactly_63 = String.duplicate("a", 63 - byte_size("ecto_insert__0")) | ||
| assert byte_size(cache_statement_name("ecto_insert_", exactly_63, "_0")) == 63 | ||
|
|
||
| assert cache_statement_name("ecto_insert_", exactly_63, "_0") == | ||
| "ecto_insert_#{exactly_63}_0" | ||
| end | ||
|
|
||
| test "caps long names at 63 bytes while keeping sources distinct" do | ||
| # These two differ only after PostgreSQL's 63-byte truncation point. | ||
| a = | ||
| cache_statement_name( | ||
| "ecto_insert_", | ||
| "business_workplace_attendance_leave_comp_rest_minutes", | ||
| "_0" | ||
| ) | ||
|
|
||
| b = | ||
| cache_statement_name( | ||
| "ecto_insert_", | ||
| "business_workplace_attendance_leave_comp_rest_minutes_event_logs", | ||
| "_0" | ||
| ) | ||
|
|
||
| assert byte_size(a) <= 63 | ||
| assert byte_size(b) <= 63 | ||
| assert a != b | ||
| assert a =~ ~r/^ecto_insert_business_workplace_attendance_leave_comp_[0-9A-Z]+_0$/ | ||
| assert String.ends_with?(a, "_0") | ||
| assert String.ends_with?(b, "_0") | ||
| end | ||
|
|
||
| test "does not split multibyte characters when truncating" do | ||
| name = cache_statement_name("ecto_insert_", String.duplicate("é", 40), "_0") | ||
| assert byte_size(name) <= 63 | ||
| assert String.valid?(name) | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the phash2 explicit limit?