OData API
On this page
Complete OData v4 query reference. For setup and tool connections, see Serve Data via OData.
Endpoints
| Method | Path | Response |
|---|---|---|
GET | /odata | Service document (lists all exposed entities) |
GET | /odata/$metadata | CSDL XML (schema for all entities) |
GET | /odata/{entity} | Query collection |
GET | /odata/{entity}(key) | Single entity by key |
GET | /odata/{entity}/$count | Row count (plain text) |
POST | /odata/$batch | JSON batch (multiple requests in one call) |
Entity names use underscores: mart.revenue becomes mart_revenue in the URL.
Query Parameters
| Parameter | Example | Effect |
|---|---|---|
$select | $select=customer,amount | Return only these columns |
$filter | $filter=amount gt 100 | Filter rows |
$orderby | $orderby=amount desc | Sort results |
$top | $top=10 | Limit rows |
$skip | $skip=20 | Offset |
$count | $count=true | Include total count in response |
$search | $search=alice | Full-text search across all string columns |
$expand | $expand=raw_orders | Inline related entities |
$apply | $apply=aggregate(amount with sum as total) | Aggregation |
$compute | $compute=amount mul 2 as doubled | Computed properties (col op literal as alias) |
Filter Operators
| Operator | Example |
|---|---|
eq, ne | customer eq 'Alice' |
gt, ge, lt, le | amount gt 100 |
and, or, not | amount gt 100 and not (active eq false) |
in | customer in ('Alice','Bob') |
has | Enum flag bitwise check |
eq null | order_date eq null |
Filter Functions
| Category | Functions |
|---|---|
| String | contains, startswith, endswith, tolower, toupper, length, trim, concat, indexof, substring |
| Date | year, month, day, hour, minute, second, now, date, time |
| Math | round, floor, ceiling |
| Arithmetic | add, sub, mul, div, mod |
| Type | cast, isof |
| Pattern | matchesPattern (regex) |
| Geo | geo.distance, geo.intersects, geo.length (requires DuckDB spatial) |
| Other | case expressions, fractionalseconds, totaloffsetminutes, totalseconds, maxdatetime, mindatetime |
$apply Aggregation
# Sum
$apply=aggregate(amount with sum as total)
# Group by + aggregate
$apply=groupby((customer),aggregate(amount with sum as total))
# Supported functions: sum, avg, min, max, count, countdistinct
$expand Navigation
Relationships between @expose models are auto-discovered when one entity has a column matching another entity’s key column (FK→PK pattern). Both models must have @expose with explicit key columns.
# Inline related orders for each customer
curl "http://localhost:8090/odata/raw_customers?\$expand=raw_orders"
Navigation properties appear in $metadata automatically. The response nests related entities inline.
$batch
Multiple requests in one HTTP call (JSON batch format):
curl -X POST http://localhost:8090/odata/\$batch \
-H "Content-Type: application/json" \
-d '{"requests":[
{"id":"1","method":"GET","url":"mart_revenue?$top=5"},
{"id":"2","method":"GET","url":"mart_revenue/$count"}
]}'
Examples
Collection and entity:
curl "http://localhost:8090/odata/mart_revenue"
curl "http://localhost:8090/odata/mart_revenue(1)"
curl "http://localhost:8090/odata/mart_revenue?\$select=customer,amount"
Filtering:
curl "http://localhost:8090/odata/mart_revenue?\$filter=amount%20gt%20100"
curl "http://localhost:8090/odata/mart_revenue?\$filter=customer%20in%20('Alice','Bob')"
curl "http://localhost:8090/odata/mart_revenue?\$filter=contains(customer,'Ali')"
curl "http://localhost:8090/odata/mart_revenue?\$filter=year(order_date)%20eq%202026"
curl "http://localhost:8090/odata/mart_revenue?\$filter=amount%20add%2050%20gt%201000"
Sorting, pagination, count:
curl "http://localhost:8090/odata/mart_revenue?\$orderby=amount%20desc&\$top=10"
curl "http://localhost:8090/odata/mart_revenue?\$top=10&\$skip=20"
curl "http://localhost:8090/odata/mart_revenue?\$filter=amount%20gt%20100&\$count=true"
Search, compute, expand:
curl "http://localhost:8090/odata/mart_revenue?\$search=alice"
curl "http://localhost:8090/odata/mart_revenue?\$compute=amount%20mul%202%20as%20doubled"
curl "http://localhost:8090/odata/raw_customers?\$expand=raw_orders"
Aggregation:
curl "http://localhost:8090/odata/mart_revenue?\$apply=aggregate(amount%20with%20sum%20as%20total)"
curl "http://localhost:8090/odata/mart_revenue?\$apply=groupby((customer),aggregate(amount%20with%20sum%20as%20total))"
Data Types
| DuckDB | OData | JSON |
|---|---|---|
VARCHAR | Edm.String | "text" |
TINYINT | Edm.Byte | 1 |
SMALLINT | Edm.Int16 | 100 |
INTEGER | Edm.Int32 | 42 |
BIGINT | Edm.Int64 | 9999999999 |
DOUBLE | Edm.Double | 3.14 |
FLOAT | Edm.Single | 1.5 |
DECIMAL | Edm.Decimal | 150.99 |
BOOLEAN | Edm.Boolean | true |
DATE | Edm.Date | "2026-01-15" |
TIMESTAMP | Edm.DateTimeOffset | "2026-01-15T08:30:00Z" |
NULL values are JSON null. Empty strings are preserved as "".
Security
- Read-only: only SELECT queries, no writes
- Column validation: queries only access columns that exist in the schema
- No SQL injection: column names validated against
information_schema, string values escaped - Localhost only: binds to
127.0.0.1by default
OndatraSQL