Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Writing an Ecto Adapter: Introducing MyXQL

Writing an Ecto Adapter: Introducing MyXQL

In this talk, we will present MyXQL, a new Elixir driver for the MySQL database.

From connecting to TCP socket, through encoding and decoding a binary protocol, all the way to integrating with DBConnection and Ecto, this talk is a whirlwind tour of building a database driver with Elixir. After watching this talk, you should know about the significant parts of the driver, how to make one (or contribute to one!) and lessons learned from working on MyXQL.

Wojtek Mach

August 30, 2019
Tweet

More Decks by Wojtek Mach

Other Decks in Programming

Transcript

  1. What is MyXQL? • Added as built-in MySQL adapter in

    ecto_sql v3.1.0 • (almost) drop-in replacement for the Mariaex- based adapter
  2. What is MyXQL? • Added as built-in MySQL adapter in

    ecto_sql v3.1.0 • (almost) drop-in replacement for the Mariaex- based adapter • Mariaex-based adapter is deprecated in v3.1.5 and removed in upcoming v3.2.
  3. Why MyXQL? • Why not use Mariaex? • History. Mariaex

    started around the time of Elixir 1.0, Ecto 0.3, and MySQL 5.6.
 A lot have changed since! • Expertise • Maintenance
  4. iex> {:ok, sock} = :gen_tcp.connect('localhost', 3306, [:binary, active: false]) {:ok,

    #Port<0.7>} iex> {:ok, data} = :gen_tcp.recv(sock, 0) {:ok, <<74, 0, 0, 0, 10, 56, 46, 48, 46, 49, 54, 0, 41, 3, 0, 0, 109, 12, 72, 1, 75, 19, 110, 65, 0, 255, 255, 45, 2, 0, 255, 195, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 8, 53, 100, 1, ...>>}
  5. iex> {:ok, sock} = :gen_tcp.connect('localhost', 3306, [:binary, active: false]) {:ok,

    #Port<0.7>} iex> {:ok, data} = :gen_tcp.recv(sock, 0) {:ok, <<74, 0, 0, 0, 10, 56, 46, 48, 46, 49, 54, 0, 41, 3, 0, 0, 109, 12, 72, 1, 75, 19, 110, 65, 0, 255, 255, 45, 2, 0, 255, 195, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 8, 53, 100, 1, ...>>} iex> :binpp.pprint(data) 0000 4A 00 00 00 0A 38 2E 30 2E 31 36 00 2D 03 00 00 J....8.0.16.-... 0001 76 13 44 5E 31 4D 3B 35 00 FF FF 2D 02 00 FF C3 v.D^1M;5.ÿÿ-..ÿÃ 0002 15 00 00 00 00 00 00 00 00 00 00 60 5E 27 32 08 ...........`^'2. 0003 70 11 14 5F 3C 23 08 00 63 61 63 68 69 6E 67 5F p.._<#..caching_ 0004 73 68 61 32 5F 70 61 73 73 77 6F 72 64 00 sha2_password. https://hex.pm/packages/binpp
  6. iex> {:ok, sock} = :gen_tcp.connect('localhost', 3306, [:binary, active: false]) {:ok,

    #Port<0.7>} iex> {:ok, data} = :gen_tcp.recv(sock, 0) {:ok, <<74, 0, 0, 0, 10, 56, 46, 48, 46, 49, 54, 0, 41, 3, 0, 0, 109, 12, 72, 1, 75, 19, 110, 65, 0, 255, 255, 45, 2, 0, 255, 195, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 8, 53, 100, 1, ...>>} iex> :binpp.pprint(data) 0000 4A 00 00 00 0A 38 2E 30 2E 31 36 00 2D 03 00 00 J....8.0.16.-... 0001 76 13 44 5E 31 4D 3B 35 00 FF FF 2D 02 00 FF C3 v.D^1M;5.ÿÿ-..ÿÃ 0002 15 00 00 00 00 00 00 00 00 00 00 60 5E 27 32 08 ...........`^'2. 0003 70 11 14 5F 3C 23 08 00 63 61 63 68 69 6E 67 5F p.._<#..caching_ 0004 73 68 61 32 5F 70 61 73 73 77 6F 72 64 00 sha2_password. https://hex.pm/packages/binpp
  7. MySQL Packet iex> << payload_length, # 3-byte/24-bit integer with little

    endianness sequence_id, # 1-byte/24-bit integer with little endianness payload # n-byte binary >> = data
  8. MySQL Packet iex> << payload_length::size(24)-little-integer, # 3-byte/24-bit integer with little

    endianness sequence_id, # 1-byte/24-bit integer with little endianness payload # n-byte binary >> = data
  9. MySQL Packet iex> << payload_length::size(24)-little-integer, # 3-byte/24-bit integer with little

    endianness sequence_id::size(8)-little-integer, # 1-byte/24-bit integer with little endianness payload # n-byte binary >> = data
  10. MySQL Packet iex> << payload_length::size(24)-little-integer, # 3-byte/24-bit integer with little

    endianness sequence_id::size(8)-little-integer, # 1-byte/24-bit integer with little endianness payload::size(payload_length)-binary # n-byte binary >> = data
  11. MySQL Packet iex> << payload_length::size(3)-unit(8)—little, # 3-byte/24-bit integer with little

    endianness sequence_id::size(1)-unit(8)—little, # 1-byte/24-bit integer with little endianness payload::size(payload_length)-binary # n-byte binary >> = data
  12. MySQL Packet iex> << payload_length::size(24)-little-integer, # 3-byte/24-bit integer with little

    endianness sequence_id::size(8)-little-integer, # 1-byte/24-bit integer with little endianness payload::size(payload_length)-binary # n-byte binary >> = data iex> payload_length 74
  13. MySQL Packet iex> << payload_length::size(24)-little-integer, # 3-byte/24-bit integer with little

    endianness sequence_id::size(8)-little-integer, # 1-byte/24-bit integer with little endianness payload::size(payload_length)-binary # n-byte binary >> = data iex> payload_length 74 iex> sequence_id 0
  14. MySQL Packet iex> << payload_length::size(24)-little-integer, # 3-byte/24-bit integer with little

    endianness sequence_id::size(8)-little-integer, # 1-byte/24-bit integer with little endianness payload::size(payload_length)-binary # n-byte binary >> = data iex> payload_length 74 iex> sequence_id 0 iex> payload <<10, 56, 46, 48, 46, 49, 54, 0, 53, 3, 0, 0, 88, 101, 53, 110, 39, 101, 117, 71, 0, 255, 255, 45, 2, 0, 255, 195, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70, 51, 58, 121, 78, 3, 31, 40, 15, 56, 116, ...>>
  15. Initial Handshake Packet iex> <<0x0A, rest::binary>> = payload iex> [server_version,

    rest] = :binary.split(rest, <<0x00>>) iex> <<connection_id::size(32)-little, rest::binary>> = rest
  16. Handshake Response iex> use Bitwise iex> capability_flags = 0x00000200 |||

    0x00008000 ||| 0x00080000 iex> max_packet_size = 65535 iex> charset = 0x21 iex> username = "myxql_test" iex> auth_response = <<0x00>> iex> client_auth_plugin = "caching_sha2_password" iex> payload = << capability_flags::size(32)-little, max_packet_size::size(32)-little, charset, 0::8*23, username::binary, 0x00, auth_response::binary, client_auth_plugin::binary, 0x00 >> iex> sequence_id = 1 iex> data = <<byte_size(payload)::24-little, sequence_id, payload::binary>>
  17. Handshake Response iex> use Bitwise iex> capability_flags = 0x00000200 |||

    0x00008000 ||| 0x00080000 iex> max_packet_size = 65535 iex> charset = 0x21 iex> username = "myxql_test" iex> auth_response = <<0x00>> iex> client_auth_plugin = "caching_sha2_password" iex> payload = << capability_flags::size(32)-little, max_packet_size::size(32)-little, charset, 0::8*23, username::binary, 0x00, auth_response::binary, client_auth_plugin::binary, 0x00 >> iex> sequence_id = 1 iex> data = <<byte_size(payload)::24-little, sequence_id, payload::binary>>
  18. Handshake Response iex> use Bitwise iex> capability_flags = 0x00000200 |||

    0x00008000 ||| 0x00080000 iex> max_packet_size = 65535 iex> charset = 0x21 iex> username = "myxql_test" iex> auth_response = <<0x00>> iex> client_auth_plugin = "caching_sha2_password" iex> payload = << capability_flags::32-little, max_packet_size::32-little, charset, 0::8*23, username::binary, 0x00, auth_response::binary, client_auth_plugin::binary, 0x00 >> iex> sequence_id = 1 iex> data = <<byte_size(payload)::24-little, sequence_id, payload::binary>>
  19. Handshake Response iex> use Bitwise iex> capability_flags = 0x00000200 |||

    0x00008000 ||| 0x00080000 iex> max_packet_size = 65535 iex> charset = 0x21 iex> username = "myxql_test" iex> auth_response = <<0x00>> iex> client_auth_plugin = "caching_sha2_password" iex> payload = << capability_flags::32-little, max_packet_size::32-little, charset, 0::8*23, username::binary, 0x00, auth_response::binary, client_auth_plugin::binary, 0x00 >> iex> sequence_id = 1 iex> data = <<byte_size(payload)::24-little, sequence_id, payload::binary>>
  20. Handshake Response iex> :ok = :gen_tcp.send(sock, data) iex> {:ok, data}

    = :gen_tcp.recv(sock, 0) iex> <<payload_length::24-little, sequence_id::8, payload::binary>> = data
  21. Handshake Response iex> :ok = :gen_tcp.send(sock, data) iex> {:ok, data}

    = :gen_tcp.recv(sock, 0) iex> <<payload_length::24-little, sequence_id::8, payload::binary>> = data iex> :binpp.pprint(payload) 0000 00 00 00 02 00 00 00
  22. int<1> defmodule MyXQL.Types do def take_int1(data) do <<value::8-little-integer, rest::binary>> =

    data {value, rest} end end iex> MyXQL.Types.take_int1(<<1, 2, 3>>) {1, <<2, 3>>}
  23. int<n> defmodule MyXQL.Types do def take_fixed_length_integer(data, size) do <<value::little-integer-size(size)-unit(8), rest::binary>>

    = data {value, rest} end end iex> MyXQL.Types.take_fixed_length_integer(<<1, 2, 3>>, 2) {513, <<3>>}
  24. int<1> (again) defmodule MyXQL.Types do defmacro int1() do quote do

    size(8)-little-integer end end end iex> import MyXQL.Types iex> <<value::int1, rest::binary>> = <<1, 2, 3>>
  25. int<n> (again) defmodule MyXQL.Types do defmacro int(size) do quote do

    size(unquote(size))-unit(8)-little-integer end end end iex> import MyXQL.Types iex> <<value::int(1), rest::binary>> = <<1, 2, 3>>
  26. OK_Packet defrecord :ok_packet, [:affected_rows, :last_insert_id, :status_flags, :warning_count] def decode_ok_packet(data) do

    <<0x00, rest::binary>> = data {affected_rows, rest} = take_length_encoded_integer(rest) {last_insert_id, rest} = take_length_encoded_integer(rest) end
  27. OK_Packet defrecord :ok_packet, [:affected_rows, :last_insert_id, :status_flags, :warning_count] def decode_ok_packet(data) do

    <<0x00, rest::binary>> = data {affected_rows, rest} = take_length_encoded_integer(rest) {last_insert_id, rest} = take_length_encoded_integer(rest) << status_flags::int(2), warning_count::int(2) >> = rest end
  28. OK_Packet defrecord :ok_packet, [:affected_rows, :last_insert_id, :status_flags, :warning_count] def decode_ok_packet(data) do

    <<0x00, rest::binary>> = data {affected_rows, rest} = take_length_encoded_integer(rest) {last_insert_id, rest} = take_length_encoded_integer(rest) << status_flags::int(2), warning_count::int(2) >> = rest ok_packet( affected_rows: affected_rows, last_insert_id: last_insert_id status_flags: status_flags, warning_count: warning_count ) end
  29. Production-quality driver • maintaining a connection pool to talk to

    the DB efficiently from multiple processes • not overloading the DB • attempting to re-connect to the DB if connection is lost • supporting common DB features like prepared statements, transactions, streaming etc • Fast!
  30. defmodule MyXQL.Connection do use DBConnection @impl true def connect(opts) do

    sock_opts = [:binary, active: false] case :gen_tcp.connect(opts[:host], opts[:port], sock_opts) do {:ok, sock} -> {:error, reason} -> end end end
  31. defmodule MyXQL.Connection do use DBConnection defstruct [:sock] @impl true def

    connect(opts) do sock_opts = [:binary, active: false] case :gen_tcp.connect(opts[:host], opts[:port], sock_opts) do {:ok, sock} -> handshake(username, sock) {:error, reason} -> {:error, %MyXQL.Error{message: "error when connecting: #{inspect(reason)}"}} end end defp handshake(username, sock) do end end
  32. defmodule MyXQL.Connection do use DBConnection import MyXQL.Protocol defstruct [:sock] @impl

    true def connect(opts) do sock_opts = [:binary, active: false] case :gen_tcp.connect(opts[:host], opts[:port], sock_opts) do {:ok, sock} -> handshake(username, sock) {:error, reason} -> {:error, %MyXQL.Error{message: "error when connecting: #{inspect(reason)}"}} end end defp handshake(username, sock) do with {:ok, data} <- :gen_tcp.recv(sock, 0), end end end
  33. defmodule MyXQL.Connection do use DBConnection import MyXQL.Protocol defstruct [:sock] @impl

    true def connect(opts) do sock_opts = [:binary, active: false] case :gen_tcp.connect(opts[:host], opts[:port], sock_opts) do {:ok, sock} -> handshake(username, sock) {:error, reason} -> {:error, %MyXQL.Error{message: "error when connecting: #{inspect(reason)}"}} end end defp handshake(username, sock) do with {:ok, data} <- :gen_tcp.recv(sock, 0), initial_handshake_packet() = decode_initial_handshake_packet(data), end end end
  34. defmodule MyXQL.Connection do use DBConnection import MyXQL.Protocol defstruct [:sock] @impl

    true def connect(opts) do sock_opts = [:binary, active: false] case :gen_tcp.connect(opts[:host], opts[:port], sock_opts) do {:ok, sock} -> handshake(username, sock) {:error, reason} -> {:error, %MyXQL.Error{message: "error when connecting: #{inspect(reason)}"}} end end defp handshake(username, sock) do with {:ok, data} <- :gen_tcp.recv(sock, 0), initial_handshake_packet() = decode_initial_handshake_packet(data), data = encode_handshake_response_packet(username), end end end
  35. defmodule MyXQL.Connection do use DBConnection import MyXQL.Protocol defstruct [:sock] @impl

    true def connect(opts) do sock_opts = [:binary, active: false] case :gen_tcp.connect(opts[:host], opts[:port], sock_opts) do {:ok, sock} -> handshake(username, sock) {:error, reason} -> {:error, %MyXQL.Error{message: "error when connecting: #{inspect(reason)}"}} end end defp handshake(username, sock) do with {:ok, data} <- :gen_tcp.recv(sock, 0), initial_handshake_packet() = decode_initial_handshake_packet(data), data = encode_handshake_response_packet(username), :ok <- :gen_tcp.send(sock, data), end end end
  36. defmodule MyXQL.Connection do use DBConnection import MyXQL.Protocol defstruct [:sock] @impl

    true def connect(opts) do sock_opts = [:binary, active: false] case :gen_tcp.connect(opts[:host], opts[:port], sock_opts) do {:ok, sock} -> handshake(username, sock) {:error, reason} -> {:error, %MyXQL.Error{message: "error when connecting: #{inspect(reason)}"}} end end defp handshake(username, sock) do with {:ok, data} <- :gen_tcp.recv(sock, 0), initial_handshake_packet() = decode_initial_handshake_packet(data), data = encode_handshake_response_packet(username), :ok <- :gen_tcp.send(sock, data), {:ok, data} <- :gen_tcp.recv(sock, 0), end end end
  37. defmodule MyXQL.Connection do use DBConnection import MyXQL.Protocol defstruct [:sock] @impl

    true def connect(opts) do sock_opts = [:binary, active: false] case :gen_tcp.connect(opts[:host], opts[:port], sock_opts) do {:ok, sock} -> handshake(username, sock) {:error, reason} -> {:error, %MyXQL.Error{message: "error when connecting: #{inspect(reason)}"}} end end defp handshake(username, sock) do with {:ok, data} <- :gen_tcp.recv(sock, 0), initial_handshake_packet() = decode_initial_handshake_packet(data), data = encode_handshake_response_packet(username), :ok <- :gen_tcp.send(sock, data), {:ok, data} <- :gen_tcp.recv(sock, 0), ok_packet() <- decode_handshake_response_packet(data) do end end end
  38. defmodule MyXQL.Connection do use DBConnection import MyXQL.Protocol defstruct [:sock] @impl

    true def connect(opts) do sock_opts = [:binary, active: false] case :gen_tcp.connect(opts[:host], opts[:port], sock_opts) do {:ok, sock} -> handshake(username, sock) {:error, reason} -> {:error, %MyXQL.Error{message: "error when connecting: #{inspect(reason)}"}} end end defp handshake(username, sock) do with {:ok, data} <- :gen_tcp.recv(sock, 0), initial_handshake_packet() = decode_initial_handshake_packet(data), data = encode_handshake_response_packet(username), :ok <- :gen_tcp.send(sock, data), {:ok, data} <- :gen_tcp.recv(sock, 0), ok_packet() <- decode_handshake_response_packet(data) do {:ok, %__MODULE__{sock: sock}} end end end
  39. defmodule MyXQL.Result do defstruct [:columns, :rows] end defmodule MyXQL.Query do

    defstruct [:statement, :statement_id] defimpl DBConnection.Query do def parse(query, _opts), do: query def describe(query, _opts), do: query def encode(_query, params, _opts), do: params def decode(_query, result, _opts), do: result end end
  40. defmodule MyXQL do def prepare(conn, name, statement, opts) do query

    = %MyXQL.Query{name: name, statement: statement} DBConnection.prepare(conn, query, opts) end end
  41. defmodule MyXQL.Connection do # ... @impl true def handle_prepare(%MyXQL.Query{statement: statement},

    _opts, state) do data = encode_com_stmt_prepare(query.statement) end end
  42. defmodule MyXQL.Connection do # ... @impl true def handle_prepare(%MyXQL.Query{statement: statement},

    _opts, state) do data = encode_com_stmt_prepare(query.statement) with :ok <- sock_send(data, state), end end end
  43. defmodule MyXQL.Connection do # ... @impl true def handle_prepare(%MyXQL.Query{statement: statement},

    _opts, state) do data = encode_com_stmt_prepare(query.statement) with :ok <- sock_send(data, state), {:ok, data} <- sock_recv(state), end end end
  44. defmodule MyXQL.Connection do # ... @impl true def handle_prepare(%MyXQL.Query{statement: statement},

    _opts, state) do data = encode_com_stmt_prepare(query.statement) with :ok <- sock_send(data, state), {:ok, data} <- sock_recv(state), com_stmt_prepare_ok(statement_id: statement_id) <- decode_com_stmt_prepare_response(data) do end end end
  45. defmodule MyXQL.Connection do # ... @impl true def handle_prepare(%MyXQL.Query{statement: statement},

    _opts, state) do data = encode_com_stmt_prepare(query.statement) with :ok <- sock_send(data, state), {:ok, data} <- sock_recv(state), com_stmt_prepare_ok(statement_id: statement_id) <- decode_com_stmt_prepare_response(data) do query = %{query | statement_id: statement_id} end end end
  46. defmodule MyXQL.Connection do # ... @impl true def handle_prepare(%MyXQL.Query{statement: statement},

    _opts, state) do data = encode_com_stmt_prepare(query.statement) with :ok <- sock_send(data, state), {:ok, data} <- sock_recv(state), com_stmt_prepare_ok(statement_id: statement_id) <- decode_com_stmt_prepare_response(data) do query = %{query | statement_id: statement_id} {:ok, query, state} end end end
  47. defmodule MyXQL.Connection do # ... @impl true def handle_prepare(%MyXQL.Query{statement: statement},

    _opts, state) do data = encode_com_stmt_prepare(query.statement) with :ok <- sock_send(data, state), {:ok, data} <- sock_recv(state), com_stmt_prepare_ok(statement_id: statement_id) <- decode_com_stmt_prepare_response(data) do query = %{query | statement_id: statement_id} {:ok, query, state} else end end end
  48. defmodule MyXQL.Connection do # ... @impl true def handle_prepare(%MyXQL.Query{statement: statement},

    _opts, state) do data = encode_com_stmt_prepare(query.statement) with :ok <- sock_send(data, state), {:ok, data} <- sock_recv(state), com_stmt_prepare_ok(statement_id: statement_id) <- decode_com_stmt_prepare_response(data) do query = %{query | statement_id: statement_id} {:ok, query, state} else err_packet(message: message) -> {:error, %MyXQL.Error{message: "error when preparing query: #{message}"}, state} end end end
  49. defmodule MyXQL.Connection do # ... @impl true def handle_prepare(%MyXQL.Query{statement: statement},

    _opts, state) do data = encode_com_stmt_prepare(query.statement) with :ok <- sock_send(data, state), {:ok, data} <- sock_recv(state), com_stmt_prepare_ok(statement_id: statement_id) <- decode_com_stmt_prepare_response(data) do query = %{query | statement_id: statement_id} {:ok, query, state} else err_packet(message: message) -> {:error, %MyXQL.Error{message: "error when preparing query: #{message}"}, state} {:error, reason} -> {:disconnect, %MyXQL.Error{message: "error when preparing query: #{inspect(reason)}"}, state} end end end
  50. defmodule MyXQL.Connection do # ... @impl true def handle_prepare(%MyXQL.Query{statement: statement},

    _opts, state) do data = encode_com_stmt_prepare(query.statement) with :ok <- sock_send(data, state), {:ok, data} <- sock_recv(state), com_stmt_prepare_ok(statement_id: statement_id) <- decode_com_stmt_prepare_response(data) do query = %{query | statement_id: statement_id} {:ok, query, state} else err_packet(message: message) -> {:error, %MyXQL.Error{message: "error when preparing query: #{message}"}, state} {:error, reason} -> {:disconnect, %MyXQL.Error{message: "error when preparing query: #{inspect(reason)}"}, state} end end defp sock_send(data, state), do: :gen_tcp.recv(state.sock, data, :infinity) defp sock_recv(state), do: :gen_tcp.send(state.sock, data) end
  51. defmodule MyXQL.Connection do use DBConnection @impl true def connect(opts) @impl

    true def disconnect(exception, state) @impl true def checkout(state) @impl true def checkin(state) end
  52. defmodule MyXQL.Connection do use DBConnection @impl true def connect(opts) @impl

    true def disconnect(exception, state) @impl true def checkout(state) @impl true def checkin(state) @impl true def ping(state) end
  53. defmodule MyXQL.Connection do use DBConnection @impl true def connect(opts) @impl

    true def disconnect(exception, state) @impl true def checkout(state) @impl true def checkin(state) @impl true def ping(state) @impl true def handle_prepare(query, opts, state) @impl true def handle_execute(query, params, opts, state) end
  54. defmodule MyXQL.Connection do use DBConnection @impl true def connect(opts) @impl

    true def disconnect(exception, state) @impl true def checkout(state) @impl true def checkin(state) @impl true def ping(state) @impl true def handle_prepare(query, opts, state) @impl true def handle_execute(query, params, opts, state) # more callbacks: transactions! end
  55. defmodule MyXQL.Connection do use DBConnection @impl true def connect(opts) @impl

    true def disconnect(exception, state) @impl true def checkout(state) @impl true def checkin(state) @impl true def ping(state) @impl true def handle_prepare(query, opts, state) @impl true def handle_execute(query, params, opts, state) # more callbacks: transactions! # even more callabacks: cursors! end
  56. defmodule MyXQL.Client do def connect(opts) do end def disconnect(exception, state)

    do end def com_stmt_prepare(statement, state) do end def com_stmt_execute(statement_id, params, state) do end # ... end
  57. defmodule MyXQL.Client do def connect(opts) do end def disconnect(exception, state)

    do end def com_stmt_prepare(statement, state) do end def com_stmt_execute(statement_id, params, state) do end # ... end defmodule MyXQL.Connection do @impl true def connect(opts) do case MyXQL.Client.connect(opts) do # ... end end # ... end
  58. (some of) Ecto Features • Changesets to cast and validate

    data before persisting into the DB • Composing Queries instead of concatenating strings • Defining Schemas that map DB data into Elixir structs • Using mix ecto.create, mix ecto.migrate etc • Using SQL Sandbox • Tap into greater Ecto ecosystem: integration with Phoenix, pagination libraries, admin builders, etc • And more!
  59. Adapter Specifications • Ecto.Adapter – minimal API required from adapters

    • Ecto.Adapter.Queryable – plan, prepare, and execute queries leveraging query cache • Ecto.Adapter.Schema – insert, update, and delete structs as well as autogenerate IDs • Ecto.Adapter.Storage – storage API used by e.g. mix ecto.create and mix ecto.drop • Ecto.Adapter.Transaction – transactions API
  60. Example: Constraints $ mysql -u root myxql_test mysql> CREATE TABLE

    users (email VARCHAR(255) UNIQUE); Query OK, 0 rows affected (0.17 sec)
  61. Example: Constraints $ mysql -u root myxql_test mysql> CREATE TABLE

    users (email VARCHAR(255) UNIQUE); Query OK, 0 rows affected (0.17 sec) mysql> INSERT INTO users VALUES ('[email protected]'); Query OK, 1 row affected (0.08 sec) mysql> INSERT INTO users VALUES ('[email protected]'); ERROR 1062 (23000): Duplicate entry '[email protected]' for key 'email'
  62. Example: Constraints defmodule MyXQL.EctoAdapter.Connection do # ... @impl true def

    to_constraints(%MyXQL.Error{mysql: %{code: 1062}, message: message}) do case :binary.split(message, " for key ") do [_, quoted] -> [unique: strip_quotes(quoted)] _ -> [] end end end
  63. Example: Constraints defmodule MyXQL.EctoAdapter.Connection do # ... @impl true def

    to_constraints(%MyXQL.Error{mysql: %{code: 1062}, message: message}) do case :binary.split(message, " for key ") do [_, quoted] -> [unique: strip_quotes(quoted)] _ -> [] end end end
  64. Example: Constraints defmodule MyXQL.EctoAdapter.Connection do # ... @impl true def

    to_constraints(%MyXQL.Error{mysql: %{code: 1062}, message: message}) do case :binary.split(message, " for key ") do [_, quoted] -> [unique: strip_quotes(quoted)] _ -> [] end end end
  65. Example: Normalizing Data defmodule MyXQL.EctoAdapter do # ... @impl true

    def loaders(:boolean, type), do: [&bool_decode/1, type] # ... def loaders(_, type), do: [type] end
  66. Example: Normalizing Data defmodule MyXQL.EctoAdapter do # ... @impl true

    def loaders(:boolean, type), do: [&bool_decode/1, type] # ... def loaders(_, type), do: [type] defp bool_decode(<<0>>), do: {:ok, false} defp bool_decode(<<1>>), do: {:ok, true} defp bool_decode(0), do: {:ok, false} defp bool_decode(1), do: {:ok, true} defp bool_decode(other), do: {:ok, other} end
  67. Tags & Excludes # test/myxql/client_test.exs defmodule MyXQL.ClientTest do @tag ssl:

    true test "default auth plugin (ssl)" do end @tag ssl: false test "client requires ssl but server does not support it" do end end
  68. Tags & Excludes # test/test_helper.exs defmodule TestHelper do def supports_ssl?()

    do end def excludes() do exclude = [] exclude = [{:ssl, not supports_ssl?()} | exclude] # ... exclude end end ExUnit.start(exclude: TestHelper.excludes())
  69. Tags & Excludes # test/test_helper.exs defmodule TestHelper do def supports_ssl?()

    do mysql!("SELECT @@have_ssl") == "@@have_ssl\nYES\n" end def excludes() do exclude = [] exclude = [{:ssl, not supports_ssl?()} | exclude] # ... exclude end end ExUnit.start(exclude: TestHelper.excludes())
  70. Links • https://github.com/elixir-ecto/myxql • https://hexdocs.pm/myxql • “Building a new MySQL

    adapter for Ecto” blog series • Part I: Hello World • Part II: Encoding/Decoding • Part III: DBConnection Integration • Part IV: Ecto Integration