linq · data queries

One query language — real database engines, or a single fused pass.

The linq chain you write is a macro. It can compile to SQL and run against a real database engine — SQLite, DuckDB, a live PostgreSQL — or fuse at compile time into one pass over das's own containers: an array, decs entities, an XML DOM, a JSON tree, a hash table. Below, the same query families run both ways — first against the engines, then across the containers — interpreter and JIT, in nanoseconds per element.

query.das
require daslib/linq_boost

struct Car {
    brand : string
    price : float
}

[export]
def main {
    var cars <- [Car(brand = "Ka", price = 900.0),
                 Car(brand = "Vaz", price = 1200.0)]
    let stats <- _fold(each(cars)
        ._group_by(_.brand)
        ._select((Brand = _._0,
                  Avg = _._1 |> select($(c : Car) => c.price)
                             |> average()))
        .to_array())
    for (s in stats) {
        print("{s.Brand}: avg {s.Avg}\n")
    }
}
$ daslang -jit query.das
Ka: avg 900
Vaz: avg 1200
 
the same chain at n = 100 000, from the matrix below:
array   2.7 ns/op · interp 43.4
sqlite  291.1 ns/op · interp 289.8
✓ same chain, engine or container · measured 2026-08-22

Your queries, against real database engines.

Every row is one query family; the chain compiles to SQL at compile time and the engine does the work — three engines side by side over the same schema: 100 000 Car rows, 100 dealers, 5 brands. The das array lane runs the same chains with no database at all — fused linq folds — and is the baseline every engine is paying its overhead against. PostgreSQL numbers include the same-box TCP round trip: the honest client-side view of a networked engine, not engine-core speed (a fully dashed PostgreSQL column means no server was reachable when the sweep ran). Cells are nanoseconds per element, lower is better; the fastest measured lane in each row is highlighted. Click a column to sort by it.

SQLite
In-memory database via the in-tree sqlite provider — the row store.
DuckDB
In-memory database via dasDuckDB — columnar/analytical, so aggregate-heavy families land very differently.
PostgreSQL
A live server via dasPostgreSQL, over localhost TCP.
Array
No database at all — the same chains as fused linq folds over array<Car>.
ns/op · lower is better · click a column to sort
loading benchmark data…

One front end, interchangeable back ends: the chain does not change when the engine does. The engine lanes live in an installable example package — the same providers any daslang program gets through daspkg.

The same queries, across das containers.

The array lane above is this table's baseline. Here the same query families run over the rest of das's data sources — decs entities, an XML DOM, a parsed JSON tree, a hash table — with every chain fused at compile time into a single pass over its container; no database anywhere in this table. Same schema, same discipline: each lane's fixture is built once, one process per lane. Cells are nanoseconds per element; the fastest measured lane in each row is highlighted.

Array
_fold over each(array<Car>) — the chain fuses into a single pass over the array.
Decs
_fold over from_decs_template — the same fusion as a per-archetype walk over ECS storage.
XML
_fold over from_xml_node — one DOM walk, reading only the fields the chain uses.
JSON
_fold over from_json — the XML lane's mirror over a parsed JSON tree.
Table
_fold over each_kv(table<int; Car>) — a fused slot walk; a key-equality where folds to an O(1) probe.
ns/op · lower is better · click a column to sort
loading benchmark data…

The speed is the macro system: the chain is rewritten at compile time — filters, projections and aggregates fused into one loop, unused fields pruned, key lookups turned into probes — and the JIT column is that fused loop compiled to native code.

How the numbers are made.

what is timed
Each cell is one query family over its lane's fixture, total time divided by the element count (ns/op), measured by the dastest --bench harness.
the fixture
One Car schema everywhere — 100 000 rows, 100 dealers, 5 brands — materialized natively in each source: an array, a decs world, a parsed DOM, a JSON tree, a hash table, a :memory: SQLite database.
isolation
One process per lane, fixture built once per process — a lane is never contaminated by another lane's code, which is what keeps the JIT column stable.
0.0 cells
Early-exit terminators — first, any, a bounded take — finish below timer resolution. Those queries are effectively free; the cell is real, not missing data.
— cells
Intentionally absent lanes — a query that has no meaning for that source (bare last() on an unordered SQL result, zip over ECS archetypes). Every dash in the matrix has a stated reason in results.md; on the engine board a fully dashed PostgreSQL column means no server was reachable.
reproduce
The matrix lanes live in benchmarks/sql/, the engine lanes in examples/benchmarks/sql/; each suite's results.md carries the sweep commands and the tool that regenerates both its tables and this page's data records.