SQL.
Before you start.
This page uses the shop.orders table from the quickstart. Table names are always fully qualified: namespace.table, never a bare table name.
namespace = "shop"
table = "orders"
primary_key = ["id"]
[[columns]]
name = "id"
ty = "str"
required = true
[[columns]]
name = "customer"
ty = "str"
[[columns]]
name = "amount_cents"
ty = "i64"
[[columns]]
name = "status"
ty = "str"
[[columns]]
name = "notes"
ty = "str"
[[columns]]
name = "placed_ms"
ty = "u64"
# Turns WHERE status = '...' into a sub-linear lookup.
[[indexes]]
name = "by_status"
columns = ["status"]
The [[indexes]] block is what turns WHERE status = … from a full scan into a lookup. EXPLAIN tells you which one you got.
Type mapping.
What a column declared with each ty looks like coming out of a SELECT, and what you get after decoding in each client. All three clients decode rows into a generic map, so the language column is what JSON decoding yields there.
| ty | JSON on the wire | TypeScript | Python | Go |
|---|---|---|---|---|
| i64 / u64 | number | number | int | float64 ⚠ |
| f64 | number | number | float | float64 |
| bool | boolean | boolean | bool | bool |
| str / text | string | string | str | string |
| decimal | string — "19.99" | string | str | string |
| uuid | string | string | str | string |
| enum | string | string | str | string |
| inet | string | string | str | string |
| bytes | hex string — "\\x4f43" | string | str | string |
| date | text — "2026-07-25" | string | str | string |
| timestamp | text — "2026-07-25 09:14:22" | string | str | string |
| time | text — "09:14:22.500000" | string | str | string |
| interval | number — milliseconds | number | int | float64 |
| json | object or array | unknown | dict / list | map[string]any / []any |
| list | array | unknown[] | list | []any |
| point | { lat, lng } object | object | dict | map[string]any |
A decimal column travels as a JSON string — "19.99" — deliberately, so no binary-float rounding is ever introduced into money. On write you may send either a string or a bare JSON number; both are canonicalised to the same stored form. SUM, AVG, arithmetic, ORDER BY and range comparisons over a decimal column are all exact and numeric, not lexical, and the result comes back in the same canonical string form. A result past the exact-decimal range is an error, never a silently wrong number.
An i64 or u64 travels as a JSON number. JavaScript decodes it to a double, and Go decodes into map[string]any as float64 — so values beyond about 9×10¹⁵ are silently rounded in both. Python is unaffected. If you store identifiers or counters that big, declare the column as str, or decode the response yourself with a big-integer-aware parser.
date, time and timestamp columns are stored as integers but rendered into familiar text on the way out — you read "2026-07-25", not an epoch count. That reformatting applies only to output columns the plan can prove are a straight pass-through of a temporal column, so a computed expression over one still comes back as a number. On write, both spellings are accepted and address the same row.
Related.
- Row CRUD — point reads and the bulk-write path, and how its upsert differs from SQL
INSERT. - Transactions — making several statements land together, and what isolation you actually get.
- Schema reference — column types, indexes, foreign keys and CHECK constraints.
- Queries in the dashboard — running the same statements from the console workbench.
- Error reference — the shared error envelope every endpoint returns.