Lattice Grid Buy a licence

blog

A grid over DuckDB, in the browser

Building…
Loading a live grid…

DuckDB is having a moment, and the moment is earned. An analytical engine that runs anywhere, reads Parquet without a load step, and now compiles to WebAssembly so it runs inside a browser tab is a genuinely new shape of tool. The obvious thing to do with it is what people are doing: query a Parquet file where the data already is, without standing up a warehouse to ask one question.

The less obvious thing is what you put in front of it. An engine that can answer a filtered, sorted, paged query over a million rows in a few milliseconds deserves a grid that will not throw that away by pulling every row into the page and sorting it in JavaScript. The grid above is doing the opposite. It is holding a one-million-row Parquet file that DuckDB is querying in the tab you are reading this in, with no server anywhere behind it, and it is fast because it lets DuckDB do the work DuckDB is good at.

You bring the engine

The first thing to say is that Lattice Grid does not ship DuckDB, and the bundle is the same size whether you use this or not. You create the connection and hand it over. In the browser that connection comes from @duckdb/duckdb-wasm, which is exactly what the demo above loads. On a server it is any DuckDB client driven through your own endpoint. The adapter is the same code either way, and it imports nothing to read the results: DuckDB returns Arrow tables and the grid reads them through their own accessors, so the Arrow library never enters your bundle either.

That separation is the point. The engine is the caller’s, the connection is the caller’s, and the grid is a driver over whatever you handed it. What you query is a from expression, which can be a table, a view, or a scan written inline. read_parquet('s3://bucket/*.parquet') is as valid there as a table name, so the grid can sit directly over a Parquet file without anything loading it into a table first.

One query does the work

When you sort a column, filter a field, or scroll to the next page, the grid does not fetch the dataset and work on it locally. It builds one SQL statement and asks DuckDB. The filter tree becomes a WHERE clause, with its and and or groups intact. A multi-column sort becomes ORDER BY in the order you set. The page you are looking at becomes LIMIT and OFFSET, so only the window on screen ever comes back. And the total number of matching rows rides along in the same statement through a window count, so the grid knows it is row two hundred of some larger number without a second round trip to find out how large.

The demo prints that statement above the grid and updates it as you interact. Sort a column and watch the ORDER BY change. Add a filter and watch the WHERE clause and the timing move. What you are reading is the exact query the adapter generated, not an illustration of one.

Filter values are bound, never pasted in

A filter is the one part of a grid a stranger can steer, so the values a user types are sent to DuckDB through prepared statements rather than dropped into the SQL text. Column names, which cannot be parameterised that way, are refused unless they are plain identifiers. If a connection has no way to prepare a statement, the adapter declines to send the filter at all rather than interpolate it, and says so once. There is no path where a user’s input reaches the engine as concatenated SQL.

What runs in the browser, said plainly

Not everything pushes down, and the honest version matters more than the impressive one. Filtering, sorting, paging and the count go to DuckDB. Aggregation does not. When you group rows or total a column, that grouping and those totals are computed in the browser over the rows DuckDB returns. When that residual work is pending the grid asks the engine for the whole matching result rather than a single page, because grouping one page would give the wrong answer, and it refuses a short result rather than presenting a fraction as the whole. You can inspect the split after any query.

So this is not a claim that the grid pushes down everything. It pushes down the parts that decide whether a large dataset feels fast, and it is precise about the part it keeps.

Why the pairing is worth it

The reason to reach for this is not novelty. It is that the usual answer to “the dataset is too big for the client” is a server-side row model, where every sort is a round trip and every filter is an endpoint someone has to write and maintain. That is a real answer for genuinely enormous data. For an analytical dataset that fits in a Parquet file, DuckDB in the tab plus a grid that pushes its query down gets you the same responsiveness with no backend to build. Point the grid at the file, and the engine that is having its moment does the heavy lifting while the grid stays out of its way.

See it running, a million rows queried in the browser with no server, or read the DuckDB guide for the full adapter.

Read next

  • Width is the harder problem

    Everybody demos a long grid. The one that hurts is the wide one, where the row you are reading loses its name somewhere around column nineteen.

  • A million rows, timed on your machine

    Every grid vendor has a benchmark number from a laptop you have never seen. The grid in this post times itself in your browser instead.

  • The column knows it holds metres

    In most grids a unit is a formatter, and a formatter is a lie that shows the moment someone sorts. Lattice Grid makes a quantity a data type, so the column stays a number.

All posts RSS