Skip to main content
Version: Next

QueryFlux — Architecture Overview

QueryFlux is a universal SQL query proxy and router. It accepts queries from clients over multiple protocols (Trino HTTP, PostgreSQL wire, MySQL wire, Arrow Flight SQL, and others), routes them to the appropriate backend engine, optionally translates the SQL dialect, and streams results back in the client's native format.

More documentation: the architecture documentation overview indexes deeper topics — motivation-and-goals.md (why the project exists), query-translation.md (sqlglot and dialects), routing-and-clusters.md (routers, groups, load balancing), cluster-variants-and-health.md (multi-warehouse variants, health/reconcile, distributed capacity), observability.md (Prometheus, Grafana, Studio, Admin API), adding-support/overview.md (Extending QueryFlux — backend, frontend).


High-level flow

StepComponentWhat happens
① ClientAny supported driver or CLISends SQL over Trino HTTP, PostgreSQL wire, MySQL wire, Flight SQL, Snowflake, etc.
② Frontendqueryflux-frontendParses the wire protocol, builds SessionContext, hands SQL to dispatch.
③ Routerqueryflux-routingEvaluates the router chain; picks a cluster group (or falls back to routingFallback).
④ Cluster managerqueryflux-cluster-managerPicks a healthy member cluster, enforces capacity (local counters or shared leases in distributed mode), may queue if the group is full.
⑤ Translationqueryflux-translationRewrites SQL when client dialect ≠ engine dialect; skipped when they already match.
⑥ Enginequeryflux-engine-adaptersRuns the query on Trino, DuckDB, StarRocks, Athena, or an ADBC SaaS warehouse.

After dispatch, async engines (Trino, Athena) store an in-flight handle in the persistence layer so the client can poll until completion. Sync engines return results in one round trip.

Result paths back to the client:

PathWhenBehavior
Async pollTrino-style groupsSubmit → persist handle → client polls proxy nextUri until done
Sync ArrowMost frontend/backend pairsStream RecordBatches, re-encode to the client protocol
Sync nativeMatching wire formats (e.g. MySQL → StarRocks)Stream driver-native chunks — no Arrow allocation

The frontend never knows which engine ran the query. The adapter never knows which client protocol was used.


Operations and persistence

QueryFlux separates query traffic (frontends, routing, engines) from operational state (config, in-flight queries, metrics history). Studio and Prometheus reach the Admin API on port 9000; the query proxy uses the same process but different listeners.

Persistence backends today:

BackendConfigIn-flight / queued queriesQuery historyMulti-replica coordination
In-memoryYAML only (no Studio CRUD)Per-processNot persistedNot available
PostgresHot-reload from DB + StudioShared across restartsStudio dashboardsOptional (distributed: true)

The coordination bucket (capacity leases, reconcile running counts, queue claims) is only used when a durable backend implements DistributedBackendStore — Postgres today. Single-replica Postgres deployments use config, state, and history only.

Background tasks (every replica, timers in main.rs):

TaskIntervalDoes
Config reload30s (configurable)Re-read routing config when using a durable backend
Health check30sProbe backends; mark clusters unhealthy
Reconcile30sSync running counts with engine ground truth; in distributed mode one leader publishes, others read
Metrics snapshot5sPublish cluster utilization to Prometheus

Distributed multi-replica details: Cluster variants, health checks & reconciliation.


Workspace Layout

queryflux/
├── crates/
│ ├── queryflux/ # main binary — wires everything together
│ ├── queryflux-core/ # shared types: ProxyQueryId, SessionContext, QueryPollResult, …
│ ├── queryflux-config/ # ConfigProvider trait + YamlFileConfigProvider
│ ├── queryflux-frontend/ # FrontendListenerTrait + protocol implementations
│ ├── queryflux-engine-adapters/ # EngineAdapterTrait + per-engine implementations
│ ├── queryflux-routing/ # RouterTrait + RouterChain + all router implementations
│ ├── queryflux-cluster-manager/ # ClusterGroupManager: load balancing + queueing
│ ├── queryflux-persistence/ # Persistence + MetricsStore + ClusterConfigStore traits + impls
│ ├── queryflux-metrics/ # PrometheusMetrics, BufferedMetricsStore, MultiMetricsStore
│ ├── queryflux-translation/ # TranslatorTrait + SqlglotTranslator (PyO3)
│ ├── queryflux-auth/ # Authentication providers, authorization, identity resolution
│ ├── queryflux-fingerprint/ # Query fingerprinting (AST-based deduplication)
│ ├── queryflux-bench/ # Proxy overhead benchmarks (mock backends)
│ └── queryflux-e2e-tests/ # Integration tests
├── queryflux-studio/ # Next.js management UI (cluster monitoring, query history)
├── prometheus/ # Prometheus scrape config
├── grafana/ # Grafana provisioning + dashboards
├── docker/ # Docker Compose files
│ ├── docker-compose.yml # Local dev: Trino + Postgres + Prometheus + Grafana
│ └── test/docker-compose.test.yml # E2E stack — full path `docker/test/docker-compose.test.yml`
├── config.local.yaml # Example config for local development
└── Makefile # build / run / test shortcuts

