Skip to content

Better docs for Repos that use Ecto.Adapters.SQL.Adapter #671

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 7, 2025

Conversation

dbernheisel
Copy link
Contributor

@dbernheisel dbernheisel commented Jun 1, 2025

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 on MyApp.query(...) to get any helpful documentation, instead requiring them to either open docs outside of their editor, or have a temporary line pointing to Ecto.Adapters.SQL.query(...) and get the docs, and then set back to MyApp.query(...)

I also switched the examples to mention the more-likely function included in the user's MyRepo first instead of Ecto.Adapters.SQL. Some of the examples where using Repo and others MyRepo-- I changed to MyRepo because 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.

@josevalim
Copy link
Member

Thank you! In order to remove the duplication, I think you could do this:

doc_disconnect_all = """
...
"""

and then use it for disconnect all, as in @doc disconnect_all and inside the __using__ like this:

@doc unquote(doc_disconnect_all)

WDYT?

@dbernheisel
Copy link
Contributor Author

Seems obvious now :) yep I'll rework that.

@dbernheisel dbernheisel force-pushed the db-add-repo-compiled-docs branch from 8fbba22 to ce9d201 Compare June 7, 2025 00:52
@dbernheisel
Copy link
Contributor Author

ready for review!

@josevalim josevalim merged commit 3084d71 into elixir-ecto:master Jun 7, 2025
11 checks passed
@josevalim
Copy link
Member

💚 💙 💜 💛 ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants