elixir-ecto / myxql Goto Github PK
View Code? Open in Web Editor NEWMySQL 5.5+ driver for Elixir
License: Apache License 2.0
MySQL 5.5+ driver for Elixir
License: Apache License 2.0
Mirror Postgrex.
Currently we store prepared statements ref => statement id
mapping on a map in connection state. By moving it to ets table we'll avoid copying data between client and connection process.
Out of curiosity, why does this repo exist? Is there a problem with Mariaex?
Currently we only return integer error code e.g.: 1062
for uniqueness constraint violation, we should return an atom as well: :er_dup_entry
[1]. Need to investigate how often error list changes.
[1]
https://dev.mysql.com/doc/refman/5.5/en/server-error-reference.html#error_er_dup_entry
Mirror Postgrex.
I found that my app is talking to mysql via ProxySQL, myxql could not connect to the database, while mariaex could.
ecto://username:[email protected]:3136/dbname?pool_size=15
Deps
Output of SHOW VARIABLES LIKE "%version%";
from Sequel Pro
innodb_version 8.0.11
protocol_version 10
slave_type_conversions
tls_version TLSv1,TLSv1.1,TLSv1.2
version 8.0.11
version_comment MySQL Community Server - GPL
version_compile_machine x86_64
version_compile_os Linux
version_compile_zlib 1.2.11
When connected with mysql CLI:
Welcome to the MySQL monitor. Commands end with ; or \g.
Server version: 8.0.11 (ProxySQL)
Error:
10:55:04.639 [error] MyXQL.Connection (#PID<0.361.0>) failed to connect: ** (MyXQL.Error) MyXQL requires MySQL server 5.7.10+
If I made the app directly talk to mysql slave (not via ProxySQL), it works well :)
I'm not sure whether it's our ProxySQL configuration issue, or ProxySQL actually does not support some features. Still investigating..
Please consider changing the default character set here:
character_sets = %{
utf8_general_ci: 0x21
}
to:
character_sets = %{
utf8mb4_general_ci: 0x2d
}
Otherwise characters off the basic multilingual plane (e.g., emoticons and many Asian characters) will, depending on the SQL mode, either silently be discarded or error:
> MyXQL.query(pid, "CREATE TABLE t (i INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, c CHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci)")
{:ok,
%MyXQL.Result{
columns: nil,
connection_id: 5,
last_insert_id: 0,
num_rows: 0,
num_warnings: 0,
rows: nil
}}
> MyXQL.query(pid, "select @@character_set_results, @@character_set_client, @@character_set_connection;")
{:ok,
%MyXQL.Result{
columns: ["@@character_set_results", "@@character_set_client",
"@@character_set_connection"],
connection_id: 5,
last_insert_id: nil,
num_rows: 1,
num_warnings: 0,
rows: [["utf8", "utf8", "utf8"]]
}}
> MyXQL.query(pid, "set session sql_mode = '';")
{:ok,
%MyXQL.Result{
columns: nil,
connection_id: 5,
last_insert_id: 0,
num_rows: 0,
num_warnings: 1,
rows: nil
}}
> MyXQL.query(pid, "insert into t (c) values (?);", ["!!!๐!!!"])
{:ok,
%MyXQL.Result{
columns: nil,
connection_id: 5,
last_insert_id: 1,
num_rows: 1,
num_warnings: 1,
rows: nil
}}
> MyXQL.query(pid, "select * from t where i = ?;", [1])
{:ok,
%MyXQL.Result{
columns: ["i", "c"],
connection_id: 5,
last_insert_id: nil,
num_rows: 1,
num_warnings: 0,
rows: [[1, "!!!????!!!"]] # <--- No emoticon
}}
> MyXQL.query(pid, "set session sql_mode = 'TRADITIONAL';")
{:ok,
%MyXQL.Result{
columns: nil,
connection_id: 5,
last_insert_id: 0,
num_rows: 0,
num_warnings: 1,
rows: nil
}}
> MyXQL.query(pid, "insert into t (c) values (?);", ["!!!๐!!!"])
{:error,
%MyXQL.Error{
connection_id: 5,
message: "(1366) (ER_TRUNCATED_WRONG_VALUE_FOR_FIELD) Incorrect string value: '\\xF0\\x9F\\x98\\x83!!...' for column 'c' at row 1",
mysql: %{code: 1366, name: :ER_TRUNCATED_WRONG_VALUE_FOR_FIELD},
statement: "insert into t (c) values (?);"
}}
Here's what that sequence looks like after making the change:
> MyXQL.query(pid, "select @@character_set_results, @@character_set_client, @@character_set_connection;")
{:ok,
%MyXQL.Result{
columns: ["@@character_set_results", "@@character_set_client",
"@@character_set_connection"],
connection_id: 6,
last_insert_id: nil,
num_rows: 1,
num_warnings: 0,
rows: [["utf8mb4", "utf8mb4", "utf8mb4"]]
}}
> MyXQL.query(pid, "insert into t (c) values (?);", ["!!!๐!!!"])
{:ok,
%MyXQL.Result{
columns: nil,
connection_id: 6,
last_insert_id: 2,
num_rows: 1,
num_warnings: 0,
rows: nil
}}
> MyXQL.query(pid, "select * from t where i = ?;", [2])
{:ok,
%MyXQL.Result{
columns: ["i", "c"],
connection_id: 6,
last_insert_id: nil,
num_rows: 1,
num_warnings: 0,
rows: [[2, "!!!๐!!!"]]
}}
(The SQL mode no longer matters since there's no longer a problem.)
In the meantime a workaround (or an alternative approach if you don't think utf8mb4
should be the default) is to issue this query after connecting:
"SET NAMES utf8mb4;"
Currently, we send "ROLLBACK TO SAVEPOINT" and "RELEASE SAVEPOINT" as separate queries as we don't fully support multiple statements yet.
Its highly unlikely as servers should guarantee it, but it should be relatively easy to do in recv_packets/3
by looking at packet sequence ids.
MySQL allows us to make this work:
[
%MySQL.Result{rows: [[1]]},
%MySQL.Result{rows: [[2]]}
] = MyXQL.query_multi!(c.conn, "SELECT 1; SELECT 2")
Thus, we could do "INSERT INTO this (...); INSERT INTO that (...)"
, so we could even replace calling mysql
CLI when running migrations. This is only possible using the text protocol.
See also #10 (comment).
Amazon Aurora Serverless only supports MySQL 5.6. What's the key issue preventing 5.6 compatibility? (I'm trying to figure out whether or not adding compatibility is within my reach.)
Currently, we only handle them in MyXQL.stream.
describe "stored procedures" do
setup [:connect, :truncate]
test "multi-resultset in text protocol", c do
MyXQL.query!(c.conn, "CALL multi();", [])
|> IO.inspect()
end
test "multi-resultset in binary protocol", c do
MyXQL.query!(c.conn, "CALL multi();", [])
|> IO.inspect()
end
end
The code here assumes that the server will always send an initial handshake packet, but as documented here the server might send an ERR packet instead.
As the page notes, an initial ERR packet will be sent before capabilities are negotiated and therefore won't include the conditional server_state
fields.
Currently, both timestamp
and datetime
are represented as NaiveDateTime
however DateTime
however when encoding them we can use DateTime
too.
This change will be closer to Postgrex's semantics (timestamptz
represented as DateTime
) however it would be kinda confusing that timestamp <> DateTime, datetime <> NaiveDateTime
.
I have a table that stores a mediumblob
in mysql 5.7.25 and it is blowing up when I try to store a larger erlang_term
directly into the DB.
** (FunctionClauseError) no function clause matching in MyXQL.Protocol.Values.decode_string_lenenc/4
The following arguments were given to MyXQL.Protocol.Values.decode_string_lenenc/4:
# 1
<<253, 71, 60, 1, 131, 116, 0, 0, 0, 5, 100, 0, 10, 95, 95, 115, 116, 114, 117, 99, 116, 95, 95, 100, 0, 24, 69, 108, 105, 120, 105, 114, 46, 83, 102, 83, 101, 114, 118, 105, 99, 101, 115, 46, 83, 99, 104, 101, 109, 97, ...>>
# 2
0
# 3
[:uint8, :binary]
# 4
["I-9 (Sample Workflow)", "hg11ZxuZwBGiCV57", ~N[2019-04-04 17:20:25], ~N[2019-04-04 17:20:25], 761]
Attempted function clauses (showing 4 out of 4):
defp decode_string_lenenc(<<n::integer()-size(8), v::binary()-size(n), r::bitstring()>>, null_bitmap, t, acc) when n < 251
defp decode_string_lenenc(<<252::integer(), n::integer()-little()-size(16), v::binary()-size(n), r::bitstring()>>, null_bitmap, t, acc)
defp decode_string_lenenc(<<253::integer(), n::integer()-little()-size(32), v::binary()-size(n), r::bitstring()>>, null_bitmap, t, acc)
defp decode_string_lenenc(<<254::integer(), n::integer()-little()-size(64), v::binary()-size(n), r::bitstring()>>, null_bitmap, t, acc)
code: assert :ok == DraftServices.Seeder.seed_team(team_guid)
stacktrace:
(myxql) lib/myxql/protocol/values.ex:480: MyXQL.Protocol.Values.decode_string_lenenc/4
(myxql) lib/myxql/protocol/messages.ex:400: MyXQL.Protocol.Messages.decode_resultset/4
(myxql) lib/myxql/client.ex:167: MyXQL.Client.recv_packets/5
(myxql) lib/myxql/connection.ex:95: MyXQL.Connection.handle_execute/4
(ecto_sql) lib/ecto/adapters/sql/sandbox.ex:369: Ecto.Adapters.SQL.Sandbox.Connection.proxy/3
(db_connection) lib/db_connection/holder.ex:277: DBConnection.Holder.holder_apply/4
(db_connection) lib/db_connection.ex:1255: DBConnection.run_execute/5
(db_connection) lib/db_connection.ex:1342: DBConnection.run/6
(db_connection) lib/db_connection.ex:540: DBConnection.parsed_prepare_execute/5
(db_connection) lib/db_connection.ex:533: DBConnection.prepare_execute/4
(myxql) lib/myxql.ex:199: MyXQL.query/4
(ecto_sql) lib/ecto/adapters/sql.ex:571: Ecto.Adapters.SQL.execute!/4
(ecto_sql) lib/ecto/adapters/sql.ex:538: Ecto.Adapters.SQL.execute/5
(ecto) lib/ecto/repo/queryable.ex:147: Ecto.Repo.Queryable.execute/4
(ecto) lib/ecto/repo/queryable.ex:18: Ecto.Repo.Queryable.all/3
I've been digging around to try and find some more context, but I'm not very strong on this topic. Please let me know if there's something that I can do to help, I'm not unwilling.
Here is the code that decodes initial handshake packets:
def decode_initial_handshake(payload) do
protocol_version = 10
<<^protocol_version, rest::binary>> = payload
{server_version, rest} = take_string_nul(rest)
<<
conn_id::uint4,
auth_plugin_data1::string(8),
0,
capability_flags1::uint2,
character_set::uint1,
status_flags::uint2,
capability_flags2::uint2,
auth_plugin_data_length::uint1,
_::uint(10),
rest::binary
>> = rest
take = max(13, auth_plugin_data_length - 8)
<<auth_plugin_data2::binary-size(take), auth_plugin_name::binary>> = rest
auth_plugin_data2 = decode_string_nul(auth_plugin_data2)
auth_plugin_name = decode_string_nul(auth_plugin_name)
<<capability_flags::uint4>> = <<capability_flags1::uint2, capability_flags2::uint2>>
auth_plugin_data = auth_plugin_data1 <> auth_plugin_data2
initial_handshake(
server_version: server_version,
conn_id: conn_id,
auth_plugin_name: auth_plugin_name,
auth_plugin_data: auth_plugin_data,
capability_flags: capability_flags,
character_set: character_set,
status_flags: status_flags
)
end
Here is an outline of the packet structure:
...
if more data in the packet:
...
if capabilities & CLIENT_PLUGIN_AUTH {
...
} else {
...
}
...
if capabilities & CLIENT_SECURE_CONNECTION {
...
if capabilities & CLIENT_PLUGIN_AUTH {
...
}
}
Notice that decoding doesn't check the capability flags the server just sent. (Imagine, for example, connecting to an older server.)
Other decoding follows the same pattern. For example, OK packet decoding implicitly assumes CLIENT_PROTOCOL_41
and !CLIENT_SESSION_TRACK
. (I understand that the current code implements a specific set of client capabilities, but ultimately the client and server have to negotiate and agree; I don't think you want to just assume client capabilities everywhere.)
According to the docs:
The client should only announce the capabilities in the Handshake Response Packet that it has in common with the server.
In effect, in the response packet the client should AND
its capabilities with the server's.
Instead, here, the client is just announcing its own capabilities.
Currently the timeout is :infinity
and cannot be configured. Since socket is always in passive mode this is OK for operations in client process, not OK for handshake which is connection process and it's actually not OK for ping because it's in connection process too.
Note it'd be an option on MyXQL.start_link
, similar to Postgrex and Mariaex.
@josevalim @fishcakez should we mirror Postgrex in calling this :timeout
? We have the following there:
:timeout - Socket receive timeout when idle in milliseconds (default: 15000);
FWIW Mariaex has:
:timeout - Connect timeout in milliseconds (default: 15000);
Or maybe we should have something more explicit like :idle_timeout
(not to be confused with :idle_interval
from dbconnection!)
I've decided to check how MyXQL+Ecto work with my staging DB. Error below appears each time I'm trying to select, via Repo.get, from any table. There are no errors when using Ecto.Adapters.MySQL
(while still using wojtekmach/ecto_sql
).
{:myxql, "~> 0.1"},
{:mariaex, ">= 0.0.0"},
{:ecto_sql, github: "wojtekmach/ecto_sql", branch: "wm-myxql", override: true}
adapter: Ecto.Adapters.MyXQL
Create a simple table and schema
sql
CREATE TABLE `dummy` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
defmodule Importer.Identity.Dummy do
use Ecto.Schema
schema "dummy" do
field :name, :string
end
end
... and add some records
Run:
Importer.Identity.Dummy |> Importer.Repo.get(1)
-> Error Appears
Switch adapter:
adapter: Ecto.Adapters.MySQL
Run:
Importer.Identity.Dummy |> Importer.Repo.get(1)
-> No error - you get results back
iex> Importer.Identity.User |> Importer.Repo.get(34)
[debug] QUERY ERROR source="phpbb_users" db=112.7ms queue=44.9ms
SELECT p0.`user_id`, p0.`username` FROM `phpbb_users` AS p0 WHERE (p0.`user_id` = ?) [34]
** (MatchError) no match of right hand side value: <<60, 0, 0, 5, 3, 100, 101, 102, 9, 110, 119, 100, 95, 115, 109, 111, 107, 105, 2, 112, 48, 11, 112, 104, 112, 98, 98, 95, 117, 115, 101, 114, 115, 8, 117, 115, 101, 114, 110, 97, 109, 101, 8, 117, 115, 101, 114, 110, 97, 109, ...>>
(myxql) lib/myxql/messages.ex:380: MyXQL.Messages.decode_com_stmt_prepare_response/3
(myxql) lib/myxql/protocol.ex:329: MyXQL.Protocol.recv_packets/4
(myxql) lib/myxql/protocol.ex:682: MyXQL.Protocol.prepare/2
(myxql) lib/myxql/protocol.ex:89: MyXQL.Protocol.handle_prepare/3
(db_connection) lib/db_connection/holder.ex:270: DBConnection.Holder.holder_apply/4
(db_connection) lib/db_connection.ex:1180: DBConnection.prepare/4
(db_connection) lib/db_connection.ex:1173: DBConnection.run_prepare/4
(db_connection) lib/db_connection.ex:1185: DBConnection.run_prepare_execute/5
(db_connection) lib/db_connection.ex:1282: DBConnection.run/6
(db_connection) lib/db_connection.ex:504: DBConnection.parsed_prepare_execute/5
(db_connection) lib/db_connection.ex:497: DBConnection.prepare_execute/4
(ecto_sql) lib/ecto/adapters/sql.ex:546: Ecto.Adapters.SQL.execute!/4
(ecto_sql) lib/ecto/adapters/sql.ex:538: Ecto.Adapters.SQL.execute/5
(ecto) lib/ecto/repo/queryable.ex:147: Ecto.Repo.Queryable.execute/4
(ecto) lib/ecto/repo/queryable.ex:18: Ecto.Repo.Queryable.all/3
(ecto) lib/ecto/repo/queryable.ex:66: Ecto.Repo.Queryable.one/3
Displaying the list with IO.puts
gives me
IO.puts([60, 0, 0, 5, 3, 100, 101, 102, 9, 110, ...)
<^@^@^E^Cdef nwd_smoki^Bp0^Kphpbb_users^Husername^Husernam
where nwd_smoki
is the DB(and DB User) name and phpbb_users
is the table name.
[debug] QUERY OK source="phpbb_users" db=51.5ms decode=16.6ms queue=44.2ms
SELECT p0.`user_id`, p0.`username` FROM `phpbb_users` AS p0 WHERE (p0.`user_id` = ?) [34]
%Importer.Identity.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "phpbb_users">,
user_id: 34,
username: "test"
}
Here the handshake code appears to take the auth_plugin_data
from the initial handshake and pass it into auth_response
. For sha256_password
and caching_sha2_password
, auth_response
then passes that data into sha256_password
here.
As far as I can tell, that isn't right. Here are the docs for sha256_password
. They say:
If the password is non-empty SHA256 auth method
XORs the clients password with the scramble buffers content
RSA encrypts that with the server's public key using RSA_PKCS1_OAEP_PADDING as padding
Empty passwords are not hashed, but sent as empty string.
It appears that the client is either expected to already have the public key or to ask for it. Here the docs explain how to ask for it. When asked the server sends the key in an AuthMoreData
packet; I don't think MyXQL implements that?
The docs have worked examples of sha256_password
logins here.
Currently we use application environment for this configuration, following Mariaex & Postgrex. If we're gonna do this, we could leave a deprecation warning for folks that were moving off Mariaex here.
Thoughts @josevalim?
See: xerions/mariaex#182
TODO:
MySQL error code 1461 (ER_MAX_PREPARED_STMT_COUNT_REACHED)
. This needs double-checking but we expect that when connection is disconnected, the server will clean up these statements. This doesn't solve the unlikely case where there's one rogue connection that took up most/all prepared statements and never made another one (which would force it to disconnect)prepare: :named (default) | :unnamed
connection option. :unnamed
will force query name to be ""
which means it will be closed after executingCurrently it's:
11:51:23.027 [error] MyXQL.Protocol (#PID<0.314.0>) failed to connect: ** (MyXQL.Error) no such file or directory
it isn't clear what's the file we attempted to use.
Set unsigned_flag
in com_stmt_execute
, add tests to types_test.exs
This way DBConnection.LogEntry.decode_time will accurately represent how much time was spent on that.
Mirror Postgrex.
Note: test with Utf8mb4 and emojis
The code here ignores rather than verifies the sequence_id
.
Currently, MyXQL defaults to protocol: :socket
(UNIX domain socket), which should yield better performance than going over tcp.
However, I noticed the default recbuf is much smaller:
iex> {:ok, sock} = :gen_tcp.connect('localhost', 3306, [:binary, packet: :raw, active: false])
iex> :inet.getopts(sock, [:buffer, :sndbuf, :recbuf])
{:ok, [buffer: 1460, sndbuf: 146988, recbuf: 408300]}
iex> {:ok, sock} = :gen_tcp.connect({:local, "/tmp/mysql.sock"}, 0, [:binary, packet: :raw, active: false])
iex> :inet.getopts(sock, [:buffer, :sndbuf, :recbuf])
{:ok, [buffer: 1460, sndbuf: 8192, recbuf: 8192]}
which yields worse performance on bigger datasets compared to connecting over tcp.
(Btw, we do the same trick as Postgrex & Mariaex when we set internal socket buffer based on sndbuf/recbuf: 384b9ba)
@josevalim @fishcakez do you have any ideas why domain socket starts with smaller buffers? From what I could read, sndbuf and rebuf set by VM should be a reasonable default. Should we try to bump them anyway? Or default to connecting over TCP like Postgrex and Mariaex does?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.