table¶
The table package provides an in-memory tabular data pipeline for filtering, projecting, joining, grouping, and aggregating lists of hashmaps. It is automatically loaded by the Kiwi runtime — no explicit import is needed.
Every transform method returns self, so operations can be chained freely. Call a terminal method to retrieve results.
Package Functions¶
table::from(rows)¶
Creates a new Table wrapping a list of hashmaps.
Parameters
| Type | Name | Description |
|---|---|---|
list |
rows |
A list of hashmaps — typically the output of a csv::pipeline() or json::pipeline() call. |
Returns
| Type | Description |
|---|---|
Table |
A new Table instance. |
Table¶
Transform methods¶
All transform methods return self for chaining.
.where(predicate)¶
Keeps only rows for which predicate(row) is truthy.
| Type | Name | Description |
|---|---|---|
lambda |
predicate |
A lambda accepting a row hashmap. |
.select(keys)¶
Projects each row to only the specified fields.
| Type | Name | Description |
|---|---|---|
list |
keys |
The field names to retain. |
.rename(old_name, new_name)¶
Renames a field in every row.
| Type | Name | Description |
|---|---|---|
string |
old_name |
The existing field name. |
string |
new_name |
The replacement field name. |
.add_field(name, compute)¶
Adds a computed field to every row.
| Type | Name | Description |
|---|---|---|
string |
name |
The new field name. |
lambda |
compute |
A lambda accepting a row and returning the new field's value. |
.drop(keys)¶
Removes the specified fields from every row.
| Type | Name | Description |
|---|---|---|
list |
keys |
The field names to remove. |
.map(transform)¶
Applies a custom transform to every row. The lambda receives a row hashmap and must return the new row.
| Type | Name | Description |
|---|---|---|
lambda |
transform |
A lambda accepting a row hashmap and returning a hashmap. |
.order_by(field, asc?)¶
Sorts rows by a field value.
| Type | Name | Description | Default |
|---|---|---|---|
string |
field |
The field name to sort by. | |
boolean |
asc |
true for ascending, false for descending. |
true |
.limit(n)¶
Keeps only the first n rows.
| Type | Name | Description |
|---|---|---|
integer |
n |
Maximum number of rows to keep. |
.offset(n)¶
Skips the first n rows.
| Type | Name | Description |
|---|---|---|
integer |
n |
Number of rows to skip. |
.distinct()¶
Removes duplicate rows by comparing all fields.
Join methods¶
All join methods return self. When a left and right row are merged, right-side fields overwrite left-side fields on name collision. Use .rename() or .select() before joining to avoid unwanted overwrites.
All four join methods use a hash index on the right-side table, giving O(n+m) performance regardless of table size.
.inner_join(other, left_key, right_key)¶
Keeps only rows that have at least one matching row in other.
| Type | Name | Description |
|---|---|---|
list |
other |
The right-side list of hashmaps. |
string |
left_key |
The join field on this table. |
string |
right_key |
The join field on other. |
.left_join(other, left_key, right_key)¶
Keeps all rows from this table. Rows with no match in other are included without any right-side fields.
| Type | Name | Description |
|---|---|---|
list |
other |
The right-side list of hashmaps. |
string |
left_key |
The join field on this table. |
string |
right_key |
The join field on other. |
.right_join(other, left_key, right_key)¶
Keeps all rows from other. Rows with no match in this table are included without any left-side fields.
| Type | Name | Description |
|---|---|---|
list |
other |
The right-side list of hashmaps. |
string |
left_key |
The join field on this table. |
string |
right_key |
The join field on other. |
.full_join(other, left_key, right_key)¶
Keeps all rows from both sides. Unmatched rows carry only their own side's fields.
| Type | Name | Description |
|---|---|---|
list |
other |
The right-side list of hashmaps. |
string |
left_key |
The join field on this table. |
string |
right_key |
The join field on other. |
Anti-join pattern¶
To find rows in this table with no match in another table (equivalent to LEFT JOIN … WHERE right.col IS NULL), use .left_join() followed by .where() checking for the absence of a right-side field:
# Users who have placed no orders
table::from(users)
.left_join(orders, "id", "user_id")
.where(do (r) => !r.keys().contains("user_id") end)
.to_list()
After a left join, unmatched rows retain only their original fields — so any field that only exists on the right side will be absent.
Grouping & Aggregation¶
.group_by(field)¶
Groups rows by the unique values of field. Must be followed by .agg().
| Type | Name | Description |
|---|---|---|
string |
field |
The field to group by. |
.agg(specs)¶
Collapses groups into one row per group. specs is a hashmap mapping output field names to aggregate lambdas. Each lambda receives the list of rows in its group and returns a value. The group-by field is automatically included in the output.
Throws — if called without a preceding .group_by().
| Type | Name | Description |
|---|---|---|
hashmap |
specs |
{ "out_field": do (rows) => … end, … } |
table::from(orders)
.group_by("product")
.agg({
"count": do (rows) => rows.size() end,
"total": do (rows) => rows.map(do (r) => r["qty"] end).sum() end,
"avg_qty": do (rows) => rows.map(do (r) => r["qty"] end).sum() / rows.size() end
})
.to_list()
Terminal methods¶
.to_list()¶
Returns the current rows as a plain list of hashmaps.
.to_hashmap(key_field)¶
Returns a hashmap keyed by the values of key_field. If multiple rows share the same key value, the last one wins.
| Type | Name | Description |
|---|---|---|
string |
key_field |
The field whose value becomes each hashmap key. |
Returns hashmap
.pluck(field)¶
Extracts a single field's values as a flat list.
| Type | Name | Description |
|---|---|---|
string |
field |
The field name to extract. |
Returns list
.first()¶
Returns the first row, or null if the table is empty.
Returns hashmap | null
.count()¶
Returns the number of rows.
Returns integer
.display(max_width?)¶
Prints the table as a formatted ASCII grid and returns self for continued chaining.
Columns are derived from the first row's keys. Each column is wide enough to fit its header and all its values. When max_width is set, any value longer than that many characters is truncated and suffixed with ….
| Type | Name | Description | Default |
|---|---|---|---|
integer |
max_width |
Maximum column width in characters. 0 means unlimited. |
0 |
Returns Table (self)
table::from(rows)
.order_by("score", false)
.display()
# +----+-------+-------+
# | id | name | score |
# +----+-------+-------+
# | 1 | Alice | 9 |
# …
# (4 row(s))
# Cap every column at 25 characters
table::from(rows).display(25)
# +----+---------------------------+-------+
# | id | name | score |
# +----+---------------------------+-------+
# | 1 | A very long name that g… | 9 |
Examples¶
Filter, project, sort¶
result = table::from(users)
.where(do (r) => r["active"] == true end)
.select(["id", "name", "score"])
.order_by("score", false)
.limit(10)
.to_list()
Computed field¶
Inner join¶
result = table::from(users)
.inner_join(orders, "id", "user_id")
.select(["name", "product", "qty"])
.to_list()
Rename before join to avoid field collision¶
# Both users and products have a "name" field.
# Rename one before joining to keep both.
product_lookup = table::from(products)
.rename("name", "product_name")
.to_list()
result = table::from(orders)
.inner_join(product_lookup, "product_id", "id")
.select(["order_id", "product_name", "qty"])
.to_list()
Group and aggregate¶
stats = table::from(orders)
.group_by("region")
.agg({
"orders": do (rows) => rows.size() end,
"total_qty": do (rows) => rows.map(do (r) => r["qty"] end).sum() end,
"total_rev": do (rows) => rows.map(do (r) => r["revenue"] end).sum() end
})
.order_by("total_rev", false)
.display()
Anti-join: rows with no match¶
# Find users who have never placed an order
no_orders = table::from(users)
.left_join(orders, "id", "user_id")
.where(do (r) => !r.keys().contains("user_id") end)
.pluck("name")