Skip to content
quartiledocsv0.1
GitHub

Arrow and worker queries

PreviewExperimentalOptional adapter

Import typed Arrow data into an application-owned DuckDB worker and request bounded aggregates, pages and chart results.

Query before rendering

Existing rows and dataset() stay synchronous. The optional query layer moves filtering and aggregation to a worker-backed source, then gives React a bounded result. It does not turn every source row into a React object or chart mark.

ImportResponsibility
@quartile/reactCharts, synchronous rows, themes and selection. No database import or worker startup.
@quartile/react/queryQuerySource contracts, useDataQuery, result boundary and linked views. No database runtime import.
@quartile/react/duckdbOptional Arrow ingestion and typed DuckDB adapter.

The worker explorer uses deterministic Arrow IPC, category totals, metrics, a 25-row detail page and bounded scatter marks. Its first matching IDs are not a representative sample.

Install peers and own the worker

First build and pack the unpublished library. Install the verified optional peer pair only when using the DuckDB adapter:

pnpm add @duckdb/duckdb-wasm@1.32.0 apache-arrow@17.0.0

Use worker and Wasm files from the same installed DuckDB release. The gallery uses Vite asset imports and single-threaded MVP/EH bundles without cross-origin isolation; other bundlers need equivalent handling.

Copy and adapt the tested gallery initializer for complete startup. It selects compatible assets, fetches Wasm on the host with HTTP/abort checks, supplies a temporary blob URL, races worker errors and an initialization deadline, and releases resources on setup failure. A failed Wasm fetch inside the DuckDB 1.32.0 worker can otherwise leave instantiate() pending.

After startup succeeds, this wrapper transfers the initialized database and worker to the adapter:

import type { AsyncDuckDB } from '@duckdb/duckdb-wasm';
import { createDuckDBBackend } from '@quartile/react/duckdb';

// The application has already initialized this database and worker.
export function openBackend(database: AsyncDuckDB, worker: Worker) {
  return createDuckDBBackend({
    create: async () => ({
      database,
      dispose: async () => {
        try { await database.terminate(); }
        finally { worker.terminate(); }
      },
    }),
  });
}

Serve Wasm as application/wasm. The application's CSP must allow worker/Wasm execution and the temporary blob URL used during initialization. The gallery serves assets from its own origin; no library-owned CDN or remote database is required. Initialize outside rendering and dispose resources created after an effect has been canceled.

Ingest once

import { tableFromArrays } from 'apache-arrow';

const backend = await openBackend(database, worker); // Initialized by the application.
const source = await backend.fromArrow(tableFromArrays({
  region: ['Europe', 'Asia', 'Europe'],
  revenue: new Float64Array([120, 80, 60]),
}), {
  id: 'orders',
  fields: { revenue: { label: 'Revenue', format: 'currency', currency: 'USD' } },
});

fromArrow accepts an Arrow Table or Uint8Array IPC bytes, copies them into a private relation, and leaves the input caller-owned. IPC decoding, re-encoding and ingestion can allocate additional buffers; this is not a zero-copy API. The schema comes from Arrow's physical types rather than a sample of returned rows.

Link aggregates and exact detail pages

import { Selection } from '@quartile/react';
import { QueryBarList, QueryDataTable, QueryKPI,
  type QuerySource } from '@quartile/react/query';

export function OrderViews({ source }: { source: QuerySource }) {
  return <Selection>
    <QueryKPI data={source} value="revenue" label="Revenue" />
    <QueryKPI data={source} aggregate="count" label="Matching orders" />
    <QueryBarList data={source} category="region" value="revenue" select />
    <QueryDataTable data={source} pageSize={25} defaultSort="-revenue"
      columns={[{ field: 'region' }, { field: 'revenue' }]} />
  </Selection>;
}

QueryBarList supports sum/count, returns at most 20 categories by default and reports truncation. QueryKPI supports sum, count, mean, min and max over matching raw records. QueryDataTable sends sorting and paging to the worker, displays the matching total and resets to the first page when another view changes the filter. Its cell bars and shares describe the current page.

