changed
CHANGELOG.md
|
@@ -1,5 +1,19 @@
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+ ## v0.2.3 (2019-05-23)
|
4
|
+
|
5
|
+ ### Enhancements
|
6
|
+
|
7
|
+ * Default charset to utf8mb4
|
8
|
+ * Raise error when server does not support required capabilities
|
9
|
+ * Implement public key exchange for sha auth methods
|
10
|
+ * Support older MySQL versions (tested against 5.5 and 5.6)
|
11
|
+ * Change `MyXQL.start_option/0` to use `:ssl.tls_client_option/0` type
|
12
|
+
|
13
|
+ ### Bug fixes
|
14
|
+
|
15
|
+ * Handle error packet on handshake
|
16
|
+
|
3
17
|
## v0.2.2 (2019-04-05)
|
4
18
|
|
5
19
|
### Bug fixes
|
changed
README.md
|
@@ -10,7 +10,11 @@ Documentation: <https://hexdocs.pm/myxql>
|
10
10
|
|
11
11
|
* Automatic decoding and encoding of Elixir values to and from MySQL text and binary protocols
|
12
12
|
* Supports transactions, prepared queries, streaming, pooling and more via [DBConnection](https://github.com/elixir-ecto/db_connection)
|
13
|
- * Supports MySQL 5.7.10+, 8.0, and MariaDB 10.3
|
13
|
+ * Supports MySQL 5.5+, 8.0, and MariaDB 10.3
|
14
|
+ * Supports `mysql_native_password`, `sha256_password` (\*), and `caching_sha256_password` (\*)
|
15
|
+ authentication plugins
|
16
|
+
|
17
|
+ \* Requires either SSL connection or the server must exchange it's public key during handshake.
|
14
18
|
|
15
19
|
## Usage
|
16
20
|
|
|
@@ -19,7 +23,7 @@ Add `:myxql` to your dependencies:
|
19
23
|
```elixir
|
20
24
|
def deps() do
|
21
25
|
[
|
22
|
- {:myxql, "~> 0.1.1"}
|
26
|
+ {:myxql, "~> 0.2.0"}
|
23
27
|
]
|
24
28
|
end
|
25
29
|
```
|
|
@@ -31,18 +35,20 @@ iex> MyXQL.query!(pid, "CREATE DATABASE IF NOT EXISTS blog")
|
31
35
|
iex> {:ok, pid} = MyXQL.start_link(username: "root", database: "blog")
|
32
36
|
iex> MyXQL.query!(pid, "CREATE TABLE posts IF NOT EXISTS (id serial primary key, title text)")
|
33
37
|
|
34
|
- iex> MyXQL.query(pid, "INSERT INTO posts (`title`) VALUES ('Post 1')")
|
35
|
- {:ok, %MyXQL.Result{columns: nil, last_insert_id: 1, num_rows: 1, rows: nil}}
|
38
|
+ iex> MyXQL.query!(pid, "INSERT INTO posts (`title`) VALUES ('Post 1')")
|
39
|
+ %MyXQL.Result{columns: nil, connection_id: 11204,, last_insert_id: 1, num_rows: 1, num_warnings: 0, rows: nil}
|
36
40
|
|
37
41
|
iex> MyXQL.query(pid, "INSERT INTO posts (`title`) VALUES (?), (?)", ["Post 2", "Post 3"])
|
38
|
- {:ok, %MyXQL.Result{columns: [], last_insert_id: 3, num_rows: 2, rows: nil}}
|
42
|
+ %MyXQL.Result{columns: nil, connection_id: 11204, last_insert_id: 2, num_rows: 2, num_warnings: 0, rows: nil}
|
39
43
|
|
40
44
|
iex> MyXQL.query(pid, "SELECT * FROM posts")
|
41
45
|
{:ok,
|
42
46
|
%MyXQL.Result{
|
43
47
|
columns: ["id", "title"],
|
48
|
+ connection_id: 11204,
|
44
49
|
last_insert_id: nil,
|
45
50
|
num_rows: 3,
|
51
|
+ num_warnings: 0,
|
46
52
|
rows: [[1, "Post 1"], [2, "Post 2"], [3, "Post 3"]]
|
47
53
|
}}
|
48
54
|
```
|
|
@@ -63,6 +69,8 @@ defmodule MyApp.Application do
|
63
69
|
end
|
64
70
|
```
|
65
71
|
|
72
|
+ and then we can refer to it by it's `:name`:
|
73
|
+
|
66
74
|
```elixir
|
67
75
|
iex> MyXQL.query!(:myxql, "SELECT NOW()").rows
|
68
76
|
[[~N[2018-12-28 13:42:31]]]
|
|
@@ -70,7 +78,7 @@ iex> MyXQL.query!(:myxql, "SELECT NOW()").rows
|
70
78
|
|
71
79
|
## Mariaex Compatibility
|
72
80
|
|
73
|
- See [Mariaex Compatibility](./MARIAEX_COMPATIBILITY.md) page for transition between drivers.
|
81
|
+ See [Mariaex Compatibility](https://github.com/elixir-ecto/myxql/blob/master/MARIAEX_COMPATIBILITY.md) page for transition between drivers.
|
74
82
|
|
75
83
|
## Data representation
|
76
84
|
|
|
@@ -120,7 +128,7 @@ mix deps.get
|
120
128
|
mix test
|
121
129
|
```
|
122
130
|
|
123
|
- See [`ci.sh`](ci.sh) for a script used to test against different server versions.
|
131
|
+ See [`scripts/ci.sh`](scripts/ci.sh) and [`scripts/test-versions.sh`](scripts/test-versions.sh) for a scripts used to test against different server versions.
|
124
132
|
|
125
133
|
## License
|
changed
hex_metadata.config
|
@@ -1,15 +1,14 @@
|
1
1
|
{<<"app">>,<<"myxql">>}.
|
2
2
|
{<<"build_tools">>,[<<"mix">>]}.
|
3
|
- {<<"description">>,<<"MySQL 5.7+ driver for Elixir">>}.
|
3
|
+ {<<"description">>,<<"MySQL 5.5+ driver for Elixir">>}.
|
4
4
|
{<<"elixir">>,<<"~> 1.4">>}.
|
5
5
|
{<<"files">>,
|
6
6
|
[<<"lib">>,<<"lib/myxql">>,<<"lib/myxql/client.ex">>,
|
7
|
- <<"lib/myxql/error.ex">>,<<"lib/myxql/protocol">>,
|
8
|
- <<"lib/myxql/protocol/flags.ex">>,<<"lib/myxql/protocol/records.ex">>,
|
9
|
- <<"lib/myxql/protocol/types.ex">>,<<"lib/myxql/protocol/auth.ex">>,
|
10
|
- <<"lib/myxql/protocol/values.ex">>,
|
11
|
- <<"lib/myxql/protocol/server_error_codes.ex">>,
|
12
|
- <<"lib/myxql/protocol/messages.ex">>,<<"lib/myxql/query.ex">>,
|
7
|
+ <<"lib/myxql/protocol.ex">>,<<"lib/myxql/error.ex">>,
|
8
|
+ <<"lib/myxql/protocol">>,<<"lib/myxql/protocol/flags.ex">>,
|
9
|
+ <<"lib/myxql/protocol/records.ex">>,<<"lib/myxql/protocol/types.ex">>,
|
10
|
+ <<"lib/myxql/protocol/auth.ex">>,<<"lib/myxql/protocol/values.ex">>,
|
11
|
+ <<"lib/myxql/protocol/server_error_codes.ex">>,<<"lib/myxql/query.ex">>,
|
13
12
|
<<"lib/myxql/cursor.ex">>,<<"lib/myxql/result.ex">>,
|
14
13
|
<<"lib/myxql/connection.ex">>,<<"lib/myxql/text_query.ex">>,
|
15
14
|
<<"lib/myxql.ex">>,<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,
|
|
@@ -33,4 +32,4 @@
|
33
32
|
{<<"optional">>,true},
|
34
33
|
{<<"repository">>,<<"hexpm">>},
|
35
34
|
{<<"requirement">>,<<"~> 1.0">>}]]}.
|
36
|
- {<<"version">>,<<"0.2.2">>}.
|
35
|
+ {<<"version">>,<<"0.2.3">>}.
|
changed
lib/myxql.ex
|
@@ -15,7 +15,7 @@ defmodule MyXQL do
|
15
15
|
| {:username, String.t()}
|
16
16
|
| {:password, String.t() | nil}
|
17
17
|
| {:ssl, boolean()}
|
18
|
- | {:ssl_opts, [:ssl.ssl_option()]}
|
18
|
+ | {:ssl_opts, [:ssl.tls_client_option()]}
|
19
19
|
| {:connect_timeout, timeout()}
|
20
20
|
| {:handshake_timeout, timeout()}
|
21
21
|
| {:ping_timeout, timeout()}
|
changed
lib/myxql/client.ex
|
@@ -2,7 +2,7 @@ defmodule MyXQL.Client do
|
2
2
|
@moduledoc false
|
3
3
|
|
4
4
|
require Logger
|
5
|
- import MyXQL.Protocol.{Flags, Messages, Records, Types}
|
5
|
+ import MyXQL.{Protocol, Protocol.Records, Protocol.Types}
|
6
6
|
alias MyXQL.Protocol.Auth
|
7
7
|
|
8
8
|
defmodule Config do
|
|
@@ -127,6 +127,12 @@ defmodule MyXQL.Client do
|
127
127
|
send_packet(payload, 0, state)
|
128
128
|
end
|
129
129
|
|
130
|
+ def send_recv_packet(payload, decoder, sequence_id, sock) do
|
131
|
+ with :ok <- send_packet(payload, sequence_id, sock) do
|
132
|
+ recv_packet(decoder, sock)
|
133
|
+ end
|
134
|
+ end
|
135
|
+
|
130
136
|
def send_packet(payload, sequence_id, state) do
|
131
137
|
data = encode_packet(payload, sequence_id)
|
132
138
|
send_data(state, data)
|
|
@@ -238,129 +244,35 @@ defmodule MyXQL.Client do
|
238
244
|
with {:ok, initial_handshake(conn_id: conn_id) = initial_handshake} <- recv_handshake(state),
|
239
245
|
state = %{state | connection_id: conn_id},
|
240
246
|
sequence_id = 1,
|
241
|
- :ok <- ensure_capabilities(initial_handshake),
|
242
|
- {:ok, sequence_id, state} <- maybe_upgrade_to_ssl(config, sequence_id, state) do
|
243
|
- send_handshake_response(config, initial_handshake, sequence_id, state)
|
244
|
- end
|
245
|
- end
|
247
|
+ {:ok, capability_flags} <- build_capability_flags(config, initial_handshake),
|
248
|
+ {:ok, sequence_id, state} <-
|
249
|
+ maybe_upgrade_to_ssl(config, capability_flags, sequence_id, state) do
|
250
|
+ result =
|
251
|
+ handle_handshake_response(
|
252
|
+ config,
|
253
|
+ initial_handshake,
|
254
|
+ capability_flags,
|
255
|
+ sequence_id,
|
256
|
+ state
|
257
|
+ )
|
246
258
|
|
247
|
- defp recv_handshake(state) do
|
248
|
- recv_packet(&decode_initial_handshake/1, state)
|
249
|
- end
|
250
|
-
|
251
|
- defp ensure_capabilities(initial_handshake(capability_flags: capability_flags)) do
|
252
|
- if has_capability_flag?(capability_flags, :client_deprecate_eof) do
|
253
|
- :ok
|
254
|
- else
|
255
|
- {:error, :server_not_supported}
|
256
|
- end
|
257
|
- end
|
258
|
-
|
259
|
- defp send_handshake_response(
|
260
|
- config,
|
261
|
- initial_handshake,
|
262
|
- sequence_id,
|
263
|
- state
|
264
|
- ) do
|
265
|
- initial_handshake(
|
266
|
- auth_plugin_name: auth_plugin_name,
|
267
|
- auth_plugin_data: auth_plugin_data
|
268
|
- ) = initial_handshake
|
269
|
-
|
270
|
- auth_response = auth_response(auth_plugin_name, auth_plugin_data, config.password)
|
271
|
-
|
272
|
- payload =
|
273
|
- encode_handshake_response_41(
|
274
|
- config.username,
|
275
|
- auth_plugin_name,
|
276
|
- auth_response,
|
277
|
- config.database,
|
278
|
- config.ssl?
|
279
|
- )
|
280
|
-
|
281
|
- with :ok <- send_packet(payload, sequence_id, state) do
|
282
|
- case recv_packet(&decode_handshake_response/1, state) do
|
259
|
+ case result do
|
283
260
|
{:ok, ok_packet()} ->
|
284
261
|
{:ok, state}
|
285
262
|
|
286
263
|
{:ok, err_packet() = err_packet} ->
|
287
264
|
{:error, err_packet}
|
288
265
|
|
289
|
- {:ok, auth_switch_request(plugin_name: plugin_name, plugin_data: plugin_data)} ->
|
290
|
- with {:ok, auth_response} <-
|
291
|
- auth_switch_response(plugin_name, config.password, plugin_data, config.ssl?),
|
292
|
- :ok <- send_packet(auth_response, sequence_id + 2, state) do
|
293
|
- case recv_packet(&decode_handshake_response/1, state) do
|
294
|
- {:ok, ok_packet(num_warnings: 0)} ->
|
295
|
- {:ok, state}
|
296
|
-
|
297
|
- {:ok, err_packet() = err_packet} ->
|
298
|
- {:error, err_packet}
|
299
|
-
|
300
|
- {:error, _reason} = error ->
|
301
|
- error
|
302
|
- end
|
303
|
- end
|
304
|
-
|
305
|
- {:ok, :full_auth} ->
|
306
|
- if config.ssl? do
|
307
|
- auth_response = config.password <> <<0x00>>
|
308
|
-
|
309
|
- with :ok <- send_packet(auth_response, sequence_id + 2, state) do
|
310
|
- case recv_packet(&decode_handshake_response/1, state) do
|
311
|
- {:ok, ok_packet(num_warnings: 0)} ->
|
312
|
- {:ok, state}
|
313
|
-
|
314
|
- {:ok, err_packet() = err_packet} ->
|
315
|
- {:error, err_packet}
|
316
|
-
|
317
|
- {:error, _reason} = error ->
|
318
|
- error
|
319
|
- end
|
320
|
- end
|
321
|
- else
|
322
|
- auth_plugin_secure_connection_error(auth_plugin_name)
|
323
|
- end
|
324
|
-
|
325
|
- {:error, _reason} = error ->
|
326
|
- error
|
266
|
+ other ->
|
267
|
+ other
|
327
268
|
end
|
328
269
|
end
|
329
270
|
end
|
330
271
|
|
331
|
- defp auth_response(_plugin_name, _plugin_data, nil),
|
332
|
- do: nil
|
333
|
-
|
334
|
- defp auth_response("mysql_native_password", plugin_data, password),
|
335
|
- do: Auth.mysql_native_password(password, plugin_data)
|
336
|
-
|
337
|
- defp auth_response(plugin_name, plugin_data, password)
|
338
|
- when plugin_name in ["sha256_password", "caching_sha2_password"],
|
339
|
- do: Auth.sha256_password(password, plugin_data)
|
340
|
-
|
341
|
- defp auth_switch_response(_plugin_name, nil, _plugin_data, _ssl?),
|
342
|
- do: {:ok, <<>>}
|
343
|
-
|
344
|
- defp auth_switch_response("mysql_native_password", password, plugin_data, _ssl?),
|
345
|
- do: {:ok, Auth.mysql_native_password(password, plugin_data)}
|
346
|
-
|
347
|
- defp auth_switch_response(plugin_name, password, _plugin_data, ssl?)
|
348
|
- when plugin_name in ["sha256_password", "caching_sha2_password"] do
|
349
|
- if ssl? do
|
350
|
- {:ok, password <> <<0x00>>}
|
351
|
- else
|
352
|
- auth_plugin_secure_connection_error(plugin_name)
|
353
|
- end
|
354
|
- end
|
355
|
-
|
356
|
- # https://dev.mysql.com/doc/refman/8.0/en/client-error-reference.html#error_cr_auth_plugin_err
|
357
|
- defp auth_plugin_secure_connection_error(plugin_name) do
|
358
|
- {:error, {:auth_plugin_error, {plugin_name, "Authentication requires secure connection"}}}
|
359
|
- end
|
360
|
-
|
361
|
- defp maybe_upgrade_to_ssl(%{ssl?: true} = config, sequence_id, state) do
|
272
|
+ defp maybe_upgrade_to_ssl(%{ssl?: true} = config, capability_flags, sequence_id, state) do
|
362
273
|
{_, sock} = state.sock
|
363
|
- payload = encode_ssl_request(config.database)
|
274
|
+ ssl_request = ssl_request(capability_flags: capability_flags)
|
275
|
+ payload = encode_ssl_request(ssl_request)
|
364
276
|
|
365
277
|
with :ok <- send_packet(payload, sequence_id, state),
|
366
278
|
{:ok, ssl_sock} <- :ssl.connect(sock, config.ssl_opts, config.connect_timeout) do
|
|
@@ -368,10 +280,106 @@ defmodule MyXQL.Client do
|
368
280
|
end
|
369
281
|
end
|
370
282
|
|
371
|
- defp maybe_upgrade_to_ssl(%{ssl?: false}, sequence_id, state) do
|
283
|
+ defp maybe_upgrade_to_ssl(%{ssl?: false}, _capability_flags, sequence_id, state) do
|
372
284
|
{:ok, sequence_id, state}
|
373
285
|
end
|
374
286
|
|
287
|
+ defp recv_handshake(state) do
|
288
|
+ recv_packet(&decode_initial_handshake/1, state)
|
289
|
+ end
|
290
|
+
|
291
|
+ defp handle_handshake_response(config, initial_handshake, capability_flags, sequence_id, state) do
|
292
|
+ initial_handshake(
|
293
|
+ auth_plugin_name: initial_auth_plugin_name,
|
294
|
+ auth_plugin_data: initial_auth_plugin_data
|
295
|
+ ) = initial_handshake
|
296
|
+
|
297
|
+ auth_response = Auth.auth_response(config, initial_auth_plugin_name, initial_auth_plugin_data)
|
298
|
+
|
299
|
+ handshake_response =
|
300
|
+ handshake_response_41(
|
301
|
+ capability_flags: capability_flags,
|
302
|
+ username: config.username,
|
303
|
+ auth_plugin_name: initial_auth_plugin_name,
|
304
|
+ auth_response: auth_response,
|
305
|
+ database: config.database
|
306
|
+ )
|
307
|
+
|
308
|
+ payload = encode_handshake_response_41(handshake_response)
|
309
|
+
|
310
|
+ case send_recv_packet(payload, &decode_auth_response/1, sequence_id, state) do
|
311
|
+ {:ok, auth_switch_request(plugin_name: auth_plugin_name, plugin_data: auth_plugin_data)} ->
|
312
|
+ auth_response = Auth.auth_response(config, auth_plugin_name, initial_auth_plugin_data)
|
313
|
+
|
314
|
+ case send_recv_packet(auth_response, &decode_auth_response/1, sequence_id + 2, state) do
|
315
|
+ {:ok, :full_auth} ->
|
316
|
+ perform_full_auth(config, auth_plugin_name, auth_plugin_data, sequence_id + 2, state)
|
317
|
+
|
318
|
+ {:ok, auth_more_data(data: public_key)} ->
|
319
|
+ perform_public_key_auth(
|
320
|
+ config.password,
|
321
|
+ public_key,
|
322
|
+ auth_plugin_data,
|
323
|
+ sequence_id + 4,
|
324
|
+ state
|
325
|
+ )
|
326
|
+
|
327
|
+ other ->
|
328
|
+ other
|
329
|
+ end
|
330
|
+
|
331
|
+ {:ok, :full_auth} ->
|
332
|
+ perform_full_auth(
|
333
|
+ config,
|
334
|
+ initial_auth_plugin_name,
|
335
|
+ initial_auth_plugin_data,
|
336
|
+ sequence_id,
|
337
|
+ state
|
338
|
+ )
|
339
|
+
|
340
|
+ {:ok, auth_more_data(data: public_key)} ->
|
341
|
+ perform_public_key_auth(
|
342
|
+ config.password,
|
343
|
+ public_key,
|
344
|
+ initial_auth_plugin_data,
|
345
|
+ sequence_id + 2,
|
346
|
+ state
|
347
|
+ )
|
348
|
+
|
349
|
+ other ->
|
350
|
+ other
|
351
|
+ end
|
352
|
+ end
|
353
|
+
|
354
|
+ defp perform_public_key_auth(password, public_key, auth_plugin_data, sequence_id, state) do
|
355
|
+ auth_response = Auth.encrypt_sha_password(password, public_key, auth_plugin_data)
|
356
|
+ send_recv_packet(auth_response, &decode_auth_response/1, sequence_id, state)
|
357
|
+ end
|
358
|
+
|
359
|
+ defp perform_full_auth(config, "caching_sha2_password", auth_plugin_data, sequence_id, state) do
|
360
|
+ auth_response =
|
361
|
+ if config.ssl? do
|
362
|
+ [config.password, 0]
|
363
|
+ else
|
364
|
+ # request public key
|
365
|
+ <<2>>
|
366
|
+ end
|
367
|
+
|
368
|
+ case send_recv_packet(auth_response, &decode_auth_response/1, sequence_id + 2, state) do
|
369
|
+ {:ok, auth_more_data(data: public_key)} ->
|
370
|
+ perform_public_key_auth(
|
371
|
+ config.password,
|
372
|
+ public_key,
|
373
|
+ auth_plugin_data,
|
374
|
+ sequence_id + 4,
|
375
|
+ state
|
376
|
+ )
|
377
|
+
|
378
|
+ other ->
|
379
|
+ other
|
380
|
+ end
|
381
|
+ end
|
382
|
+
|
375
383
|
defp start_handshake_timer(:infinity, _), do: :infinity
|
376
384
|
|
377
385
|
defp start_handshake_timer(timeout, sock) do
|
changed
lib/myxql/connection.ex
|
@@ -3,8 +3,7 @@ defmodule MyXQL.Connection do
|
3
3
|
|
4
4
|
use DBConnection
|
5
5
|
import MyXQL.Protocol.{Flags, Records}
|
6
|
- alias MyXQL.{Client, Cursor, Query, Result, TextQuery}
|
7
|
- alias MyXQL.Protocol.ServerErrorCodes
|
6
|
+ alias MyXQL.{Client, Cursor, Query, Protocol, Result, TextQuery}
|
8
7
|
|
9
8
|
@disconnect_on_error_codes [
|
10
9
|
:ER_MAX_PREPARED_STMT_COUNT_REACHED
|
|
@@ -31,7 +30,7 @@ defmodule MyXQL.Connection do
|
31
30
|
@disconnect_on_error_codes ++ Keyword.get(opts, :disconnect_on_error_codes, [])
|
32
31
|
|
33
32
|
case Client.connect(opts) do
|
34
|
- {:ok, state} ->
|
33
|
+ {:ok, %{} = state} ->
|
35
34
|
state = %__MODULE__{
|
36
35
|
prepare: prepare,
|
37
36
|
disconnect_on_error_codes: disconnect_on_error_codes,
|
|
@@ -42,6 +41,9 @@ defmodule MyXQL.Connection do
|
42
41
|
|
43
42
|
{:ok, state}
|
44
43
|
|
44
|
+ {:ok, err_packet() = err_packet} ->
|
45
|
+ {:error, error(err_packet)}
|
46
|
+
|
45
47
|
{:error, :enoent} ->
|
46
48
|
exception = error(:enoent)
|
47
49
|
{:local, socket} = config.address
|
|
@@ -322,22 +324,10 @@ defmodule MyXQL.Connection do
|
322
324
|
end
|
323
325
|
|
324
326
|
defp error(err_packet(code: code, message: message)) do
|
325
|
- name = ServerErrorCodes.code_to_name(code)
|
327
|
+ name = Protocol.error_code_to_name(code)
|
326
328
|
%MyXQL.Error{message: "(#{code}) (#{name}) " <> message, mysql: %{code: code, name: name}}
|
327
329
|
end
|
328
330
|
|
329
|
- defp error({:auth_plugin_error, {auth_plugin, message}}) do
|
330
|
- message = "Authentication plugin '#{auth_plugin}' reported error: #{message}"
|
331
|
- code = 2061
|
332
|
- name = :CR_AUTH_PLUGIN_ERR
|
333
|
- %MyXQL.Error{message: "(#{code}) (#{name}) " <> message, mysql: %{code: code, name: name}}
|
334
|
- end
|
335
|
-
|
336
|
- defp error(:server_not_supported) do
|
337
|
- message = "MyXQL requires MySQL server 5.7.10+"
|
338
|
- %MyXQL.Error{message: message}
|
339
|
- end
|
340
|
-
|
341
331
|
defp error(reason) do
|
342
332
|
%DBConnection.ConnectionError{message: format_reason(reason)}
|
343
333
|
end
|
added
lib/myxql/protocol.ex
|
@@ -0,0 +1,529 @@
|
1
|
+ defmodule MyXQL.Protocol do
|
2
|
+ @moduledoc false
|
3
|
+
|
4
|
+ import MyXQL.Protocol.{Flags, Records, Types}
|
5
|
+ alias MyXQL.Protocol.Values
|
6
|
+ use Bitwise
|
7
|
+
|
8
|
+ defdelegate error_code_to_name(code), to: MyXQL.Protocol.ServerErrorCodes, as: :code_to_name
|
9
|
+
|
10
|
+ # https://dev.mysql.com/doc/internals/en/com-stmt-execute.html
|
11
|
+ @cursor_types %{
|
12
|
+ cursor_type_no_cursor: 0x00,
|
13
|
+ cursor_type_read_only: 0x01,
|
14
|
+ cursor_type_for_update: 0x02,
|
15
|
+ cursor_type_scrollable: 0x04
|
16
|
+ }
|
17
|
+
|
18
|
+ ###########################################################
|
19
|
+ # Basic packets
|
20
|
+ #
|
21
|
+ # https://dev.mysql.com/doc/internals/en/mysql-packet.html
|
22
|
+ ###########################################################
|
23
|
+
|
24
|
+ def encode_packet(payload, sequence_id) do
|
25
|
+ payload_length = IO.iodata_length(payload)
|
26
|
+ [<<payload_length::uint3, sequence_id::uint1>>, payload]
|
27
|
+ end
|
28
|
+
|
29
|
+ def decode_generic_response(<<0x00, rest::bits>>) do
|
30
|
+ decode_ok_packet_body(rest)
|
31
|
+ end
|
32
|
+
|
33
|
+ def decode_generic_response(<<0xFF, rest::bits>>) do
|
34
|
+ decode_err_packet_body(rest)
|
35
|
+ end
|
36
|
+
|
37
|
+ defp decode_ok_packet_body(rest) do
|
38
|
+ {affected_rows, rest} = take_int_lenenc(rest)
|
39
|
+ {last_insert_id, rest} = take_int_lenenc(rest)
|
40
|
+
|
41
|
+ <<
|
42
|
+ status_flags::uint2,
|
43
|
+ num_warnings::uint2,
|
44
|
+ info::binary
|
45
|
+ >> = rest
|
46
|
+
|
47
|
+ ok_packet(
|
48
|
+ affected_rows: affected_rows,
|
49
|
+ last_insert_id: last_insert_id,
|
50
|
+ status_flags: status_flags,
|
51
|
+ num_warnings: num_warnings,
|
52
|
+ info: info
|
53
|
+ )
|
54
|
+ end
|
55
|
+
|
56
|
+ defp decode_err_packet_body(
|
57
|
+ <<code::uint2, _sql_state_marker::string(1), _sql_state::string(5), message::bits>>
|
58
|
+ ) do
|
59
|
+ err_packet(code: code, message: message)
|
60
|
+ end
|
61
|
+
|
62
|
+ def decode_eof_packet(<<0xFE, rest::binary>>) do
|
63
|
+ decode_eof_packet_body(rest)
|
64
|
+ end
|
65
|
+
|
66
|
+ defp decode_eof_packet_body(<<num_warnings::uint2, status_flags::uint2>>) do
|
67
|
+ eof_packet(
|
68
|
+ status_flags: status_flags,
|
69
|
+ num_warnings: num_warnings
|
70
|
+ )
|
71
|
+ end
|
72
|
+
|
73
|
+ defp decode_connect_err_packet_body(<<code::uint2, message::bits>>) do
|
74
|
+ err_packet(code: code, message: message)
|
75
|
+ end
|
76
|
+
|
77
|
+ ##############################################################
|
78
|
+ # Connection Phase
|
79
|
+ #
|
80
|
+ # https://dev.mysql.com/doc/internals/en/connection-phase.html
|
81
|
+ ##############################################################
|
82
|
+
|
83
|
+ def decode_initial_handshake(<<10, rest::binary>>) do
|
84
|
+ {server_version, rest} = take_string_nul(rest)
|
85
|
+
|
86
|
+ <<
|
87
|
+ conn_id::uint4,
|
88
|
+ auth_plugin_data1::string(8),
|
89
|
+ 0,
|
90
|
+ capability_flags1::uint2,
|
91
|
+ character_set::uint1,
|
92
|
+ status_flags::uint2,
|
93
|
+ capability_flags2::uint2,
|
94
|
+ rest::binary
|
95
|
+ >> = rest
|
96
|
+
|
97
|
+ <<capability_flags::uint4>> = <<capability_flags1::uint2, capability_flags2::uint2>>
|
98
|
+ # all set in servers since MySQL 4.1
|
99
|
+ required_capabilities = [:client_protocol_41, :client_plugin_auth, :client_secure_connection]
|
100
|
+
|
101
|
+ with :ok <- ensure_capabilities(capability_flags, required_capabilities) do
|
102
|
+ <<
|
103
|
+ auth_plugin_data_length::uint1,
|
104
|
+ _::uint(10),
|
105
|
+ rest::binary
|
106
|
+ >> = rest
|
107
|
+
|
108
|
+ take = max(13, auth_plugin_data_length - 8)
|
109
|
+ <<auth_plugin_data2::binary-size(take), auth_plugin_name::binary>> = rest
|
110
|
+ auth_plugin_data2 = decode_string_nul(auth_plugin_data2)
|
111
|
+ auth_plugin_name = decode_string_nul(auth_plugin_name)
|
112
|
+ auth_plugin_data = auth_plugin_data1 <> auth_plugin_data2
|
113
|
+
|
114
|
+ initial_handshake(
|
115
|
+ server_version: server_version,
|
116
|
+ conn_id: conn_id,
|
117
|
+ auth_plugin_name: auth_plugin_name,
|
118
|
+ auth_plugin_data: auth_plugin_data,
|
119
|
+ capability_flags: capability_flags,
|
120
|
+ character_set: character_set,
|
121
|
+ status_flags: status_flags
|
122
|
+ )
|
123
|
+ end
|
124
|
+ end
|
125
|
+
|
126
|
+ def decode_initial_handshake(<<0xFF, rest::bits>>) do
|
127
|
+ decode_connect_err_packet_body(rest)
|
128
|
+ end
|
129
|
+
|
130
|
+ defp ensure_capabilities(capability_flags, names) do
|
131
|
+ Enum.reduce_while(names, :ok, fn name, _acc ->
|
132
|
+ if has_capability_flag?(capability_flags, name) do
|
133
|
+ {:cont, :ok}
|
134
|
+ else
|
135
|
+ {:halt, {:error, {:server_missing_capability, name}}}
|
136
|
+ end
|
137
|
+ end)
|
138
|
+ end
|
139
|
+
|
140
|
+ def build_capability_flags(config, initial_handshake) do
|
141
|
+ initial_handshake(capability_flags: server_capability_flags) = initial_handshake
|
142
|
+
|
143
|
+ client_capability_flags =
|
144
|
+ put_capability_flags([
|
145
|
+ :client_protocol_41,
|
146
|
+ :client_plugin_auth,
|
147
|
+ :client_secure_connection,
|
148
|
+ :client_found_rows,
|
149
|
+ :client_multi_results,
|
150
|
+ # set by servers since 4.0
|
151
|
+ :client_transactions
|
152
|
+ ])
|
153
|
+ |> maybe_put_capability_flag(:client_connect_with_db, !is_nil(config.database))
|
154
|
+ |> maybe_put_capability_flag(:client_ssl, config.ssl?)
|
155
|
+
|
156
|
+ if config.ssl? && !has_capability_flag?(server_capability_flags, :client_ssl) do
|
157
|
+ {:error, :server_does_not_support_ssl}
|
158
|
+ else
|
159
|
+ client_capabilities = list_capability_flags(client_capability_flags)
|
160
|
+
|
161
|
+ with :ok <- ensure_capabilities(server_capability_flags, client_capabilities) do
|
162
|
+ {:ok, client_capability_flags}
|
163
|
+ end
|
164
|
+ end
|
165
|
+ end
|
166
|
+
|
167
|
+ defp maybe_put_capability_flag(flags, name, true), do: put_capability_flags(flags, [name])
|
168
|
+ defp maybe_put_capability_flag(flags, _name, false), do: flags
|
169
|
+
|
170
|
+ def encode_handshake_response_41(
|
171
|
+ handshake_response_41(
|
172
|
+ capability_flags: capability_flags,
|
173
|
+ max_packet_size: max_packet_size,
|
174
|
+ character_set: character_set,
|
175
|
+ username: username,
|
176
|
+ auth_plugin_name: auth_plugin_name,
|
177
|
+ auth_response: auth_response,
|
178
|
+ database: database
|
179
|
+ )
|
180
|
+ ) do
|
181
|
+ auth_response = if auth_response, do: encode_string_lenenc(auth_response), else: <<0>>
|
182
|
+ database = if database, do: <<database::binary, 0x00>>, else: ""
|
183
|
+
|
184
|
+ <<
|
185
|
+ capability_flags::uint4,
|
186
|
+ max_packet_size::uint4,
|
187
|
+ character_set,
|
188
|
+ 0::uint(23),
|
189
|
+ <<username::binary, 0x00>>,
|
190
|
+ auth_response::binary,
|
191
|
+ database::binary,
|
192
|
+ (<<auth_plugin_name::binary, 0x00>>)
|
193
|
+ >>
|
194
|
+ end
|
195
|
+
|
196
|
+ def encode_ssl_request(
|
197
|
+ ssl_request(
|
198
|
+ capability_flags: capability_flags,
|
199
|
+ max_packet_size: max_packet_size,
|
200
|
+ character_set: character_set
|
201
|
+ )
|
202
|
+ ) do
|
203
|
+ <<
|
204
|
+ capability_flags::uint4,
|
205
|
+ max_packet_size::uint4,
|
206
|
+ character_set,
|
207
|
+ 0::uint(23)
|
208
|
+ >>
|
209
|
+ end
|
210
|
+
|
211
|
+ def decode_auth_response(<<0x00, rest::binary>>) do
|
212
|
+ decode_ok_packet_body(rest)
|
213
|
+ end
|
214
|
+
|
215
|
+ def decode_auth_response(<<0xFF, rest::binary>>) do
|
216
|
+ decode_err_packet_body(rest)
|
217
|
+ end
|
218
|
+
|
219
|
+ def decode_auth_response(<<0x01, 0x04>>) do
|
220
|
+ :full_auth
|
221
|
+ end
|
222
|
+
|
223
|
+ def decode_auth_response(<<0x01, rest::binary>>) do
|
224
|
+ auth_more_data(data: rest)
|
225
|
+ end
|
226
|
+
|
227
|
+ def decode_auth_response(<<0xFE, rest::binary>>) do
|
228
|
+ {plugin_name, rest} = take_string_nul(rest)
|
229
|
+ {plugin_data, ""} = take_string_nul(rest)
|
230
|
+
|
231
|
+ auth_switch_request(plugin_name: plugin_name, plugin_data: plugin_data)
|
232
|
+ end
|
233
|
+
|
234
|
+ #################################################################
|
235
|
+ # Text & Binary Protocol
|
236
|
+ #
|
237
|
+ # https://dev.mysql.com/doc/internals/en/text-protocol.html
|
238
|
+ # https://dev.mysql.com/doc/internals/en/prepared-statements.html
|
239
|
+ #################################################################
|
240
|
+
|
241
|
+ # https://dev.mysql.com/doc/internals/en/com-ping.html
|
242
|
+ def encode_com(:com_ping) do
|
243
|
+ <<0x0E>>
|
244
|
+ end
|
245
|
+
|
246
|
+ # https://dev.mysql.com/doc/internals/en/com-query.html
|
247
|
+ def encode_com({:com_query, query}) do
|
248
|
+ [0x03, query]
|
249
|
+ end
|
250
|
+
|
251
|
+ # https://dev.mysql.com/doc/internals/en/com-stmt-prepare.html#packet-COM_STMT_PREPARE
|
252
|
+ def encode_com({:com_stmt_prepare, query}) do
|
253
|
+ [0x16, query]
|
254
|
+ end
|
255
|
+
|
256
|
+ # https://dev.mysql.com/doc/internals/en/com-stmt-close.html
|
257
|
+ def encode_com({:com_stmt_close, statement_id}) do
|
258
|
+ [0x19, <<statement_id::uint4>>]
|
259
|
+ end
|
260
|
+
|
261
|
+ # https://dev.mysql.com/doc/internals/en/com-stmt-reset.html
|
262
|
+ def encode_com({:com_stmt_reset, statement_id}) do
|
263
|
+ [0x1A, <<statement_id::uint4>>]
|
264
|
+ end
|
265
|
+
|
266
|
+ # https://dev.mysql.com/doc/internals/en/com-stmt-execute.html
|
267
|
+ def encode_com({:com_stmt_execute, statement_id, params, cursor_type}) do
|
268
|
+ command = 0x17
|
269
|
+ flags = Map.fetch!(@cursor_types, cursor_type)
|
270
|
+
|
271
|
+ # Always 0x01
|
272
|
+ iteration_count = 0x01
|
273
|
+
|
274
|
+ new_params_bound_flag = 1
|
275
|
+ {null_bitmap, types, values} = encode_params(params)
|
276
|
+
|
277
|
+ <<
|
278
|
+ command,
|
279
|
+ statement_id::uint4,
|
280
|
+ flags::uint1,
|
281
|
+ iteration_count::uint4,
|
282
|
+ null_bitmap::bitstring,
|
283
|
+ new_params_bound_flag::uint1,
|
284
|
+ types::binary,
|
285
|
+ values::binary
|
286
|
+ >>
|
287
|
+ end
|
288
|
+
|
289
|
+ # https://dev.mysql.com/doc/internals/en/com-stmt-fetch.html
|
290
|
+ def encode_com({:com_stmt_fetch, statement_id, num_rows}) do
|
291
|
+ <<
|
292
|
+ 0x1C,
|
293
|
+ statement_id::uint4,
|
294
|
+ num_rows::uint4
|
295
|
+ >>
|
296
|
+ end
|
297
|
+
|
298
|
+ # https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-COM_QUERY_Response
|
299
|
+ def decode_com_query_response(<<0x00, rest::binary>>, "", :initial) do
|
300
|
+ {:halt, decode_ok_packet_body(rest)}
|
301
|
+ end
|
302
|
+
|
303
|
+ def decode_com_query_response(<<0xFF, rest::binary>>, "", :initial) do
|
304
|
+ {:halt, decode_err_packet_body(rest)}
|
305
|
+ end
|
306
|
+
|
307
|
+ def decode_com_query_response(payload, next_data, state) do
|
308
|
+ decode_resultset(payload, next_data, state, &Values.decode_text_row/2)
|
309
|
+ end
|
310
|
+
|
311
|
+ def decode_com_stmt_prepare_response(
|
312
|
+ <<0x00, statement_id::uint4, num_columns::uint2, num_params::uint2, 0,
|
313
|
+ num_warnings::uint2>>,
|
314
|
+ next_data,
|
315
|
+ :initial
|
316
|
+ ) do
|
317
|
+ result =
|
318
|
+ com_stmt_prepare_ok(
|
319
|
+ statement_id: statement_id,
|
320
|
+ num_columns: num_columns,
|
321
|
+ num_params: num_params,
|
322
|
+ num_warnings: num_warnings
|
323
|
+ )
|
324
|
+
|
325
|
+ cond do
|
326
|
+ num_params > 0 ->
|
327
|
+ {:cont, {result, :params, num_params, num_columns}}
|
328
|
+
|
329
|
+ num_columns > 0 ->
|
330
|
+ {:cont, {result, :columns, num_columns}}
|
331
|
+
|
332
|
+ true ->
|
333
|
+ "" = next_data
|
334
|
+ {:halt, result}
|
335
|
+ end
|
336
|
+ end
|
337
|
+
|
338
|
+ def decode_com_stmt_prepare_response(<<rest::binary>>, "", :initial) do
|
339
|
+ {:halt, decode_generic_response(rest)}
|
340
|
+ end
|
341
|
+
|
342
|
+ # for now, we're simply consuming column_definition packets for params and columns,
|
343
|
+ # we might decode them in the future.
|
344
|
+
|
345
|
+ def decode_com_stmt_prepare_response(
|
346
|
+ payload,
|
347
|
+ _next_data,
|
348
|
+ {com_stmt_prepare_ok, :params, num_params, num_columns}
|
349
|
+ ) do
|
350
|
+ if num_params > 0 do
|
351
|
+ column_def() = decode_column_def(payload)
|
352
|
+ {:cont, {com_stmt_prepare_ok, :params, num_params - 1, num_columns}}
|
353
|
+ else
|
354
|
+ eof_packet() = decode_eof_packet(payload)
|
355
|
+
|
356
|
+ if num_columns > 0 do
|
357
|
+ {:cont, {com_stmt_prepare_ok, :columns, num_columns}}
|
358
|
+ else
|
359
|
+ {:halt, com_stmt_prepare_ok}
|
360
|
+ end
|
361
|
+ end
|
362
|
+ end
|
363
|
+
|
364
|
+ def decode_com_stmt_prepare_response(
|
365
|
+ payload,
|
366
|
+ next_data,
|
367
|
+ {com_stmt_prepare_ok, :columns, num_columns}
|
368
|
+ ) do
|
369
|
+ if num_columns > 0 do
|
370
|
+ column_def() = decode_column_def(payload)
|
371
|
+ {:cont, {com_stmt_prepare_ok, :columns, num_columns - 1}}
|
372
|
+ else
|
373
|
+ "" = next_data
|
374
|
+ eof_packet() = decode_eof_packet(payload)
|
375
|
+ {:halt, com_stmt_prepare_ok}
|
376
|
+ end
|
377
|
+ end
|
378
|
+
|
379
|
+ defp encode_params(params) do
|
380
|
+ null_type = 0x06
|
381
|
+
|
382
|
+ {count, null_bitmap, types, values} =
|
383
|
+ Enum.reduce(params, {0, 0, <<>>, <<>>}, fn
|
384
|
+ value, {idx, null_bitmap, types, values} ->
|
385
|
+ null_value = if value == nil, do: 1, else: 0
|
386
|
+ null_bitmap = null_bitmap ||| null_value <<< idx
|
387
|
+
|
388
|
+ if value == nil do
|
389
|
+ {idx + 1, null_bitmap, <<types::binary, null_type, unsigned_flag(value)>>, values}
|
390
|
+ else
|
391
|
+ {type, binary} = Values.encode_binary_value(value)
|
392
|
+ type = Values.type_atom_to_code(type)
|
393
|
+
|
394
|
+ {idx + 1, null_bitmap, <<types::binary, type, unsigned_flag(value)>>,
|
395
|
+ <<values::binary, binary::binary>>}
|
396
|
+ end
|
397
|
+ end)
|
398
|
+
|
399
|
+ null_bitmap_size = div(count + 7, 8)
|
400
|
+ {<<null_bitmap::uint(null_bitmap_size)>>, types, values}
|
401
|
+ end
|
402
|
+
|
403
|
+ defp unsigned_flag(value) when is_integer(value) and value >= 1 <<< 63, do: 0x80
|
404
|
+ defp unsigned_flag(_), do: 0x00
|
405
|
+
|
406
|
+ # https://dev.mysql.com/doc/internals/en/com-stmt-execute-response.html
|
407
|
+ def decode_com_stmt_execute_response(<<0x00, rest::binary>>, "", :initial) do
|
408
|
+ {:halt, decode_ok_packet_body(rest)}
|
409
|
+ end
|
410
|
+
|
411
|
+ def decode_com_stmt_execute_response(<<0xFF, rest::binary>>, "", :initial) do
|
412
|
+ {:halt, decode_err_packet_body(rest)}
|
413
|
+ end
|
414
|
+
|
415
|
+ def decode_com_stmt_execute_response(payload, next_data, state) do
|
416
|
+ decode_resultset(payload, next_data, state, &Values.decode_binary_row/2)
|
417
|
+ end
|
418
|
+
|
419
|
+ # https://dev.mysql.com/doc/internals/en/com-stmt-fetch-response.html
|
420
|
+ def decode_com_stmt_fetch_response(<<0xFF, rest::binary>>, "", {:initial, _column_defs}) do
|
421
|
+ {:halt, decode_err_packet_body(rest)}
|
422
|
+ end
|
423
|
+
|
424
|
+ def decode_com_stmt_fetch_response(payload, next_data, {:initial, column_defs}) do
|
425
|
+ decode_com_stmt_fetch_response(payload, next_data, {:rows, column_defs, 0, []})
|
426
|
+ end
|
427
|
+
|
428
|
+ def decode_com_stmt_fetch_response(payload, next_data, state) do
|
429
|
+ decode_resultset(payload, next_data, state, &Values.decode_binary_row/2)
|
430
|
+ end
|
431
|
+
|
432
|
+ def decode_column_def(<<3, "def", rest::binary>>) do
|
433
|
+ {_schema, rest} = take_string_lenenc(rest)
|
434
|
+ {_table, rest} = take_string_lenenc(rest)
|
435
|
+ {_org_table, rest} = take_string_lenenc(rest)
|
436
|
+ {name, rest} = take_string_lenenc(rest)
|
437
|
+ {_org_name, rest} = take_string_lenenc(rest)
|
438
|
+
|
439
|
+ <<
|
440
|
+ 0x0C,
|
441
|
+ _character_set::uint2,
|
442
|
+ _column_length::uint4,
|
443
|
+ type::uint1,
|
444
|
+ flags::uint2,
|
445
|
+ _decimals::uint1,
|
446
|
+ 0::uint2
|
447
|
+ >> = rest
|
448
|
+
|
449
|
+ column_def(
|
450
|
+ name: name,
|
451
|
+ type: Values.type_code_to_atom(type),
|
452
|
+ flags: flags,
|
453
|
+ unsigned?: has_column_flag?(flags, :unsigned_flag)
|
454
|
+ )
|
455
|
+ end
|
456
|
+
|
457
|
+ defp decode_resultset(payload, _next_data, :initial, _row_decoder) do
|
458
|
+ {:cont, {:column_defs, decode_int_lenenc(payload), []}}
|
459
|
+ end
|
460
|
+
|
461
|
+ defp decode_resultset(payload, _next_data, {:column_defs, num_columns, acc}, _row_decoder) do
|
462
|
+ column_def = decode_column_def(payload)
|
463
|
+ acc = [column_def | acc]
|
464
|
+
|
465
|
+ if num_columns > 1 do
|
466
|
+ {:cont, {:column_defs, num_columns - 1, acc}}
|
467
|
+ else
|
468
|
+ {:cont, {:column_defs_eof, Enum.reverse(acc)}}
|
469
|
+ end
|
470
|
+ end
|
471
|
+
|
472
|
+ defp decode_resultset(
|
473
|
+ <<0xFE, num_warnings::uint2, status_flags::uint2>>,
|
474
|
+ next_data,
|
475
|
+ {:column_defs_eof, column_defs},
|
476
|
+ _row_decoder
|
477
|
+ ) do
|
478
|
+ if has_status_flag?(status_flags, :server_status_cursor_exists) do
|
479
|
+ "" = next_data
|
480
|
+
|
481
|
+ {:halt,
|
482
|
+ resultset(
|
483
|
+ column_defs: column_defs,
|
484
|
+ num_rows: 0,
|
485
|
+ rows: [],
|
486
|
+ num_warnings: num_warnings,
|
487
|
+ status_flags: status_flags
|
488
|
+ )}
|
489
|
+ else
|
490
|
+ {:cont, {:rows, column_defs, 0, []}}
|
491
|
+ end
|
492
|
+ end
|
493
|
+
|
494
|
+ defp decode_resultset(
|
495
|
+ <<0xFE, num_warnings::uint2, status_flags::uint2>>,
|
496
|
+ _next_data,
|
497
|
+ {:rows, column_defs, num_rows, acc},
|
498
|
+ _row_decoder
|
499
|
+ ) do
|
500
|
+ resultset =
|
501
|
+ resultset(
|
502
|
+ column_defs: column_defs,
|
503
|
+ num_rows: num_rows,
|
504
|
+ rows: Enum.reverse(acc),
|
505
|
+ num_warnings: num_warnings,
|
506
|
+ status_flags: status_flags
|
507
|
+ )
|
508
|
+
|
509
|
+ if has_status_flag?(status_flags, :server_more_results_exists) do
|
510
|
+ {:cont, {:trailing_ok_packet, resultset}}
|
511
|
+ else
|
512
|
+ {:halt, resultset}
|
513
|
+ end
|
514
|
+ end
|
515
|
+
|
516
|
+ defp decode_resultset(payload, _next_data, {:rows, column_defs, num_rows, acc}, row_decoder) do
|
517
|
+ row = row_decoder.(payload, column_defs)
|
518
|
+ {:cont, {:rows, column_defs, num_rows + 1, [row | acc]}}
|
519
|
+ end
|
520
|
+
|
521
|
+ defp decode_resultset(payload, "", {:trailing_ok_packet, resultset}, _row_decoder) do
|
522
|
+ ok_packet(status_flags: status_flags) = decode_generic_response(payload)
|
523
|
+ {:halt, resultset(resultset, status_flags: status_flags)}
|
524
|
+ end
|
525
|
+
|
526
|
+ defp decode_resultset(_payload, _next_data, {:trailing_ok_packet, _resultset}, _row_decoder) do
|
527
|
+ {:error, :multiple_results}
|
528
|
+ end
|
529
|
+ end
|
changed
lib/myxql/protocol/auth.ex
|
@@ -14,6 +14,33 @@ defmodule MyXQL.Protocol.Auth do
|
14
14
|
sha_hash(:sha256, password, auth_plugin_data)
|
15
15
|
end
|
16
16
|
|
17
|
+ def encrypt_sha_password(password, public_key, auth_plugin_data) do
|
18
|
+ password = password <> <<0>>
|
19
|
+ xor = :crypto.exor(password, :binary.part(auth_plugin_data, 0, byte_size(password)))
|
20
|
+ [entry] = :public_key.pem_decode(public_key)
|
21
|
+ public_key = :public_key.pem_entry_decode(entry)
|
22
|
+ :public_key.encrypt_public(xor, public_key, rsa_pad: :rsa_pkcs1_oaep_padding)
|
23
|
+ end
|
24
|
+
|
25
|
+ def auth_response(config, auth_plugin_name, initial_auth_plugin_data) do
|
26
|
+ cond do
|
27
|
+ config.password == nil ->
|
28
|
+ ""
|
29
|
+
|
30
|
+ auth_plugin_name == "mysql_native_password" ->
|
31
|
+ mysql_native_password(config.password, initial_auth_plugin_data)
|
32
|
+
|
33
|
+ auth_plugin_name == "sha256_password" and config.ssl? ->
|
34
|
+ config.password <> <<0>>
|
35
|
+
|
36
|
+ auth_plugin_name == "sha256_password" and not config.ssl? ->
|
37
|
+ <<1>>
|
38
|
+
|
39
|
+ auth_plugin_name == "caching_sha2_password" ->
|
40
|
+ sha256_password(config.password, initial_auth_plugin_data)
|
41
|
+ end
|
42
|
+ end
|
43
|
+
|
17
44
|
## Helpers
|
18
45
|
|
19
46
|
defp sha_hash(type, password, auth_plugin_data)
|
removed
lib/myxql/protocol/messages.ex
|
@@ -1,412 +0,0 @@
|
1
|
- defmodule MyXQL.Protocol.Messages do
|
2
|
- @moduledoc false
|
3
|
- import MyXQL.Protocol.{Flags, Records, Types}
|
4
|
- alias MyXQL.Protocol.Values
|
5
|
- use Bitwise
|
6
|
-
|
7
|
- @max_packet_size 16_777_215
|
8
|
-
|
9
|
- defp capability_flags(database, ssl?) do
|
10
|
- put_capability_flags([
|
11
|
- :client_protocol_41,
|
12
|
- :client_deprecate_eof,
|
13
|
- :client_plugin_auth,
|
14
|
- :client_secure_connection,
|
15
|
- :client_found_rows,
|
16
|
- :client_multi_results,
|
17
|
- :client_transactions
|
18
|
- ])
|
19
|
- |> maybe_put_capability_flag(:client_connect_with_db, !is_nil(database))
|
20
|
- |> maybe_put_capability_flag(:client_ssl, ssl?)
|
21
|
- end
|
22
|
-
|
23
|
- defp maybe_put_capability_flag(flags, name, true), do: put_capability_flags(flags, [name])
|
24
|
- defp maybe_put_capability_flag(flags, _name, false), do: flags
|
25
|
-
|
26
|
- # https://dev.mysql.com/doc/internals/en/character-set.html#packet-Protocol::CharacterSet
|
27
|
- character_sets = %{
|
28
|
- utf8_general_ci: 0x21
|
29
|
- }
|
30
|
-
|
31
|
- for {name, code} <- character_sets do
|
32
|
- def character_set_name_to_code(unquote(name)), do: unquote(code)
|
33
|
- end
|
34
|
-
|
35
|
- # https://dev.mysql.com/doc/internals/en/com-stmt-execute.html
|
36
|
- @cursor_types %{
|
37
|
- cursor_type_no_cursor: 0x00,
|
38
|
- cursor_type_read_only: 0x01,
|
39
|
- cursor_type_for_update: 0x02,
|
40
|
- cursor_type_scrollable: 0x04
|
41
|
- }
|
42
|
-
|
43
|
- ###########################################################
|
44
|
- # Basic packets
|
45
|
- #
|
46
|
- # https://dev.mysql.com/doc/internals/en/mysql-packet.html
|
47
|
- ###########################################################
|
48
|
-
|
49
|
- def encode_packet(payload, sequence_id) do
|
50
|
- payload_length = IO.iodata_length(payload)
|
51
|
- [<<payload_length::uint3, sequence_id::uint1>>, payload]
|
52
|
- end
|
53
|
-
|
54
|
- def decode_generic_response(<<0x00, rest::binary>>) do
|
55
|
- {affected_rows, rest} = take_int_lenenc(rest)
|
56
|
- {last_insert_id, rest} = take_int_lenenc(rest)
|
57
|
-
|
58
|
- <<
|
59
|
- status_flags::uint2,
|
60
|
- num_warnings::uint2,
|
61
|
- info::binary
|
62
|
- >> = rest
|
63
|
-
|
64
|
- ok_packet(
|
65
|
- affected_rows: affected_rows,
|
66
|
- last_insert_id: last_insert_id,
|
67
|
- status_flags: status_flags,
|
68
|
- num_warnings: num_warnings,
|
69
|
- info: info
|
70
|
- )
|
71
|
- end
|
72
|
-
|
73
|
- def decode_generic_response(
|
74
|
- <<0xFF, code::uint2, _sql_state_marker::string(1), _sql_state::string(5),
|
75
|
- message::binary>>
|
76
|
- ) do
|
77
|
- err_packet(code: code, message: message)
|
78
|
- end
|
79
|
-
|
80
|
- # Note: header is last argument to allow binary optimization
|
81
|
- def decode_generic_response(<<rest::binary>>, header) do
|
82
|
- decode_generic_response(<<header, rest::binary>>)
|
83
|
- end
|
84
|
-
|
85
|
- ##############################################################
|
86
|
- # Connection Phase
|
87
|
- #
|
88
|
- # https://dev.mysql.com/doc/internals/en/connection-phase.html
|
89
|
- ##############################################################
|
90
|
-
|
91
|
- def decode_initial_handshake(payload) do
|
92
|
- protocol_version = 10
|
93
|
- <<^protocol_version, rest::binary>> = payload
|
94
|
- {server_version, rest} = take_string_nul(rest)
|
95
|
-
|
96
|
- <<
|
97
|
- conn_id::uint4,
|
98
|
- auth_plugin_data1::string(8),
|
99
|
- 0,
|
100
|
- capability_flags1::uint2,
|
101
|
- character_set::uint1,
|
102
|
- status_flags::uint2,
|
103
|
- capability_flags2::uint2,
|
104
|
- auth_plugin_data_length::uint1,
|
105
|
- _::uint(10),
|
106
|
- rest::binary
|
107
|
- >> = rest
|
108
|
-
|
109
|
- take = max(13, auth_plugin_data_length - 8)
|
110
|
- <<auth_plugin_data2::binary-size(take), auth_plugin_name::binary>> = rest
|
111
|
- auth_plugin_data2 = decode_string_nul(auth_plugin_data2)
|
112
|
- auth_plugin_name = decode_string_nul(auth_plugin_name)
|
113
|
- <<capability_flags::uint4>> = <<capability_flags1::uint2, capability_flags2::uint2>>
|
114
|
- auth_plugin_data = auth_plugin_data1 <> auth_plugin_data2
|
115
|
-
|
116
|
- initial_handshake(
|
117
|
- server_version: server_version,
|
118
|
- conn_id: conn_id,
|
119
|
- auth_plugin_name: auth_plugin_name,
|
120
|
- auth_plugin_data: auth_plugin_data,
|
121
|
- capability_flags: capability_flags,
|
122
|
- character_set: character_set,
|
123
|
- status_flags: status_flags
|
124
|
- )
|
125
|
- end
|
126
|
-
|
127
|
- def encode_handshake_response_41(
|
128
|
- username,
|
129
|
- auth_plugin_name,
|
130
|
- auth_response,
|
131
|
- database,
|
132
|
- ssl?
|
133
|
- ) do
|
134
|
- capability_flags = capability_flags(database, ssl?)
|
135
|
- auth_response = if auth_response, do: encode_string_lenenc(auth_response), else: <<0>>
|
136
|
- database = if database, do: <<database::binary, 0x00>>, else: ""
|
137
|
-
|
138
|
- <<
|
139
|
- capability_flags::uint4,
|
140
|
- @max_packet_size::uint4,
|
141
|
- character_set_name_to_code(:utf8_general_ci),
|
142
|
- 0::uint(23),
|
143
|
- <<username::binary, 0x00>>,
|
144
|
- auth_response::binary,
|
145
|
- database::binary,
|
146
|
- (<<auth_plugin_name::binary, 0x00>>)
|
147
|
- >>
|
148
|
- end
|
149
|
-
|
150
|
- def encode_ssl_request(database) do
|
151
|
- capability_flags = capability_flags(database, true)
|
152
|
-
|
153
|
- <<
|
154
|
- capability_flags::uint4,
|
155
|
- @max_packet_size::uint4,
|
156
|
- character_set_name_to_code(:utf8_general_ci),
|
157
|
- 0::uint(23)
|
158
|
- >>
|
159
|
- end
|
160
|
-
|
161
|
- def decode_handshake_response(<<header, rest::binary>>) when header in [0x00, 0xFF] do
|
162
|
- decode_generic_response(rest, header)
|
163
|
- end
|
164
|
-
|
165
|
- def decode_handshake_response(<<0x01, 0x04>>) do
|
166
|
- :full_auth
|
167
|
- end
|
168
|
-
|
169
|
- def decode_handshake_response(<<0xFE, rest::binary>>) do
|
170
|
- {plugin_name, rest} = take_string_nul(rest)
|
171
|
- {plugin_data, ""} = take_string_nul(rest)
|
172
|
-
|
173
|
- auth_switch_request(plugin_name: plugin_name, plugin_data: plugin_data)
|
174
|
- end
|
175
|
-
|
176
|
- #################################################################
|
177
|
- # Text & Binary Protocol
|
178
|
- #
|
179
|
- # https://dev.mysql.com/doc/internals/en/text-protocol.html
|
180
|
- # https://dev.mysql.com/doc/internals/en/prepared-statements.html
|
181
|
- #################################################################
|
182
|
-
|
183
|
- # https://dev.mysql.com/doc/internals/en/com-ping.html
|
184
|
- def encode_com(:com_ping) do
|
185
|
- <<0x0E>>
|
186
|
- end
|
187
|
-
|
188
|
- # https://dev.mysql.com/doc/internals/en/com-query.html
|
189
|
- def encode_com({:com_query, query}) do
|
190
|
- [0x03, query]
|
191
|
- end
|
192
|
-
|
193
|
- # https://dev.mysql.com/doc/internals/en/com-stmt-prepare.html#packet-COM_STMT_PREPARE
|
194
|
- def encode_com({:com_stmt_prepare, query}) do
|
195
|
- [0x16, query]
|
196
|
- end
|
197
|
-
|
198
|
- # https://dev.mysql.com/doc/internals/en/com-stmt-close.html
|
199
|
- def encode_com({:com_stmt_close, statement_id}) do
|
200
|
- [0x19, <<statement_id::uint4>>]
|
201
|
- end
|
202
|
-
|
203
|
- # https://dev.mysql.com/doc/internals/en/com-stmt-reset.html
|
204
|
- def encode_com({:com_stmt_reset, statement_id}) do
|
205
|
- [0x1A, <<statement_id::uint4>>]
|
206
|
- end
|
207
|
-
|
208
|
- # https://dev.mysql.com/doc/internals/en/com-stmt-execute.html
|
209
|
- def encode_com({:com_stmt_execute, statement_id, params, cursor_type}) do
|
210
|
- command = 0x17
|
211
|
- flags = Map.fetch!(@cursor_types, cursor_type)
|
212
|
-
|
213
|
- # Always 0x01
|
214
|
- iteration_count = 0x01
|
215
|
-
|
216
|
- new_params_bound_flag = 1
|
217
|
- {null_bitmap, types, values} = encode_params(params)
|
218
|
-
|
219
|
- <<
|
220
|
- command,
|
221
|
- statement_id::uint4,
|
222
|
- flags::uint1,
|
223
|
- iteration_count::uint4,
|
224
|
- null_bitmap::bitstring,
|
225
|
- new_params_bound_flag::uint1,
|
226
|
- types::binary,
|
227
|
- values::binary
|
228
|
- >>
|
229
|
- end
|
230
|
-
|
231
|
- # https://dev.mysql.com/doc/internals/en/com-stmt-fetch.html
|
232
|
- def encode_com({:com_stmt_fetch, statement_id, num_rows}) do
|
233
|
- <<
|
234
|
- 0x1C,
|
235
|
- statement_id::uint4,
|
236
|
- num_rows::uint4
|
237
|
- >>
|
238
|
- end
|
239
|
-
|
240
|
- # https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-COM_QUERY_Response
|
241
|
- def decode_com_query_response(<<header, rest::binary>>, "", :initial)
|
242
|
- when header in [0x00, 0xFF] do
|
243
|
- {:halt, decode_generic_response(rest, header)}
|
244
|
- end
|
245
|
-
|
246
|
- def decode_com_query_response(payload, next_data, state) do
|
247
|
- decode_resultset(payload, next_data, state, &Values.decode_text_row/2)
|
248
|
- end
|
249
|
-
|
250
|
- def decode_com_stmt_prepare_response(
|
251
|
- <<0x00, statement_id::uint4, num_columns::uint2, num_params::uint2, 0,
|
252
|
- num_warnings::uint2>>,
|
253
|
- next_data,
|
254
|
- :initial
|
255
|
- ) do
|
256
|
- result =
|
257
|
- com_stmt_prepare_ok(
|
258
|
- statement_id: statement_id,
|
259
|
- num_columns: num_columns,
|
260
|
- num_params: num_params,
|
261
|
- num_warnings: num_warnings
|
262
|
- )
|
263
|
-
|
264
|
- if num_columns + num_params > 0 do
|
265
|
- {:cont, {result, num_columns + num_params}}
|
266
|
- else
|
267
|
- "" = next_data
|
268
|
- {:halt, result}
|
269
|
- end
|
270
|
- end
|
271
|
-
|
272
|
- def decode_com_stmt_prepare_response(<<rest::binary>>, "", :initial) do
|
273
|
- {:halt, decode_generic_response(rest)}
|
274
|
- end
|
275
|
-
|
276
|
- # for now, we're simply consuming column_definition packets for params and columns,
|
277
|
- # we might decode them in the future.
|
278
|
- def decode_com_stmt_prepare_response(_payload, next_data, {com_stmt_prepare_ok, packets_left}) do
|
279
|
- if packets_left > 1 do
|
280
|
- {:cont, {com_stmt_prepare_ok, packets_left - 1}}
|
281
|
- else
|
282
|
- "" = next_data
|
283
|
- {:halt, com_stmt_prepare_ok}
|
284
|
- end
|
285
|
- end
|
286
|
-
|
287
|
- defp encode_params(params) do
|
288
|
- null_type = 0x06
|
289
|
-
|
290
|
- {count, null_bitmap, types, values} =
|
291
|
- Enum.reduce(params, {0, 0, <<>>, <<>>}, fn
|
292
|
- value, {idx, null_bitmap, types, values} ->
|
293
|
- null_value = if value == nil, do: 1, else: 0
|
294
|
- null_bitmap = null_bitmap ||| null_value <<< idx
|
295
|
-
|
296
|
- if value == nil do
|
297
|
- {idx + 1, null_bitmap, <<types::binary, null_type, unsigned_flag(value)>>, values}
|
298
|
- else
|
299
|
- {type, binary} = Values.encode_binary_value(value)
|
300
|
- type = Values.type_atom_to_code(type)
|
301
|
-
|
302
|
- {idx + 1, null_bitmap, <<types::binary, type, unsigned_flag(value)>>,
|
303
|
- <<values::binary, binary::binary>>}
|
304
|
- end
|
305
|
- end)
|
306
|
-
|
307
|
- null_bitmap_size = div(count + 7, 8)
|
308
|
- {<<null_bitmap::uint(null_bitmap_size)>>, types, values}
|
309
|
- end
|
310
|
-
|
311
|
- defp unsigned_flag(value) when is_integer(value) and value >= 1 <<< 63, do: 0x80
|
312
|
- defp unsigned_flag(_), do: 0x00
|
313
|
-
|
314
|
- # https://dev.mysql.com/doc/internals/en/com-stmt-execute-response.html
|
315
|
- def decode_com_stmt_execute_response(<<header, rest::binary>>, "", :initial)
|
316
|
- when header in [0x00, 0xFF] do
|
317
|
- {:halt, decode_generic_response(rest, header)}
|
318
|
- end
|
319
|
-
|
320
|
- def decode_com_stmt_execute_response(payload, next_data, state) do
|
321
|
- decode_resultset(payload, next_data, state, &Values.decode_binary_row/2)
|
322
|
- end
|
323
|
-
|
324
|
- # https://dev.mysql.com/doc/internals/en/com-stmt-fetch-response.html
|
325
|
- def decode_com_stmt_fetch_response(<<0xFF, rest::binary>>, "", {:initial, _column_defs}) do
|
326
|
- {:halt, decode_generic_response(rest, 0xFF)}
|
327
|
- end
|
328
|
-
|
329
|
- def decode_com_stmt_fetch_response(payload, next_data, {:initial, column_defs}) do
|
330
|
- decode_com_stmt_fetch_response(payload, next_data, {:rows, column_defs, 0, []})
|
331
|
- end
|
332
|
-
|
333
|
- def decode_com_stmt_fetch_response(payload, next_data, state) do
|
334
|
- decode_resultset(payload, next_data, state, &Values.decode_binary_row/2)
|
335
|
- end
|
336
|
-
|
337
|
- def decode_column_def(<<3, "def", rest::binary>>) do
|
338
|
- {_schema, rest} = take_string_lenenc(rest)
|
339
|
- {_table, rest} = take_string_lenenc(rest)
|
340
|
- {_org_table, rest} = take_string_lenenc(rest)
|
341
|
- {name, rest} = take_string_lenenc(rest)
|
342
|
- {_org_name, rest} = take_string_lenenc(rest)
|
343
|
-
|
344
|
- <<
|
345
|
- 0x0C,
|
346
|
- _character_set::uint2,
|
347
|
- _column_length::uint4,
|
348
|
- type::uint1,
|
349
|
- flags::uint2,
|
350
|
- _decimals::uint1,
|
351
|
- 0::uint2
|
352
|
- >> = rest
|
353
|
-
|
354
|
- column_def(
|
355
|
- name: name,
|
356
|
- type: Values.type_code_to_atom(type),
|
357
|
- flags: flags,
|
358
|
- unsigned?: has_column_flag?(flags, :unsigned_flag)
|
359
|
- )
|
360
|
- end
|
361
|
-
|
362
|
- defp decode_resultset(payload, _next_data, :initial, _row_decoder) do
|
363
|
- {:cont, {:column_defs, decode_int_lenenc(payload), []}}
|
364
|
- end
|
365
|
-
|
366
|
- defp decode_resultset(payload, _next_data, {:column_defs, num_columns, acc}, _row_decoder) do
|
367
|
- column_def = decode_column_def(payload)
|
368
|
- acc = [column_def | acc]
|
369
|
-
|
370
|
- if num_columns > 1 do
|
371
|
- {:cont, {:column_defs, num_columns - 1, acc}}
|
372
|
- else
|
373
|
- {:cont, {:rows, Enum.reverse(acc), 0, []}}
|
374
|
- end
|
375
|
- end
|
376
|
-
|
377
|
- defp decode_resultset(
|
378
|
- <<0xFE, 0, 0, status_flags::uint2, num_warnings::uint2>>,
|
379
|
- _next_data,
|
380
|
- {:rows, column_defs, num_rows, acc},
|
381
|
- _row_decoder
|
382
|
- ) do
|
383
|
- resultset =
|
384
|
- resultset(
|
385
|
- column_defs: column_defs,
|
386
|
- num_rows: num_rows,
|
387
|
- rows: Enum.reverse(acc),
|
388
|
- num_warnings: num_warnings,
|
389
|
- status_flags: status_flags
|
390
|
- )
|
391
|
-
|
392
|
- if has_status_flag?(status_flags, :server_more_results_exists) do
|
393
|
- {:cont, {:trailing_ok_packet, resultset}}
|
394
|
- else
|
395
|
- {:halt, resultset}
|
396
|
- end
|
397
|
- end
|
398
|
-
|
399
|
- defp decode_resultset(payload, _next_data, {:rows, column_defs, num_rows, acc}, row_decoder) do
|
400
|
- row = row_decoder.(payload, column_defs)
|
401
|
- {:cont, {:rows, column_defs, num_rows + 1, [row | acc]}}
|
402
|
- end
|
403
|
-
|
404
|
- defp decode_resultset(payload, "", {:trailing_ok_packet, resultset}, _row_decoder) do
|
405
|
- ok_packet(status_flags: status_flags) = decode_generic_response(payload)
|
406
|
- {:halt, resultset(resultset, status_flags: status_flags)}
|
407
|
- end
|
408
|
-
|
409
|
- defp decode_resultset(_payload, _next_data, {:trailing_ok_packet, _resultset}, _row_decoder) do
|
410
|
- {:error, :multiple_results}
|
411
|
- end
|
412
|
- end
|
changed
lib/myxql/protocol/records.ex
|
@@ -3,9 +3,18 @@ defmodule MyXQL.Protocol.Records do
|
3
3
|
|
4
4
|
import Record
|
5
5
|
|
6
|
+ @default_max_packet_size 16_777_215
|
7
|
+
|
8
|
+ # https://dev.mysql.com/doc/internals/en/character-set.html#packet-Protocol::CharacterSet
|
9
|
+ # utf8mb4
|
10
|
+ @default_charset 45
|
11
|
+
|
6
12
|
# https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html
|
7
13
|
defrecord :ok_packet, [:affected_rows, :last_insert_id, :status_flags, :num_warnings, :info]
|
8
14
|
|
15
|
+ # https://dev.mysql.com/doc/internals/en/packet-EOF_Packet.html
|
16
|
+ defrecord :eof_packet, [:status_flags, :num_warnings]
|
17
|
+
|
9
18
|
# https://dev.mysql.com/doc/internals/en/packet-ERR_Packet.html
|
10
19
|
defrecord :err_packet, [:code, :message]
|
11
20
|
|
|
@@ -21,18 +30,27 @@ defmodule MyXQL.Protocol.Records do
|
21
30
|
]
|
22
31
|
|
23
32
|
# https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::HandshakeResponse
|
24
|
- defrecord :handshake_response_41, [
|
25
|
- :capability_flags,
|
26
|
- :max_packet_size,
|
27
|
- :character_set,
|
28
|
- :username,
|
29
|
- :auth_response,
|
30
|
- :database
|
31
|
- ]
|
33
|
+ defrecord :handshake_response_41,
|
34
|
+ capability_flags: nil,
|
35
|
+ max_packet_size: @default_max_packet_size,
|
36
|
+ character_set: @default_charset,
|
37
|
+ username: nil,
|
38
|
+ database: nil,
|
39
|
+ auth_plugin_name: nil,
|
40
|
+ auth_response: nil
|
41
|
+
|
42
|
+ # https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::SSLRequest
|
43
|
+ defrecord :ssl_request,
|
44
|
+ capability_flags: nil,
|
45
|
+ max_packet_size: @default_max_packet_size,
|
46
|
+ character_set: @default_charset
|
32
47
|
|
33
48
|
# https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest
|
34
49
|
defrecord :auth_switch_request, [:plugin_name, :plugin_data]
|
35
50
|
|
51
|
+ # https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthMoreData
|
52
|
+ defrecord :auth_more_data, [:data]
|
53
|
+
|
36
54
|
# https://dev.mysql.com/doc/internals/en/com-stmt-prepare-response.html#packet-COM_STMT_PREPARE_OK
|
37
55
|
defrecord :com_stmt_prepare_ok, [:statement_id, :num_columns, :num_params, :num_warnings]
|
changed
mix.exs
|
@@ -1,7 +1,7 @@
|
1
1
|
defmodule MyXQL.MixProject do
|
2
2
|
use Mix.Project
|
3
3
|
|
4
|
- @version "0.2.2"
|
4
|
+ @version "0.2.3"
|
5
5
|
@source_url "https://github.com/elixir-ecto/myxql"
|
6
6
|
|
7
7
|
def project() do
|
|
@@ -10,7 +10,8 @@ defmodule MyXQL.MixProject do
|
10
10
|
version: @version,
|
11
11
|
elixir: "~> 1.4",
|
12
12
|
start_permanent: Mix.env() == :prod,
|
13
|
- description: "MySQL 5.7+ driver for Elixir",
|
13
|
+ name: "MyXQL",
|
14
|
+ description: "MySQL 5.5+ driver for Elixir",
|
14
15
|
source_url: @source_url,
|
15
16
|
package: package(),
|
16
17
|
docs: docs(),
|