API Reference›Databases
API Reference

Databases

Group catalogs into id-addressable databases. Each database auto-creates a default catalog and can attach existing connections under an optional alias. A query that carries the X-Database-Id header is planned only against that database's catalogs.

List databases

GET/v1/databases

List databases in the workspace, newest first, one page at a time. When no limit is given a default page size is applied, so a single call returns at most one page rather than every database. If the response's has_more is true, pass its next_cursor value back as the cursor query parameter to fetch the next page. Pass search to return only databases whose name contains that text, ignoring the case of unaccented Latin letters and digits; to fetch the single database whose name matches exactly, use GET /v1/databases/by-name instead. Pass batch with the batch_id returned by a bulk-creation call to list only that batch's databases.

Query parameters

  • limit integer — Maximum number of databases to return in this page (1–100). Values outside the range are clamped.
  • cursor string — Opaque pagination cursor from a previous response's next_cursor.
  • search string — Case-insensitive substring filter on the database name. When set, only databases whose name contains this text are returned; paging and newest-first ordering are unchanged.
  • batch string — List only the databases belonging to one bulk-creation batch, identified by the batch_id that call returned. Bulk-created databases also appear in the unfiltered listing alongside every other database; this narrows the listing to one batch. Paging works the same way, but results are ordered by database id rather than newest-first, because every database in a batch is created at once.

Response 200 — One page of databases

  • count integer,null — Number of databases returned in this page. Min: 0
  • databases DatabaseSummary[] — required
    • created_at string,null — When the database was created.
    • default_catalog string — required. Name the database's default catalog answers to inside its query scope.
    • default_schema string — required. Schema that unqualified table names resolve to inside this database's query scope. main unless the database declares a single schema or a default_schema was set at create time.
    • expires_at string,null
    • forked_from null | ForkedFromInfo
    • id string — required
    • name string,null
  • has_more boolean,null — Whether more databases exist beyond this page.
  • limit integer,null — Page size applied to this response (after clamping to the maximum). Min: 0
  • next_cursor string,null — Opaque cursor for the next page; present only when has_more is true.
{
  "count": 0,
  "databases": [
    {
      "created_at": "2026-01-01T00:00:00Z",
      "default_catalog": "string",
      "default_schema": "string",
      "expires_at": "2026-01-01T00:00:00Z",
      "forked_from": {
        "database_id": "dbidk9p34y6n3wd25rq4f5zr37e3p3",
        "description": "string",
        "forked_at": "2026-01-01T00:00:00Z",
        "name": "string",
        "snapshot_id": 0
      },
      "id": "string",
      "name": "string"
    }
  ],
  "has_more": true,
  "limit": 0,
  "next_cursor": "string"
}

Create database

POST/v1/databases

Create a new database (a metadata-only grouping). A managed default catalog is auto-created and addressable inside the database as default (or the optional default_catalog name), with a main schema pre-declared so default.main.<table> works out of the box. The optional name is a free-form display label and is not required to be unique; when omitted, a label derived from the new database's ID is assigned. Optional default_catalog overrides the name the default catalog answers to; it must be a valid SQL identifier and may not collide with the reserved catalog names hotdata or information_schema. Optional schemas declares additional schemas/tables on the default catalog at create time; declared tables can be loaded via the standard managed-tables-load endpoint targeting default_connection_id. Optional expires_at sets when the database expires — accepts either an RFC 3339 timestamp or a relative duration suffixed with h (hours), m (minutes), or d (days), e.g. 24h, 48h, 90m, 7d. When omitted, the database never expires. Expiry is best-effort: the database will not be deleted before expires_at, but cleanup may run later than the exact timestamp. Optional if_not_exists makes this a get-or-create: when a database already carries the requested name, it is returned with status 200 and nothing is created, which lets a client bind to its database on every start-up without first looking one up.

Request body

  • default_catalog string,null — Optional name the database's auto-created default catalog answers to inside its query scope. Must be a valid SQL identifier ([a-z0-9_], not starting with a digit) and may not collide with the reserved catalog names hotdata or information_schema. Defaults to default when omitted, so default.main.<table> keeps working.
  • default_schema string,null — Optional schema that unqualified table names resolve to inside this database's query scope. Must be a valid SQL identifier ([a-z0-9_], not starting with a digit). When omitted, a database that declares exactly one schema adopts that schema as its default; otherwise unqualified names resolve to main. Fully-qualified names (<catalog>.<schema>.<table>) are unaffected, and a per-query default_schema still takes precedence.
  • expires_at string,null — When this database expires. Accepts either an RFC 3339 timestamp (e.g. "2026-06-01T00:00:00Z") or a relative duration suffixed with h (hours), m (minutes), or d (days) — for example "24h", "48h", or "7d". Omitted (or empty) means the database never expires. Expiry is best-effort: the database will not be deleted before expires_at, but cleanup may run later than the exact timestamp.
  • if_not_exists boolean — Return the database that already carries this name instead of creating a second one with it. Requires name; the comparison is case-insensitive for unaccented Latin letters and digits, and ignores surrounding whitespace, matching GET /v1/databases/by-name. A match responds 200 with the existing database rather than 201, and is returned exactly as it stands: the rest of this request — schemas, expires_at, default_catalog, default_schema — is neither compared against it nor applied to it. Nothing is created, so this is safe to call on every start-up.
  • name string,null — Optional free-form display label (for UIs/CLIs). When omitted, a label derived from the database's ID is assigned. Not unique. Not an identifier — databases are always addressed by id. Accepts the legacy description key as an alias so clients that predate the rename keep populating this field.
  • schemas DatabaseDefaultSchemaDecl[] — Optional schemas/tables to declare on the database's auto-created default catalog. Tables declared here can be loaded via the standard managed-table load endpoint targeting default_connection_id. Omitted or empty means the default catalog starts empty.
    • name string — required
    • tables DatabaseDefaultTableDecl[]
      • constant_per_key string[] — Columns whose value is determined by this table's key: for every uploaded row, every stored row sharing its key holds the same value of these columns. Declaring this lets a keyed mutation (delete, update, upsert) restrict its search for prior versions to the values the upload carries, which prunes far harder than the key alone when the key's own file statistics are weak. Omit (the default) for the unrestricted search. Correctness-affecting, not a hint. If the assertion is false, a mutation supersedes one version of a key and appends beside another, silently duplicating it. Declare it only where the invariant is established.
      • key string[] — Columns that uniquely identify a row, enabling the key-based load modes (delete, update, upsert) on this table: those loads match rows by these columns' values. Omit (the default) to declare no key; the table can still be loaded with replace and append, but key-based modes are then rejected.
      • name string — required
      • partition_by TablePartitionKey[] — Partition keys for this table, applied in order. Omit for no partitioning. Declared when the table is created and fixed thereafter.
      • sorted_by TableSortKey[] — Sort keys for this table, applied in order. Omit for no sort order. Declared when the table is created and fixed thereafter. A table with a sort order cannot be indexed: creating any index on it is refused, so leave this empty on a table you plan to search.
{
  "schemas": [
    {
      "name": "sales"
    }
  ]
}

