Better docs for Repos that use Ecto.Adapters.SQL.Adapter
#671
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
The functions added by
Ecto.Adapters.SQL.Adapter.__before_compile__into Repos have some sparse documentation telling the user to go look elsewhere to the underlying function; this makes it harder for folks using LSPs and and their cursor is onMyApp.query(...)to get any helpful documentation, instead requiring them to either open docs outside of their editor, or have a temporary line pointing toEcto.Adapters.SQL.query(...)and get the docs, and then set back toMyApp.query(...)I also switched the examples to mention the more-likely function included in the user's
MyRepofirst instead ofEcto.Adapters.SQL. Some of the examples where usingRepoand othersMyRepo-- I changed toMyRepobecause that seemed clearer to me.Before
iex(1)> h Foo.Repo.query def query(sql, params \\ [], opts \\ []) A convenience function for SQL-based repositories that executes the given query. See Ecto.Adapters.SQL.query/4 for more information.After
iex(2)> h Foo.Repo.query def query(sql, params \\ [], opts \\ []) @spec query(iodata(), Ecto.Adapters.SQL.query_params(), Keyword.t()) :: {:ok, Ecto.Adapters.SQL.query_result()} | {:error, Exception.t()} Runs a custom SQL query. If the query was successful, it will return an :ok tuple containing a map with at least two keys: • :num_rows - the number of rows affected • :rows - the result set as a list. nil may be returned instead of the list if the command does not yield any row as result (but still yields the number of affected rows, like a delete command without returning would) ## Options • :log - When false, does not log the query • :timeout - Execute request timeout, accepts: :infinity (default: 15000); ## Examples iex> MyRepo.query("SELECT $1::integer + $2", [40, 2]) {:ok, %{rows: [[42]], num_rows: 1}} iex> Ecto.Adapters.SQL.query(MyRepo, "SELECT $1::integer + $2", [40, 2]) {:ok, %{rows: [[42]], num_rows: 1}}full outputs
iex(31)> h Ecto.Adapters.SQL.query() def query(repo, sql, params \\ [], opts \\ []) @spec query( pid() | Ecto.Repo.t() | Ecto.Adapter.adapter_meta(), iodata(), query_params(), Keyword.t() ) :: {:ok, query_result()} | {:error, Exception.t()} Runs a custom SQL query. If the query was successful, it will return an :ok tuple containing a map with at least two keys: • :num_rows - the number of rows affected • :rows - the result set as a list. nil may be returned instead of the list if the command does not yield any row as result (but still yields the number of affected rows, like a delete command without returning would) ## Options • :log - When false, does not log the query • :timeout - Execute request timeout, accepts: :infinity (default: 15000); ## Examples iex> MyRepo.query("SELECT $1::integer + $2", [40, 2]) {:ok, %{rows: [[42]], num_rows: 1}} iex> Ecto.Adapters.SQL.query(MyRepo, "SELECT $1::integer + $2", [40, 2]) {:ok, %{rows: [[42]], num_rows: 1}} iex(32)> ############################################################# iex(33)> h Foo.Repo.query() def query(sql, params \\ [], opts \\ []) @spec query(iodata(), Ecto.Adapters.SQL.query_params(), Keyword.t()) :: {:ok, Ecto.Adapters.SQL.query_result()} | {:error, Exception.t()} Runs a custom SQL query. If the query was successful, it will return an :ok tuple containing a map with at least two keys: • :num_rows - the number of rows affected • :rows - the result set as a list. nil may be returned instead of the list if the command does not yield any row as result (but still yields the number of affected rows, like a delete command without returning would) ## Options • :log - When false, does not log the query • :timeout - Execute request timeout, accepts: :infinity (default: 15000); ## Examples iex> MyRepo.query("SELECT $1::integer + $2", [40, 2]) {:ok, %{rows: [[42]], num_rows: 1}} iex> Ecto.Adapters.SQL.query(MyRepo, "SELECT $1::integer + $2", [40, 2]) {:ok, %{rows: [[42]], num_rows: 1}} iex(34)> ############################################################# iex(35)> h Ecto.Adapters.SQL.query!() def query!(repo, sql, params \\ [], opts \\ []) @spec query!( pid() | Ecto.Repo.t() | Ecto.Adapter.adapter_meta(), iodata(), query_params(), Keyword.t() ) :: query_result() Same as query/3 but returns result directly without :ok tuple and raises on invalid queries iex(36)> ############################################################# iex(37)> h Foo.Repo.query!() def query!(sql, params \\ [], opts \\ []) @spec query!(iodata(), Ecto.Adapters.SQL.query_params(), Keyword.t()) :: Ecto.Adapters.SQL.query_result() Same as query/3 but returns result directly without :ok tuple and raises on invalid queries iex(38)> ############################################################# iex(39)> h Ecto.Adapters.SQL.query_many() def query_many(repo, sql, params \\ [], opts \\ []) @spec query_many( pid() | Ecto.Repo.t() | Ecto.Adapter.adapter_meta(), iodata(), query_params(), Keyword.t() ) :: {:ok, [query_result()]} | {:error, Exception.t()} Runs a custom SQL query that returns multiple results on the given repo. In case of success, it must return an :ok tuple containing a list of maps with at least two keys: • :num_rows - the number of rows affected • :rows - the result set as a list. nil may be returned instead of the list if the command does not yield any row as result (but still yields the number of affected rows, like a delete command without returning would) ## Options • :log - When false, does not log the query • :timeout - Execute request timeout, accepts: :infinity (default: 15000); ## Examples iex> MyRepo.query_many("SELECT $1; SELECT $2;", [40, 2]) {:ok, [%{rows: [[40]], num_rows: 1}, %{rows: [[2]], num_rows: 1}]} iex> Ecto.Adapters.SQL.query_many(MyRepo, "SELECT $1; SELECT $2;", [40, 2]) {:ok, [%{rows: [[40]], num_rows: 1}, %{rows: [[2]], num_rows: 1}]} iex(40)> ############################################################# iex(41)> h Foo.Repo.query_many() def query_many(sql, params \\ [], opts \\ []) @spec query_many(iodata(), Ecto.Adapters.SQL.query_params(), Keyword.t()) :: {:ok, [Ecto.Adapters.SQL.query_result()]} | {:error, Exception.t()} Runs a custom SQL query that returns multiple results on the given repo. In case of success, it must return an :ok tuple containing a list of maps with at least two keys: • :num_rows - the number of rows affected • :rows - the result set as a list. nil may be returned instead of the list if the command does not yield any row as result (but still yields the number of affected rows, like a delete command without returning would) ## Options • :log - When false, does not log the query • :timeout - Execute request timeout, accepts: :infinity (default: 15000); ## Examples iex> MyRepo.query_many("SELECT $1; SELECT $2;", [40, 2]) {:ok, [%{rows: [[40]], num_rows: 1}, %{rows: [[2]], num_rows: 1}]} iex> Ecto.Adapters.SQL.query_many(MyRepo, "SELECT $1; SELECT $2;", [40, 2]) {:ok, [%{rows: [[40]], num_rows: 1}, %{rows: [[2]], num_rows: 1}]} iex(42)> ############################################################# iex(43)> h Ecto.Adapters.SQL.query_many!() def query_many!(repo, sql, params \\ [], opts \\ []) @spec query_many!( Ecto.Repo.t() | Ecto.Adapter.adapter_meta(), iodata(), query_params(), Keyword.t() ) :: [query_result()] Same as query_many/4 but returns result directly without :ok tuple and raises on invalid queries iex(44)> ############################################################# iex(45)> h Foo.Repo.query_many!() def query_many!(sql, params \\ [], opts \\ []) @spec query_many!(iodata(), Ecto.Adapters.SQL.query_params(), Keyword.t()) :: [ Ecto.Adapters.SQL.query_result() ] Same as query_many/4 but returns result directly without :ok tuple and raises on invalid queries iex(46)> ############################################################# iex(47)> h Ecto.Adapters.SQL.to_sql() def to_sql(kind, repo, queryable) @spec to_sql( :all | :update_all | :delete_all, Ecto.Repo.t(), Ecto.Queryable.t() ) :: {String.t(), query_params()} Converts the given query to SQL according to its kind and the adapter in the given repository. ## Examples The examples below are meant for reference. Each adapter will return a different result: iex> MyRepo.to_sql(:all, Post) {"SELECT p.id, p.title, p.inserted_at, p.created_at FROM posts as p", []} iex> MyRepo.to_sql(:update_all, from(p in Post, update: [set: [title: ^"hello"]])) {"UPDATE posts AS p SET title = $1", ["hello"]} iex> Ecto.Adapters.SQL.to_sql(:all, MyRepo, Post) {"SELECT p.id, p.title, p.inserted_at, p.created_at FROM posts as p", []} iex(48)> ############################################################# iex(49)> h Foo.Repo.to_sql() def to_sql(operation, queryable) @spec to_sql(:all | :update_all | :delete_all, Ecto.Queryable.t()) :: {String.t(), Ecto.Adapters.SQL.query_params()} Converts the given query to SQL according to its kind and the adapter in the given repository. ## Examples The examples below are meant for reference. Each adapter will return a different result: iex> MyRepo.to_sql(:all, Post) {"SELECT p.id, p.title, p.inserted_at, p.created_at FROM posts as p", []} iex> MyRepo.to_sql(:update_all, from(p in Post, update: [set: [title: ^"hello"]])) {"UPDATE posts AS p SET title = $1", ["hello"]} iex> Ecto.Adapters.SQL.to_sql(:all, MyRepo, Post) {"SELECT p.id, p.title, p.inserted_at, p.created_at FROM posts as p", []} iex(50)> ############################################################# iex(51)> h Ecto.Adapters.SQL.explain() def explain(repo, operation, queryable, opts \\ []) @spec explain( pid() | Ecto.Repo.t() | Ecto.Adapter.adapter_meta(), :all | :update_all | :delete_all, Ecto.Queryable.t(), opts :: Keyword.t() ) :: String.t() | Exception.t() | [map()] Executes an EXPLAIN statement or similar for the given query according to its kind and the adapter in the given repository. ## Examples # Postgres iex> MyRepo.explain(:all, Post) "Seq Scan on posts p0 (cost=0.00..12.12 rows=1 width=443)" iex> Ecto.Adapters.SQL.explain(Repo, :all, Post) "Seq Scan on posts p0 (cost=0.00..12.12 rows=1 width=443)" # MySQL iex> MyRepo.explain(:all, from(p in Post, where: p.title == "title")) |> IO.puts() +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | p0 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.0 | Using where | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ # Shared opts iex> MyRepo.explain(:all, Post, analyze: true, timeout: 20_000) "Seq Scan on posts p0 (cost=0.00..11.70 rows=170 width=443) (actual time=0.013..0.013 rows=0 loops=1)\nPlanning Time: 0.031 ms \nExecution Time: 0.021 ms" It's safe to execute it for updates and deletes, no data change will be committed: iex> MyRepo.explain(Repo, :update_all, from(p in Post, update: [set: [title: "new title"]])) "Update on posts p0 (cost=0.00..11.70 rows=170 width=449)\n -> Seq Scan on posts p0 (cost=0.00..11.70 rows=170 width=449)" This function is also available under the repository with name explain: iex> MyRepo.explain(:all, from(p in Post, where: p.title == "title")) "Seq Scan on posts p0 (cost=0.00..12.12 rows=1 width=443)\n Filter: ((title)::text = 'title'::text)" ### Options Built-in adapters support passing opts to the EXPLAIN statement according to the following: Adapter | Supported opts Postgrex | analyze, verbose, costs, settings, buffers, timing, summary, format, plan MyXQL | format All options except format are boolean valued and default to false. The allowed format values are :map, :yaml, and :text: • :map is the deserialized JSON encoding. • :yaml and :text return the result as a string. The built-in adapters support the following formats: • Postgrex: :map, :yaml and :text • MyXQL: :map and :text The :plan option in Postgrex can take the values :custom or :fallback_generic. When :custom is specified, the explain plan generated will consider the specific values of the query parameters that are supplied. When using :fallback_generic, the specific values of the query parameters will be ignored. :fallback_generic does not use PostgreSQL's built-in support for a generic explain plan (available as of PostgreSQL 16), but instead uses a special implementation that works for PostgreSQL versions 12 and above. Defaults to :custom. Any other value passed to opts will be forwarded to the underlying adapter query function, including shared Repo options such as :timeout. Non built-in adapters may have specific behaviour and you should consult their documentation for more details. For version compatibility, please check your database's documentation: • _Postgrex_: PostgreSQL doc (https://www.postgresql.org/docs/current/sql-explain.html). • _MyXQL_: MySQL doc (https://dev.mysql.com/doc/refman/8.0/en/explain.html). iex(52)> ############################################################# iex(53)> h Foo.Repo.explain() def explain(operation, queryable, opts \\ []) @spec explain( :all | :update_all | :delete_all, Ecto.Queryable.t(), opts :: Keyword.t() ) :: String.t() | Exception.t() | [map()] Executes an EXPLAIN statement or similar for the given query according to its kind and the adapter in the given repository. ## Examples # Postgres iex> MyRepo.explain(:all, Post) "Seq Scan on posts p0 (cost=0.00..12.12 rows=1 width=443)" iex> Ecto.Adapters.SQL.explain(Repo, :all, Post) "Seq Scan on posts p0 (cost=0.00..12.12 rows=1 width=443)" # MySQL iex> MyRepo.explain(:all, from(p in Post, where: p.title == "title")) |> IO.puts() +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | p0 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.0 | Using where | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ # Shared opts iex> MyRepo.explain(:all, Post, analyze: true, timeout: 20_000) "Seq Scan on posts p0 (cost=0.00..11.70 rows=170 width=443) (actual time=0.013..0.013 rows=0 loops=1)\nPlanning Time: 0.031 ms \nExecution Time: 0.021 ms" It's safe to execute it for updates and deletes, no data change will be committed: iex> MyRepo.explain(Repo, :update_all, from(p in Post, update: [set: [title: "new title"]])) "Update on posts p0 (cost=0.00..11.70 rows=170 width=449)\n -> Seq Scan on posts p0 (cost=0.00..11.70 rows=170 width=449)" This function is also available under the repository with name explain: iex> MyRepo.explain(:all, from(p in Post, where: p.title == "title")) "Seq Scan on posts p0 (cost=0.00..12.12 rows=1 width=443)\n Filter: ((title)::text = 'title'::text)" ### Options Built-in adapters support passing opts to the EXPLAIN statement according to the following: Adapter | Supported opts Postgrex | analyze, verbose, costs, settings, buffers, timing, summary, format, plan MyXQL | format All options except format are boolean valued and default to false. The allowed format values are :map, :yaml, and :text: • :map is the deserialized JSON encoding. • :yaml and :text return the result as a string. The built-in adapters support the following formats: • Postgrex: :map, :yaml and :text • MyXQL: :map and :text The :plan option in Postgrex can take the values :custom or :fallback_generic. When :custom is specified, the explain plan generated will consider the specific values of the query parameters that are supplied. When using :fallback_generic, the specific values of the query parameters will be ignored. :fallback_generic does not use PostgreSQL's built-in support for a generic explain plan (available as of PostgreSQL 16), but instead uses a special implementation that works for PostgreSQL versions 12 and above. Defaults to :custom. Any other value passed to opts will be forwarded to the underlying adapter query function, including shared Repo options such as :timeout. Non built-in adapters may have specific behaviour and you should consult their documentation for more details. For version compatibility, please check your database's documentation: • _Postgrex_: PostgreSQL doc (https://www.postgresql.org/docs/current/sql-explain.html). • _MyXQL_: MySQL doc (https://dev.mysql.com/doc/refman/8.0/en/explain.html). iex(54)> ############################################################# iex(55)> h Ecto.Adapters.SQL.disconnect_all() def disconnect_all(repo, interval, opts \\ []) @spec disconnect_all( pid() | Ecto.Repo.t() | Ecto.Adapter.adapter_meta(), non_neg_integer(), opts :: Keyword.t() ) :: :ok Forces all connections in the repo pool to disconnect within the given interval. Once this function is called, the pool will disconnect all of its connections as they are checked in or as they are pinged. Checked in connections will be randomly disconnected within the given time interval. Pinged connections are immediately disconnected - as they are idle (according to :idle_interval). If the connection has a backoff configured (which is the case by default), disconnecting means an attempt at a new connection will be done immediately after, without starting a new process for each connection. However, if backoff has been disabled, the connection process will terminate. In such cases, disconnecting all connections may cause the pool supervisor to restart depending on the max_restarts/max_seconds configuration of the pool, so you will want to set those carefully. iex(56)> ############################################################# iex(57)> h Foo.Repo.disconnect_all() def disconnect_all(interval, opts \\ []) @spec disconnect_all(non_neg_integer(), opts :: Keyword.t()) :: :ok Forces all connections in the repo pool to disconnect within the given interval. Once this function is called, the pool will disconnect all of its connections as they are checked in or as they are pinged. Checked in connections will be randomly disconnected within the given time interval. Pinged connections are immediately disconnected - as they are idle (according to :idle_interval). If the connection has a backoff configured (which is the case by default), disconnecting means an attempt at a new connection will be done immediately after, without starting a new process for each connection. However, if backoff has been disabled, the connection process will terminate. In such cases, disconnecting all connections may cause the pool supervisor to restart depending on the max_restarts/max_seconds configuration of the pool, so you will want to set those carefully.