PropTypeDefaultDescription
datarequiredQuerySource——
aggregate'sum' | 'count'value ? 'sum' : 'count'Counts and totals. For nullable means/min/max use an explicit query result view.
variant'bar' | 'fill'—bar: label and value on one line with a thin bar under them. fill: a compact row with the bar drawn as a soft fill behind the label.
valuekeyof R & string—Measure to aggregate. Omit it to count rows per category.
idstring—Publisher id for the selection. Defaults to a generated id.
onSelect(predicate: Predicate | null) => void—Called after a toggle with the new predicate for category (or null).
formatFormatter—Formats values. Defaults to compact currency, integers for counts, else the field's format.
sort'desc' | 'asc' | 'none'SortOrder'desc'Ranking order. Defaults to descending.
limitnumber20Shows at most this many rows; the rest are summarized in a footer line.
selectboolean—Clicking a row (or Enter on it) toggles its category in the nearest Selection.
selectionstring | false—Selection to read from and publish to. Defaults to the nearest; false opts out.
categoryrequiredkeyof R & string—Field whose values become the rows of the list.
preparedboolean—One already-aggregated numeric value per distinct, typed category. Skips raw-row grouping.
invertDeltaboolean—Treats a fall in the delta as good (e.g. refunds), flipping its color.

Also accepts the props of Omit<BarListProps, 'data' | 'delta' | 'aggregate'>, spread onto the root element.

Frame and state props shared with the charts (11) · ChartStateProps, ChartBaseProps
PropTypeDefaultDescription
loadingboolean | string—Shows the loading state at the chart's size, so the layout never jumps.
empty{ title?: string; description?: string; action?: ReactNode }—Title and description for the empty state. Shown when there are no rows.
errorboolean | string | Error | null—Shows the error state. A string or Error becomes the message.
aria-labelstring—Accessible name. A summary of the data is generated and rendered as hidden text inside the figure.
classNamestring——
styleCSSProperties——
heightnumber—Plot height in px. Width always fills the container.
errorCodestring—Shown in the error state; copyable for support.
onRetry() => void——
view'chart' | 'table'—table renders the same data as a table, with the same formats, at the chart's size. Large lazy tables expose a data-view control rather than mounting every hidden row.
tooltipNotestring—Plain-text context shown with tooltips and included in the accessible chart description.
PropTypeDefaultDescription
datarequiredQuerySource——
valuestring——
aggregate'sum' | 'count' | 'mean' | 'min' | 'max'AggregateName'sum'—
loadingboolean—Shows a skeleton at the card's size.
aria-labelstring——
classNamestring——
styleCSSProperties——
labelrequiredReactNode—What the number is, e.g. "Net revenue".
currencystring—ISO 4217 code for currency formats.
formatFormatter—Value format. Defaults to the field's format (linked) or "number" (static).
unitReactNode—Mono tag in the top-right corner, e.g. "USD". Defaults to the field's unit in linked mode.
selectionstring | false—Selection to read from. Defaults to the nearest; false ignores it.
deltanumber—The change to show, as a ratio for "percent" (0.046 → +4.6%) or in points for "pt". Computed from compareValue or compare when omitted.
deltaKind'percent' | 'pt'—Defaults to "pt" for percent-formatted values, "percent" otherwise.
deltaDigitsnumber—Delta decimal places, an integer from 0 to 20. Defaults to 1 for percent and 2 for pt.
invertboolean—For metrics where down is good (churn, cost): flips the delta color.
comparisonReactNode—What the delta is measured against, e.g. "vs. previous 30 days".
trendreadonly number[]—Values for the inline sparkline (static mode, or to override trendBy).
targetKPITarget—Shows progress toward a goal instead of a sparkline.
compareKPICompare | boolean—Two bars, this period against the previous one. Pass the numbers, or true in linked mode to use the aggregated value and compareValue.
idstring—Publisher id; use the returned sourceId on a chart that publishes this query's selection.
predicatesreadonly Predicate[]—Additional application filters, applied together with source-excluding selection filters.

Also accepts the props of Omit<KPIProps, 'data' | 'value' | 'aggregate' | 'compareValue' | 'trendBy'>, spread onto the root element.

PropTypeDefaultDescription
datarequiredQuerySource——
pageSizenumber25Bounded remote page size; default 25. Grouping belongs in an explicit aggregate plan.
defaultSortstring——
aria-labelstring——
classNamestring——
idstring—Publisher id.
styleCSSProperties——
titleReactNode——
heightnumber—Scroller height in px when virtualized (or to cap the table). Defaults to 480 when virtualized.
multipleboolean—Select several rows (default) or one.
footerReactNode—Right side of the footer.
columnsrequiredDataTableColumn<Row>[]——
rowKeystring | ((row: Row, index: number) => string)—Stable row identity: a field, or a function.
sortsreadonly SortState[]—Ordered multi-sort priorities. When supplied, takes precedence over the single sort prop.
defaultSortsreadonly SortState[]——
onSortsChange(sorts: SortState[]) => void——
pagenumber—Controlled zero-based page.
defaultPagenumber——
onPageChange(page: number) => void——
selectstring—Field published to the selection when a row is clicked or Enter is pressed; selected rows highlight with a check. The table is not filtered by its own selection.
captionReactNode—Secondary line under the title.
toolbarReactNode—Controls on the right of the title row.
nounstring—Plural noun for the page label, e.g. "products" in "1–8 of 12 products".
virtualizenumber | false—Row count above which only the visible rows render inside a fixed-height scroller. Defaults to 200; false turns it off. Ignored when paginating.
selectionstring | false—Selection to read from and publish to. Defaults to the nearest; false opts out.
onRowClick(row: Row) => void——
onCellEdit(edit: DataTableEdit) => void | Promise<void>—Controlled write: update the caller's data after accepting this edit. Rejections remain visible.

