Skip to content

Tables

Shape uses Table<T> for structured row data.

The examples below assume:

from std::core::intrinsics use { Table }
from std::core::queryable use { Queryable }
type Event {
id: int,
value: number,
tag: string
}

You can create tables inline using row literal syntax — comma-separated bracket groups after a Table<T> type annotation:

let events: Table<Event> =
[1, 50.0, "alpha"],
[2, 75.5, "beta"],
[3, 120.0, "gamma"]

Each [...] group is one row, with values in field order. The compiler checks that the number of elements matches the struct’s field count and resolves field types at compile time.

The LSP shows field names as parameter hints before each value, and the formatter auto-aligns columns across rows for readability.

from app::events use { load_events }
let events: Table<Event> = load_events("/tmp/events.csv")?
let filtered = events
.filter(|row| row.value > 10)
.limit(100)
print(filtered)

Shape query syntax desugars into typed table operations:

let ids = from row in events
where row.value > 10
select row.id

Use whichever form is clearer for the code you are writing.

Table<T> carries its row type through method chains. Type-preserving methods (filter, orderBy, head, tail, limit) return Table<T>. Transform methods (map, select) return Table<U> where U is the new row shape.

MethodSignatureDescription
filterfn(T) -> boolTable<T>Keep matching rows
mapfn(T) -> UTable<U>Transform each row
orderByfn(T) -> any, stringTable<T>Sort by key
groupByfn(T) -> KVec<{key: K, group: Table<T>}>Group rows
selectfn(T) -> UTable<U>Project columns
head / tailnumberTable<T>First/last N rows
limitnumberTable<T>Cap row count
count()numberRow count
forEachfn(T) -> voidvoidSide-effect iteration
describe()Table<any>Summary statistics

Methods chain naturally. The row type T flows through the entire pipeline:

let top_events = events
.filter(|row| row.value > 10)
.orderBy(|row| row.value, "desc")
.limit(5)
// top_events is Table<Event> — same row type as events

Table<T> implements the Queryable<T> trait, which provides a uniform query interface shared by database extensions. Code written against Queryable<T> works on both in-memory tables and database queries:

trait Queryable<T> {
filter(predicate): any,
map(transform): any,
orderBy(column, direction): any,
limit(n): any,
execute(): any
}

Database connectors (PostgreSQL, DuckDB) are available as Shape packages that return lazy query objects also implementing Queryable. The same .filter(...).orderBy(...).limit(n).execute() chain works across all data sources — in-memory tables execute immediately while database queries build SQL and execute on .execute(). These packages use extern C fn to call the underlying C libraries. For HTTP/REST APIs, use the built-in http stdlib module.

See Packages & Building and Native C Interop for how database packages are structured.

Combine table row literals with content string chart format specifiers to visualize data inline:

type Revenue {
month: int,
revenue: number,
cost: number
}
let data: Table<Revenue> =
[1, 120.0, 80.0],
[2, 135.0, 85.0],
[3, 150.0, 90.0],
[4, 142.0, 88.0],
[5, 168.0, 95.0],
[6, 190.0, 100.0]
print(c"{data: chart(line), x(month), y(revenue, cost)}")

The chart(line) format specifier renders the table as a line chart, with x(month) selecting the x-axis column and y(revenue, cost) selecting the series. See Content Strings for all chart types and options.

  • Table<T> keeps row shapes explicit and type-checked.
  • Prefer explicit row types for stable APIs and better LSP hints.
  • External source schemas should be locked via the project/script lockflow.
  • Table<T>.execute() returns itself — it’s a no-op for in-memory tables but required for lazy database queries.