Skip to content

THE INCQL BOOK

8. Write your first query block

Shape a typed relation with SQL-familiar clauses, then cross the same explicit Session execution boundary.

Part IV

Query blocks

Chapter 1 of 2

Goal

Activate IncQL's query vocabulary, begin with a typed CSV-backed LazyFrame[Order], express filtering, projection, ordering, and a limit in one query { ... } expression, then collect the resulting plan.

Change the authoring surface, not the system boundary

A query block is an IncQL expression embedded in ordinary Incan code. It is not a SQL string and it does not open a file, choose an engine, or execute itself. import pub::incql activates the dependency-owned vocabulary, while the Session still owns source registration and execution. This means the source setup from the core Book remains valid: read_csv(...) registers a logical name and returns deferred work whose intended row shape is described by Order.

The block then reads top to bottom. FROM orders establishes the current relation. WHERE .status == "paid" adds a predicate against that relation. SELECT publishes a new three-column query schema through explicit aliases. ORDER BY desc(.amount) reads the projected amount column and requests descending order; IncQL uses the desc(...) helper rather than postfix SQL such as .amount DESC. Finally, LIMIT 10 caps the ordered result.

With this example, you are asking for paid orders only, retaining order_id, customer_id, and amount, ordering the projected rows from highest to lowest amount, and returning no more than ten. Because the FROM value is a LazyFrame, the query expression also returns a LazyFrame. Its clauses become deferred carrier operations—ReadNamedTable → Filter → SelectProject → OrderBy → Limit—before session.collect(...) crosses the runtime boundary.

One typed source · one checked clause flow

Follow the query from activation to rows

The authoring syntax changes, but the ownership story does not: IncQL builds deferred intent and Session performs the concrete DataFusion collection.

  1. 01
    Activate and registerMake the vocabulary available, then establish a typed logical source
    import pub::incql # (1)!
    
    orders: LazyFrame[Order] = session.read_csv(
        "tutorial_orders_query",
        "orders.csv",
    )?
    
    1. Activate the dependency-owned vocabulary. The plain import pub::incql makes query and its scoped helpers available to this compilation unit. The separate from pub::incql import ... statement in the complete source imports the ordinary types and functions that the surrounding Incan code names directly.

    Owner
    Session source registration
    Artifact
    LazyFrame[Order] rooted at ReadNamedTable
    Knowable now
    The logical source identity and intended row model; no rows have been collected
  2. 02
    Author the queryPublish a projected schema while keeping the result deferred
    paid_orders: LazyFrame[PaidOrderReview] = query { # (1)!
        FROM orders
        WHERE .status == "paid"
        SELECT
            .order_id as order_id,
            .customer_id as customer_id,
            .amount as amount,
        ORDER BY desc(.amount) # (2)!
        LIMIT 10
    }
    
    1. Describe the intended output without pretending validation is stronger than it is. A current query block requires FROM and SELECT. Because orders is lazy, the expression returns deferred work. LazyFrame[PaidOrderReview] documents the intended projected row model; the current implementation checks query-schema evolution and selected aliases, while complete field-for-field and type compatibility validation against that annotation remains follow-up work.

    2. Resolve fields against the clause's current schema. Leading-dot references name fields visible at that point in the query. The SELECT aliases publish order_id, customer_id, and amount for later clauses, so .amount in ORDER BY refers to the projected column. Use asc(...) for the opposite direction; postfix ASC/DESC tokens are not query-block syntax.

    Owner
    IncQL query vocabulary and carrier planning
    Artifact
    LazyFrame[PaidOrderReview]
    Knowable now
    The filter, projected schema, ordering, and upper row bound; execution is still absent
  3. 03
    Collect the resultBind and execute the deferred query through Session
    result = session.collect(paid_orders)? # (1)!
    println(f"columns: {result.resolved_columns():?}")
    println(f"rows: {result.row_count()}")
    println(result.preview_text())
    
    1. Cross the execution boundary deliberately. collect(...) submits the deferred query through the selected backend and returns a materialized DataFrame[PaidOrderReview] on success. Use collect_observed(...) instead when the caller also needs the structured success-or-failure observation introduced earlier in the Book.

    Backend
    datafusion
    Columns
    order_id, customer_id, amount
    Rows
    2 paid orders, highest amount first
    Query plan Materialized result

The query block and an equivalent method chain converge on the same relational carrier semantics. Here, however, every relation-shaping step appears in one clause-oriented expression. That makes the current query schema especially visible: the source model supplies the fields before SELECT, and the aliases published by SELECT supply the fields seen by ORDER BY.

The intended output type still deserves precise language. PaidOrderReview is valuable documentation and gives the surrounding Incan program a typed carrier contract, but this release does not yet prove complete compatibility between every selected field/type and that annotated model. The runnable checkpoint verifies the actual resolved column names and materialized rows instead of presenting the annotation alone as physical-schema evidence.

Open the complete Chapter 8 checkpoint
"""Chapter 8: filter and shape typed data with IncQL query clauses."""

import pub::incql

from domain import Order, PaidOrderReview
from pub::incql import LazyFrame, Session, SessionError, desc, report_session_error


def query_paid_orders(mut session: Session) -> Result[None, SessionError]:
    """Run a SQL-familiar query block without using relational method chains."""
    orders: LazyFrame[Order] = session.read_csv("tutorial_orders_query", "orders.csv")?
    paid_orders: LazyFrame[PaidOrderReview] = query {
        FROM orders
        WHERE .status == "paid"
        SELECT
            .order_id as order_id,
            .customer_id as customer_id,
            .amount as amount,
        ORDER BY desc(.amount)
        LIMIT 10
    }

    result = session.collect(paid_orders)?
    println(f"columns: {result.resolved_columns():?}")
    println(f"rows: {result.row_count()}")
    println(result.preview_text())
    return Ok(None)


def main() -> None:
    mut session = Session.default()
    match query_paid_orders(session):
        Ok(_) => println("Chapter 8: query block completed")
        Err(error) => report_session_error("tutorial.chapter_08", error)

Run it from the included tutorial project:

incan run src/chapter_08.incn

Expected evidence

The checkpoint reports the resolved columns order_id, customer_id, and amount, then reports two rows. Its preview places order 1001 before order 1003, demonstrating that the paid-row filter and descending order both reached execution. The final line confirms that Chapter 8 completed.

For the complete clause inventory, expression operators, alias rules, and current limitations, use Query blocks.

Try it

Change desc(.amount) to asc(.amount) and reduce the limit to 1. Run the checkpoint and explain why order 1003 is now the only preview row. Restore the original ordering and limit before continuing.

Chapter complete

You can activate IncQL's query vocabulary, explain how leading-dot fields and aliases follow the current query schema, distinguish deferred query authoring from Session collection, and state the present output-model validation limit honestly.