Also accepts the props of Omit< DataTableProps, | 'data' | 'groupBy' | 'transform' | 'limit' | 'sort' | 'defaultSort' | 'onSortChange' | 'pageSize' | 'rank' | 'manualSort' | 'typedSelection' >, spread onto the root element.

Frame and state props shared with the charts (5) · ChartStateProps
PropTypeDefaultDescription
loadingboolean | string—Shows the loading state at the chart's size, so the layout never jumps.
empty{ title?: string; description?: string; action?: ReactNode }—Title and description for the empty state. Shown when there are no rows.
errorboolean | string | Error | null—Shows the error state. A string or Error becomes the message.
errorCodestring—Shown in the error state; copyable for support.
onRetry() => void——

Build a bounded custom chart

import { ScatterPlot } from '@quartile/react';
import { QueryResultView, useDataQuery,
  type QuerySource } from '@quartile/react/query';

export function RequestScatter({ source }: { source: QuerySource }) {
  const query = useDataQuery(source, {
    kind: 'rows',
    fields: ['id', 'latency', 'bytes', 'service'],
    orderBy: [{ field: 'id', direction: 'asc' }],
    window: { offset: 0, limit: 2000 },
  }, { id: 'request-scatter' });

  return <QueryResultView query={query}>
    {(data, result) => <>
      <p>{data.rows.length} points of {result.totalRows} matching requests</p>
      <ScatterPlot data={data} id={query.sourceId} x="bytes" y="latency"
        color="service" select="service" typedSelection renderer="canvas"
        aria-label="Latency against response size" />
    </>}
  </QueryResultView>;
}

useDataQuery excludes its own publisher's predicates and applies all others before grouping or limiting. Pass its returned sourceId to the selecting chart. Keep the chart inside QueryResultView: this prevents already-consumed filters being reapplied to a bounded or aggregated result, while retaining publication and highlighting.

typedSelection preserves physical field identities in custom ScatterPlot or DataTable results, including ISO-looking nominal strings and nulls. BarList prepared accepts one numeric row per typed category without regrouping; QueryBarList configures this automatically. Display scalar query results as static KPI values. Counting grouped rows would count groups.

The hook returns idle/loading/ready/error, hides old results when inputs change, and ignores superseded successes and failures. retry() repeats the request. durationMs includes backend queueing, execution, transfer and decoding; it is not engine CPU time.

PropTypeDefaultDescription
idstring—Publisher id; use the returned sourceId on a chart that publishes this query's selection.
selectionstring | false—Defaults to the nearest Selection; false opts out.
predicatesreadonly Predicate[]—Additional application filters, applied together with source-excluding selection filters.
PropTypeDefaultDescription
statusrequired'idle' | 'loading' | 'ready' | 'error'——
datarequiredDataset | undefined——
resultrequiredQueryResult | undefined——
errorrequiredError | undefined——
sourceIdrequiredstring——
durationMsrequirednumber | undefined—Request start to bounded result receipt; includes the backend queue and decoding.
retryrequired() => void——
appliedPredicatesrequiredReadonlySet<Predicate>—Selection predicates consumed by this result, for QueryResultView.
PropTypeDefaultDescription
queryrequiredDataQueryState——
childrenrequired(data: Dataset, result: QueryResult) => ReactNode—Runs only for the current, successful result. Use query.sourceId on a selecting chart.
loadingReactNode——
error(error: Error, retry: () => void) => ReactNode——

Plans, bounds and result counts

KindBoundtotalRows means
rowsExplicit offset and limit, optional projection and ordering.Matching raw records before windowing.
aggregateGroup fields, named measures, ordering and explicit limit.Result groups before limiting; one for a scalar.
histogramExplicit increasing finite edges.Number of bins, including empty bins.