Response 200 — A database already carried the requested name, and is returned unchanged (if_not_exists only)

  • created boolean,null — Whether this call brought the database into existence. Only false when if_not_exists found a database already carrying the requested name, in which case nothing was created and the existing one is returned. The response status says the same thing — 201 against 200 — but generated clients often surface only the body, so it is stated here as well. Always sent. It is declared optional so that a client built against a newer version of this API still accepts a response from a deployment that predates the field. Absent therefore means "this deployment cannot say", which is not the same as false — test for the two values explicitly rather than for truthiness.
  • default_catalog string — required. Name the database's default catalog answers to inside its query scope (default unless overridden at create time).
  • default_connection_id string — required. Id of the connection that backs this database's default catalog. Two uses: pass it as connection_id to POST /v1/databases/{other}/catalogs to attach this database's catalog into another database, and as the connection in the managed-tables load endpoint (POST /v1/connections/{id}/schemas/{s}/tables/{t}/loads) to load tables declared at create time. Other connection endpoints (list, get, health, delete, cache purge) refuse to act on it. In SQL, address the catalog as default inside an X-Database-Id scope, not by this id.
  • default_schema string — required. Schema that unqualified table names resolve to inside this database's query scope. main unless the database declares a single schema or a default_schema was set at create time.
  • expires_at string,null — When this database expires.
  • forked_from null | ForkedFromInfo
  • id string — required
  • name string,null
{
  "created": true,
  "default_catalog": "string",
  "default_connection_id": "string",
  "default_schema": "string",
  "expires_at": "2026-01-01T00:00:00Z",
  "forked_from": {
    "database_id": "dbidk9p34y6n3wd25rq4f5zr37e3p3",
    "description": "string",
    "forked_at": "2026-01-01T00:00:00Z",
    "name": "string",
    "snapshot_id": 0
  },
  "id": "string",
  "name": "string"
}

Response 201 — Database created

  • created boolean,null — Whether this call brought the database into existence. Only false when if_not_exists found a database already carrying the requested name, in which case nothing was created and the existing one is returned. The response status says the same thing — 201 against 200 — but generated clients often surface only the body, so it is stated here as well. Always sent. It is declared optional so that a client built against a newer version of this API still accepts a response from a deployment that predates the field. Absent therefore means "this deployment cannot say", which is not the same as false — test for the two values explicitly rather than for truthiness.
  • default_catalog string — required. Name the database's default catalog answers to inside its query scope (default unless overridden at create time).
  • default_connection_id string — required. Id of the connection that backs this database's default catalog. Two uses: pass it as connection_id to POST /v1/databases/{other}/catalogs to attach this database's catalog into another database, and as the connection in the managed-tables load endpoint (POST /v1/connections/{id}/schemas/{s}/tables/{t}/loads) to load tables declared at create time. Other connection endpoints (list, get, health, delete, cache purge) refuse to act on it. In SQL, address the catalog as default inside an X-Database-Id scope, not by this id.
  • default_schema string — required. Schema that unqualified table names resolve to inside this database's query scope. main unless the database declares a single schema or a default_schema was set at create time.
  • expires_at string,null — When this database expires.
  • forked_from null | ForkedFromInfo
  • id string — required
  • name string,null
{
  "created": true,
  "default_catalog": "string",
  "default_connection_id": "string",
  "default_schema": "string",
  "expires_at": "2026-01-01T00:00:00Z",
  "forked_from": {
    "database_id": "dbidk9p34y6n3wd25rq4f5zr37e3p3",
    "description": "string",
    "forked_at": "2026-01-01T00:00:00Z",
    "name": "string",
    "snapshot_id": 0
  },
  "id": "string",
  "name": "string"
}

Errors

StatusDescription
400Invalid request
409if_not_exists was requested for a name carried by more than one database, so there is no single one to return
500Internal server error

Create many databases at once

POST/v1/databases/bulk

Create many databases from one template in a single request. The databases are created in the background: the response returns immediately with a batch and a job to poll.

The databases are not returned inline. List the ones a batch created with GET /databases?batch=<batch_id>; they also appear in the normal database listing alongside every other database.

Each database gets a default catalog and schema. Declare tables on all of them by passing schemas, in the same shape a single create accepts — a batch of 10,000 declaring one table yields 10,000 databases that each hold that table and are ready to load, with no follow-up call per database. Omit schemas and the databases are created empty. Either way, load data into them exactly as you would a database created individually.

