Selecto Output Formats and Execution Workbook
Setup 1) Install Dependencies
Run this cell first. It installs Selecto and related packages from Hex for this Livebook session.
selecto_dep = {:selecto, "~> 0.3.5"}
Mix.install([
selecto_dep,
{:postgrex, "~> 0.17"},
{:ecto_sql, "~> 3.11"},
{:jason, "~> 1.4"},
{:kino, "~> 0.12"}
])
IO.puts("Using Selecto dependency: #{inspect(selecto_dep)}")
Overview
This workbook focuses on execution-time output options and runtime metadata.
-
Standard execution (
execute/2) withrows/columns/aliases -
Rich metadata (
execute_with_metadata/2) -
Single-row contract (
execute_one/2) -
Built-in output formats (
:maps,:json,:csv) -
Advanced formats (
:typed_maps,:stream)
flowchart LR
query[Selecto query]
exec[execute / execute_with_metadata / execute_one]
format[format option]
result[result payload]
query --> exec --> result
query --> format --> result
graph TD
raw[raw tuple rows]
maps[maps]
json[json string]
csv[csv text]
stream[lazy stream]
raw --> maps
raw --> json
raw --> csv
raw --> stream
Before running, initialize the sample database:
cd selecto_examples
mix setup
Setup 2) Connect to Repo and Build Domains
This cell defines a local Repo plus minimal product and order domains.
defmodule OutputLab.Repo do
use Ecto.Repo,
otp_app: :output_lab_livebook,
adapter: Ecto.Adapters.Postgres
end
defmodule OutputLab.Domains do
@moduledoc false
def product_domain do
%{
name: "Products",
source: %{
source_table: "products",
primary_key: :id,
fields: [:id, :name, :sku, :price, :active, :inserted_at],
columns: %{
id: %{type: :integer},
name: %{type: :string},
sku: %{type: :string},
price: %{type: :decimal},
active: %{type: :boolean},
inserted_at: %{type: :utc_datetime}
},
associations: %{}
},
schemas: %{},
joins: %{},
default_selected: ["name", "sku", "price"],
default_order_by: [{"name", :asc}]
}
end
def order_domain do
%{
name: "Orders",
source: %{
source_table: "orders",
primary_key: :id,
fields: [:id, :order_number, :status, :total, :inserted_at],
columns: %{
id: %{type: :integer},
order_number: %{type: :string},
status: %{type: :string},
total: %{type: :decimal},
inserted_at: %{type: :utc_datetime}
},
associations: %{}
},
schemas: %{},
joins: %{},
default_selected: ["order_number", "status", "total"],
default_order_by: [{"total", :desc}]
}
end
end
repo_config = [
database: System.get_env("SELECTO_EXAMPLES_DB", "selecto_examples_dev"),
username: System.get_env("SELECTO_EXAMPLES_DB_USER", "postgres"),
password: System.get_env("SELECTO_EXAMPLES_DB_PASS", "postgres"),
hostname: System.get_env("SELECTO_EXAMPLES_DB_HOST", "localhost"),
port: String.to_integer(System.get_env("SELECTO_EXAMPLES_DB_PORT", "5432")),
pool_size: 5
]
case Process.whereis(OutputLab.Repo) do
nil -> {:ok, _pid} = OutputLab.Repo.start_link(repo_config)
_pid -> :ok
end
config = %{
repo: OutputLab.Repo,
product_domain: OutputLab.Domains.product_domain(),
order_domain: OutputLab.Domains.order_domain()
}
{:ok, p_result} = Ecto.Adapters.SQL.query(config.repo, "select count(*) from products", [])
{:ok, o_result} = Ecto.Adapters.SQL.query(config.repo, "select count(*) from orders", [])
[[product_count]] = p_result.rows
[[order_count]] = o_result.rows
IO.puts("Connected. Products: #{product_count}, Orders: #{order_count}")
config
Setup 3) Define Helper
This helper prints SQL and parameters once for each query.
defmodule OutputLab.Helpers do
@moduledoc false
def explain(label, query) do
Selecto.Livebook.explain(label, query)
end
end
1) Baseline execute/2 (Raw Tuple Output)
execute/2 returns {:ok, {rows, columns, aliases}} by default.
raw_query =
Selecto.configure(config.product_domain, config.repo)
|> Selecto.select(["name", "sku", "price"])
|> Selecto.filter({"active", true})
|> Selecto.order_by({"name", :asc})
|> Selecto.limit(5)
OutputLab.Helpers.explain("Raw execute/2", raw_query)
case Selecto.execute(raw_query) do
{:ok, {rows, columns, aliases}} ->
IO.puts("Columns: #{inspect(columns)}")
IO.puts("Aliases: #{inspect(aliases)}")
IO.inspect(rows, label: "Rows")
{:error, error} ->
IO.puts("Error: #{inspect(error)}")
end
2) execute_with_metadata/2
Get result data plus metadata like SQL text, params, and execution time.
metadata_query =
Selecto.configure(config.order_domain, config.repo)
|> Selecto.select(["order_number", "status", "total"])
|> Selecto.filter({"status", "delivered"})
|> Selecto.order_by({"total", :desc})
|> Selecto.limit(3)
OutputLab.Helpers.explain("execute_with_metadata/2", metadata_query)
case Selecto.execute_with_metadata(metadata_query) do
{:ok, {rows, columns, _aliases}, metadata} ->
IO.puts("Columns: #{inspect(columns)}")
IO.inspect(rows, label: "Rows")
IO.puts("Metadata keys: #{inspect(Map.keys(metadata))}")
IO.puts("Execution time (ms): #{inspect(metadata[:execution_time])}")
IO.puts("Metadata params: #{inspect(metadata[:params])}")
IO.puts("Metadata SQL:\n#{metadata[:sql]}")
{:error, error} ->
IO.puts("Error: #{inspect(error)}")
end
3) execute_one/2 Success and Guard Behavior
execute_one/2 enforces exactly one row.
one_row_query =
Selecto.configure(config.order_domain, config.repo)
|> Selecto.select(["order_number", "status", "total"])
|> Selecto.filter({"status", "delivered"})
|> Selecto.order_by({"total", :desc})
|> Selecto.limit(1)
many_rows_query =
Selecto.configure(config.order_domain, config.repo)
|> Selecto.select(["order_number", "status", "total"])
|> Selecto.filter({"status", "delivered"})
|> Selecto.limit(2)
OutputLab.Helpers.explain("execute_one/2 success case", one_row_query)
case Selecto.execute_one(one_row_query) do
{:ok, {row, aliases}} ->
IO.puts("Single row: #{inspect(row)}")
IO.puts("Aliases: #{inspect(aliases)}")
{:error, error} ->
IO.puts("Unexpected error: #{inspect(error)}")
end
OutputLab.Helpers.explain("execute_one/2 guard case", many_rows_query)
case Selecto.execute_one(many_rows_query) do
{:ok, {row, _aliases}} ->
IO.puts("Unexpected single row: #{inspect(row)}")
{:error, error} ->
IO.puts("Expected guard error: #{inspect(error)}")
end
4) Built-in Output Formats (:maps, :json, :csv)
Switch format at execution time to get payloads ready for APIs or exports.
format_query =
Selecto.configure(config.product_domain, config.repo)
|> Selecto.select(["name", "sku", "price"])
|> Selecto.filter({"active", true})
|> Selecto.order_by({"name", :asc})
|> Selecto.limit(5)
OutputLab.Helpers.explain("Formatted output query", format_query)
IO.puts("\nFormat :maps")
case Selecto.execute(format_query, format: :maps) do
{:ok, maps} -> IO.inspect(maps, label: "maps")
{:error, error} -> IO.puts("Error: #{inspect(error)}")
end
IO.puts("\nFormat {:json, [pretty: true]}")
case Selecto.execute(format_query, format: {:json, [pretty: true]}) do
{:ok, json_string} -> IO.puts(json_string)
{:error, error} -> IO.puts("Error: #{inspect(error)}")
end
IO.puts("\nFormat :csv")
case Selecto.execute(format_query, format: :csv) do
{:ok, csv_string} -> IO.puts(csv_string)
{:error, error} -> IO.puts("Error: #{inspect(error)}")
end
5) Advanced Formats (:typed_maps, :stream)
Use type-coerced maps and lazy streaming for larger processing pipelines.
advanced_format_query =
Selecto.configure(config.product_domain, config.repo)
|> Selecto.select(["name", "price", "active"])
|> Selecto.filter({"active", true})
|> Selecto.order_by({"name", :asc})
|> Selecto.limit(10)
OutputLab.Helpers.explain("Advanced format query", advanced_format_query)
IO.puts("\nFormat {:typed_maps, [keys: :strings, coerce: :safe]}")
case Selecto.execute(advanced_format_query, format: {:typed_maps, [keys: :strings, coerce: :safe]}) do
{:ok, typed_maps} ->
typed_maps
|> Enum.take(5)
|> IO.inspect(label: "typed_maps preview")
{:error, error} ->
IO.puts("Error: #{inspect(error)}")
end
IO.puts("\nFormat {:stream, :maps}")
case Selecto.execute(advanced_format_query, format: {:stream, :maps}) do
{:ok, row_stream} ->
row_stream
|> Enum.take(5)
|> IO.inspect(label: "stream preview")
{:error, error} ->
IO.puts("Error: #{inspect(error)}")
end
Next Steps
To extend this workbook, add:
-
Benchmarks comparing
:mapsvs:typed_mapsoverhead for larger result sets - A section writing CSV output directly to disk for export workflows
- A stream-processing section folding results into aggregate statistics in Elixir