The adapter caps row/aggregate output at 10,000 and histogram output at 1,000 bins, rejecting unbounded requests. complete means the result covers everything from offset zero. Requests and results carry IDs and immutable source revisions; mismatches are rejected.

PropTypeDefaultDescription
kindrequired'rows' | 'aggregate' | 'histogram'——
fieldsreadonly string[]——
orderByreadonly QueryOrder[]——
window{ offset: number; limit: number }——
groupByreadonly string[]——
measuresreadonly QueryMeasure[]——
limitnumber——
fieldstring——
edgesreadonly number[]—Increasing finite edges. Bins are [lo, hi), except the inclusive final edge.
PropTypeDefaultDescription
requestIdrequiredstring——
sourceVersionrequiredstring——
rowsrequiredRow[]——
schemarequiredSchema——
totalRowsrequirednumber—Exact result count before the requested window/limit (groups for aggregate plans).
completerequiredboolean—True when the returned rows cover the entire result, without windowing/truncation.
PropTypeDefaultDescription
kindrequired'query-source'——
idrequiredstring——
versionrequiredstring—Immutable revision. Replace the source object when its data or schema changes.
schemarequiredSchema——
queryrequired(plan: QueryPlan, options: QueryOptions) => Promise<QueryResult>——
disposerequired() => Promise<void>—Idempotent; rejects subsequent queries. Does not dispose another source's resources.

Physical types and matching

  • Equality/inclusion follow the Arrow physical type. Date-looking strings in nominal fields stay strings. Temporal values accept epoch milliseconds, Date or ISO date strings; date-only strings use UTC midnight here. Synchronous row predicates retain their local-date convention.
  • Null equality and inclusion are explicit. Empty inclusion matches nothing. Ranges include both ends and support null open endpoints.
  • Numeric aggregates require numeric fields. Empty sum is zero; empty mean/min/max is null. Overflowing nonfinite aggregate results reject the query; raw float rows retain their physical values. Numeric-looking strings and booleans are not numeric measures.
  • Count without a field counts records; count with a field excludes null and empty strings. Histogram bins include their lower edge, exclude the upper edge, and include the final edge.
  • Text order is binary lexical, nulls sort last, and raw pages break remaining ties by original input order. Ordinary in-memory DataTable sorting is locale-aware.

Supported inputs are integers up to 32 bits, Float32/Float64, booleans, strings and string dictionaries, and dates/timestamps at millisecond precision or coarser. Exact decimals, 64-bit input integers, nested types and sub-millisecond timestamps are rejected. Convert deliberately before import. Metadata overrides can change labels, formats, currency and units, but not physical types.

Dispose, cancel and define application boundaries

source.dispose() drops that source's private relation. backend.dispose() disposes imported sources and adapter-owned connections. Both are idempotent. A create factory owns cleanup; a supplied database stays caller-owned, and a supplied connection is borrowed for exclusive use while the adapter is active.

Aborted requests reject promptly and queued work is skipped. Active execution may continue in the worker, retaining its execution lock until cleanup settles. Stale-result suppression is not a guarantee of engine interruption. With an owned create factory, backend disposal immediately invokes the required database/worker termination without waiting for pending SQL. This releases owned relations and connections even after fatal worker errors leave request promises unresolved. Monitor worker errors and create a fresh backend to recover.

Borrowed database/connection disposal remains graceful: it waits for active work before dropping private relations and closing adapter-created connections. A fatal worker failure can leave this wait pending; the caller must terminate a failed borrowed worker. Individual source disposal also waits for active work. Use owned backend disposal to recover from a failed owned worker. Pass exactly one of create, database or connection to the backend constructor.

Plans contain no SQL, URLs or relation names. The adapter validates fields, quotes identifiers and binds values. The application still owns authorization and may only ingest data the current user can access. Generated dashboard specs currently use synchronous datasets; they do not create connections or query SQL.

PropTypeDefaultDescription
databaseAsyncDuckDB | never——
connectionnever | AsyncDuckDBConnection——
createnever | () => Promise<OwnedDuckDB>——
PropTypeDefaultDescription
fromArrowrequired(input: Table | Uint8Array, options: ArrowSourceOptions) => Promise<DuckDBSource>—Copies into a private relation; input buffers/tables remain caller-owned.
disposerequired() => Promise<void>—Terminates an owned factory; otherwise drops relations and closes adapter-created connections.
PropTypeDefaultDescription
idrequiredstring——
versionstring——
fieldsRecord<string, FieldOverride>——

Read Canvas and WebGL scatter and Performance for rendering and measurement boundaries. Public source guide.

Report an issueBuilt Oct 21, 2026