SQL
mink-query is the SQL engine. It is a separate process built on DataFusion with the cluster registered as a catalog. A statement is planned into one read per bucket, the reads run in parallel, and the result streams out as Arrow. Nothing is cached between statements and nothing is written.
Catalog
| DataFusion | Mink |
|---|---|
Catalog mink | The cluster |
| Schema | Database |
| Table | Table, with its Arrow schema from the descriptor |
The catalog is refreshed before every statement with list_databases and list_tables, so a table created a moment ago is visible. A table referenced by a statement is opened on first use: descriptor, partition list, and a lookup client when it has a primary key. information_schema is on. Unqualified names resolve in the database given by -d, or default.
Pushdown
DataFusion hands the table provider a projection, a limit and a list of filters. Each filter is classified:
| Filter | Classification | Effect |
|---|---|---|
| Pins only partition keys | Exact | Prunes partitions. Not re-applied |
| Pins a partition, bucket or primary-key column | Inexact | Routes to buckets or keys. Re-applied above the scan |
| Converts to a lake predicate | Inexact | Skips Iceberg files and row groups. Re-applied above the scan |
| Anything else | Unsupported | Evaluated by DataFusion above the scan |
A filter pins a column when it is column = literal, column IN (literals), an OR of those on one column, or an AND of pins. Pins on the same column across filters intersect. A lake predicate is a comparison of a column with a literal, IN, IS NULL, IS NOT NULL, NOT, AND or OR, over the types the Iceberg scan can evaluate. A widening cast that DataFusion inserts around a column folds into the literal. A narrowing one does not.
Projection is pushed to every split as field indexes: the node projects log batches before sending and the Iceberg reader projects Parquet columns. The limit stops each split's stream at limit rows and DataFusion applies the global limit above.
Splits
One statement over one table becomes a set of splits. The shape of the table and what the filters pin decide the kind:
| Split | Reads | From |
|---|---|---|
Lookup | The pinned keys, at most 1024 combinations, one lookup action | Bucket leaders |
Snapshot | The KV store of one bucket | Bucket leader |
Union | Lake files for one bucket at the pinned snapshot, then the log tail from log_end_offset to the high watermark, merged as in Union read | Object storage and the bucket leader |
Lake | Lake files for one partition, no bucket. Used when rows are not bucketed by key so files cannot be assigned to a bucket | Object storage |
Which buckets appear:
| Pinned | Buckets |
|---|---|
| Every partition key and every bucket key, at most 1024 combinations | Only the buckets the rows route to |
| Some partition keys | Every bucket of the matching partitions |
| Nothing routable | Every bucket |
The lake snapshot is pinned once per statement from lake_snapshot: one snapshot_id and each bucket's log_end_offset. Every split reads the same snapshot, so the statement is consistent across buckets. Iceberg planning happens once per table with the lake predicate applied and the resulting file tasks are handed to the splits.
Execution
MinkScan is the physical operator. It declares one DataFusion partition per split, so a scan over a table with 16 buckets runs 16 streams concurrently. target_partitions does not change this. It sets the parallelism of the operators above the scan.
Each stream is opened when DataFusion polls it:
- A
Unionsplit reads Parquet from object storage through the Iceberg catalog in[lake]and the log tail from the bucket leader with aScanbounded tolog_end_offsetthrough the high watermark. The merge runs inmink-queryon the same code the node uses for itsUnionticket. - A
Lakesplit reads its files the same way, with no log. - A
Snapshotsplit is oneSnapshotticket to the leader. - A
Lookupsplit is onelookupaction with every key row.
EXPLAIN shows the scan as MinkScan: table=, splits=, files=, keys=, filter=, projection=, limit=. Row and byte statistics come from the Iceberg file metadata and the log offsets, so DataFusion can choose join sides.
| Setting | Default | Effect |
|---|---|---|
batch_size | 8192 | Rows per Arrow batch through the plan |
target_partitions | Cores | Parallelism above the scan |
memory_limit | None | Fair spill pool for sorts, joins and aggregates |
Flight SQL
mink-query serve exposes Flight SQL on 0.0.0.0:9130. The statement handle is the SQL text, so any mink-query replica behind a load balancer can serve any ticket.
| Command | Result |
|---|---|
CommandStatementQuery | Plans once for the schema, then plans and executes on DoGet |
CreatePreparedStatement, CommandPreparedStatementQuery | Same, with the SQL as the handle. No parameters |
GetCatalogs, GetDbSchemas, GetTables, GetTableTypes | From the refreshed catalog. One catalog, one table type TABLE |
GetSqlInfo | Server name, version, Arrow version, FlightSqlServerReadOnly = true |
DDL and DML are not served.
Configuration
| Flag | Environment | Default |
|---|---|---|
-b, --bootstrap | MINK_BOOTSTRAP | grpc://127.0.0.1:9123, comma separated for several |
-c, --config | MINK_QUERY_CONFIG | None. TOML file with [lake] and engine settings |
-d, --database | MINK_QUERY_DATABASE | default |
--listen on serve | MINK_QUERY_LISTEN | 0.0.0.0:9130 |
The TOML file carries the same [lake] block as a node, so the engine reads the tables the node tiers, plus batch_size, target_partitions and memory_limit. MINK_QUERY_* environment variables override the file. Without a [lake] block every table is read from the log and KV state alone.
toml
memory_limit = 4294967296
[lake]
format = "iceberg"
catalog = "rest"
uri = "http://iceberg-rest:8181"
warehouse = "s3://mink-lake"
[lake.properties]
"s3.endpoint" = "http://rustfs:9000"
"s3.region" = "us-east-1"| Command | Purpose |
|---|---|
serve | Flight SQL |
exec -e <sql>, exec -f <file>, exec < stdin | Run statements. --format table, json or csv |
shell | Interactive |