Request body

  • count integer — required. How many databases to create. Min: 1. Max: 10000
  • default_catalog string,null — Name the default catalog answers to inside each database, as on a single create. Defaults to default.
  • default_schema string,null — Schema that unqualified table names resolve to inside each database.
  • expires_at string,null — When the created databases expire. Accepts an RFC 3339 timestamp or a relative duration such as 24h, 90m, or 7d.
  • idempotency_key string,null — Repeat this value to retry a request safely. A retry carrying a key that was already used returns the original batch — the same batch_id and the same databases — instead of creating a second set. The key identifies the request, not its contents: reusing a key with a different count or template returns the original batch unchanged rather than reporting a mismatch. Use a fresh key per distinct request.
  • name_template string,null — Optional display-label pattern for each database. Must contain {index}, which is replaced with the database's zero-based position, padded with leading zeros to the width of the batch's largest index — with a count of 100, tenant-{index} produces tenant-00 through tenant-99. A pattern without {index} is rejected, since every database in the batch would be given the same label. Each label also carries a suffix identifying the batch, so repeating the same pattern on a later call does not reuse labels from this one. When omitted, each database is labelled from its own ID.
  • schemas DatabaseDefaultSchemaDecl[] — Schemas and tables to declare on every database in the batch, in the same shape a single create accepts. The declaration applies identically to each database, so a batch of 10,000 declaring one table yields 10,000 databases that each hold that table and are ready to load — with no follow-up call per database. Omitted or empty means each database starts with no tables.
    • name string — required
    • tables DatabaseDefaultTableDecl[]
      • constant_per_key string[] — Columns whose value is determined by this table's key: for every uploaded row, every stored row sharing its key holds the same value of these columns. Declaring this lets a keyed mutation (delete, update, upsert) restrict its search for prior versions to the values the upload carries, which prunes far harder than the key alone when the key's own file statistics are weak. Omit (the default) for the unrestricted search. Correctness-affecting, not a hint. If the assertion is false, a mutation supersedes one version of a key and appends beside another, silently duplicating it. Declare it only where the invariant is established.
      • key string[] — Columns that uniquely identify a row, enabling the key-based load modes (delete, update, upsert) on this table: those loads match rows by these columns' values. Omit (the default) to declare no key; the table can still be loaded with replace and append, but key-based modes are then rejected.
      • name string — required
      • partition_by TablePartitionKey[] — Partition keys for this table, applied in order. Omit for no partitioning. Declared when the table is created and fixed thereafter.
      • sorted_by TableSortKey[] — Sort keys for this table, applied in order. Omit for no sort order. Declared when the table is created and fixed thereafter. A table with a sort order cannot be indexed: creating any index on it is refused, so leave this empty on a table you plan to search.
{
  "count": 100,
  "default_catalog": "default",
  "default_schema": "main",
  "expires_at": "7d",
  "idempotency_key": "3f6b1c1e-9a2b-4a5f-9a1e-2f0d6a7c8b91",
  "name_template": "tenant-{index}",
  "schemas": [
    {
      "name": "sales"
    }
  ]
}

Response 202 — Batch accepted and filling in the background

  • batch_id string — required
  • cancel_requested boolean — required. True once stopping has been requested. Databases already created are kept; only further creation stops.
  • count integer — required. How many databases the batch was asked to create.
  • created_count integer — required. How many exist so far. Advances as the batch fills.
  • expires_at string,null
  • job_id string,null — Job filling this batch. Poll it for status.
  • status_url string,null
{
  "batch_id": "string",
  "cancel_requested": true,
  "count": 0,
  "created_count": 0,
  "expires_at": "2026-01-01T00:00:00Z",
  "job_id": "string",
  "status_url": "string"
}

Errors

StatusDescription
400Invalid count, template, or expiry
409A batch with this idempotency key is already running

Get a database batch

GET/v1/databases/bulk/{batch_id}

Fetch a batch by id: how many databases were requested and how many exist so far. Poll this to follow progress.

Path parameters

  • batch_id string — required. Batch ID

Response 200 — The batch

  • batch_id string — required
  • cancel_requested boolean — required. True once stopping has been requested. Databases already created are kept; only further creation stops.
  • count integer — required. How many databases the batch was asked to create.
  • created_count integer — required. How many exist so far. Advances as the batch fills.
  • expires_at string,null
  • job_id string,null — Job filling this batch. Poll it for status.
  • status_url string,null
{
  "batch_id": "string",
  "cancel_requested": true,
  "count": 0,
  "created_count": 0,
  "expires_at": "2026-01-01T00:00:00Z",
  "job_id": "string",
  "status_url": "string"
}

Errors

StatusDescription
404Batch not found

Delete a database batch

DELETE/v1/databases/bulk/{batch_id}

Stop a batch that is still filling and delete the databases it created, then the batch itself.

Only batches whose databases hold no data can be removed this way. Tables that were declared but never loaded do not prevent it, so a batch created with schemas stays deletable. If any database in the batch has had data loaded into it, the request is rejected and those databases must be deleted one at a time — removing a database that holds data is per-database work that cannot be batched.

Path parameters

  • batch_id string — required. Batch ID

Response 200 — Batch deleted

  • batch_id string — required
  • deleted_count integer — required. How many databases were removed.
{
  "batch_id": "string",
  "deleted_count": 0
}

Errors

StatusDescription
404Batch not found
409Some of the batch's databases hold loaded data

Look up a database by name

GET/v1/databases/by-name

Fetch a single database by its exact name. This is the counterpart to the listing's search filter, which matches any database whose name merely contains the text.

Matching ignores case for unaccented Latin letters and digits, and only for those. Every other character has to match exactly, so a name containing an accented letter or a non-Latin script must be looked up with the capitalisation it was created with.

Returns 404 when no database has that name. A name shared by more than one database returns 409 rather than picking one of them; address those by id.

