FlightSQL Server¶
flight-sql-srv is the Apache Arrow FlightSQL service that answers SQL queries
against the data lake. It runs a DataFusion engine over the partitions written by
ingestion and materialized by the maintenance daemon,
and streams results back over gRPC.
Clients — the Python API, micromegas-query, the
Grafana plugin, and the analytics web app —
all connect here.
Running the binary¶
The gRPC listener binds to 0.0.0.0:50051 by default; change it with
--flightsql-listen-addr. See Listen addresses. The Docker image
(flight-sql.Dockerfile) exposes that port as its entrypoint.
Environment variables¶
| Variable | Required | Description |
|---|---|---|
MICROMEGAS_SQL_CONNECTION_STRING |
Yes | PostgreSQL connection for lake metadata |
MICROMEGAS_OBJECT_STORE_URI |
Yes | Object store holding the partitions; see Object Storage |
MICROMEGAS_OIDC_CONFIG |
No | OIDC configuration JSON |
MICROMEGAS_STATIC_TABLES_URL |
No | Location of static lookup tables to load at startup; any Object Storage URI form |
MICROMEGAS_AUTH_CACHE_TTL_SECONDS |
No | Shared whole-table snapshot cache TTL, in seconds, for the API-key, audience-grant, settings, and local-group (admins and any group: selector) stores — default 60. Membership and admin changes take effect within this TTL per process. See Groups |
MICROMEGAS_PUBLIC_VIEW_SETS |
No | Env fallback for the public_view_sets setting (a stored value wins). Comma-separated view-set names OwnershipRewrite skips entirely (no audience filtering) — an operator-responsibility allowlist for genuinely aggregated/non-PII view sets only; unset (empty) by default |
MICROMEGAS_DEFAULT_AUDIENCE |
No | The audience a credential with no bound ingestion audience is stamped with at write time (default public). Set it identically on every role that builds a lakehouse — this one, the Maintenance daemon, the monolith, and ingestion — since the maintenance role bakes the value into partitions and the ingestion role stamps new rows with it. See Audience stamping |
MICROMEGAS_SHUTDOWN_GRACE_PERIOD_SECONDS |
No | Drain timeout on SIGTERM (default: 25) |
MICROMEGAS_DATAFUSION_MEMORY_BUDGET_MB |
No | Query engine memory budget in MB; unset means an unbounded pool (the local-development default). This is set in real deployments — each FlightSQL query gets its own ScopedMemoryPool wrapper over this shared budget, and its peak usage is reported per query as peak_memory_bytes in the query audit log |
MICROMEGAS_DATAFUSION_MAX_TEMP_DIRECTORY_MB |
No | Cap on total spill-file bytes across all concurrent queries, in MB; default 100 GB (DataFusion's own default), far larger than a typical Fargate container's local disk. Exceeding the cap fails whichever query's spill write pushes past it — not necessarily the query that consumed most of the budget |
MICROMEGAS_QUERY_DENY_REFRESH_SECONDS |
No | Query deny list snapshot refresh / last_hit_at flush interval; default 10. Also the bound on cross-replica propagation of a newly created or removed rule — the inserting replica applies its own rule immediately, other replicas within one tick |
MICROMEGAS_QUERY_DENY_MAX_RULES |
No | Query deny list rule cap; default 100. Bounds the per-query evaluation cost (~3.4 µs at one rule, ~45 µs at the cap) |
MICROMEGAS_VIEW_DEFINITION_REFRESH_SECONDS |
No | How often this replica reloads DDL-defined materialized views from Postgres; default 60. A CREATE/DROP MATERIALIZED VIEW executed on this replica reloads immediately after commit — this interval only bounds how long a different replica lags the change |
MICROMEGAS_ADMISSION_BUDGET_GB |
No | Per-replica budget of query admission, in GB of estimated scan bytes; decimals allowed (e.g. 0.5). Unset disables admission, and queries are only measured. A value that does not parse fails startup |
MICROMEGAS_ADMISSION_MIN_COST_MB |
No | Floor charged per query so a query with no scan cost still occupies a slot; default 8. Read only when the budget is set |
MICROMEGAS_ADMISSION_MAX_PER_IDENTITY_GB |
No | The most scan cost one identity may hold in flight, in GB, decimals allowed; default 80% of the budget. Read only when the budget is set |
MICROMEGAS_ADMISSION_OPAQUE_LEAF_COST_MB |
No | Flat cost charged for a plan leaf whose scan cannot be seen at plan time, such as a trace-generating table function; default 64. Also used to fill estimated_scan_bytes when admission is disabled |
CREATE [OR REPLACE] MATERIALIZED VIEW / DROP MATERIALIZED VIEW are ordinary SQL statements sent
to this server, gated by the same admin check as the admin SQL
functions — see Materialized Views.
CLI flags¶
| Flag | Default | Description |
|---|---|---|
--flightsql-listen-addr |
0.0.0.0:50051 |
gRPC bind address (env MICROMEGAS_FLIGHTSQL_LISTEN_ADDR) |
--disable-auth |
off | Disable authentication (development only) |
--health-listen-addr |
none | Address for the HTTP health/readiness sidecar (e.g. 0.0.0.0:8082; env MICROMEGAS_FLIGHTSQL_HEALTH_LISTEN_ADDR) |
--shutdown-grace-period-seconds |
25 |
Seconds to drain in-flight RPCs on SIGTERM |
Authentication¶
If neither MICROMEGAS_OIDC_CONFIG nor a non-empty analytics_api_keys DB
table is present, the server refuses to start unless --disable-auth is
passed. Membership in the reserved admins local group
(see Groups) grants access to administrative SQL functions — see
Admin SQL Functions. For provider configuration and
precedence, see Authentication.
flight-sql validates analytics_api_keys (see API Keys) but
mints nothing over HTTP itself — it has no key-management routes of its own.
Analytics keys are minted, listed, and revoked through
analytics-web-srv's own HTTP routes instead — see
API Keys. This also covers
the key-only deployment (no OIDC) some Grafana setups use — see
Grafana Authentication.
--disable-auth treats every FlightSQL caller as admin — it is a development-only
flag, never for production use. An analytics_api_keys caller normally
carries no email and so can never match a user:/group: member of admins,
meaning it cannot call the admin SQL functions —
except while admins still holds its seeded wildcard (*) member, in which
case every authenticated caller, API keys included, is admin (see
Groups's upgrade path). bulk_ingest (CommandStatementIngest)
is likewise admin-gated, via a separate mechanism from the admin SQL functions
— see
bulk_ingest(table_name, table)
for detail.
Query deny list¶
Every replica checks each query against a small, shared set of admin-managed deny rules before spending any real work on it — see Query Deny List for the SQL functions and Admin → Query Deny List for the web screen. Two things worth knowing operationally:
- Propagation is polled, not pushed. Rules live in Postgres; each replica refreshes its own
in-memory copy every
MICROMEGAS_QUERY_DENY_REFRESH_SECONDS(default 10s). The replica that creates or removes a rule applies it to itself immediately; every other replica picks it up within one tick — negligible against an incident measured in minutes. - Fail-open, by design. A refresh that can't reach Postgres keeps the previous snapshot
(with a
warn!and aquery_deny_refresh_error_countmetric) rather than denying every query; a rule whose expression a given replica can't compile (e.g. after a downgrade) is dropped from that replica's snapshot alone (query_deny_compile_error_count), never enforced blindly and never fatal. This is an availability valve, not an authorization control — those (ReadScope, audience guards) fail closed and are unaffected.
Anti-jam escape hatch. A rule that happens to match every query an admin's own recovery
statement would send can't lock the valve shut: the check is skipped for a statement naming
deny_queries/remove_query_denial/list_query_denials, from a caller who could reach those
functions anyway (an admin, or any authenticated caller on a deployment with no admin principal
at all — see Admin SQL Functions above).
Watching for denials¶
A denial shows up at three different volumes, deliberately: a warn! line per denial (visible to
anything already watching warning-level logs), a per-rule rate metric, and the full-detail audit
row. Paste these straight into a dashboard:
-- every denial in the last hour, one row each
SELECT time, msg
FROM log_entries
WHERE level <= 3 -- Fatal, Error, Warn
AND msg LIKE 'query denied%'
AND time >= NOW() - INTERVAL '1 hour'
ORDER BY time DESC;
-- denial rate per rule, per minute
SELECT date_bin(INTERVAL '1 minute', time) AS minute,
property_get(properties, 'rule_id') AS rule_id,
sum(value) AS denied
FROM measures
WHERE name = 'query_denied'
AND time >= NOW() - INTERVAL '6 hours'
GROUP BY minute, rule_id
ORDER BY minute;
For the full-detail row (SQL text, fingerprint, complete attribution), see the query audit
log — filter on error_class = "denied".
Query admission¶
One identity can fill the query engine's memory pool on its own, for example a shared service account behind dashboards that auto-refresh. Query admission keeps that from starving everyone else, without ever rejecting a query: each identity gets its own first-in, first-out queue, a budget caps how much scan work runs at once, and when capacity frees up the next query comes from the identity with the least work in flight.
- Cost. A query's cost is the number of compressed parquet bytes its physical plan will scan,
estimated from the columns it reads. Footers that are in the metadata cache give an exact
figure; otherwise the cost is the file size scaled by the share of columns read. Table
functions whose work is invisible at plan time are charged
MICROMEGAS_ADMISSION_OPAQUE_LEAF_COST_MB. Materialization that happens while the query is planned is not queued. - Identity. The service account when the request delegates for a user, otherwise the user.
Everyone has the same weight. With
--disable-auththe identity comes from the client-assertedx-user-idheader, and callers without it share one queue. - Cap per identity. One identity may hold at most
MICROMEGAS_ADMISSION_MAX_PER_IDENTITY_GBin flight, so the rest of the budget stays free and a query from anyone else starts at once. A refresh storm from one identity waits in its own queue. Leavebudget - maxlarge enough for a typical query from another caller. A query larger than the cap is charged the cap and still runs. - Disabled by default. Without
MICROMEGAS_ADMISSION_BUDGET_GBnothing is queued, but the estimate is still written to the query audit log.
The budget and the fairness apply per replica, so size the budget against each replica's own
MICROMEGAS_DATAFUSION_MEMORY_BUDGET_MB pool. The budget is in scan bytes, not memory bytes: run
the estimate-versus-actual query in the audit log guide for a few days, compare
estimated_scan_bytes with peak_memory_bytes, and pick a budget whose queries together fit in
the pool. Wait time shows up as queue_wait_ms, and the admission_queue_wait,
admission_queued, admission_in_flight_cost and admission_queued_queries metrics are emitted
without per-identity tags.
A query that waits longer than the temporary-file expiry of about an hour may fail with an object-not-found error if its partitions were replaced while it waited; a wait that long means the service is badly overloaded.
Health and readiness¶
The gRPC server does not itself serve HTTP. Pass --health-listen-addr to start
a lightweight sidecar that serves GET /health (unconditional) and GET /ready
(probes PostgreSQL and object storage):
Omit the flag and no sidecar starts. See FlightSQL health sidecar for details.
Scaling¶
FlightSQL is stateless with respect to the lake — every instance reads the same partitions — so it scales horizontally behind a gRPC-aware load balancer. JIT partition materialization writes to both object storage (the new partition's Parquet file) and PostgreSQL (its metadata row); add instances to serve more concurrent queries. Heavy or slow-object-store deployments benefit from the object cache, which fronts the object store with a shared read-through cache. See Operating in a High-Availability Environment for how FlightSQL behaves during a database failover.