---
title: "NoSQL Tables"
description: "Schema-free document tables for structured data alongside vectors and memory"
canonical: "https://docs.ainative.studio/docs/zerodb/tables"
last-updated: "2026-10-03T21:47:10.764Z"
---

# NoSQL Tables

Source: https://docs.ainative.studio/docs/zerodb/tables

> Schema-free document tables for structured data alongside vectors and memory

## 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

```bash
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`.

```bash
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

```bash
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

```bash
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.

```bash
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:

```bash
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

```json
{
  "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

:::warning `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](/docs/zerodb/postgresql), 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`:

```json
{
  "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

```bash
curl -X DELETE https://api.ainative.studio/api/v1/projects/{project_id}/database/tables/customers/rows/{row_id} \
  -H "Authorization: Bearer $TOKEN"
```

## Python SDK

```python
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 |