Query parameters

  • name string — required. Exact name to look up. Unlike the listing's search, which matches any database whose name contains the text, this matches the whole name. Case is ignored for unaccented Latin letters and digits, and only for those. Every other character has to match exactly, so look a name holding one up with the capitalisation it was created with.

Response 200 — Database details

  • attachments DatabaseAttachmentInfo[] — required
    • alias string,null — Alias under which this catalog is reachable inside the database. When None, the catalog is reachable by its original connection name.
    • connection_id string — required
  • created_at string,null — When the database was created.
  • default_catalog string — required. Name the database's default catalog answers to inside its query scope (default unless overridden at create time).
  • default_connection_id string — required. Id of the connection backing this database's default catalog. Pass it as connection_id to POST /v1/databases/{other}/catalogs to attach this database's catalog into another database. In SQL, address the catalog as default inside an X-Database-Id scope, not by id.
  • default_schema string — required. Schema that unqualified table names resolve to inside this database's query scope. main unless the database declares a single schema or a default_schema was set at create time.
  • expires_at string,null — When this database expires.
  • forked_from null | ForkedFromInfo
  • id string — required
  • name string,null
{
  "attachments": [
    {
      "alias": "string",
      "connection_id": "string"
    }
  ],
  "created_at": "2026-01-01T00:00:00Z",
  "default_catalog": "string",
  "default_connection_id": "string",
  "default_schema": "string",
  "expires_at": "2026-01-01T00:00:00Z",
  "forked_from": {
    "database_id": "dbidk9p34y6n3wd25rq4f5zr37e3p3",
    "description": "string",
    "forked_at": "2026-01-01T00:00:00Z",
    "name": "string",
    "snapshot_id": 0
  },
  "id": "string",
  "name": "string"
}

Errors

StatusDescription
400The name parameter is empty or too long
404No database has that name
409More than one database has that name

Count databases

GET/v1/databases/count

Return the total number of databases in the workspace. This is the whole-workspace total, not a page size: the count field on the listing reports how many rows that one page returned, so totalling a workspace from GET /v1/databases means walking every page. Pass search to count only databases whose name contains that text, ignoring the case of unaccented Latin letters and digits, or batch with the batch_id returned by a bulk-creation call to count only that batch's databases. The filters mean exactly what they mean on the listing, so a count and a listing given the same filters describe the same set.

Query parameters

  • search string — Case-insensitive substring filter on the database name. When set, only databases whose name contains this text are counted.
  • batch string — Count only the databases belonging to one bulk-creation batch, identified by the batch_id that call returned.

Response 200 — Total databases in the workspace

  • total integer — required. Total databases matching the filters, across all pages.
{
  "total": 0
}

Get database

GET/v1/databases/{database_id}

Fetch a database by id. The name field is a display label only and is not accepted as an identifier here; to fetch a database by its name instead, use GET /v1/databases/by-name.

Path parameters

  • database_id string — required. Database ID

Response 200 — Database details

  • attachments DatabaseAttachmentInfo[] — required
    • alias string,null — Alias under which this catalog is reachable inside the database. When None, the catalog is reachable by its original connection name.
    • connection_id string — required
  • created_at string,null — When the database was created.
  • default_catalog string — required. Name the database's default catalog answers to inside its query scope (default unless overridden at create time).
  • default_connection_id string — required. Id of the connection backing this database's default catalog. Pass it as connection_id to POST /v1/databases/{other}/catalogs to attach this database's catalog into another database. In SQL, address the catalog as default inside an X-Database-Id scope, not by id.
  • default_schema string — required. Schema that unqualified table names resolve to inside this database's query scope. main unless the database declares a single schema or a default_schema was set at create time.
  • expires_at string,null — When this database expires.
  • forked_from null | ForkedFromInfo
  • id string — required
  • name string,null
{
  "attachments": [
    {
      "alias": "string",
      "connection_id": "string"
    }
  ],
  "created_at": "2026-01-01T00:00:00Z",
  "default_catalog": "string",
  "default_connection_id": "string",
  "default_schema": "string",
  "expires_at": "2026-01-01T00:00:00Z",
  "forked_from": {
    "database_id": "dbidk9p34y6n3wd25rq4f5zr37e3p3",
    "description": "string",
    "forked_at": "2026-01-01T00:00:00Z",
    "name": "string",
    "snapshot_id": 0
  },
  "id": "string",
  "name": "string"
}

Errors

StatusDescription
404Database not found

Delete database

DELETE/v1/databases/{database_id}

Delete a database and its auto-created default catalog. Catalogs attached to it are detached (the catalogs themselves are not deleted). Refused while another database attaches this one's catalog — detach it there first — unless this database is past its expires_at, in which case it can be deleted regardless and the attaching database loses the catalog. A database that attaches one should watch that date.

Path parameters

  • database_id string — required. Database ID

Response 204 — Database deleted

Errors

StatusDescription
404Database not found
409Another database attaches this one's catalog

Attach catalog to database

POST/v1/databases/{database_id}/catalogs

Attach a catalog to a database so its tables are queryable alongside the database's own. Pass another database's default_connection_id as connection_id to read across the two in one query. Inside the database the catalog answers to alias when set, otherwise to the name it already answers to in its own scope. That name may not be default, a reserved name, or this database's own default catalog name — so attaching a database that kept the stock default catalog needs an alias. Attaching is read-only and copies nothing: loads still target the database's own default catalog, and detaching withdraws visibility rather than deleting data. A database's own default catalog is always attached and cannot be attached again. Attaching is not transitive — a database sees the catalog it attached, not that catalog's own attachments.

Path parameters

  • database_id string — required. Database ID

Request body

  • alias string,null — Optional alias under which this catalog is reachable inside the database. When omitted, it is reachable by the connection's name.
  • connection_id string — required
{
  "alias": "warehouse",
  "connection_id": "connk9p34y6n3wd25rq4f5zr37e3p3"
}

