Skip to content

Frames

Keep SQL results host-resident and build lazy dataframe plans.

Updated View as Markdown

AS FRAME keeps query results in host-resident columnar storage:

frame := FROM Booking
    WHERE created_at >= cutoff
    SELECT { id, nights }
    AS FRAME?

The result is Frame[Row]. Row is the compile-time schema used when rows are materialized. The frame itself is a thin, generation-tagged resource handle; it does not contain guest rows.

Engines

Two engines have distinct jobs:

Layer Engine Responsibility
Query datasource engine Execute FROM Model ...; DataFusion is one possible query engine
Manipulation Polars Execute every Df[T] operation after frame.df()

A DataFusion result bridges its Arrow batches into Polars without copying the columns. Results from Turso, PostgreSQL, and MySQL are converted from their host-side cell grids. The Atoll surface is identical in both cases.

Frame operations

count := frame.count()
rows := frame.collect()?

for index in 0..frame.batches() {
    batch := frame.collect_batch(index)?
    consume(batch)
}

frame.release()

Dropping a frame releases its host entry; release() does so early. Generation tagging prevents a stale handle from reading a later frame.

count() reads only metadata. collect() copies and decodes every row into guest memory, while collect_batch() bounds that copy to one host batch. A row-engine frame currently reports one logical batch; a columnar result may have many.

An out-of-range batch or stale frame returns QueryError from collection. count() and batches() report 0 for a stale handle. release() is idempotent, and Drop releases a frame on normal exit, error propagation, or cancellation.

Lazy plans

frame.df() creates Df[T], a lazy Polars plan:

top := frame.df()
    .filter(col("nights").gt(lit(3)))
    .sort("nights", true)
    .head(10)

rows := top.collect()?

Deriving a plan does not scan the data. collect() executes and copies rows to the guest; to_frame() executes into another host-resident Frame[T].

Schema-preserving operations include head, tail, slice, unique, sort, sort_by, and filter. Schema-changing operations include select[R], with_columns[R], inner and left joins, and group_by(...).agg[R](...).

The result type R is a plain row struct. Output columns bind to its fields by name, not expression order. Right-side fields made nullable by a left join must be optional in R.

Expressions

col("name") and lit(value) create Expr leaves. Comparisons are methods such as .eq, .lt, and .ge; Atoll comparison operators themselves return bool and therefore cannot build an expression tree. Combine predicates with &, |, and !:

matches := frame.df().filter(
    col("nights").ge(lit(3))
        & col("status").eq(lit("confirmed"))
)

Expressions also support arithmetic, aliases, aggregates such as .sum() and .count(), scalar-list membership, and literal substring predicates. Schema-changing expressions must be named with a bare column or .alias("field_name") so they can bind to R.

Frame queries

A local Frame[Row] or Df[Row] can be a FROM source:

rows := FROM frame
    WHERE nights > minimum
    ORDER BY nights DESC?

This syntax lowers to dataframe operations, not database SQL. AS DF is available for frame-local queries and returns the lazy plan without Result. AS DF on a table query is rejected.

Frame-source terminals mirror table-source shapes:

Terminal Type
default Result[[]Row, QueryError]
ONE FROM Result[Row?, QueryError]
AS FRAME Result[Frame[Row], QueryError]
AS DF Df[Row]

The supported frame-query subset includes predicates, projections, grouping, having, ordering, paging, and same-named-key joins. CTEs, windows, set operations, runtime sort directions, conditional clauses, right/cross joins, and renamed join keys currently diagnose.

Files

read_parquet[T](path) and read_csv[T](path) return lazy Df[T] plans. The host validates the file schema against T; extra columns are projected away, while missing or incompatible required columns return QueryError. CSV uses its header and the declared T rather than inferring a changing public shape.

These scans are lazy: opening metadata validates the schema, and a terminal executes the scan. File ingestion does not require a configured datasource. Writing frames to files, raw dataframe SQL, regular-expression predicates, cloud or glob scans, and window operations over frames are not part of the current surface.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close