# ExeQute
An Elixir client for [KDB+](https://kx.com/products/kdb/), the high-performance time-series database used in financial markets. ExeQute handles the KDB+ IPC wire protocol, type system, and pub/sub — letting you query and subscribe to KDB+ instances from any Elixir application.
The name is a play on **Ex**ir + **Q** (the KDB+ query language) + e**xecute**.
## Installation
```elixir
def deps do
[
{:exe_qute, "~> 0.1.0"}
]
end
```
## Querying
### One-shot query
Opens a connection, runs the query, closes the connection. Good for infrequent or one-off queries.
```elixir
{:ok, result} = ExeQute.query("select from trade", host: "kdb-host", port: 5010)
```
### Persistent connection
```elixir
{:ok, conn} = ExeQute.connect(host: "kdb-host", port: 5010)
{:ok, result} = ExeQute.query(conn, "select from trade")
{:ok, result} = ExeQute.query(conn, "select from trade where sym=`AAPL")
ExeQute.disconnect(conn)
```
### Named connections
Register a connection under an atom so any part of your application can use it without passing the pid around.
```elixir
ExeQute.connect(host: "kdb-host", port: 5010, name: :trades)
{:ok, result} = ExeQute.query(:trades, "select from trade")
```
### Parameterized queries
Pass typed arguments directly rather than interpolating them into strings. Arguments are encoded as KDB+ types on the wire.
```elixir
{:ok, result} = ExeQute.query(conn, "{x + y}", [1, 2])
{:ok, result} = ExeQute.query(conn, ".myns.getquotes", ["USD/JPY", ~D[2024-01-01]])
```
### Publishing (fire-and-forget)
Send data to a KDB+ function asynchronously — no response is expected. Useful for writing to feed handlers or triggering side-effects.
```elixir
:ok = ExeQute.publish(conn, ".feed.upd", ["trade", rows])
```
## Connection options
| Option | Default | Description |
|---|---|---|
| `:host` | `"localhost"` | KDB+ server hostname or IP |
| `:port` | `5001` | KDB+ server port |
| `:username` | `nil` | Username (omit for unauthenticated servers) |
| `:password` | `nil` | Password |
| `:timeout` | `5000` | Connection and query timeout in ms |
| `:encoding` | `"utf8"` | Character encoding for string data |
| `:name` | `nil` | Register connection under this atom name |
## Type mapping
### Decoding (KDB+ → Elixir)
| KDB+ type | Elixir type |
|---|---|
| boolean | `true` / `false` |
| short, int, long | `integer()` |
| real, float | `float()` |
| char | `<<byte>>` |
| symbol | `String.t()` |
| timestamp | `%DateTime{}` (UTC, microsecond precision) |
| date | `%Date{}` |
| time, minute, second | `%Time{}` |
| timespan | `integer()` (nanoseconds) |
| list | `[term()]` |
| dictionary | `%{term() => term()}` |
| table | `[%{String.t() => term()}]` (list of row maps) |
| keyed table | `[%{String.t() => term()}]` (key and value columns merged) |
| null values | `nil` |
| infinity (`0Wf`, `-0Wf`) | `:infinity` / `:neg_infinity` |
### Encoding (Elixir → KDB+)
| Elixir value | KDB+ type |
|---|---|
| `true` / `false` | boolean |
| `integer()` | long (64-bit) |
| `float()` | float (64-bit) |
| `String.t()` | char vector |
| `atom()` | symbol (e.g. `:AAPL` → `` `AAPL ``) |
| `%DateTime{}` | timestamp |
| `%Date{}` | date |
| `%Time{}` | time (e.g. `~T[17:00:00]`) |
| `[term()]` | generic list |
| `%{term() => term()}` | dictionary |
Types not listed (char atom, short/int atoms, timespan, minute, second, guid) cannot be sent as typed parameters. Use an inline q expression string for those cases.
## Pub/Sub
ExeQute supports KDB+ tickerplant subscriptions. One TCP connection to the tickerplant is shared across any number of subscribing processes in your application.
### Process-based subscriptions
Each subscribing process receives `{:exe_qute, table, data}` messages in its own mailbox. This works naturally in LiveViews, GenServers, or any `handle_info`-capable process.
```elixir
defmodule MyApp.TradeHandler do
use GenServer
def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
def subscribe do
ExeQute.subscribe("trade", host: "tp-host", port: 5010)
ExeQute.subscribe("quote", host: "tp-host", port: 5010)
end
def unsubscribe do
ExeQute.unsubscribe("trade", host: "tp-host", port: 5010)
ExeQute.unsubscribe("quote", host: "tp-host", port: 5010)
end
@impl true
def init(_opts), do: {:ok, %{}}
@impl true
def handle_info({:exe_qute, "trade", data}, state) do
IO.inspect(data, label: "trade")
{:noreply, state}
end
def handle_info({:exe_qute, "quote", data}, state) do
IO.inspect(data, label: "quote")
{:noreply, state}
end
end
{:ok, _pid} = MyApp.TradeHandler.start_link([])
MyApp.TradeHandler.subscribe()
Process.sleep(30_000)
MyApp.TradeHandler.unsubscribe()
```
### Named subscriber
For applications that want explicit control over the subscriber lifecycle — for example, to start it in a supervision tree.
```elixir
defmodule MyApp.TradeHandler do
use GenServer
def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
def subscribe do
ExeQute.subscribe(:tp, "trade")
ExeQute.subscribe(:tp, "quote")
end
def unsubscribe do
ExeQute.unsubscribe(:tp, "trade")
ExeQute.unsubscribe(:tp, "quote")
end
@impl true
def init(_opts), do: {:ok, %{}}
@impl true
def handle_info({:exe_qute, table, data}, state) do
IO.inspect({table, data})
{:noreply, state}
end
end
ExeQute.Subscriber.start_link(host: "tp-host", port: 5010, name: :tp)
{:ok, _pid} = MyApp.TradeHandler.start_link([])
MyApp.TradeHandler.subscribe()
Process.sleep(30_000)
MyApp.TradeHandler.unsubscribe()
```
### Callback-based subscriptions
For cases where you want a function called directly rather than receiving mailbox messages.
```elixir
ExeQute.Subscriber.start_link(host: "tp-host", port: 5010, name: :tp)
{:ok, trade_ref} = ExeQute.subscribe(:tp, "trade", fn {table, data} ->
IO.inspect({table, data})
end)
{:ok, quote_ref} = ExeQute.subscribe(:tp, "quote", ["AAPL", "MSFT"], fn {_table, data} ->
IO.inspect(data)
end)
Process.sleep(30_000)
ExeQute.unsubscribe(:tp, trade_ref)
ExeQute.unsubscribe(:tp, quote_ref)
```
### Multiple subscribers, one connection
Multiple processes can subscribe to the same table. Only one `.u.sub` is sent to the tickerplant regardless of how many local processes subscribe. When the last subscriber unsubscribes, `.u.unsub` is sent automatically.
```elixir
# All three processes receive {:exe_qute, "trade", data} independently
ExeQute.subscribe(:tp, "trade") # from LiveView A
ExeQute.subscribe(:tp, "trade") # from LiveView B
ExeQute.subscribe(:tp, "trade") # from a GenServer
```
### Starting the subscriber in a supervision tree
```elixir
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
{ExeQute.Subscriber, host: "tp-host", port: 5010, name: :tp},
MyApp.TradeHandler
]
Supervisor.start_link(children, strategy: :one_for_one)
end
end
```
## Introspection
KDB+ instances often accumulate years of q functions across many namespaces. ExeQute lets you explore a live instance programmatically — useful for building admin dashboards, documentation generators, or dynamic query builders that adapt to whatever functions a given server exposes.
Results are cached per-connection after the first call, so repeated introspection is free. The cache is tied to the connection process and clears automatically when it dies. Call `ExeQute.refresh_introspection/1` to force a fresh fetch after deploying new code to KDB+.
### Namespaces
```elixir
{:ok, ns} = ExeQute.namespaces(conn)
#=> [".myns", ".feed", ".util", ".q", ".Q", ".h"]
```
### Functions
Returns each function's name, parameter list, and source body. The body is the verbatim q source — useful for displaying what a function does without leaving Elixir, or for building lightweight documentation tooling around a KDB+ instance.
```elixir
{:ok, fns} = ExeQute.functions(conn, ".util")
#=> [
#=> %{
#=> "name" => ".util.getquotes",
#=> "params" => ["sym", "start", "end"],
#=> "body" => "{[sym;start;end] select from quote where sym=sym, date within (start;end)}"
#=> },
#=> %{
#=> "name" => ".util.lasttrade",
#=> "params" => ["sym"],
#=> "body" => "{[sym] last select from trade where sym=sym}"
#=> }
#=> ]
```
Omit the namespace argument to list functions in the root namespace:
```elixir
{:ok, fns} = ExeQute.functions(conn)
```
### Variables and tables
```elixir
{:ok, vars} = ExeQute.variables(conn, ".myns")
{:ok, tables} = ExeQute.tables(conn)
```
### Refreshing the cache
```elixir
ExeQute.refresh_introspection(conn)
```
## Livebook integration (work in progress)
### Interactive explorer
`ExeQute.Explorer` renders a QStudio-style widget inside a Livebook cell — connect to a server, browse namespaces, inspect tables and functions, run ad-hoc queries, and see results as tables or charts, all without leaving the notebook.
```elixir
ExeQute.Explorer.new(host: "kdb-host", port: 5010)
```
Results can be captured and used in subsequent cells:
```elixir
ExeQute.Explorer.new()
# ... interact in the UI, assign result to "my_data" ...
my_data = ExeQute.Explorer.get("my_data")
```
### Live chart widget
`ExeQute.EChart` is a low-overhead [Apache ECharts](https://echarts.apache.org) widget for Livebook, designed for high-frequency streaming data. It is the rendering backend used by the **KDB+ Chart** smart cell and can be driven directly when building custom subscription callbacks.
```elixir
chart = ExeQute.EChart.new(height: 400)
ExeQute.EChart.render(chart, initial_options)
ExeQute.subscribe(:tp, "trade", fn {_table, raw} ->
rows = ExeQute.to_rows(raw)
cfg = %{x_field: "time", x_type: :temporal, y_field: "price",
y_type: :quantitative, color_field: "", chart_type: :line, window: 500}
{buf, _} = ExeQute.EChart.update_buffer({[], %{}}, rows, cfg)
ExeQute.EChart.push(chart, ExeQute.EChart.options_from_buffer(cfg, {buf, %{}}))
end)
```
> **Note:** Both modules are functional but not yet fully polished — APIs may change in future releases.
## Error handling
All public functions return tagged tuples — no exceptions reach your code under normal operation.
```elixir
case ExeQute.query(:trades, "select from trade") do
{:ok, result} -> handle(result)
{:error, :not_connected} -> reconnect()
{:error, :timeout} -> retry()
{:error, reason} -> Logger.error(inspect(reason))
end
```
Errors returned by KDB+ itself (e.g. `'type`) are returned as `{:error, {:kdb_error, "type"}}`.