Response 204 — Catalog attached

Errors

StatusDescription
400Invalid request
404Database or connection not found
409Catalog already attached or alias collides with an existing attachment

Detach catalog from database

DELETE/v1/databases/{database_id}/catalogs/{connection_id}

Path parameters

  • database_id string — required. Database ID
  • connection_id string — required. Connection ID

Response 204 — Catalog detached

Errors

StatusDescription
400Cannot detach a database's own default catalog
404Database or attachment not found

Fork database

POST/v1/databases/{database_id}/fork

Create a new database that is an independent fork of an existing one. The fork has its own default catalog and contains the same schemas, tables, and data as the source; the source is left unchanged. External catalogs attached to the source are re-attached to the fork. Optional description records why the fork is being taken; it is returned with the fork's forked_from and in the lineage of both databases. Optional name sets the fork's display label; when omitted, the fork takes the source's label followed by a short suffix derived from the fork's own ID, so the two stay distinguishable. Optional expires_at sets when the fork expires — accepts an RFC 3339 timestamp or a relative duration suffixed with h (hours), m (minutes), or d (days), e.g. 24h, 90m, 7d. When omitted, a still-future expiry on the source is carried over; otherwise the fork never expires. Indexes on the source's tables are carried over and can be used on the fork straight away, however large they are, with two exceptions that are not carried over and must be created again on the fork: a vector index that uses the ivf algorithm, and an index that is not up to date with its table's latest load when the fork is taken. A fork adds no stored bytes at first, because it starts out sharing the source's storage, indexes included. Loading data into a forked table gives it its own copy of that table's indexes from then on. Routine maintenance can later rewrite a shared table into the fork's own storage, and the fork is billed for that copy from then on. Whether and when that happens depends on the table, so a fork that is only read can keep sharing indefinitely.

Path parameters

  • database_id string — required. Source database ID

Request body

  • description string,null — Optional note saying why the fork is being taken — for example "backfill test before the March migration". It is kept with the record of the fork and returned wherever that record appears: forked_from on the fork, and the fork's entry in both databases' lineage. Surrounding whitespace is trimmed, and an empty value is treated as absent. At most 4096 bytes; line breaks and tabs are allowed, other control characters are not.
  • expires_at string,null — When the fork expires. Accepts either an RFC 3339 timestamp (e.g. "2026-06-01T00:00:00Z") or a relative duration suffixed with h (hours), m (minutes), or d (days) — for example "24h" or "7d". When omitted, a still-future expiry on the source is carried over; otherwise the fork never expires.
  • name string,null — Optional display label for the fork. When omitted, the fork takes the source's label followed by a short suffix derived from the fork's own ID, so the two stay distinguishable. A source with no usable label of its own gives a fork named from that ID alone.

All fields are optional. Send only the ones you want to set.

Response 201 — Database forked

  • created boolean,null — Whether this call brought the database into existence. Only false when if_not_exists found a database already carrying the requested name, in which case nothing was created and the existing one is returned. The response status says the same thing — 201 against 200 — but generated clients often surface only the body, so it is stated here as well. Always sent. It is declared optional so that a client built against a newer version of this API still accepts a response from a deployment that predates the field. Absent therefore means "this deployment cannot say", which is not the same as false — test for the two values explicitly rather than for truthiness.
  • default_catalog string — required. Name the database's default catalog answers to inside its query scope (default unless overridden at create time).
  • default_connection_id string — required. Id of the connection that backs this database's default catalog. Two uses: pass it as connection_id to POST /v1/databases/{other}/catalogs to attach this database's catalog into another database, and as the connection in the managed-tables load endpoint (POST /v1/connections/{id}/schemas/{s}/tables/{t}/loads) to load tables declared at create time. Other connection endpoints (list, get, health, delete, cache purge) refuse to act on it. In SQL, address the catalog as default inside an X-Database-Id scope, not by this id.
  • default_schema string — required. Schema that unqualified table names resolve to inside this database's query scope. main unless the database declares a single schema or a default_schema was set at create time.
  • expires_at string,null — When this database expires.
  • forked_from null | ForkedFromInfo
  • id string — required
  • name string,null
{
  "created": true,
  "default_catalog": "string",
  "default_connection_id": "string",
  "default_schema": "string",
  "expires_at": "2026-01-01T00:00:00Z",
  "forked_from": {
    "database_id": "dbidk9p34y6n3wd25rq4f5zr37e3p3",
    "description": "string",
    "forked_at": "2026-01-01T00:00:00Z",
    "name": "string",
    "snapshot_id": 0
  },
  "id": "string",
  "name": "string"
}

Errors

StatusDescription
400The source database can't be forked as-is
404Source database not found
409The source database changed while it was being forked; retry the request

Get database lineage

GET/v1/databases/{database_id}/lineage

Trace where a database came from and what came from it.

ancestors walks the fork chain from this database's immediate source up to the original it descends from, nearest first, and each entry says which state of that source the next database down copied. forks lists the databases forked directly from this one, most recently forked first, with fork_count giving the true total when the list is only a sample of it.

Lineage is a historical record, not a live link: a fork is an independent database from the moment it is created, and either side can change or be deleted without affecting the other. Deleting either one does not erase the record: a deleted ancestor keeps its place in the chain, a deleted fork still appears among forks, and both are marked with exists set to false.

A database that was never forked, and was never forked from, answers with empty lists and itself as root_id. Forks taken before lineage was recorded carry none: their provenance was never written and cannot be reconstructed.

Path parameters

  • database_id string — required. Database ID

Query parameters

  • forks_limit integer — How many of the databases forked from this one to list (1-100, default 25). Values outside the range are clamped. fork_count always reports the true total, whatever this is set to.

