Add placeholder option to insert_all#290
Conversation
|
Hi @Un3qual! I have added some comments on the ecto PR which I think will simplify this PR. Some other notes:
|
| {%Ecto.Query{} = query, params_counter}, counter -> | ||
| {[?(, all(query), ?)], counter + params_counter} | ||
|
|
||
| {:placeholder, placeholder_key}, counter -> |
There was a problem hiding this comment.
On unzip_inserts, make it so this is passed as {:placeholder, placeholder_index}, this way you don't need to pass the placeholder_index_map to this function.
|
@josevalim I've made the suggested improvements and a few others, namely ensuring when a placeholder value is dumped as one type, its key can't be used with a column of another type because it can cause unexpected behavior. I'm just stuck on this last MyXQL test: https://github.com/elixir-ecto/ecto_sql/pull/290/checks?check_run_id=1583183874. The first failure looks like it might be an issue with the decimal library (4.00000000000 should be equal to 4.0 right?). The second and third I have no idea. I don't think my changes should be changing the values of anything that isn't a placeholder. Any ideas? |
| end | ||
|
|
||
| def insert(_prefix, _table, _header, _rows, _on_conflict, _returning, _counter_start) do | ||
| error!(nil, "The :placeholders is not supported by MySQL") |
There was a problem hiding this comment.
| error!(nil, "The :placeholders is not supported by MySQL") | |
| error!(nil, ":placeholders is not supported by MySQL") |
|
|
||
| @impl true | ||
| def insert(prefix, table, header, rows, on_conflict, returning) do | ||
| def insert(prefix, table, header, rows, on_conflict, returning, counter_start \\ 1) do |
There was a problem hiding this comment.
I think we don't need the \\. We should remove it and make sure everything passes it as necessary. :)
lib/ecto/adapters/sql.ex
Outdated
| |> Enum.map(fn {ph_key, ph_idx} -> | ||
| {ph_idx, Map.fetch!(placeholder_map, ph_key)} | ||
| end) | ||
| |> List.keysort(0) |
There was a problem hiding this comment.
Enum.sort should be more efficient and enough since the first tuple element will be the first to sort on:
| |> List.keysort(0) | |
| |> Enum.sort() |
lib/ecto/adapters/sql.ex
Outdated
| |> List.keysort(0) | ||
| |> Enum.map(&elem(&1, 1)) | ||
|
|
||
| all_params = placeholder_values ++ Enum.reverse(params) ++ conflict_params |
There was a problem hiding this comment.
Small optimization we didn't apply before:
| all_params = placeholder_values ++ Enum.reverse(params) ++ conflict_params | |
| all_params = placeholder_values ++ Enum.reverse(params, conflict_params) |
lib/ecto/adapters/sql.ex
Outdated
| {{query, length(query_params)}, Enum.reverse(query_params) ++ acc} | ||
| { | ||
| {query, length(query_params)}, | ||
| {Enum.reverse(query_params) ++ values_acc, placeholder_idx_acc, counter} |
There was a problem hiding this comment.
| {Enum.reverse(query_params) ++ values_acc, placeholder_idx_acc, counter} | |
| {Enum.reverse(query_params, values_acc), placeholder_idx_acc, counter} |
lib/ecto/adapters/sql.ex
Outdated
| end | ||
| end | ||
|
|
||
|
|
|
@josevalim Got all of the tests (except for mysql 8.0) passing and all of your suggestions implemented. |
|
|
||
| defp insert(prefx, table, header, rows, on_conflict, returning) do | ||
| IO.iodata_to_binary SQL.insert(prefx, table, header, rows, on_conflict, returning) | ||
| IO.iodata_to_binary SQL.insert(prefx, table, header, rows, on_conflict, returning, []) |
There was a problem hiding this comment.
Let's also add that checks the proper references are used and counted correctly for placeholders!
There was a problem hiding this comment.
Both in postgres and tds suites!
|
Thank you, this looks great and we are almost there, just some tests. Also please update mix.exs to point to ecto master git repo as the other PR has been merged. :) happy new year! |
|
💚 💙 💜 💛 ❤️ |
|
Merged and I will push the pending tests, thank you. |
|
@josevalim thanks for getting this merged. Sorry i've been MIA. I did some benchmarks with bulk inserts and placeholders actually ended up being slightly slower, but they were very unscientific. The slight slowdown might be worth the saved bandwidth though |
A few days ago, I created this post on Elixir Forum. This PR adds support for "placeholders" in the insert_all operation. It can save bandwidth by allowing you to send less data over the wire. It may also increase performance by decreasing how much you have to typecheck before you insert.
Example
Before
results in the SQL:
with these args passed in:
After
results in the SQL:
with these args passed in:
Implementation
So far, I have only implemented this for postgres, but doing so for tds and myxql shouldn't be too difficult. I just want to get the implementation details hashed out first. This could also be extended to be used for update_all in addition to insert_all, and could be useful elsewhere as well. TDD is not my strong point, so I've just written three simple tests so far.
When fetching from the map of placeholders, I use
Map.fetch!/2right now. I'm not sure what function is preferred for stuff like this.👍s
👎s
Ecto.Adapters.SQL.unzip_inserts/2is considerably more complicated than beforeOther possibilities
This could be implemented in other ways. There could just be a
dedupoption that can be provided to insert_all and update_all, where the list of fields is deduped automatically, but that seems computationally expensive especially in the cases where this feature is most useful (large bulk inserts).Changes to Ecto
This feature requires changes to elixir-ecto/ecto as well for tests to pass. The PR for that is elixir-ecto/ecto#3515. I'm not sure how to link two seperate PRs though without creating a circular dependency issue (this needs the ecto changes for its tests to pass). If someone has some input on that, I would appreciate it.