Skip to main content

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.

OperatorEffectNotes
$setSets field valuesCreates the field if absent
$incAdds a number to a numeric fieldMissing field is treated as 0; negative values decrement
$pushAppends a value to an array fieldCreates an empty array if the field is absent
$pullRemoves all occurrences of a value from an array fieldNo-op if the field is absent
$unsetRemoves fieldsThe 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 filter is 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 primitive

A 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​

ParameterTypeRequiredDescription
filtersobjectNoMongoDB-style filter (e.g., {"role": "admin"})
limitintegerNoMax rows to return (default 100, max 1000)
skipintegerNoPagination offset
sortobjectNoSort order (e.g., {"credits": -1})
projectionstring[]NoFields to include in results

Endpoints​

MethodPathDescription
POST/projects/{id}/database/tablesCreate a table
GET/projects/{id}/database/tablesList tables
POST/projects/{id}/database/tables/{name}/rowsInsert a row
GET/projects/{id}/database/tables/{name}/rowsList rows (paginated)
POST/projects/{id}/database/tables/{name}/queryQuery 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/bulkBulk update with filter
DELETE/projects/{id}/database/tables/{name}/rows/{row_id}Delete a row
DELETE/projects/{id}/database/tables/{name}/rows/bulkBulk delete with filter
DELETE/projects/{id}/database/tables/{name}Delete a table