Response 200 — Database lineage

  • ancestors LineageAncestorInfo[] — required. Fork ancestry, nearest parent first, ending at the database named by root_id unless ancestors_truncated says otherwise. A deleted generation does not cut the chain short — it is listed with exists false and the ancestry continues past it. Empty when this database is not a fork.
    • database_id string — required
    • description string,null — Why the next database down the chain was forked from it, as given at the time. Absent when none was given.
    • exists boolean — required. False once the ancestor has been deleted. Its place in the chain is kept either way, and the ancestry continues past it.
    • forked_at string,null — When the next database down the chain was forked from it.
    • name string,null — The ancestor's current label, or the one captured at fork time when it no longer exists.
    • snapshot_id integer,null — Version of this ancestor's data that the next database down the chain copied. See forked_from.snapshot_id.
  • ancestors_truncated boolean — required. True when the ancestry is longer than one response carries, so ancestors stops before reaching root_id.
  • database_id string — required. The database this lineage describes.
  • fork_count integer — required. How many databases were forked directly from this one in total — so you can tell whether forks is the whole set or only the newest of it.
  • forks LineageForkInfo[] — required. The databases forked directly from this one, most recently forked first, including any that have since been deleted. A fork of a fork appears in its own parent's lineage, not here.
    • database_id string — required
    • description string,null — Why the fork was taken, as given when it was created. Absent when none was given.
    • exists boolean — required. False once the fork has been deleted. The record of it is kept either way, so a source can still account for everything taken from it.
    • forked_at string,null — When the fork was taken.
    • name string,null — Absent once the fork has been deleted — only the record of it remains.
    • snapshot_id integer,null — Version of this database's data that the fork copied. See forked_from.snapshot_id.
  • root_id string — required. Top of the family tree: the database the whole chain of forks descends from. Equal to database_id when this database is not a fork.
{
  "ancestors": [
    {
      "database_id": "string",
      "description": "string",
      "exists": true,
      "forked_at": "2026-01-01T00:00:00Z",
      "name": "string",
      "snapshot_id": 0
    }
  ],
  "ancestors_truncated": true,
  "database_id": "string",
  "fork_count": 0,
  "forks": [
    {
      "database_id": "string",
      "description": "string",
      "exists": true,
      "forked_at": "2026-01-01T00:00:00Z",
      "name": "string",
      "snapshot_id": 0
    }
  ],
  "root_id": "string"
}

Errors

StatusDescription
404Database not found

Add schema to database default catalog

POST/v1/databases/{database_id}/schemas

Declare a new schema (and optionally its tables) on the database's auto-created default catalog after creation. The schema becomes reachable inside the database scope (e.g. default.<schema>.<table> and information_schema.schemata) without the caller naming the database's default connection. Identifiers are normalized to lowercase.

Path parameters

  • database_id string — required. Database ID

Request body

  • name string — required
  • tables AddManagedTableDecl[]
    • constant_per_key string[] — Columns whose value is determined by this table's key: for every uploaded row, every stored row sharing its key holds the same value of these columns. Declaring this lets a keyed mutation (delete, update, upsert) restrict its search for prior versions to the values the upload carries, which prunes far harder than the key alone when the key's own file statistics are weak. Omit (the default) for the unrestricted search. Correctness-affecting, not a hint. If the assertion is false, a mutation supersedes one version of a key and appends beside another, silently duplicating it. Declare it only where the invariant is established.
    • key string[] — Columns that uniquely identify a row, enabling the key-based load modes (delete, update, upsert) on this table: those loads match rows by these columns' values. Omit (the default) to declare no key; the table can still be loaded with replace and append, but key-based modes are then rejected.
    • name string — required
    • partition_by TablePartitionKey[] — Partition keys for this table, applied in order. Omit for no partitioning. Declared when the table is created and fixed thereafter.
      • column string — required. Column the key reads.
      • transform string — required. How the value is derived from the column. One of identity (the column value itself), year, month, day, or hour.
    • sorted_by TableSortKey[] — Sort keys for this table, applied in order. Omit for no sort order. Declared when the table is created and fixed thereafter. A table with a sort order cannot be indexed: creating any index on it is refused, so leave this empty on a table you plan to search.
      • column string — required
      • direction string,null — asc (the default) or desc. Null when the table was declared without an explicit direction for this key.
      • nulls string,null — Where nulls are placed: first or last. Defaults to the SQL default for the chosen direction. Null when the table was declared without an explicit placement for this key.
{
  "name": "sales",
  "tables": [
    {
      "constant_per_key": [
        "event_date"
      ],
      "key": [
        "order_id"
      ],
      "name": "orders"
    }
  ]
}

Response 201 — Schema added

  • connection_id string — required. Connection backing the catalog the schema was added to. For a database default catalog this is the database's default_connection_id.
  • schema string — required
  • tables string[] — required
{
  "connection_id": "string",
  "schema": "string",
  "tables": [
    "string"
  ]
}

Errors

StatusDescription
400Invalid identifier
404Database not found
409Schema already exists

Add table to database default catalog

POST/v1/databases/{database_id}/schemas/{schema}/tables

Declare a new table on an existing schema of the database's default catalog after creation. The table is added empty (declared-but-unloaded) and can be populated via the managed-table load endpoint targeting the default connection. Identifiers are normalized to lowercase.

Path parameters

  • database_id string — required. Database ID
  • schema string — required. Schema name

