Lotus ClickHouse

ClickHouse source adapter for Lotus. Run Lotus queries, dashboards, and AI-assisted exploration against a ClickHouse cluster the same way you would against Postgres, MySQL, or SQLite.

A Lotus statement here is ordinary SQL text, but it is ClickHouse SQL: typed {$0:Type} placeholders instead of $1, no transactions, and a read-only guarantee the server enforces rather than the session — see Writing Queries.

0.1.0 — first release. The adapter is complete against the Lotus.Source.Adapters.Ecto.Dialect contract in Lotus v1 and its integration suite runs against a live ClickHouse server, but its own surface — the type mapping, the editor configuration, the safety defaults — has no production users yet and may still move. Pin accordingly.

Installation

Add both lotus and lotus_clickhouse to your mix.exs:

def deps do
[
{:lotus, "~> 1.0"},
{:lotus_clickhouse, "~> 0.1"}
]
end

This pulls in ecto_ch (the Ecto adapter for ClickHouse) and ch (the underlying HTTP driver). Requires Elixir 1.18 or later. CI runs the suite on Elixir 1.18, 1.19 and 1.20 against ClickHouse 24.8.

Configuring a data source

A ClickHouse source is a plain Ecto repo, exactly like a Postgres one:

defmodule MyApp.ClickHouseRepo do
use Ecto.Repo,
otp_app: :my_app,
adapter: Ecto.Adapters.ClickHouse
end
config :my_app, MyApp.ClickHouseRepo,
hostname: "localhost",
port: 8123,
scheme: "http",
database: "analytics",
username: "default",
password: "secret",
pool_size: 5
config :lotus,
storage_repo: MyApp.Repo,
default_source: "postgres",
source_adapters: [Lotus.Source.Adapters.ClickHouse],
data_sources: %{
"postgres" => MyApp.Repo,
"clickhouse" => MyApp.ClickHouseRepo
}

Listing the adapter in :source_adapters is what makes the repo resolvable. The generated can_handle?/1 claims any repo whose __adapter__/0 is Ecto.Adapters.ClickHouse, so :data_sources entries stay bare repo modules — no map form, no :adapter key.

Then start the repo in your supervision tree alongside your other repos. Full walkthrough in Installation.

What works

What this adapter does differently, or not at all

Safety

Two independent layers, and they block different things.

Core's SQL sanitizer (sanitize_query/3, shared by every Ecto-backed adapter) rejects multi-statement input and, when read_only is set, any statement matching \b(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|GRANT|REVOKE|VACUUM|ANALYZE|CALL|LOCK)\b. It is a keyword regex, so it is blunt in both directions: a column literally named analyze_count in a SELECT trips it, and it knows nothing of ClickHouse-specific mutations such as OPTIMIZE or SYSTEM.

ClickHouse's readonly=1 is what actually stops writes. It is a server setting applied to every query the adapter executes, and it covers the ClickHouse-specific verbs the regex misses. Both layers are lifted together by read_only: false, which exists for controlled callers such as the test harness inserting fixtures.

Separately, builtin_denies/1 hides relations from queries before any host visibility rule is consulted: the whole system, INFORMATION_SCHEMA and information_schema databases, plus the Ecto migration table (from the repo's :migration_source, defaulting to schema_migrations) and the six lotus_* metadata tables — each listed both unqualified and qualified with the repo's configured database. builtin_schema_denies/1 keeps the same three system databases out of schema listings.

One caveat worth stating plainly: the AI syntax notes and error patterns above only reach the LLM prompt if you also list this adapter in config :lotus, :trusted_source_adapters. Otherwise core strips the context down to :language alone.

Development

mix deps.get
mix test.setup # docker compose up -d
mix test

The docker-compose service publishes ClickHouse on non-standard host ports — 9123 for HTTP and 9100 for the native protocol — so it does not collide with a ClickHouse you may already be running on 8123. The test repo in config/test.exs expects exactly that: localhost:9123, user default, password clickhouse, database lotus_test.

Nearly the whole suite needs that server. Only test/lotus_clickhouse/dialect_test.exs is pure; test/test_helper.exs connects and creates the test_users, test_posts and test_events tables before any test runs, so with no server up the suite fails at startup rather than skipping. Lotus's own metadata tables live in an in-memory SQLite repo, so no second server is needed. Tests are async: false throughout — ClickHouse has no Ecto.Adapters.SQL.Sandbox, so isolation is a TRUNCATE between tests.

mix format --check-formatted
mix compile --warnings-as-errors
mix credo --strict

Guides

License

MIT — see LICENSE.