Core Abstractions

SessionContext (queryflux-core)

Protocol-agnostic metadata that travels with a query from frontend through routing and into the engine adapter. Each frontend extracts the common fields at session initialization and places remaining protocol-specific key-value data into extra.

pub struct SessionContext {
pub user: Option<String>,
pub database: Option<String>,
pub tags: QueryTags,
/// Protocol-specific key-value bag. Key conventions:
/// - Trino / ClickHouse HTTP: HTTP header names (lowercase) → values
/// - Postgres wire: startup parameter names → values
/// - MySQL wire: session variables → values
pub extra: HashMap<String, String>,
}

QueryExecution (queryflux-core)

Engines fall into two models. The adapter declares which model it uses; dispatch handles both uniformly.

QueryExecution::Async { backend_query_id, next_uri, initial_body }
→ dispatcher stores handle in Persistence
→ client polls proxy until complete

QueryExecution::Sync { result: QueryPollResult }
→ dispatcher returns result immediately
→ no Persistence needed
EngineModelNotes
TrinoAsyncSubmit → poll nextUri until done
DuckDBSyncRuns on spawn_blocking, result available immediately
StarRocksSyncMySQL protocol, single round-trip
ClickHouseSyncHTTP interface, ArrowStream response decoded to Arrow record batches

Engine adapters (queryflux-engine-adapters)

There is no single EngineAdapterTrait. Engines implement SyncAdapter (DuckDB, StarRocks, ClickHouse, ADBC) or AsyncAdapter (Trino, Athena).

// SyncAdapter — execute_as_arrow / optional execute_native
async fn cancel_query(&self, backend_id: &BackendQueryId) -> Result<()>; // default no-op

// AsyncAdapter — submit_query + poll_query
async fn cancel_query(&self, backend_id: &BackendQueryId) -> Result<()>; // required

On the sync path, dispatch holds a SyncCancelGuard. Adapters publish a BackendQueryId into a shared slot as soon as the engine id is known (before the blocking wait). If the client disconnects, the guard calls cancel_query (ClickHouse KILL QUERY WHERE query_id = …, StarRocks KILL QUERY <connection_id>, DuckDB interrupt(), Athena StopQueryExecution, Trino DELETE /v1/query/{id}). DuckDB HTTP and ADBC have no cross-thread kill API — cancel is a documented no-op; dropping the HTTP request is best-effort.

RouterTrait (queryflux-routing)

pub trait RouterTrait: Send + Sync {
fn type_name(&self) -> &'static str;
async fn route(
&self,
sql: &str,
session: &SessionContext,
frontend_protocol: &FrontendProtocol,
) -> Result<Option<ClusterGroupName>>;
}

RouterChain evaluates routers in config order. First Ok(Some(group)) wins. Falls back to routingFallback if every router returns Ok(None). route_with_trace builds a RoutingTrace for debugging and observability.


Implemented Components

Frontends

ProtocolStatusPort
Trino HTTPDone8080
PostgreSQL wireDone5432
MySQL wireDone3306
Arrow Flight SQLDone (query execution)
Snowflake HTTP wire + SQL API v2Doneconfigurable (e.g. 8443)
Admin / Prometheus metricsDone9000
ClickHouse HTTPPlanned8123

Trino HTTP routes:

MethodPathDescription
POST/v1/statementSubmit a new query
GET/v1/statement/qf/queued/{id}/{seq}Poll a queued query (with backoff)
GET/v1/statement/qf/executing/{id}Poll an executing query
DELETE/v1/statement/qf/executing/{id}Cancel a running query

Engine Adapters

EngineStatusConnectionFormatExecution model
Trino (HTTP)DoneTrinoHttpAsync — transparent nextUri proxying; raw bytes, zero copy
Trino (ADBC)DoneArrowSync — ADBC driver, Arrow result set
ADBC (Snowflake, Databricks, BigQuery, Redshift, …)DoneArrowSync — ADBC driver; built-in health/reconcile introspection for SaaS warehouses
DuckDBDoneArrowSync embedded — spawn_blocking + Arrow result set
StarRocksDoneMysqlWireSync — mysql_async pool; native path (zero Arrow) for MySQL wire clients
AthenaDoneArrowAsync AWS SDK — StartQueryExecution → poll → GetQueryResults
ClickHouseDoneArrowSync — HTTP interface (default_format=ArrowStream), Arrow result set

Routers