Request body

  • constant_per_key string[] — Columns whose value is determined by this table's key: for every uploaded row, every stored row sharing its key holds the same value of these columns. Declaring this lets a keyed mutation (delete, update, upsert) restrict its search for prior versions to the values the upload carries, which prunes far harder than the key alone when the key's own file statistics are weak. Omit (the default) for the unrestricted search. Correctness-affecting, not a hint. If the assertion is false, a mutation supersedes one version of a key and appends beside another, silently duplicating it. Declare it only where the invariant is established.
  • key string[] — Columns that uniquely identify a row, enabling the key-based load modes (delete, update, upsert) on this table: those loads match rows by these columns' values. Omit (the default) to declare no key; the table can still be loaded with replace and append, but key-based modes are then rejected.
  • name string — required
  • partition_by TablePartitionKey[] — Partition keys for this table, applied in order. Omit for no partitioning. Declared when the table is created and fixed thereafter.
    • column string — required. Column the key reads.
    • transform string — required. How the value is derived from the column. One of identity (the column value itself), year, month, day, or hour.
  • sorted_by TableSortKey[] — Sort keys for this table, applied in order. Omit for no sort order. Declared when the table is created and fixed thereafter. A table with a sort order cannot be indexed: creating any index on it is refused, so leave this empty on a table you plan to search.
    • column string — required
    • direction string,null — asc (the default) or desc. Null when the table was declared without an explicit direction for this key.
    • nulls string,null — Where nulls are placed: first or last. Defaults to the SQL default for the chosen direction. Null when the table was declared without an explicit placement for this key.
{
  "constant_per_key": [
    "event_date"
  ],
  "key": [
    "order_id"
  ],
  "name": "orders",
  "partition_by": [
    {
      "column": "created_at",
      "transform": "day"
    }
  ],
  "sorted_by": [
    {
      "column": "created_at",
      "direction": "asc",
      "nulls": "last"
    }
  ]
}

Response 201 — Table added

  • connection_id string — required
  • schema string — required
  • table string — required
{
  "connection_id": "string",
  "schema": "string",
  "table": "string"
}

Errors

StatusDescription
400Invalid identifier
404Database or schema not found
409Table already exists

Declare which columns are constant per key

PUT/v1/databases/{database_id}/schemas/{schema}/tables/{table}/constant-per-key

Replace the columns a table declares constant for a given key: for every row, any other row sharing its key holds the same value of these columns. Declaring this lets a keyed mutation (delete, update, upsert) narrow its search for prior versions to the values the upload carries.

Unlike partition_by and sorted_by, this is NOT fixed when the table is created — it changes only which files a mutation opens, never how rows are written — so a populated table can adopt it with no rewrite, taking effect on the next load. Send an empty array to revoke it.

Correctness-affecting, not a hint. If the assertion is false, a keyed mutation supersedes one version of a key and appends beside another, silently duplicating it. Declare it only where the invariant is established.

Path parameters

  • database_id string — required. Database ID
  • schema string — required. Schema name
  • table string — required. Table name

Request body

  • constant_per_key string[] — Columns whose value is the same for every row sharing this table's key. Send [] to revoke the declaration, which restores the unrestricted search on the next load — this is the kill switch if a declaration turns out to be false. Correctness-affecting, not a hint. If the assertion is false, a keyed mutation supersedes one version of a key and appends beside another, silently duplicating it.
{
  "constant_per_key": [
    "event_date"
  ]
}

Response 200 — The table's declaration as it now stands

  • connection_id string — required. Connection backing the catalog the table belongs to. For a database default catalog this is the database's default_connection_id, so it is the value that addresses the table through the connection-scoped endpoints — not the database id the request may have used.
  • constant_per_key string[] — required. The columns now declared constant per key. Empty means no declaration, i.e. the unrestricted search.
  • schema string — required. Schema the table belongs to, as stored: lowercased, which may differ from the spelling in the request path.
  • table string — required. Table the declaration was written to, as stored: lowercased, which may differ from the spelling in the request path.
{
  "connection_id": "string",
  "constant_per_key": [
    "string"
  ],
  "schema": "string",
  "table": "string"
}

Errors

StatusDescription
400An invalid column name, or a table with no key
404Database, schema or table not found
409The declaration changed concurrently; retry

Load database table from inline data, upload, or query result

POST/v1/databases/{database_id}/schemas/{schema}/tables/{table}/loads

Publish data as the new contents of a table on the database's default catalog, from one of three sources — provide exactly one. The database-scoped equivalent of the connection-scoped managed-table load — addressed by database_id, so no default_connection_id is needed. With data, CSV text is sent inline in this request, up to 2 MiB; column types are detected from the data unless columns declares them, and a larger payload is rejected with 413 and the error code INLINE_DATA_TOO_LARGE, at which point the data should be uploaded and loaded by upload_id instead. With upload_id, a previously-uploaded file is published: CSV, JSON, and Parquet are supported; the format is auto-detected or set via format. With result_id, a persisted query result is copied into the table, so the table keeps its data even after the result expires. If the target table (or its schema) has not been declared yet, it is created automatically as part of the load — declaring tables up front is optional. mode selects how the data is applied: replace overwrites the table's contents, append inserts the new rows on top of the existing data. Concurrent loads against the same upload return 409. For an upload or inline data, set async to run the load in the background and get back a job ID to poll; add async_after_ms to wait briefly for it to finish before falling back to a job ID. A result_id load runs synchronously.

Path parameters

  • database_id string — required. Database ID
  • schema string — required. Schema name
  • table string — required. Table name

