NoSQL Tables
NoSQL Tables
ZeroDB provides schema-free NoSQL tables for storing structured data alongside vectors and memory.
All table endpoints are project-scoped: /api/v1/projects/{project_id}/database/tables/...
Create a Table
curl -X POST https://api.ainative.studio/api/v1/projects/{project_id}/database/tables \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "customers",
"description": "Customer records"
}'
Insert a Row
Rows are inserted one at a time. The body key is row_data.
curl -X POST https://api.ainative.studio/api/v1/projects/{project_id}/database/tables/customers/rows \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"row_data": {"name": "Alice", "email": "alice@example.com", "plan": "pro"}
}'
Query Rows
curl -X POST https://api.ainative.studio/api/v1/projects/{project_id}/database/tables/customers/query \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"filters": {"plan": "pro"},
"limit": 10,
"skip": 0
}'
Update a Row
curl -X PUT https://api.ainative.studio/api/v1/projects/{project_id}/database/tables/customers/rows/{row_id} \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"row_data": {"plan": "business"}
}'
Bulk Update (filter-based)
The bulk endpoint selects rows with a filter and mutates them with
MongoDB-style update operators.
curl -X PUT https://api.ainative.studio/api/v1/projects/{project_id}/database/tables/customers/rows/bulk \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"filter": {"plan": "free"},
"update": {"$set": {"plan": "starter"}}
}'
Supported update operators
Five update operators are implemented server-side. Anything else is rejected with a 400 — unknown operators are never silently ignored.
| Operator | Effect | Notes |
|---|---|---|
$set | Sets field values | Creates the field if absent |
$inc | Adds a number to a numeric field | Missing field is treated as 0; negative values decrement |
$push | Appends a value to an array field | Creates an empty array if the field is absent |
$pull | Removes all occurrences of a value from an array field | No-op if the field is absent |
$unset | Removes fields | The supplied value is ignored ("" or 1 are conventional) |
All five accept dot notation for nested fields (e.g. "usage.tokens").
Operators are applied in a fixed order regardless of key order in your
request: $set, $unset, $inc, $push, $pull.
$inc is a real arithmetic increment, not a literal write:
curl -X PUT https://api.ainative.studio/api/v1/projects/{project_id}/database/tables/customers/rows/bulk \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"filter": {"plan": "pro"},
"update": {
"$inc": {"credits": 100, "usage.api_calls": 1},
"$set": {"last_topup": "2026-09-30"},
"$push": {"audit": "monthly credit grant"},
"$unset": {"trial_expires_at": ""}
}
}'
$inc returns a 400 if the increment value is non-numeric, or if the target
field already holds a non-numeric value.
Filter operators
filter supports plain equality ({"plan": "pro"}) plus these comparison
operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin.
Response
{
"matched_count": 12,
"modified_count": 11,
"filter_used": {"plan": "pro"},
"update_operators": {"$inc": {"credits": 100}},
"execution_time_ms": 42.7,
"warnings": []
}
matched_count— rows the filter selected.modified_count— rows whose data actually changed. A matched row whose new data is byte-identical to its old data counts as matched but not modified.warnings— per-row failures. A row that fails to update does not abort the batch; the others still commit and the failure is reported here.
Safety rules
- An empty
filteris rejected with a 400. Bulk update cannot target every row by omission. - If the filter matches more than 100 rows, the request is rejected with a
400 unless you also pass
"confirm_large_operation": true.
Concurrency semantics
matched_count is not a compare-and-swap primitiveA filter+update is not an atomic conditional update. The server reads
the matching rows, applies the operators in application code, and writes
them back — without row-level locks. Two concurrent requests can both match
the same row under a predicate like {"status": "free"}, both see
matched_count: 1, and both commit. The second write wins and no error is
raised.
So matched_count / modified_count are reliable as observability — how
much your call actually touched — but they cannot be used as a
compare-and-swap guard.
Do not rely on this pattern for conflict-sensitive work where a double-apply
is a correctness bug: double-booking the same slot, paying out the same
bounty twice, or decrementing inventory below zero. For those, serialize the
operation in a store that gives you real atomic conditional writes (the
project's provisioned Postgres, reachable via the
PostgreSQL API, supports SELECT ... FOR UPDATE and
constraint-backed uniqueness).
$inc itself is safe from lost-update-by-overwrite in the sense that it
always adds to whatever value it read — but the read and the write are not a
single atomic step, so concurrent increments can still interleave and lose a
count.
From MCP (zerodb_update_rows)
The zerodb_update_rows MCP tool maps onto the same operator engine. Pass a
filter plus an operator update:
{
"table_id": "customers",
"filter": {"plan": "pro"},
"update": {"$inc": {"credits": 100}}
}
It returns matched_count and modified_count, with the same semantics and
the same concurrency caveats as the REST bulk endpoint above.
filter is required whenever you pass update — there is no implicit
"all rows". To replace a single row wholesale instead, pass row_id together
with row_data and omit update.
Delete a Row
curl -X DELETE https://api.ainative.studio/api/v1/projects/{project_id}/database/tables/customers/rows/{row_id} \
-H "Authorization: Bearer $TOKEN"
Python SDK
import requests
API_KEY = "your-api-key"
PROJECT_ID = "your-project-id"
headers = {"Authorization": f"Bearer {API_KEY}"}
base = f"https://api.ainative.studio/api/v1/projects/{PROJECT_ID}/database/tables"
## Create a table
requests.post(base, headers=headers, json={
"name": "user_profiles",
"description": "Agent user preferences"
})
## Insert a row — body key is row_data
requests.post(f"{base}/user_profiles/rows", headers=headers, json={
"row_data": {"name": "Alice", "role": "admin", "credits": 500}
})
## Query with filters and sorting
response = requests.post(f"{base}/user_profiles/query", headers=headers, json={
"filters": {"role": "developer", "credits": {"$gte": 100}},
"sort": {"credits": -1},
"limit": 10
})
for row in response.json()["rows"]:
print(f"{row['row_data']['name']}: {row['row_data']['credits']} credits")
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
filters | object | No | MongoDB-style filter (e.g., {"role": "admin"}) |
limit | integer | No | Max rows to return (default 100, max 1000) |
skip | integer | No | Pagination offset |
sort | object | No | Sort order (e.g., {"credits": -1}) |
projection | string[] | No | Fields to include in results |
Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /projects/{id}/database/tables | Create a table |
| GET | /projects/{id}/database/tables | List tables |
| POST | /projects/{id}/database/tables/{name}/rows | Insert a row |
| GET | /projects/{id}/database/tables/{name}/rows | List rows (paginated) |
| POST | /projects/{id}/database/tables/{name}/query | Query rows with filters |
| GET | /projects/{id}/database/tables/{name}/rows/{row_id} | Get a row by ID |
| PUT | /projects/{id}/database/tables/{name}/rows/{row_id} | Update a row by ID |
| PUT | /projects/{id}/database/tables/{name}/rows/bulk | Bulk update with filter |
| DELETE | /projects/{id}/database/tables/{name}/rows/{row_id} | Delete a row |
| DELETE | /projects/{id}/database/tables/{name}/rows/bulk | Bulk delete with filter |
| DELETE | /projects/{id}/database/tables/{name} | Delete a table |