RouterMatching criteria
protocolBasedWhich frontend protocol the client used
headerHTTP header value (Trino HTTP only)
queryRegexRegex patterns against SQL text
tagsQuery tag key/value conditions (AND logic within a rule)
pythonScriptCustom Python function (`def route(query, ctx) -> str
compoundMultiple conditions combined with all (AND) or any (OR) logic

Persistence

Persistence is pluggable behind traits in queryflux-persistence (Persistence, MetricsStore, ClusterConfigStore, CapacityStore, …). The binary wires an in-memory or Postgres implementation today.

BackendStatusUse case
In-memory (DashMap)DoneSingle-instance dev; config from YAML
PostgreSQL (JSONB)DoneDurable config, query history, in-flight state, optional distributed coordination
RedisPlannedFaster shared state; routing config would stay on the durable store

Distributed coordination (queryflux.distributed: true + a backend that implements DistributedBackendStore, Postgres today): fleet-wide capacity leases, reconcile-published running counts, and queue claims. See Cluster variants, health checks & reconciliation.

Metrics

StoreStatusPurpose
PrometheusMetricsDoneReal-time operational metrics at /metrics
NoopMetricsStoreDoneDefault — zero overhead
PostgresStore (MetricsStore)DoneHistorical query records for the management UI
BufferedMetricsStoreDoneAsync write buffer wrapping any MetricsStore

Prometheus metrics exposed:

MetricTypeLabels
queryflux_queries_totalCounterengine_type, cluster_group, status, protocol
queryflux_query_duration_secondsHistogramengine_type, cluster_group
queryflux_translated_queries_totalCountersrc_dialect, tgt_dialect
queryflux_running_queriesGaugecluster_group, cluster_name
queryflux_queued_queriesGaugecluster_group

SQL Translation

Translation is handled by sqlglot (Python, 31+ dialects) called via PyO3.

When translation runs: only when the incoming client dialect differs from the target engine's dialect. Trino client → Trino cluster = zero overhead passthrough.

Two translation modes (both implemented in queryflux-translation; see query-translation.md):

  1. Dialect-only (empty SchemaContext): sqlglot.transpile(sql, read=src, write=tgt) — this is what the main dispatch path uses today (SchemaContext::default()).
  2. Schema-aware (non-empty SchemaContext): parse → sqlglot.optimizer.optimize with MappingSchema → emit in target dialect, with fallback to dialect-only if optimization fails.

Source dialect is inferred from the frontend protocol (TrinoHttp → Trino, PostgresWire → Postgres, etc.). Target dialect comes from the selected cluster’s engine type (via the adapter).

Translation gracefully degrades: if sqlglot is unavailable at startup, the service disables itself and SQL passes through untranslated.


Configuration

queryflux:
externalAddress: http://localhost:8080
frontends:
trinoHttp: { enabled: true, port: 8080 }
postgresWire: { enabled: false, port: 5432 }
mysqlWire: { enabled: false, port: 3306 }
flightSql: { enabled: false, port: 50051 }
persistence:
inMemory: {} # or: postgres: { databaseUrl: "postgres://..." }
adminApi:
port: 9000

clusters:
trino-1:
engine: trino
endpoint: http://trino:8080
enabled: true
duckdb-1:
engine: duckDb
enabled: true
databasePath: /data/analytics.duckdb # omit for in-memory

clusterGroups:
trino-default:
enabled: true
maxRunningQueries: 100
members: [trino-1]

duckdb-local:
enabled: true
maxRunningQueries: 4
members: [duckdb-1]

translation:
errorOnUnsupported: false

routers:
- type: protocolBased
trinoHttp: trino-default

- type: header
headerName: X-Target-Engine
headerValueToGroup:
duckdb: duckdb-local

- type: pythonScript
script: |
def route(query, ctx):
if "big_table" in query:
return "trino-default"
return None

routingFallback: duckdb-local

Local Development

Prerequisites

  • Rust (stable)
  • Docker + Docker Compose
  • Python 3.10+

Setup

# Install Python dependencies (sqlglot)
make setup

# Export Python path for PyO3
export PYO3_PYTHON=$(pwd)/.venv/bin/python3

# Start backing services (Trino, Postgres, etc.)
make env
# In a separate terminal, run the proxy
make server

Services

ServiceURLCredentials
QueryFlux (Trino HTTP)http://localhost:8080
Prometheus metricshttp://localhost:9000/metrics
Trino (direct)http://localhost:8081
Prometheushttp://localhost:9090
Grafanahttp://localhost:3000admin / admin
PostgreSQLlocalhost:5433queryflux / queryflux

Send a query

# Via Trino CLI
trino --server http://localhost:8080 --execute "SELECT 42"

# Via curl
curl -s -X POST http://localhost:8080/v1/statement \
-H "X-Trino-User: dev" \
-d "SELECT current_date"

Roadmap

PhaseFeatureStatus
P1Trino HTTP frontend + DuckDB/Trino backendsDone
P1sqlglot translation (dialect-only)Done
P1Prometheus metricsDone
P1Postgres persistence + query historyDone
P1PostgreSQL wire frontendDone
P1MySQL wire frontend + StarRocks backendDone
P1Arrow Flight SQL frontendDone
P1Snowflake HTTP wire + SQL API v2 frontendDone
P1QueryFlux Studio — management UIDone
P1Athena backendDone
P1Authentication / authorization (queryflux-auth)Done
P2Wire SchemaContext from catalog into dispatch — see Catalog ProviderDone (fallback/glue/hiveMetastore/icebergRest)
P3ClickHouse backend (HTTP, Arrow)Done
P3ClickHouse HTTP frontendPlanned