Request body

  • async boolean — When true, run the load as a background job and return a job ID to poll instead of blocking until it finishes. Recommended for large uploads, which can take longer than an HTTP request should stay open. Default: false
  • async_after_ms integer,null — If set (requires async = true), wait up to this many milliseconds for the load to finish: if it completes in time the full result is returned (200), otherwise a 202 with a job ID to poll. Must be between 1000 and the server maximum; a value out of that range, or set without async = true, is rejected with 400. Min: 1000
  • columns object,null — Column types for inline data, keyed by column name. Optional — types are detected from the data when omitted. Each value is either a type name ("VARCHAR", "BIGINT", "DECIMAL(10,2)") or an object carrying explicit parameters ({"type": "DECIMAL", "precision": 10, "scale": 2}). Supported types: VARCHAR, TEXT, STRING, CHAR, BOOLEAN, TINYINT, SMALLINT, INTEGER, BIGINT, UTINYINT, USMALLINT, UINTEGER, UBIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, DATE, TIME, TIMESTAMP, TIMESTAMPTZ, BINARY, BLOB, UUID, and JSON. When given, it must name every column in the CSV header and no others. Only valid together with data.
  • data string,null — The data to load, sent inline in this request instead of being uploaded first — the quickest way to get a small table in. CSV text with a header row, up to 2 MiB. Larger payloads are rejected with 413 and the error code INLINE_DATA_TOO_LARGE; upload the file (see POST /v1/uploads) and load it by upload_id instead. Column types are detected from the data unless columns declares them. Provide exactly one of this, upload_id, or result_id.
  • format string,null — File format of the upload: "csv", "json", or "parquet". Optional — when omitted, the format is auto-detected from the upload's Content-Type and, failing that, from the file contents. Provide it explicitly to override detection or when the contents are ambiguous. "json" expects newline-delimited JSON (one object per line), not a JSON array. With inline data the only accepted value is "csv" (the default); upload the file and load it by upload_id for any other format. Not valid with result_id — query results are always parquet.
  • idempotency_key string,null — A key of your own that makes this load safe to retry. Send the same key again — after a timeout, a dropped connection, or any answer you did not receive — and the load runs at most once: the retry returns the original result instead of loading the rows a second time. Generate the key before the first attempt (a UUID is a good choice) and reuse it for every retry of that same data. Use a new key for the next batch: sending different data under a key already used — or the same data to a different table or with a different mode — returns 409 with the error code IDEMPOTENCY_KEY_CONFLICT, and loads nothing. Do not retry that one: resending the same request cannot succeed. A retry sent while the first attempt is still running also returns 409, with a different error code, because the table is busy with it; wait and send the same key again. A 409 never means the data was loaded twice — under one key it is loaded once or not at all. Only valid with inline data. A load from upload_id is already safe to retry — re-send the same upload_id. Keys are at most 255 characters.
  • key string[] | null — Key columns identifying rows for "delete", "update", and "upsert" loads — the columns whose values decide which existing row an incoming row removes, updates, or replaces. Omit to use the key the table was created with. Keep the key consistent across loads of the same table: changing it re-targets which rows are matched. Ignored for "replace" and "append".
  • mode string — required. How the data is applied to the table. One of five values; any other value is rejected with a 400. - "replace" — the uploaded rows become the table's entire contents, superseding what was there. - "append" — the uploaded rows are inserted on top of the existing data, which is kept. - "delete" — the upload carries only the key columns, and every existing row whose key matches one of them is removed. - "update" — the upload carries whole replacement rows (the key columns plus the new values); every existing row whose key matches is replaced. An uploaded row whose key is not present is ignored. - "upsert" — like "update", except an uploaded row whose key is not present is inserted instead of ignored. "delete", "update", and "upsert" match rows by key, so they need a key: either the one the table was created with or one given in key on this request. An "append" must carry every column the table already has. Leaving one out is rejected with 400, because a column absent from the load would be dropped from the table — send the column with a null value for rows that have none. Carrying a column the table does not have is fine: it is added, and the rows already in the table read null for it.
  • result_id string,null — ID of a persisted query result (see GET /v1/results/{result_id}) to publish as the table's contents. The result is copied into the table, so the table keeps its data even after the result expires. A result can be loaded into any number of tables. Provide exactly one of this, upload_id, or data.
  • upload_id string,null — ID of a previously-staged upload (see POST /v1/uploads). The upload is claimed atomically; concurrent loads against the same upload_id return 409. Provide exactly one of this, result_id, or data.
{
  "mode": "replace",
  "data": "order_id,customer_id,amount\n1001,42,1999\n1002,7,4550\n"
}

Response 200 — Table loaded

  • arrow_schema_json string — required. Schema of the loaded table, as JSON.
  • connection_id string — required
  • row_count integer — required. Total number of rows in the table after the load. Min: 0
  • schema_name string — required
  • table_name string — required
{
  "arrow_schema_json": "string",
  "connection_id": "string",
  "row_count": 0,
  "schema_name": "string",
  "table_name": "string"
}

Response 202 — Load accepted and running in the background; poll the returned job for status and result

  • id string — required. Job ID for status polling.
  • status JobStatus — required. Current status of a background job. One of five values: - pending — queued, not started yet. - running — currently executing. - succeeded — finished, everything it was asked to do worked. - partially_succeeded — finished, but some of its work failed while the rest succeeded (for example some of the databases in a bulk creation). Read error_message for what was skipped. - failed — did not finish; every retry was used up. Read error_message. succeeded, partially_succeeded, and failed are final — a job in one of those will not change again, so a poll loop can stop there. pending and running mean keep polling. Treat partially_succeeded as its own outcome rather than folding it into either success or failure: the job produced a result, but not for all of its work.
  • status_url string — required. URL to poll for job status.
{
  "id": "string",
  "status": "pending",
  "status_url": "string"
}

Errors

StatusDescription
400Invalid request (bad mode, none or several of upload_id/result_id/data, format combined with result_id, columns without data, a non-CSV inline format, unparseable inline data, invalid identifier, bad parquet, or the result failed to compute)
404Database, upload, or result not found, or the table was deleted
409Upload already consumed or in flight, the result is still being computed, or the incoming data changes a column's type incompatibly (only widening to a larger compatible type can be applied automatically); the existing data is unchanged and remains queryable
413Inline data is over the 2 MiB limit (error code INLINE_DATA_TOO_LARGE); upload the data and load it by upload_id instead