openapi: 3.1.0
info:
  title: Hotdata API
  description: Powerful data platform API for instant databases, queries, and analytics.
  version: 1.0.0
  license:
    name: MIT
    identifier: MIT
  contact:
    name: Hotdata
    email: developers@hotdata.dev
servers:
  - url: https://api.hotdata.dev
    description: Production
security:
  - BearerAuth: []
tags:
  - name: Workspaces
    description: Workspace management
  - name: Query
    description: >-
      Execute SQL queries against connected data sources. Use standard Postgres-compatible SQL syntax to query and join
      across multiple connections. Results are returned inline and also persisted asynchronously for later retrieval.
  - name: Connections
    description: >-
      Manage connections to remote databases (Postgres, MySQL, Snowflake, BigQuery, DuckLake, etc.). Creating a
      connection registers the source and triggers automatic schema discovery. Each connection's tables are cached
      locally for fast query performance.
  - name: Information Schema
    description: >-
      Inspect table and column metadata across all connections. Returns schema information including column names, data
      types, and sync status for every discovered table.
  - name: Results
    description: >-
      Retrieve persisted query results. Every query execution persists its results asynchronously. Results transition
      through statuses: processing → ready (or failed). Once ready, the full result data can be retrieved by ID.
  - name: Query Runs
    description: >-
      View the history of executed queries. Each query run records the SQL text, execution time, result reference, and
      status. Useful for auditing and debugging query performance.
  - name: Uploads
    description: >-
      Upload files, then reference them by ID when loading a managed table. You create an upload session, then `PUT` the
      bytes straight to the URL it returns (so the file never passes through this API), and finalize.


      ### Upload flow


      1. `POST /v1/uploads` — returns a one-time `finalize_token` plus one of three shapes:

      • a single `url` (`mode: single`) — `PUT` the whole file to it;

      • a list of `part_urls` (`mode: multipart`) when you declared the size — `PUT` each part to its entry;

      • when you omit `declared_size_bytes` (streaming), `mode: multipart` with a `part_size` but no `part_urls` — call
      `POST /v1/uploads/{upload_id}/parts` with a batch of `part_numbers` to get a `PUT` URL for each part you're about
      to upload.

      2. `PUT` the bytes: the whole file to `url`, or each part to its URL (keeping each part's `ETag`). For a streaming
      upload, slice the file into `part_size` chunks and mint part URLs as you go.

      3. `POST /v1/uploads/{upload_id}/finalize` with the token (and, for multipart, the part `ETag`s) to confirm the
      upload and make it usable.


      ### Which approach to use


      - **One file, or a handful** — create a session, `PUT`, then finalize.

      - **Many small files** — `POST /v1/uploads/batch` creates up to 100 sessions in one call; upload and finalize each
      one independently, at your own pace.

      - **A large file** — small files are returned as a single `PUT`; larger files are returned as a multi-part upload
      automatically, which you can upload in parallel and resume part by part.
  - name: Saved Queries
    description: >-
      Save, version, and execute named SQL queries. Each update creates a new version, preserving the full history.
      Saved queries are automatically classified by category (e.g., aggregation, join, filtered scan) and can be
      executed by ID.
  - name: Indexes
    description: >-
      Create, list, and delete indexes on cached tables. Supports sorted indexes for range queries and BM25 full-text
      indexes for keyword search.
  - name: Embedding Providers
    description: >-
      Manage embedding providers that generate vector embeddings for text columns. Providers can be service-based (e.g.,
      OpenAI) or local. Register a provider, then reference it when creating vector indexes on text columns.
  - name: Jobs
    description: >-
      Track background jobs. Jobs are submitted internally by other APIs when async execution is requested. Poll job
      status by ID or list all jobs.
  - name: Database context
    description: Store and retrieve named text or Markdown documents scoped to a specific database.
  - name: Databases
    description: >-
      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.
paths:
  /v1/workspaces:
    get:
      tags:
        - Workspaces
      summary: List workspaces
      description: Lists all workspaces in the user's organization.
      operationId: listWorkspaces
      security:
        - BearerAuth: []
      parameters:
        - name: organization_public_id
          in: query
          required: false
          description: Filter by organization. Defaults to the user's current organization.
          schema:
            type: string
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListWorkspacesResponse'
        '401':
          description: Missing or invalid authorization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Forbidden — not a member of the organization or workspace token used
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Organization not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      tags:
        - Workspaces
      summary: Create a workspace
      description: Creates a new workspace in the specified organization.
      operationId: createWorkspace
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  description: Name for the new workspace.
                  example: production-analytics
                organization_public_id:
                  type: string
                  description: Target organization. Defaults to the user's current organization.
      responses:
        '201':
          description: Workspace created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateWorkspaceResponse'
        '400':
          description: Invalid JSON body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid authorization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Organization not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Validation error (e.g. name required)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/workspaces/{public_id}:
    parameters:
      - name: public_id
        in: path
        required: true
        schema:
          type: string
        description: Public id of the workspace.
    delete:
      tags:
        - Workspaces
      summary: Delete a workspace
      description: >-
        Hard-deletes the workspace. Namespace, storage, and catalog deprovisioning runs asynchronously after the row is
        removed.
      operationId: deleteWorkspace
      security:
        - BearerAuth: []
      responses:
        '204':
          description: Workspace deleted
        '401':
          description: Missing or invalid authorization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Workspace-scoped tokens are not allowed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Workspace not found, or caller is not a member of its organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/connections:
    get:
      tags:
        - Connections
      summary: List connections
      description: List all registered database connections.
      operationId: list_connections
      responses:
        '200':
          description: List of connections
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListConnectionsResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
    post:
      tags:
        - Connections
      summary: Create connection
      description: >-
        Register a new database connection. Provide the source type and connection config (host, port, database, etc.).
        Credentials can be supplied inline (password/token fields are auto-converted to secrets) or by referencing an
        existing secret by name or ID. Schema discovery runs automatically after registration.
      operationId: create_connection
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateConnectionRequest'
        required: true
      responses:
        '201':
          description: Connection created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateConnectionResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Connection already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/connections/{connection_id}:
    get:
      tags:
        - Connections
      summary: Get connection
      description: Get details for a specific connection, including table and sync counts.
      operationId: get_connection
      parameters:
        - name: connection_id
          in: path
          description: Connection ID
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Connection details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetConnectionResponse'
        '404':
          description: Connection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
    delete:
      tags:
        - Connections
      summary: Delete connection
      description: Delete a connection and its cached data.
      operationId: delete_connection
      parameters:
        - name: connection_id
          in: path
          description: Connection ID
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Connection deleted
        '404':
          description: Connection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: >-
            Connection backs a database's default catalog, or is attached to one or more databases as a non-default
            catalog; detach via DELETE /v1/databases/{database_id}/catalogs/{connection_id} first
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/connections/{connection_id}/cache:
    delete:
      tags:
        - Connections
      summary: Purge connection cache
      description: >-
        Purge all cached data for a connection. The next query against these tables will trigger a fresh sync from the
        remote source.
      operationId: purge_connection_cache
      parameters:
        - name: connection_id
          in: path
          description: Connection ID
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Cache purged
        '400':
          description: Managed catalogs own their data and cannot be cache-purged
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Connection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Connection backs a database's default catalog and cannot be purged directly
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/connections/{connection_id}/schemas:
    post:
      tags:
        - Connections
      summary: Add managed schema
      description: >-
        Declare a new schema (and optionally its tables) on an existing managed catalog after creation. The schema is
        added to the connection's declaration; declared tables can then be populated via the managed-table load
        endpoint. Only valid against connections whose source type is `managed`. Identifiers are normalized to
        lowercase.
      operationId: add_managed_schema
      parameters:
        - name: connection_id
          in: path
          description: Connection ID
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddManagedSchemaRequest'
        required: true
      responses:
        '201':
          description: Schema added
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedSchemaResponse'
        '400':
          description: Connection is not a managed catalog or identifier is invalid
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Connection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Schema already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/connections/{connection_id}/schemas/{schema}/tables:
    post:
      tags:
        - Connections
      summary: Add managed table
      description: >-
        Declare a new table on an existing schema of a managed catalog after creation. The table is added empty
        (declared-but-unloaded) and can be populated via the managed-table load endpoint. Only valid against connections
        whose source type is `managed`. Identifiers are normalized to lowercase.
      operationId: add_managed_table
      parameters:
        - name: connection_id
          in: path
          description: Connection ID
          required: true
          schema:
            type: string
        - name: schema
          in: path
          description: Schema name
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddManagedTableRequest'
        required: true
      responses:
        '201':
          description: Table added
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedTableResponse'
        '400':
          description: Connection is not a managed catalog or identifier is invalid
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Connection or schema not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Table already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/connections/{connection_id}/schemas/{schema}/tables/{table}:
    delete:
      tags:
        - Connections
      summary: Delete managed table
      description: >-
        Delete a single managed-catalog table. The table and its data are removed. Only valid against connections whose
        source type is `managed`.
      operationId: delete_managed_table
      parameters:
        - name: connection_id
          in: path
          description: Connection ID
          required: true
          schema:
            type: string
        - name: schema
          in: path
          description: Schema name
          required: true
          schema:
            type: string
        - name: table
          in: path
          description: Table name
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Managed table deleted
        '400':
          description: Connection is not a managed catalog
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Connection or table not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/connections/{connection_id}/schemas/{schema}/tables/{table}/loads:
    post:
      tags:
        - Connections
      summary: Load managed table from inline data, upload, or query result
      description: >-
        Publish data as the new contents of a managed table from one of three sources — provide exactly one. 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 from
        the upload's `Content-Type` and file contents, or set explicitly via the `format` field. With `result_id`, a
        persisted query 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. 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.
      operationId: load_managed_table
      parameters:
        - name: connection_id
          in: path
          description: Connection ID
          required: true
          schema:
            type: string
        - name: schema
          in: path
          description: Schema name
          required: true
          schema:
            type: string
        - name: table
          in: path
          description: Table name
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoadManagedTableRequest'
            example:
              data: |
                order_id,customer_id,amount
                1001,42,1999
                1002,7,4550
              mode: replace
        required: true
      responses:
        '200':
          description: Managed table loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoadManagedTableResponse'
        '202':
          description: Load accepted and running in the background; poll the returned job for status and result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitJobResponse'
        '400':
          description: >-
            Invalid 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, non-managed
            connection, invalid identifier, bad parquet, or the result failed to compute)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Connection, upload, or result not found, or the table was deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: >-
            Upload 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
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413':
          description: >-
            Inline `data` is over the 2 MiB limit (error code `INLINE_DATA_TOO_LARGE`); upload the data and load it by
            `upload_id` instead
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/connections/{connection_id}/tables/{schema}/{table}/cache:
    delete:
      tags:
        - Connections
      summary: Purge table cache
      description: Purge the cached data for a single table. The next query will trigger a fresh sync.
      operationId: purge_table_cache
      parameters:
        - name: connection_id
          in: path
          description: Connection ID
          required: true
          schema:
            type: string
        - name: schema
          in: path
          description: Schema name
          required: true
          schema:
            type: string
        - name: table
          in: path
          description: Table name
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Table cache purged
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/connections/{connection_id}/tables/{schema}/{table}/indexes:
    get:
      tags:
        - Indexes
      summary: List indexes on a table
      description: List all indexes created on a cached table.
      operationId: list_indexes
      parameters:
        - name: connection_id
          in: path
          description: Connection ID
          required: true
          schema:
            type: string
        - name: schema
          in: path
          description: Schema name
          required: true
          schema:
            type: string
        - name: table
          in: path
          description: Table name
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Indexes listed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListIndexesResponse'
        '404':
          description: Table not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
    post:
      tags:
        - Indexes
      summary: Create an index on a table
      description: Create a sorted or BM25 full-text index on a cached table.
      operationId: create_index
      parameters:
        - name: connection_id
          in: path
          description: Connection ID
          required: true
          schema:
            type: string
        - name: schema
          in: path
          description: Schema name
          required: true
          schema:
            type: string
        - name: table
          in: path
          description: Table name
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateIndexRequest'
        required: true
      responses:
        '201':
          description: Index created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IndexInfoResponse'
        '202':
          description: Index build accepted and running in the background; poll the returned job for status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitJobResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Table not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/connections/{connection_id}/tables/{schema}/{table}/indexes/{index_name}:
    delete:
      tags:
        - Indexes
      summary: Delete an index
      description: Delete a specific index from a cached table.
      operationId: delete_index
      parameters:
        - name: connection_id
          in: path
          description: Connection ID
          required: true
          schema:
            type: string
        - name: schema
          in: path
          description: Schema name
          required: true
          schema:
            type: string
        - name: table
          in: path
          description: Table name
          required: true
          schema:
            type: string
        - name: index_name
          in: path
          description: Index name
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Index deleted
        '404':
          description: Index not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/connections/{connection_id}/tables/{schema}/{table}/profile:
    get:
      tags:
        - Connections
      summary: Get table profile
      description: >-
        Get column-level statistics for a synced table. Returns per-column profiles including cardinality, null counts,
        and type-specific details (distinct values for categorical columns, min/max for temporal/numeric, length stats
        for text). Profiles are computed at sync time.
      operationId: get_table_profile
      parameters:
        - name: connection_id
          in: path
          description: Connection ID
          required: true
          schema:
            type: string
        - name: schema
          in: path
          description: Schema name
          required: true
          schema:
            type: string
        - name: table
          in: path
          description: Table name
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Column profile statistics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TableProfileResponse'
        '404':
          description: Table or profile not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/databases:
    get:
      tags:
        - Databases
      summary: List databases
      description: >-
        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 (case-insensitive). Pass `batch` with the `batch_id`
        returned by a bulk-creation call to list only that batch's databases.
      operationId: list_databases
      parameters:
        - name: limit
          in: query
          description: |-
            Maximum number of databases to return in this page (1–100). Values
            outside the range are clamped.
          required: false
          schema:
            type: integer
            maximum: 100
            minimum: 1
        - name: cursor
          in: query
          description: Opaque pagination cursor from a previous response's `next_cursor`.
          required: false
          schema:
            type: string
        - name: search
          in: query
          description: |-
            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.
          required: false
          schema:
            type: string
        - name: batch
          in: query
          description: |-
            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.
          required: false
          schema:
            type: string
      responses:
        '200':
          description: One page of databases
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListDatabasesResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
    post:
      tags:
        - Databases
      summary: Create database
      description: >-
        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.
      operationId: create_database
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDatabaseRequest'
        required: true
      responses:
        '201':
          description: Database created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateDatabaseResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/databases/bulk:
    post:
      tags:
        - Databases
      summary: Create many databases at once
      description: >-
        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.
      operationId: bulk_create_databases
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkCreateDatabasesRequest'
        required: true
      responses:
        '202':
          description: Batch accepted and filling in the background
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatabaseBatchResponse'
        '400':
          description: Invalid count, template, or expiry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: A batch with this idempotency key is already running
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/databases/bulk/{batch_id}:
    get:
      tags:
        - Databases
      summary: Get a database batch
      description: 'Fetch a batch by id: how many databases were requested and how many exist so far. Poll this to follow progress.'
      operationId: get_database_batch
      parameters:
        - name: batch_id
          in: path
          description: Batch ID
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The batch
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatabaseBatchResponse'
        '404':
          description: Batch not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
    delete:
      tags:
        - Databases
      summary: Delete a database batch
      description: >-
        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.
      operationId: delete_database_batch
      parameters:
        - name: batch_id
          in: path
          description: Batch ID
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Batch deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteDatabaseBatchResponse'
        '404':
          description: Batch not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Some of the batch's databases hold loaded data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/databases/count:
    get:
      tags:
        - Databases
      summary: Count databases
      description: >-
        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
        (case-insensitive), 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.
      operationId: count_databases
      parameters:
        - name: search
          in: query
          description: |-
            Case-insensitive substring filter on the database name. When set, only
            databases whose name contains this text are counted.
          required: false
          schema:
            type: string
        - name: batch
          in: query
          description: |-
            Count only the databases belonging to one bulk-creation batch,
            identified by the `batch_id` that call returned.
          required: false
          schema:
            type: string
      responses:
        '200':
          description: Total databases in the workspace
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatabaseCountResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/databases/{database_id}:
    get:
      tags:
        - Databases
      summary: Get database
      description: Fetch a database by id. The `name` field is a display label only; it is not accepted as an identifier here.
      operationId: get_database
      parameters:
        - name: database_id
          in: path
          description: Database ID
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Database details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DatabaseDetailResponse'
        '404':
          description: Database not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
    delete:
      tags:
        - Databases
      summary: Delete database
      description: >-
        Delete a database and its auto-created default catalog. Attached catalogs are detached (their underlying
        connections are not deleted).
      operationId: delete_database
      parameters:
        - name: database_id
          in: path
          description: Database ID
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Database deleted
        '404':
          description: Database not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/databases/{database_id}/catalogs:
    post:
      tags:
        - Databases
      summary: Attach catalog to database
      description: >-
        Attach an existing connection (catalog) to a database with an optional alias. Inside the database the catalog is
        reachable as the alias (when set) or its original name.
      operationId: attach_database_catalog
      parameters:
        - name: database_id
          in: path
          description: Database ID
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AttachDatabaseCatalogRequest'
        required: true
      responses:
        '204':
          description: Catalog attached
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Database or connection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Catalog already attached or alias collides with an existing attachment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/databases/{database_id}/catalogs/{connection_id}:
    delete:
      tags:
        - Databases
      summary: Detach catalog from database
      operationId: detach_database_catalog
      parameters:
        - name: database_id
          in: path
          description: Database ID
          required: true
          schema:
            type: string
        - name: connection_id
          in: path
          description: Connection ID
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Catalog detached
        '400':
          description: Cannot detach a database's own default catalog
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Database or attachment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/databases/{database_id}/context:
    get:
      tags:
        - Database context
      summary: List database contexts
      operationId: list_database_contexts
      parameters:
        - name: database_id
          in: path
          description: Database ID
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Contexts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListDatabaseContextsResponse'
        '404':
          description: Database not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
    post:
      tags:
        - Database context
      summary: Create or update database context
      description: Stores a named document (for example Markdown) scoped to a database. Reuses the same name to replace content.
      operationId: upsert_database_context
      parameters:
        - name: database_id
          in: path
          description: Database ID
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertDatabaseContextRequest'
        required: true
      responses:
        '200':
          description: Context saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpsertDatabaseContextResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Database not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/databases/{database_id}/context/{name}:
    get:
      tags:
        - Database context
      summary: Get one database context
      operationId: get_database_context
      parameters:
        - name: database_id
          in: path
          description: Database ID
          required: true
          schema:
            type: string
        - name: name
          in: path
          description: 'Context key: same character rules as a table name'
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Context found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetDatabaseContextResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Database or context not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
    delete:
      tags:
        - Database context
      summary: Delete database context
      description: Removes a named context document from a database.
      operationId: delete_database_context
      parameters:
        - name: database_id
          in: path
          description: Database ID
          required: true
          schema:
            type: string
        - name: name
          in: path
          description: 'Context key: same character rules as a table name'
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Context deleted
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Database or context not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/databases/{database_id}/fork:
    post:
      tags:
        - Databases
      summary: Fork database
      description: >-
        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 `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. Any indexes on the source's
        tables are not carried over.
      operationId: fork_database
      parameters:
        - name: database_id
          in: path
          description: Source database ID
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ForkDatabaseRequest'
        required: true
      responses:
        '201':
          description: Database forked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateDatabaseResponse'
        '400':
          description: >-
            The source database can't be forked as-is (for example, one of its tables has rows that were individually
            deleted or updated)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Source database not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/databases/{database_id}/schemas:
    post:
      tags:
        - Databases
      summary: Add schema to database default catalog
      description: >-
        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.
      operationId: add_database_schema
      parameters:
        - name: database_id
          in: path
          description: Database ID
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddManagedSchemaRequest'
        required: true
      responses:
        '201':
          description: Schema added
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedSchemaResponse'
        '400':
          description: Invalid identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Database not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Schema already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/databases/{database_id}/schemas/{schema}/tables:
    post:
      tags:
        - Databases
      summary: Add table to database default catalog
      description: >-
        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.
      operationId: add_database_table
      parameters:
        - name: database_id
          in: path
          description: Database ID
          required: true
          schema:
            type: string
        - name: schema
          in: path
          description: Schema name
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddManagedTableRequest'
        required: true
      responses:
        '201':
          description: Table added
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManagedTableResponse'
        '400':
          description: Invalid identifier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Database or schema not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Table already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/databases/{database_id}/schemas/{schema}/tables/{table}/loads:
    post:
      tags:
        - Databases
      summary: Load database table from inline data, upload, or query result
      description: >-
        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.
      operationId: load_database_table
      parameters:
        - name: database_id
          in: path
          description: Database ID
          required: true
          schema:
            type: string
        - name: schema
          in: path
          description: Schema name
          required: true
          schema:
            type: string
        - name: table
          in: path
          description: Table name
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoadManagedTableRequest'
            example:
              data: |
                order_id,customer_id,amount
                1001,42,1999
                1002,7,4550
              mode: replace
        required: true
      responses:
        '200':
          description: Table loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoadManagedTableResponse'
        '202':
          description: Load accepted and running in the background; poll the returned job for status and result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitJobResponse'
        '400':
          description: >-
            Invalid 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)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Database, upload, or result not found, or the table was deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: >-
            Upload 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
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '413':
          description: >-
            Inline `data` is over the 2 MiB limit (error code `INLINE_DATA_TOO_LARGE`); upload the data and load it by
            `upload_id` instead
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/embedding-providers:
    get:
      tags:
        - Embedding Providers
      summary: List embedding providers
      description: List all registered embedding providers.
      operationId: list_embedding_providers
      responses:
        '200':
          description: List of embedding providers
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListEmbeddingProvidersResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
    post:
      tags:
        - Embedding Providers
      summary: Create embedding provider
      description: >-
        Register a new embedding provider that can be used to generate vector embeddings for text columns. Providers can
        be service-based (e.g., OpenAI) or local.
      operationId: create_embedding_provider
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateEmbeddingProviderRequest'
        required: true
      responses:
        '201':
          description: Embedding provider created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateEmbeddingProviderResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Provider with this name already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/embedding-providers/{id}:
    get:
      tags:
        - Embedding Providers
      summary: Get embedding provider
      operationId: get_embedding_provider
      parameters:
        - name: id
          in: path
          description: Embedding provider ID
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Embedding provider details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmbeddingProviderResponse'
        '404':
          description: Provider not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
    put:
      tags:
        - Embedding Providers
      summary: Update embedding provider
      operationId: update_embedding_provider
      parameters:
        - name: id
          in: path
          description: Embedding provider ID
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateEmbeddingProviderRequest'
        required: true
      responses:
        '200':
          description: Embedding provider updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpdateEmbeddingProviderResponse'
        '404':
          description: Provider not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
    delete:
      tags:
        - Embedding Providers
      summary: Delete embedding provider
      operationId: delete_embedding_provider
      parameters:
        - name: id
          in: path
          description: Embedding provider ID
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Embedding provider deleted
        '404':
          description: Provider not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/indexes:
    get:
      tags:
        - Indexes
      summary: List indexes across tables in a database
      description: >-
        List all indexes in the database identified by the required X-Database-Id header, paginated. Optional filters
        narrow by connection, schema, table, or index type.
      operationId: list_indexes_collection
      parameters:
        - name: X-Database-Id
          in: header
          description: Database to scope to (required)
          required: true
          schema:
            type: string
        - name: connection_id
          in: query
          description: Filter to one connection
          required: false
          schema:
            type: string
        - name: schema
          in: query
          description: Filter by schema name
          required: false
          schema:
            type: string
        - name: table
          in: query
          description: Filter by table name
          required: false
          schema:
            type: string
        - name: index_type
          in: query
          description: Filter by index type
          required: false
          schema:
            type: string
        - name: limit
          in: query
          description: Max indexes per page
          required: false
          schema:
            type: integer
            minimum: 0
        - name: cursor
          in: query
          description: Pagination cursor
          required: false
          schema:
            type: string
      responses:
        '200':
          description: Indexes listed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListIndexesPageResponse'
        '400':
          description: Missing X-Database-Id or bad cursor
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Database not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/information_schema:
    get:
      tags:
        - Information Schema
      summary: List tables
      description: >-
        List discovered tables with optional filtering and pagination. Supports wildcard patterns (SQL %) for schema and
        table name filters. Set include_columns=true to include column definitions (omitted by default). Every table
        carries its declared storage layout — `partition_by` and `sorted_by` — which is fixed when the table is created
        and cannot be changed afterwards. Both are always present; an empty array means none was declared. Only tables
        in a Hotdata instant database declare a layout here, so a table discovered from an external connection always
        reports empty arrays.
      operationId: information_schema
      parameters:
        - name: connection_id
          in: query
          description: Filter by connection ID
          required: false
          schema:
            type: string
        - name: schema
          in: query
          description: Filter by schema name (supports % wildcards)
          required: false
          schema:
            type: string
        - name: table
          in: query
          description: Filter by table name (supports % wildcards)
          required: false
          schema:
            type: string
        - name: include_columns
          in: query
          description: 'Include column definitions (default: false)'
          required: false
          schema:
            type: boolean
        - name: limit
          in: query
          description: Maximum number of tables per page
          required: false
          schema:
            type: integer
            minimum: 0
        - name: cursor
          in: query
          description: Pagination cursor from a previous response
          required: false
          schema:
            type: string
      responses:
        '200':
          description: Table metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InformationSchemaResponse'
        '404':
          description: Connection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/jobs:
    get:
      tags:
        - Jobs
      summary: List jobs
      description: List background jobs with optional filters by type and status.
      operationId: list_jobs
      parameters:
        - name: job_type
          in: query
          description: Filter by job type
          required: false
          schema:
            $ref: '#/components/schemas/JobType'
        - name: status
          in: query
          description: Filter by status (comma-separated, e.g. status=pending,running)
          required: false
          schema:
            type: string
        - name: limit
          in: query
          description: Max results (default 50)
          required: false
          schema:
            type: integer
            minimum: 0
        - name: offset
          in: query
          description: Offset for pagination
          required: false
          schema:
            type: integer
            minimum: 0
      responses:
        '200':
          description: List of jobs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListJobsResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/jobs/{id}:
    get:
      tags:
        - Jobs
      summary: Get job status
      description: Get the current status of a background job. Poll this endpoint to track job progress.
      operationId: get_job
      parameters:
        - name: id
          in: path
          description: Job ID
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Job status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobStatusResponse'
        '404':
          description: Job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/queries:
    get:
      tags:
        - Saved Queries
      summary: List saved queries
      operationId: list_saved_queries
      parameters:
        - name: limit
          in: query
          description: Maximum number of results
          required: false
          schema:
            type: integer
            minimum: 0
        - name: offset
          in: query
          description: Pagination offset
          required: false
          schema:
            type: integer
            minimum: 0
      responses:
        '200':
          description: List of saved queries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListSavedQueriesResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
    post:
      tags:
        - Saved Queries
      summary: Create saved query
      description: >-
        Save a named SQL query. The SQL is stored as version 1 and automatically analyzed for classification metadata
        (category, table count, predicate/join/aggregation flags).
      operationId: create_saved_query
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSavedQueryRequest'
        required: true
      responses:
        '201':
          description: Saved query created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SavedQueryDetail'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/queries/{id}:
    get:
      tags:
        - Saved Queries
      summary: Get saved query
      operationId: get_saved_query
      parameters:
        - name: id
          in: path
          description: Saved query ID
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Saved query details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SavedQueryDetail'
        '404':
          description: Saved query not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
    put:
      tags:
        - Saved Queries
      summary: Update saved query
      description: >-
        Update a saved query. If the SQL changes, a new version is created (previous versions are preserved). Name,
        tags, description, and classification overrides can also be updated.
      operationId: update_saved_query
      parameters:
        - name: id
          in: path
          description: Saved query ID
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateSavedQueryRequest'
        required: true
      responses:
        '200':
          description: Saved query updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SavedQueryDetail'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Saved query not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
    delete:
      tags:
        - Saved Queries
      summary: Delete saved query
      operationId: delete_saved_query
      parameters:
        - name: id
          in: path
          description: Saved query ID
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Saved query deleted
        '404':
          description: Saved query not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/queries/{id}/execute:
    post:
      tags:
        - Saved Queries
      summary: Execute saved query
      description: >-
        Execute a saved query, scoped to a database (required `X-Database-Id` header). By default runs the latest
        version. Optionally specify a version number to execute a previous version. The SQL runs inside the given
        database scope, the same way POST /v1/query does. Returns the same response format as POST /v1/query.
      operationId: execute_saved_query
      parameters:
        - name: id
          in: path
          description: Saved query ID
          required: true
          schema:
            type: string
        - name: X-Database-Id
          in: header
          description: >-
            Required. Scope execution to this database (its id). A missing or malformed value is a 400; an unknown
            database id is a 404.
          required: true
          schema:
            type: string
      requestBody:
        description: Optional version to execute
        content:
          application/json:
            schema:
              oneOf:
                - type: 'null'
                - $ref: '#/components/schemas/ExecuteSavedQueryRequest'
      responses:
        '200':
          description: Query executed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueryResponse'
        '400':
          description: Invalid request (including a missing X-Database-Id header)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Saved query or database not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/queries/{id}/versions:
    get:
      tags:
        - Saved Queries
      summary: List saved query versions
      operationId: list_saved_query_versions
      parameters:
        - name: id
          in: path
          description: Saved query ID
          required: true
          schema:
            type: string
        - name: limit
          in: query
          description: Maximum number of versions
          required: false
          schema:
            type: integer
            minimum: 0
        - name: offset
          in: query
          description: Pagination offset
          required: false
          schema:
            type: integer
            minimum: 0
      responses:
        '200':
          description: List of versions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListSavedQueryVersionsResponse'
        '404':
          description: Saved query not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/query:
    post:
      tags:
        - Query
      summary: Execute SQL query
      description: >-
        Execute a SQL query scoped to a database. A database is the only window into catalogs: the query sees only that
        database's auto `default` catalog plus any catalogs explicitly attached to it. Select the database with EITHER
        the `X-Database-Id` header OR the `database_id` body field (exactly one must be given; if both are sent and
        disagree, that's a 400). Use standard Postgres-compatible SQL; reference the default catalog as
        `default.<schema>.<table>` (or just `<schema>.<table>` / `<table>`) and attached catalogs by their alias.
        Results are returned inline and a `result_id` is provided for later retrieval via the Results API.


        Set `async: true` to execute asynchronously — returns a query run ID for polling. Optionally set
        `async_after_ms` to attempt synchronous execution first, falling back to async if the query exceeds the timeout.
      operationId: query
      parameters:
        - name: X-Database-Id
          in: header
          description: >-
            Database id to scope the query to. Required unless the `database_id` body field is set; if both are present
            they must match. Only that database's catalogs are visible during planning. A malformed value is a 400; an
            unknown database id is a 404.
          required: false
          schema:
            type:
              - string
              - 'null'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryRequest'
        required: true
      responses:
        '200':
          description: Query executed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueryResponse'
        '202':
          description: Query submitted asynchronously
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AsyncQueryResponse'
        '400':
          description: Invalid request (no database specified, or header/body database_id conflict)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Database not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '429':
          description: >-
            The engine was too busy to run this query right now — too many concurrent queries, or not enough memory
            available (often because of other queries running at the same time). Retry after the Retry-After delay; if
            it persists, narrowing the query (add a filter or LIMIT) may help.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '503':
          description: >-
            Result store temporarily unavailable (a truncated result could not be persisted); retry after the
            Retry-After delay
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/query-runs:
    get:
      tags:
        - Query Runs
      summary: List query runs
      description: List query runs for the database named by the required X-Database-Id header.
      operationId: list_query_runs
      parameters:
        - name: X-Database-Id
          in: header
          description: Database to scope the query runs to (required)
          required: true
          schema:
            type: string
        - name: limit
          in: query
          description: Maximum number of results
          required: false
          schema:
            type: integer
            minimum: 0
        - name: cursor
          in: query
          description: Pagination cursor
          required: false
          schema:
            type: string
        - name: status
          in: query
          description: Filter by status (comma-separated, e.g. status=running,failed)
          required: false
          schema:
            type: string
        - name: saved_query_id
          in: query
          description: Filter by saved query ID
          required: false
          schema:
            type: string
      responses:
        '200':
          description: List of query runs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListQueryRunsResponse'
        '400':
          description: Missing or malformed X-Database-Id header
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Database not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/query-runs/{id}:
    get:
      tags:
        - Query Runs
      summary: Get query run
      description: >-
        Get the status and details of a specific query run by ID, scoped to the database named by the required
        X-Database-Id header.
      operationId: get_query_run
      parameters:
        - name: id
          in: path
          description: Query run ID
          required: true
          schema:
            type: string
        - name: X-Database-Id
          in: header
          description: Database the query run belongs to (required)
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Query run details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueryRunInfo'
        '400':
          description: Missing or malformed X-Database-Id header
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Query run or database not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/results:
    get:
      tags:
        - Results
      summary: List results
      description: List stored results for the database named by the required X-Database-Id header.
      operationId: list_results
      parameters:
        - name: X-Database-Id
          in: header
          description: Database to scope the results to (required)
          required: true
          schema:
            type: string
        - name: limit
          in: query
          description: 'Maximum number of results (default: 100, max: 1000)'
          required: false
          schema:
            type: integer
            minimum: 0
        - name: offset
          in: query
          description: 'Pagination offset (default: 0)'
          required: false
          schema:
            type: integer
            minimum: 0
      responses:
        '200':
          description: List of results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListResultsResponse'
        '400':
          description: Missing or malformed X-Database-Id header
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Database not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/results/{id}:
    get:
      tags:
        - Results
      summary: Get result
      description: >-
        Retrieve a persisted query result by ID. The response format for the `ready` state is selected by `Accept`
        header or `?format=` query param; non-ready states use the same status codes and JSON body shape regardless of
        format.


        | Result status         | Status × body                                                                |

        |-----------------------|------------------------------------------------------------------------------|

        | `ready` + JSON        | 200 `application/json` — `GetResultResponse` with `columns`, `rows`, etc.    |

        | `ready` + Arrow       | 200 `application/vnd.apache.arrow.stream` — schema, RecordBatches, EOS       |

        | `ready` + CSV         | 200 `text/csv; charset=utf-8` — single header row, streamed batch-by-batch   |

        | `ready` + Markdown    | 200 `text/markdown; charset=utf-8` — GitHub-flavored pipe table, streamed   |

        | `ready` + Parquet     | 200 `application/vnd.apache.parquet` — raw parquet bytes (no conversion)     |

        | `pending`/`processing`| 202 `application/json` `{status, result_id}` + `Retry-After`                 |

        | `failed`              | 409 `application/json` `{status, result_id, error_message}`                  |

        | not found             | 404 `application/json` (`ApiErrorResponse`)                                  |


        `?format=` accepts `arrow`, `json`, `csv`, `md`, `parquet` and takes precedence over `Accept`. `markdown` is
        accepted as a runtime alias for `md`. Use `?offset=N&limit=M` to slice the result; `offset` defaults to 0 and
        `limit` is unbounded by default. Both must be non-negative; invalid values return 400. When a finite `limit`
        doesn't reach the end of the result, a `Link` header with `rel="next"` points at the following page.
        `?offset`/`?limit` are ignored for `format=parquet` since that path returns the underlying file unchanged.


        Ready responses (Arrow, CSV, Markdown, JSON) carry `X-Total-Row-Count` (the full result row count, independent
        of offset/limit). Responses are streamed end-to-end, so a client can disconnect at any time and the server stops
        reading.


        IEEE special floats (`±Inf`, `NaN`) have no canonical JSON representation. For cross-format consistency the
        JSON, CSV, and Markdown paths emit them as `null` / empty cells, and JSON `nullable[]` is widened to match. The
        Arrow IPC and Parquet bodies are binary round-trip formats and preserve the raw IEEE values; callers
        cross-checking a result across CSV and Parquet should not byte-compare those slots.
      operationId: get_result
      parameters:
        - name: id
          in: path
          description: Result ID
          required: true
          schema:
            type: string
        - name: X-Database-Id
          in: header
          description: Database the result belongs to (required)
          required: true
          schema:
            type: string
        - name: offset
          in: query
          description: 'Rows to skip (default: 0)'
          required: false
          schema:
            type: integer
            minimum: 0
        - name: limit
          in: query
          description: 'Maximum rows to return (default: unbounded)'
          required: false
          schema:
            type: integer
            minimum: 0
        - name: format
          in: query
          description: >-
            `arrow`, `json`, `csv`, `md`, or `parquet` — overrides the `Accept` header. `markdown` is also accepted at
            runtime as an alias for `md`.
          required: false
          schema:
            $ref: '#/components/schemas/ResultsFormatQuery'
      responses:
        '200':
          description: >-
            Result data. The body depends on the negotiated format: JSON callers receive `GetResultResponse`; Arrow
            callers receive an Arrow IPC stream; CSV callers receive comma-separated text (LF-terminated, double-quote
            escaped, RFC 4180-style quoting but not RFC 4180-strict on line endings); Markdown callers receive a single
            GitHub-flavored pipe table; Parquet callers receive the raw parquet bytes, served as-is. Non-finite floats
            (`±Inf`, `NaN`) render as `null` (JSON) or empty cells (CSV, Markdown) for cross-format consistency.
            `Accept` is treated as a flat content-type list — `q=` quality values are ignored; use `?format=` to
            disambiguate.
          headers:
            Link:
              schema:
                type: string
              description: >-
                RFC 5988 `Link` header with `rel="next"` pointing at the next page when a finite `limit` does not reach
                the end of the result.
            X-Total-Row-Count:
              schema:
                type: integer
                minimum: 0
              description: Total rows in the full result, ignoring offset/limit. Present only when status is `ready`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetResultResponse'
            application/vnd.apache.arrow.stream:
              schema:
                $ref: '#/components/schemas/ArrowIpcStreamBody'
            text/csv:
              schema:
                $ref: '#/components/schemas/CsvResultBody'
            text/markdown:
              schema:
                $ref: '#/components/schemas/MarkdownResultBody'
            application/vnd.apache.parquet:
              schema:
                $ref: '#/components/schemas/ParquetResultBody'
        '202':
          description: Result is still being computed (`pending` or `processing`). Poll the same URL.
          headers:
            Retry-After:
              schema:
                type: integer
                format: int64
                minimum: 0
              description: Suggested seconds before the next poll.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetResultResponse'
        '400':
          description: Invalid offset, limit, or format.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Result not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Result computation failed. Body carries `error_message` describing the failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetResultResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/uploads:
    post:
      tags:
        - Uploads
      summary: Create upload session
      description: >-
        Create an upload session for a file you will upload directly to the URL the response carries, without sending it
        through this API. The response is one of three shapes. For a small file (`mode: single`) it contains a
        short-lived `url` to `PUT` the whole file to. For a large file with a known size (`mode: multipart`) it contains
        `part_urls` and `part_size`: split the file into `part_size`-byte chunks (the last is the remainder) and `PUT`
        chunk *i* (1-based) to `part_urls[i - 1]`, keeping each response's `ETag`. Slice by `part_size`, not by an even
        division across the number of part URLs (which can make a non-final part too small). For a file whose size you
        do not know up front, omit `declared_size_bytes`: the response is `mode: multipart` with a `part_size` but NO
        `part_urls`. As you stream, call `POST /v1/uploads/{upload_id}/parts` with a batch of `part_numbers` to mint
        per-part `PUT` URLs, `PUT` each part and keep its `ETag`, then finalize as for any multipart upload. In all
        cases the response also includes a one-time `finalize_token`. After uploading, call the finalize endpoint with
        the token (and, for multipart, the `{part_number, e_tag}` list) to make the upload usable as managed-table
        contents. The returned upload ID can then be passed to the managed-table load endpoint.


        You may hint a preferred part size with `part_size`; the service clamps it to the allowed range and ignores it
        for single-`PUT` uploads.


        A `501` with error code `PRESIGN_UNSUPPORTED` means this deployment cannot issue upload URLs; send the data
        inline on the load endpoint instead.
      operationId: create_upload_session_handler
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUploadRequest'
        required: true
      responses:
        '201':
          description: Upload session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadSessionResponse'
        '400':
          description: Invalid request (e.g. file too large, unsupported checksum algorithm)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '501':
          description: This deployment cannot issue upload URLs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/uploads/batch:
    post:
      tags:
        - Uploads
      summary: Create upload sessions in bulk
      description: >-
        Create upload sessions for several files in one request. Each file is planned independently and the response
        returns one session per requested file, in the same order. Each session is finalized separately via the finalize
        endpoint, so you can upload and finalize files at your own pace.


        A `501` with error code `PRESIGN_UNSUPPORTED` means this deployment cannot issue upload URLs; send the data
        inline on the load endpoint instead.
      operationId: create_upload_sessions_batch_handler
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchCreateUploadRequest'
        required: true
      responses:
        '201':
          description: Upload sessions created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchCreateUploadResponse'
        '400':
          description: Invalid request (e.g. a file too large, unsupported checksum algorithm)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '501':
          description: This deployment cannot issue upload URLs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/uploads/{upload_id}/finalize:
    post:
      tags:
        - Uploads
      summary: Finalize upload
      description: >-
        Confirm that a file has been uploaded and make it usable as managed-table contents. Supply the `finalize_token`
        returned when the session was created, in the `X-Upload-Finalize-Token` header. When you declared a size at
        create time, the uploaded file's size is validated against it and a mismatch is rejected. An upload created
        without a declared size is finalized from its uploaded parts; it must be non-empty and is rejected if it exceeds
        the server's maximum upload size. Finalize is exactly-once: a second finalize of the same upload is rejected.
      operationId: finalize_upload_handler
      parameters:
        - name: upload_id
          in: path
          description: Upload session ID returned at create time
          required: true
          schema:
            type: string
        - name: X-Upload-Finalize-Token
          in: header
          description: One-time finalize token returned when the session was created
          required: true
          schema:
            type: string
      requestBody:
        description: Optional; send `parts` only for a multi-part upload. Single-`PUT` uploads finalize with no body.
        content:
          application/json:
            schema:
              oneOf:
                - type: 'null'
                - $ref: '#/components/schemas/FinalizeUploadRequest'
      responses:
        '200':
          description: Upload finalized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FinalizeUploadResponse'
        '400':
          description: Invalid finalize token, uploaded size mismatch, missing file, or upload not finalizable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Upload session not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/uploads/{upload_id}/parts:
    post:
      tags:
        - Uploads
      summary: Mint upload part URLs
      description: >-
        Get short-lived upload URLs for specific parts of a multi-part upload. This is required for a streaming
        (unknown-size) upload — created by omitting the declared size — which mints no part URLs up front. It also works
        for a known-size multi-part upload: use it to re-mint a part whose URL expired before you uploaded that part.
        Supply the `finalize_token` returned when the session was created, in the `X-Upload-Finalize-Token` header, and
        the 1-based `part_numbers` you want URLs for. `PUT` each part's bytes to its URL, keep each response's `ETag`,
        then pass the `{part_number, e_tag}` list to finalize. You may mint parts in batches as you upload, and re-mint
        a part number whose URL expired before you finished uploading it.
      operationId: mint_upload_parts_handler
      parameters:
        - name: upload_id
          in: path
          description: Upload session ID returned at create time
          required: true
          schema:
            type: string
        - name: X-Upload-Finalize-Token
          in: header
          description: One-time finalize token returned when the session was created
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MintUploadPartsRequest'
        required: true
      responses:
        '200':
          description: Minted part URLs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MintUploadPartsResponse'
        '400':
          description: Invalid finalize token, invalid part numbers, batch too large, or the upload is not a multi-part upload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Upload session not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '501':
          description: This deployment cannot issue upload URLs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
  /v1/usage:
    get:
      tags:
        - Usage
      summary: Get workspace usage snapshot
      description: >-
        Return aggregated bytes scanned and current storage size for a billing period. Pass `since` as the start of your
        billing period so the totals line up with your invoice rather than the calendar month.
      operationId: get_usage
      parameters:
        - name: since
          in: query
          description: Billing period start (ISO-8601). Defaults to the start of the current UTC calendar month when omitted.
          required: false
          schema:
            type: string
      responses:
        '200':
          description: Workspace usage snapshot
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceUsageResponse'
      security:
        - BearerAuth: []
          WorkspaceId: []
components:
  schemas:
    WorkspaceListItem:
      type: object
      required:
        - public_id
        - name
        - active
        - favorite
        - provision_status
      properties:
        public_id:
          type: string
          example: workm4lz2mp899l2i7h9lk9u84azg3
        name:
          type: string
          example: production-analytics
        active:
          type: boolean
        favorite:
          type: boolean
        provision_status:
          type: string
          example: provisioned
    WorkspaceDetail:
      type: object
      required:
        - public_id
        - name
        - provision_status
      properties:
        public_id:
          type: string
          example: workm4lz2mp899l2i7h9lk9u84azg3
        name:
          type: string
          example: production-analytics
        provision_status:
          type: string
          example: pending
    ListWorkspacesResponse:
      type: object
      required:
        - ok
        - workspaces
      properties:
        ok:
          type: boolean
          example: true
        workspaces:
          type: array
          items:
            $ref: '#/components/schemas/WorkspaceListItem'
    CreateWorkspaceResponse:
      type: object
      required:
        - ok
        - workspace
      properties:
        ok:
          type: boolean
          example: true
        workspace:
          $ref: '#/components/schemas/WorkspaceDetail'
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string
          description: Machine-readable error code.
          example: missing_authorization
    AddManagedSchemaRequest:
      type: object
      description: |-
        Request body for adding a schema to an existing managed catalog:
        `POST /v1/connections/{id}/schemas` and
        `POST /v1/databases/{id}/schemas`. `tables` is optional — omit it to
        declare an empty schema and add tables later.
      required:
        - name
      properties:
        name:
          type: string
          example: sales
        tables:
          type: array
          items:
            $ref: '#/components/schemas/AddManagedTableDecl'
    AddManagedTableDecl:
      type: object
      description: One table declaration inside an add-schema request body.
      required:
        - name
      properties:
        key:
          type: array
          items:
            type: string
          description: |-
            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.
          example:
            - order_id
        name:
          type: string
          example: orders
        partition_by:
          type: array
          items:
            $ref: '#/components/schemas/TablePartitionKey'
          description: |-
            Partition keys for this table, applied in order. Omit for no
            partitioning. Declared when the table is created and fixed thereafter.
        sorted_by:
          type: array
          items:
            $ref: '#/components/schemas/TableSortKey'
          description: |-
            Sort keys for this table, applied in order. Omit for no sort order.
            Declared when the table is created and fixed thereafter.
    AddManagedTableRequest:
      type: object
      description: |-
        Request body for adding a table to an existing schema:
        `POST /v1/connections/{id}/schemas/{schema}/tables` and
        `POST /v1/databases/{id}/schemas/{schema}/tables`.
      required:
        - name
      properties:
        key:
          type: array
          items:
            type: string
          description: |-
            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.
          example:
            - order_id
        name:
          type: string
          example: orders
        partition_by:
          type: array
          items:
            $ref: '#/components/schemas/TablePartitionKey'
          description: |-
            Partition keys for this table, applied in order. Omit for no
            partitioning. Declared when the table is created and fixed thereafter.
        sorted_by:
          type: array
          items:
            $ref: '#/components/schemas/TableSortKey'
          description: |-
            Sort keys for this table, applied in order. Omit for no sort order.
            Declared when the table is created and fixed thereafter.
    ApiErrorDetail:
      type: object
      description: Error detail within an API error response
      required:
        - message
        - code
      properties:
        code:
          type: string
        message:
          type: string
    ApiErrorResponse:
      type: object
      description: Standard error response body
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/ApiErrorDetail'
    ArrowIpcStreamBody:
      type: string
      format: binary
      description: |-
        The Arrow IPC stream returned by `GET /v1/results/{id}` when the negotiated
        format is Arrow.
    AsyncQueryResponse:
      type: object
      description: |-
        Response returned when a query is submitted asynchronously (202 Accepted).

        Poll GET /query-runs/{id} to track progress, sending the same `X-Database-Id`
        header used to submit the query — the endpoint is scoped to that database and
        returns 400 without it. Once status is "succeeded", retrieve results via
        GET /results/{result_id}.
      required:
        - query_run_id
        - status
        - status_url
      properties:
        query_run_id:
          type: string
          description: Unique identifier for the query run.
        reason:
          type:
            - string
            - 'null'
          description: Human-readable reason why the query went async (e.g., caching tables for the first time).
        status:
          type: string
          description: Current status of the query run.
        status_url:
          type: string
          description: |-
            URL to poll for query run status. Requires the same `X-Database-Id`
            header used to submit the query.
    AttachDatabaseCatalogRequest:
      type: object
      description: Request body for POST /databases/{database_id}/catalogs
      required:
        - connection_id
      properties:
        alias:
          type:
            - string
            - 'null'
          description: |-
            Optional alias under which this catalog is reachable inside the
            database. When omitted, it is reachable by the connection's name.
          example: warehouse
        connection_id:
          type: string
          example: connk9p34y6n3wd25rq4f5zr37e3p3
    BatchCreateUploadRequest:
      type: object
      description: |-
        Request body for `POST /v1/uploads/batch`: create several upload sessions in
        one call. Each entry is planned independently; the response returns one
        session per request, in the same order.
      required:
        - uploads
      properties:
        uploads:
          type: array
          items:
            $ref: '#/components/schemas/CreateUploadRequest'
          example:
            - content_type: text/csv
              declared_size_bytes: 4096
              filename: orders.csv
            - content_type: text/csv
              declared_size_bytes: 2048
              filename: customers.csv
    BatchCreateUploadResponse:
      type: object
      description: |-
        Response body for `POST /v1/uploads/batch`: one created session per
        requested file, in request order.
      required:
        - uploads
      properties:
        uploads:
          type: array
          items:
            $ref: '#/components/schemas/UploadSessionResponse'
    BooleanProfileDetail:
      type: object
      description: Boolean column.
      required:
        - true_count
        - false_count
      properties:
        false_count:
          type: integer
          format: int64
          description: Number of false values
          minimum: 0
        true_count:
          type: integer
          format: int64
          description: Number of true values
          minimum: 0
    BulkCreateDatabasesRequest:
      type: object
      description: |-
        Request body for POST /databases/bulk.

        One template plus a count, so the body stays small whatever `count` is. Any
        schemas and tables declared here are applied to every database in the batch;
        load data into them afterwards exactly as you would for a database created
        one at a time.
      required:
        - count
      properties:
        count:
          type: integer
          format: int64
          description: How many databases to create.
          example: 100
          maximum: 10000
          minimum: 1
        default_catalog:
          type:
            - string
            - 'null'
          description: |-
            Name the default catalog answers to inside each database, as on a single
            create. Defaults to `default`.
          example: default
        default_schema:
          type:
            - string
            - 'null'
          description: Schema that unqualified table names resolve to inside each database.
          example: main
        expires_at:
          type:
            - string
            - 'null'
          description: |-
            When the created databases expire. Accepts an RFC 3339 timestamp or a
            relative duration such as `24h`, `90m`, or `7d`.
          example: 7d
        idempotency_key:
          type:
            - string
            - 'null'
          description: |-
            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.
          example: 3f6b1c1e-9a2b-4a5f-9a1e-2f0d6a7c8b91
        name_template:
          type:
            - string
            - 'null'
          description: |-
            Optional display-label pattern for each database. `{index}` is replaced
            with the database's zero-based position — for example `tenant-{index}`
            produces `tenant-0`, `tenant-1`, and so on. When omitted, each database
            is labelled from its own ID. Labels are not identifiers and are not
            required to be unique.
          example: tenant-{index}
        schemas:
          type: array
          items:
            $ref: '#/components/schemas/DatabaseDefaultSchemaDecl'
          description: |-
            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.
    BulkCreateDatabasesResult:
      type: object
      description: What a bulk-create job reports when it finishes.
      required:
        - batch_id
        - requested
        - created
        - cancelled
      properties:
        batch_id:
          type: string
          description: Batch these databases belong to.
        cancelled:
          type: boolean
          description: True when the batch was stopped before finishing.
        created:
          type: integer
          format: int64
          description: How many were created.
          minimum: 0
        requested:
          type: integer
          format: int64
          description: How many databases were asked for.
          minimum: 0
    CategoricalProfileDetail:
      type: object
      description: |-
        Type-specific column profile detail. The `type` discriminator field determines which
        variant is present. Profile type is chosen based on the column's Arrow data type and
        cardinality:

        - **categorical**: Text or numeric columns with ≤200 distinct values. Lists each value with its frequency.
        - **text**: Text columns with >200 distinct values. Reports string length statistics.
        - **numeric**: Numeric columns with >200 distinct values. Reports min, max, and mean.
        - **temporal**: Date and timestamp columns. Reports min and max as ISO-8601 strings.
        - **boolean**: Boolean columns. Reports true and false counts.
          Low-cardinality column (≤200 distinct values). Values sorted by frequency descending.
      required:
        - values
      properties:
        values:
          type: array
          items:
            $ref: '#/components/schemas/CategoryValueInfo'
          description: Distinct values with their counts, ordered by count descending
    CategoryValueInfo:
      type: object
      description: A distinct value with its frequency count, used in categorical profiles.
      required:
        - count
      properties:
        count:
          type: integer
          format: int64
          description: Number of occurrences
          minimum: 0
        value:
          type:
            - string
            - 'null'
          description: The distinct value (as a string, or null)
    ColumnDefinition:
      oneOf:
        - type: string
          description: 'A bare type name, optionally parameterized: `"VARCHAR"`, `"DECIMAL(10,2)"`.'
        - $ref: '#/components/schemas/ColumnTypeSpec'
          description: A type name plus explicit parameters.
      description: |-
        The declared type of one column, as written in a load request's `columns`
        map.

        Accepts either a bare type name or an object carrying the extra parameters
        some types need:

        - simple: `"VARCHAR"`, `"BIGINT"`, `"DECIMAL(10,2)"`
        - detailed: `{ "type": "DECIMAL", "precision": 10, "scale": 2 }`
    ColumnInfo:
      type: object
      description: Column metadata for API responses
      required:
        - name
        - data_type
        - nullable
      properties:
        data_type:
          type: string
        name:
          type: string
        nullable:
          type: boolean
    ColumnProfileDetail:
      oneOf:
        - allOf:
            - $ref: '#/components/schemas/CategoricalProfileDetail'
            - type: object
              required:
                - type
              properties:
                type:
                  type: string
                  enum:
                    - categorical
        - allOf:
            - $ref: '#/components/schemas/TextProfileDetail'
            - type: object
              required:
                - type
              properties:
                type:
                  type: string
                  enum:
                    - text
        - allOf:
            - $ref: '#/components/schemas/NumericProfileDetail'
            - type: object
              required:
                - type
              properties:
                type:
                  type: string
                  enum:
                    - numeric
        - allOf:
            - $ref: '#/components/schemas/TemporalProfileDetail'
            - type: object
              required:
                - type
              properties:
                type:
                  type: string
                  enum:
                    - temporal
        - allOf:
            - $ref: '#/components/schemas/BooleanProfileDetail'
            - type: object
              required:
                - type
              properties:
                type:
                  type: string
                  enum:
                    - boolean
    ColumnProfileInfo:
      type: object
      description: Statistics for a single column.
      required:
        - name
        - data_type
        - null_count
        - null_percentage
        - cardinality
      properties:
        cardinality:
          type: integer
          format: int64
          description: Approximate number of distinct non-null values
          minimum: 0
        data_type:
          type: string
          description: Arrow data type (e.g. "Utf8", "Int32", "Timestamp(Microsecond, Some(\"UTC\"))")
        name:
          type: string
          description: Column name
        null_count:
          type: integer
          format: int64
          description: Number of null values
          minimum: 0
        null_percentage:
          type: number
          format: double
          description: Percentage of null values (0.0 to 100.0)
        profile:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/ColumnProfileDetail'
              description: Type-specific profile detail. Null when the column is all-null or has an unsupported type.
    ColumnTypeSpec:
      type: object
      description: A column type plus the parameters that cannot be expressed by a bare name.
      required:
        - type
      properties:
        precision:
          type:
            - integer
            - 'null'
          format: int32
          description: Total number of digits for `DECIMAL` / `NUMERIC` (1–38).
          example: 10
          minimum: 0
        scale:
          type:
            - integer
            - 'null'
          format: int32
          description: |-
            Number of digits after the decimal point for `DECIMAL` / `NUMERIC`.
            Requires `precision`, and cannot exceed it.
          example: 2
        type:
          type: string
          description: The type name, e.g. `"DECIMAL"`, `"TIMESTAMP"`, `"VARCHAR"`.
          example: DECIMAL
      additionalProperties: false
    ConnectionInfo:
      type: object
      description: Single connection metadata for API responses
      required:
        - id
        - name
        - source_type
      properties:
        id:
          type: string
        name:
          type: string
        source_type:
          type: string
    CreateConnectionRequest:
      type: object
      description: Request body for POST /connections
      required:
        - name
        - source_type
        - config
      properties:
        config:
          type: object
          description: Connection configuration object. Fields vary by source type (host, port, database, etc.).
          additionalProperties: {}
          propertyNames:
            type: string
          example:
            database: analytics
            host: db.example.com
            port: 5432
            user: readonly
        name:
          type: string
          example: prod-postgres
        secret_id:
          type:
            - string
            - 'null'
          description: |-
            Optional reference to a secret by ID (e.g., "secr_abc123").
            If provided, this secret will be used for authentication.
            Mutually exclusive with `secret_name`.
        secret_name:
          type:
            - string
            - 'null'
          description: |-
            Optional reference to a secret by name.
            If provided, this secret will be used for authentication.
            Mutually exclusive with `secret_id`.
          example: prod-postgres-password
        skip_discovery:
          type: boolean
          description: |-
            If true, skip automatic schema discovery after registering the connection.
            The connection will be created but no tables will be discovered.
            You can run discovery later via the refresh endpoint.
          default: false
        source_type:
          type: string
          example: postgres
    CreateConnectionResponse:
      type: object
      description: Response body for POST /connections
      required:
        - id
        - name
        - source_type
        - tables_discovered
        - discovery_status
      properties:
        discovery_error:
          type:
            - string
            - 'null'
        discovery_status:
          $ref: '#/components/schemas/DiscoveryStatus'
        id:
          type: string
        name:
          type: string
        source_type:
          type: string
        tables_discovered:
          type: integer
          minimum: 0
    CreateDatabaseRequest:
      type: object
      description: Request body for POST /databases
      properties:
        default_catalog:
          type:
            - string
            - 'null'
          description: |-
            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:
          type:
            - string
            - 'null'
          description: |-
            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:
          type:
            - string
            - 'null'
          description: |-
            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.
        name:
          type:
            - string
            - 'null'
          description: |-
            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:
          type: array
          items:
            $ref: '#/components/schemas/DatabaseDefaultSchemaDecl'
          description: |-
            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.
    CreateDatabaseResponse:
      type: object
      description: Response body for POST /databases
      required:
        - id
        - default_connection_id
        - default_catalog
        - default_schema
      properties:
        default_catalog:
          type: string
          description: |-
            Name the database's default catalog answers to inside its query scope
            (`default` unless overridden at create time).
        default_connection_id:
          type: string
          description: |-
            Internal id of the connection that backs this database's `default`
            catalog. Workspace-level connection endpoints (list, get, health,
            delete, cache purge) refuse to act on this id — it is exposed only
            for the managed-tables load endpoint
            (`POST /v1/connections/{id}/schemas/{s}/tables/{t}/loads`) so callers
            can load data into tables declared at database-create time.
            Addressing it directly in SQL is not the recommended path — use
            `default` inside an `X-Database-Id` scope instead.
        default_schema:
          type: string
          description: |-
            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:
          type:
            - string
            - 'null'
          format: date-time
          description: When this database expires.
        id:
          type: string
        name:
          type:
            - string
            - 'null'
    CreateEmbeddingProviderRequest:
      type: object
      description: Request body for POST /embedding-providers
      required:
        - name
        - provider_type
      properties:
        api_key:
          type:
            - string
            - 'null'
          description: |-
            Inline API key. If provided, a secret is auto-created and referenced.
            Cannot be used together with `secret_name`.
        config:
          type: object
          description: Provider-specific configuration (model name, base URL, dimensions, etc.)
          additionalProperties: {}
          propertyNames:
            type: string
          example:
            base_url: https://api.openai.com/v1
            dimensions: 1536
            model: text-embedding-3-small
        name:
          type: string
          example: openai-text-embedding-3-small
        provider_type:
          type: string
          description: 'Provider type: "local" or "service"'
          example: service
        secret_name:
          type:
            - string
            - 'null'
          description: |-
            Reference an existing stored secret by name (for service providers).

            A stored secret is only sent to an approved provider origin — by
            default OpenAI's public API. To use a different endpoint, supply the
            key inline with `api_key` instead, or ask your operator to approve the
            origin.
          example: openai-api-key
    CreateEmbeddingProviderResponse:
      type: object
      description: Response body for POST /embedding-providers
      required:
        - id
        - name
        - provider_type
        - config
        - created_at
      properties:
        config: {}
        created_at:
          type: string
          format: date-time
        id:
          type: string
        name:
          type: string
        provider_type:
          type: string
    CreateIndexRequest:
      type: object
      description: Request body for POST .../indexes
      required:
        - index_name
        - columns
      properties:
        async:
          type: boolean
          description: When true, create the index as a background job and return a job ID for polling.
          default: false
        async_after_ms:
          type:
            - integer
            - 'null'
          format: int32
          description: |-
            If set (requires `async` = true), wait up to this many milliseconds for the index build to
            finish: if it completes in time the index is returned (201), 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.
          minimum: 1000
        columns:
          type: array
          items:
            type: string
          description: Columns to index. Required for all index types.
          example:
            - customer_id
        description:
          type:
            - string
            - 'null'
          description: User-facing description of the embedding (e.g., "product descriptions").
        dimensions:
          type:
            - integer
            - 'null'
          description: |-
            Output vector dimensions. Some models support multiple dimension sizes
            (e.g., OpenAI text-embedding-3-small supports 512 or 1536). If omitted,
            the model's default dimensions are used
          minimum: 0
        embedding_provider_id:
          type:
            - string
            - 'null'
          description: |-
            Embedding provider ID. When set for a vector index, the source column is treated
            as text and embeddings are generated automatically. The vector index is then built
            on the generated embedding column (`{column}_embedding` by default).
        index_name:
          type: string
          example: orders_customer_id
        index_type:
          type: string
          description: Index type. `sorted` supports range queries, `bm25` full-text search, and `vector` similarity search.
          default: sorted
          enum:
            - sorted
            - bm25
            - vector
        metric:
          type:
            - string
            - 'null'
          description: |-
            Distance metric for vector indexes: "l2", "cosine", or "dot".
            When omitted, defaults to "l2" for float array columns or the provider's
            preferred metric for text columns with auto-embedding.
        output_column:
          type:
            - string
            - 'null'
          description: Custom name for the generated embedding column. Defaults to `{column}_embedding`.
    CreateSavedQueryRequest:
      type: object
      description: Request body for POST /v1/queries
      required:
        - name
        - sql
      properties:
        description:
          type:
            - string
            - 'null'
          example: Ten highest-spending customers by order total
        name:
          type: string
          example: top-customers
        sql:
          type: string
          example: SELECT customer_id, sum(amount) AS total FROM orders GROUP BY customer_id ORDER BY total DESC LIMIT 10
        tags:
          type:
            - array
            - 'null'
          items:
            type: string
          example:
            - sales
            - weekly
    CreateUploadRequest:
      type: object
      description: |-
        Request body for `POST /v1/uploads` and for each entry of
        `POST /v1/uploads/batch`.

        Describes a single file you intend to upload. The response carries a
        short-lived URL to `PUT` the bytes to, so the file never passes through the
        API itself. The declared size is validated against the bytes you actually
        upload when you finalize.
      properties:
        checksum_algo:
          type:
            - string
            - 'null'
          description: |-
            Integrity checksum algorithm you are volunteering for this file.
            Currently only `sha256` is accepted. Optional; pair with
            `checksum_value`.
          example: sha256
        checksum_value:
          type:
            - string
            - 'null'
          description: Integrity checksum value, paired with `checksum_algo`. Optional.
          example: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
        content_encoding:
          type:
            - string
            - 'null'
          description: |-
            Content encoding to record for the uploaded file (for example `gzip`).
            Optional.
        content_type:
          type:
            - string
            - 'null'
          description: |-
            Content type to record for the uploaded file (for example the Parquet,
            CSV, or JSON MIME type). Optional.
          example: application/vnd.apache.parquet
        declared_size_bytes:
          type:
            - integer
            - 'null'
          format: int64
          description: |-
            The exact size, in bytes, of the file you will upload. Optional. When
            provided, it is validated at create time against the maximum allowed size,
            and again at finalize against the bytes actually uploaded — a mismatch
            fails the finalize. Omit it to create a streaming (unknown-size) upload: the
            session is always multi-part and returns no part URLs up front; instead
            you mint part URLs on demand from `POST /v1/uploads/{upload_id}/parts` as
            you upload, and finalize validates only that the file is non-empty.
          example: 10485760
          minimum: 0
        filename:
          type:
            - string
            - 'null'
          description: |-
            Original file name, recorded with the upload for your own bookkeeping.
            Optional and advisory — it does not affect how the file is uploaded or
            loaded.
          example: orders.parquet
        part_size:
          type:
            - integer
            - 'null'
          format: int64
          description: |-
            Preferred size, in bytes, of each part for a large (multi-part) upload.
            Optional hint — the service clamps it to the allowed part-size range and
            to the maximum number of parts, and ignores it for small files uploaded
            with a single `PUT`. Omit to let the service choose.
          example: 8388608
          minimum: 0
    CsvResultBody:
      type: string
      description: |-
        The CSV returned by `GET /v1/results/{id}` when the negotiated format is
        CSV. RFC 4180 CSV with a single header row, streamed batch-by-batch.
    DatabaseAttachmentInfo:
      type: object
      description: One attached catalog inside a database.
      required:
        - connection_id
      properties:
        alias:
          type:
            - string
            - 'null'
          description: |-
            Alias under which this catalog is reachable inside the database.
            When `None`, the catalog is reachable by its original connection name.
        connection_id:
          type: string
    DatabaseBatchResponse:
      type: object
      description: |-
        A bulk-creation batch: the handle for progress, listing, and cancellation.

        The databases themselves are listed with `GET /databases?batch=<batch_id>`;
        this carries the batch's own state rather than its members.
      required:
        - batch_id
        - count
        - created_count
        - cancel_requested
      properties:
        batch_id:
          type: string
        cancel_requested:
          type: boolean
          description: |-
            True once stopping has been requested. Databases already created are
            kept; only further creation stops.
        count:
          type: integer
          format: int64
          description: How many databases the batch was asked to create.
        created_count:
          type: integer
          format: int64
          description: How many exist so far. Advances as the batch fills.
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
        job_id:
          type:
            - string
            - 'null'
          description: Job filling this batch. Poll it for status.
        status_url:
          type:
            - string
            - 'null'
    DatabaseContextEntry:
      type: object
      description: One context entry returned by the API.
      required:
        - name
        - content
        - updated_at
      properties:
        content:
          type: string
        name:
          type: string
        updated_at:
          type: string
          format: date-time
    DatabaseCountResponse:
      type: object
      description: Response body for GET /databases/count.
      required:
        - total
      properties:
        total:
          type: integer
          format: int64
          description: Total databases matching the filters, across all pages.
    DatabaseDefaultSchemaDecl:
      type: object
      description: |-
        One schema declaration inside the database's default catalog, supplied at
        create time. `tables` defaults to empty, so you can declare just a schema
        name and add tables later.
      required:
        - name
      properties:
        name:
          type: string
          example: sales
        tables:
          type: array
          items:
            $ref: '#/components/schemas/DatabaseDefaultTableDecl'
    DatabaseDefaultTableDecl:
      type: object
      description: |-
        One table declaration inside a default-catalog schema, supplied at
        database-create time.
      required:
        - name
      properties:
        key:
          type: array
          items:
            type: string
          description: |-
            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.
          example:
            - order_id
        name:
          type: string
          example: orders
        partition_by:
          type: array
          items:
            $ref: '#/components/schemas/TablePartitionKey'
          description: |-
            Partition keys for this table, applied in order. Omit for no
            partitioning. Declared when the table is created and fixed thereafter.
        sorted_by:
          type: array
          items:
            $ref: '#/components/schemas/TableSortKey'
          description: |-
            Sort keys for this table, applied in order. Omit for no sort order.
            Declared when the table is created and fixed thereafter.
    DatabaseDetailResponse:
      type: object
      description: Response body for GET /databases/{database_id}
      required:
        - id
        - default_connection_id
        - default_catalog
        - default_schema
        - attachments
      properties:
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/DatabaseAttachmentInfo'
        created_at:
          type:
            - string
            - 'null'
          format: date-time
          description: When the database was created.
        default_catalog:
          type: string
          description: |-
            Name the database's default catalog answers to inside its query scope
            (`default` unless overridden at create time).
        default_connection_id:
          type: string
        default_schema:
          type: string
          description: |-
            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:
          type:
            - string
            - 'null'
          format: date-time
          description: When this database expires.
        id:
          type: string
        name:
          type:
            - string
            - 'null'
    DatabaseSummary:
      type: object
      description: Summary item in GET /databases
      required:
        - id
        - default_catalog
        - default_schema
      properties:
        created_at:
          type:
            - string
            - 'null'
          format: date-time
          description: When the database was created.
        default_catalog:
          type: string
          description: Name the database's default catalog answers to inside its query scope.
        default_schema:
          type: string
          description: |-
            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:
          type:
            - string
            - 'null'
          format: date-time
        id:
          type: string
        name:
          type:
            - string
            - 'null'
    DeleteDatabaseBatchResponse:
      type: object
      description: Response body for DELETE /databases/bulk/{batch_id}.
      required:
        - batch_id
        - deleted_count
      properties:
        batch_id:
          type: string
        deleted_count:
          type: integer
          format: int64
          description: How many databases were removed.
    DiscoveryStatus:
      type: string
      description: Discovery status for connection creation
      enum:
        - success
        - skipped
        - failed
    EmbeddingProviderResponse:
      type: object
      description: Single embedding provider for API responses
      required:
        - id
        - name
        - provider_type
        - config
        - has_secret
        - source
        - created_at
        - updated_at
      properties:
        config: {}
        created_at:
          type: string
          format: date-time
        has_secret:
          type: boolean
        id:
          type: string
        name:
          type: string
        provider_type:
          type: string
        source:
          type: string
          description: 'Provider source: "system" (from config) or "user" (created via API).'
        updated_at:
          type: string
          format: date-time
    ExecuteSavedQueryRequest:
      type: object
      description: Request body for POST /v1/queries/{id}/execute
      properties:
        version:
          type:
            - integer
            - 'null'
          format: int32
    FinalizeUploadPart:
      type: object
      description: |-
        One part of a multi-part upload, supplied at finalize. Single-`PUT` uploads
        have no parts.
      required:
        - part_number
        - e_tag
      properties:
        e_tag:
          type: string
          description: The `ETag` response header returned by that part's `PUT`.
          example: '"9f8c1e5b7a2d4f60b3c8e1a9d7f4b206"'
        part_number:
          type: integer
          format: int32
          description: The 1-based part number you uploaded this part as.
          example: 1
    FinalizeUploadRequest:
      type: object
      description: |-
        Request body for `POST /v1/uploads/{upload_id}/finalize`.

        Finalizing confirms the bytes were uploaded and makes the upload usable as
        managed-table contents. The request body is optional for single-`PUT`
        uploads; send `parts` only for a multi-part upload.
      properties:
        parts:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/FinalizeUploadPart'
          description: |-
            Parts to assemble, for a multi-part upload. Omit for single-`PUT`
            uploads (the common case).
    FinalizeUploadResponse:
      type: object
      description: |-
        Response body for `POST /v1/uploads/{upload_id}/finalize`: the finalized
        upload, ready to be loaded into a managed table.
      required:
        - upload_id
        - status
        - size_bytes
        - created_at
      properties:
        content_type:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        size_bytes:
          type: integer
          format: int64
          description: The validated size of the uploaded file in bytes.
        status:
          type: string
        upload_id:
          type: string
    ForkDatabaseRequest:
      type: object
      description: Request body for POST /databases/{database_id}/fork
      properties:
        expires_at:
          type:
            - string
            - 'null'
          description: |-
            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:
          type:
            - string
            - 'null'
          description: |-
            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.
    GetConnectionResponse:
      type: object
      description: Response body for GET /connections/{connection_id}
      required:
        - id
        - name
        - source_type
        - table_count
        - synced_table_count
      properties:
        id:
          type: string
        name:
          type: string
        source_type:
          type: string
        synced_table_count:
          type: integer
          minimum: 0
        table_count:
          type: integer
          minimum: 0
    GetDatabaseContextResponse:
      type: object
      description: Response body for GET `/v1/databases/{database_id}/context/{name}`.
      required:
        - context
      properties:
        context:
          $ref: '#/components/schemas/DatabaseContextEntry'
    GetResultResponse:
      type: object
      description: |-
        Response body for GET /results/{id}
        Returns status and optionally the result data
      required:
        - result_id
        - status
      properties:
        columns:
          type:
            - array
            - 'null'
          items:
            type: string
        error_message:
          type:
            - string
            - 'null'
        nullable:
          type:
            - array
            - 'null'
          items:
            type: boolean
        result_id:
          type: string
        row_count:
          type:
            - integer
            - 'null'
          format: int64
        rows:
          type:
            - array
            - 'null'
          items:
            type: array
            items: {}
          description: Array of rows, where each row is an array of column values.
        status:
          type: string
    IndexEntryResponse:
      allOf:
        - $ref: '#/components/schemas/IndexInfoResponse'
        - type: object
          required:
            - schema_name
            - table_name
          properties:
            connection_id:
              type:
                - string
                - 'null'
            schema_name:
              type: string
            table_name:
              type: string
      description: |-
        One index in a cross-table listing: the index itself plus the connection,
        schema, and table it belongs to.
    IndexInfoResponse:
      type: object
      description: Result payload for a `create_index` job, and response for index endpoints.
      required:
        - index_name
        - index_type
        - columns
        - status
        - updated_at
        - created_at
      properties:
        columns:
          type: array
          items:
            type: string
        created_at:
          type: string
          format: date-time
        index_name:
          type: string
        index_type:
          type: string
        metric:
          type:
            - string
            - 'null'
          description: Distance metric this index was built with. Only present for vector indexes.
        source_column:
          type:
            - string
            - 'null'
          description: |-
            Source text column for an embedding-backed vector index. A query searches
            it via `vector_distance(<source_column>, …)`; the indexed `columns` hold
            the generated embedding column instead. Absent for BM25, sorted, and
            direct (existing-column) vector indexes.
        status:
          $ref: '#/components/schemas/IndexStatus'
        updated_at:
          type: string
          format: date-time
    IndexStatus:
      type: string
      description: Index build status exposed to API consumers.
      enum:
        - ready
        - pending
    InformationSchemaResponse:
      type: object
      description: Response body for GET /information_schema
      required:
        - tables
        - count
        - limit
        - has_more
      properties:
        count:
          type: integer
          minimum: 0
        has_more:
          type: boolean
        limit:
          type: integer
          minimum: 0
        next_cursor:
          type:
            - string
            - 'null'
        tables:
          type: array
          items:
            $ref: '#/components/schemas/TableInfo'
    JobResult:
      oneOf:
        - $ref: '#/components/schemas/IndexInfoResponse'
          description: Result of an index creation.
        - $ref: '#/components/schemas/LoadManagedTableResponse'
          description: Result of a managed-table load (row count + published schema).
        - $ref: '#/components/schemas/BulkCreateDatabasesResult'
          description: Counters from a bulk database creation.
      description: |-
        Job-specific result payload. The shape depends on the job type.
        Null while the job is pending or running.
    JobStatus:
      type: string
      description: Current status of a background job.
      enum:
        - pending
        - running
        - succeeded
        - partially_succeeded
        - failed
    JobStatusResponse:
      type: object
      description: Response body for GET /v1/jobs/{id}
      required:
        - id
        - job_type
        - status
        - attempts
        - created_at
      properties:
        attempts:
          type: integer
          format: int32
          description: Number of execution attempts (including the current one).
        completed_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        error_message:
          type:
            - string
            - 'null'
          description: Error or warning message. Set when status is `failed` or `partially_succeeded`.
        id:
          type: string
        job_type:
          $ref: '#/components/schemas/JobType'
        result:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/JobResult'
              description: Job-specific result payload. Null while the job is pending or running.
        status:
          $ref: '#/components/schemas/JobStatus'
    JobType:
      type: string
      description: Background job types returned by the API.
      enum:
        - noop
        - bulk_create_databases
        - create_index
        - managed_load
        - ducklake_vacuum
        - ducklake_orphan_cleanup
        - result_deletion
        - stale_result_cleanup
        - result_retention
        - ducklake_compaction
        - ducklake_table_compaction
    ListConnectionsResponse:
      type: object
      description: Response body for GET /connections
      required:
        - connections
      properties:
        connections:
          type: array
          items:
            $ref: '#/components/schemas/ConnectionInfo'
    ListDatabaseContextsResponse:
      type: object
      description: Response body for GET `/v1/databases/{database_id}/context`.
      required:
        - contexts
      properties:
        contexts:
          type: array
          items:
            $ref: '#/components/schemas/DatabaseContextEntry'
    ListDatabasesResponse:
      type: object
      description: |-
        Response body for GET /databases. Results are returned one page at a time,
        newest first. When `has_more` is true, pass `next_cursor` back as the
        `cursor` query parameter to fetch the following page.
      required:
        - databases
      properties:
        count:
          type:
            - integer
            - 'null'
          description: Number of databases returned in this page.
          minimum: 0
        databases:
          type: array
          items:
            $ref: '#/components/schemas/DatabaseSummary'
        has_more:
          type:
            - boolean
            - 'null'
          description: Whether more databases exist beyond this page.
        limit:
          type:
            - integer
            - 'null'
          description: Page size applied to this response (after clamping to the maximum).
          minimum: 0
        next_cursor:
          type:
            - string
            - 'null'
          description: Opaque cursor for the next page; present only when `has_more` is true.
    ListEmbeddingProvidersResponse:
      type: object
      description: Response body for GET /embedding-providers
      required:
        - embedding_providers
      properties:
        embedding_providers:
          type: array
          items:
            $ref: '#/components/schemas/EmbeddingProviderResponse'
    ListIndexesPageResponse:
      type: object
      description: Response body for `GET /v1/indexes` (paginated, cross-table).
      required:
        - indexes
        - count
        - limit
        - has_more
      properties:
        count:
          type: integer
          minimum: 0
        has_more:
          type: boolean
        indexes:
          type: array
          items:
            $ref: '#/components/schemas/IndexEntryResponse'
        limit:
          type: integer
          minimum: 0
        next_cursor:
          type:
            - string
            - 'null'
    ListIndexesResponse:
      type: object
      description: Response body for GET .../indexes
      required:
        - indexes
      properties:
        indexes:
          type: array
          items:
            $ref: '#/components/schemas/IndexInfoResponse'
    ListJobsResponse:
      type: object
      description: Response body for GET /v1/jobs
      required:
        - jobs
      properties:
        jobs:
          type: array
          items:
            $ref: '#/components/schemas/JobStatusResponse'
    ListQueryRunsResponse:
      type: object
      description: Response body for GET /query-runs
      required:
        - query_runs
        - count
        - limit
        - has_more
      properties:
        count:
          type: integer
          minimum: 0
        has_more:
          type: boolean
        limit:
          type: integer
          minimum: 0
        next_cursor:
          type:
            - string
            - 'null'
        query_runs:
          type: array
          items:
            $ref: '#/components/schemas/QueryRunInfo'
    ListResultsResponse:
      type: object
      description: Response body for GET /results
      required:
        - results
        - count
        - offset
        - limit
        - has_more
      properties:
        count:
          type: integer
          description: Number of results returned in this response
          minimum: 0
        has_more:
          type: boolean
          description: Whether there are more results available after this page
        limit:
          type: integer
          description: Limit used for this request
          minimum: 0
        offset:
          type: integer
          description: Pagination offset used for this request
          minimum: 0
        results:
          type: array
          items:
            $ref: '#/components/schemas/ResultInfo'
    ListSavedQueriesResponse:
      type: object
      description: Response body for GET /v1/queries
      required:
        - queries
        - count
        - offset
        - limit
        - has_more
      properties:
        count:
          type: integer
          minimum: 0
        has_more:
          type: boolean
        limit:
          type: integer
          minimum: 0
        offset:
          type: integer
          minimum: 0
        queries:
          type: array
          items:
            $ref: '#/components/schemas/SavedQuerySummary'
    ListSavedQueryVersionsResponse:
      type: object
      description: Response body for GET /v1/queries/{id}/versions
      required:
        - saved_query_id
        - versions
        - count
        - offset
        - limit
        - has_more
      properties:
        count:
          type: integer
          minimum: 0
        has_more:
          type: boolean
        limit:
          type: integer
          minimum: 0
        offset:
          type: integer
          minimum: 0
        saved_query_id:
          type: string
        versions:
          type: array
          items:
            $ref: '#/components/schemas/SavedQueryVersionInfo'
    LoadManagedTableRequest:
      type: object
      description: |-
        Request body for the managed-table load endpoints — the connection-scoped
        `POST /v1/connections/{connection_id}/schemas/{schema}/tables/{table}/loads`
        and the database-scoped equivalent.

        Publishes data to the named table from one of three sources: a previously
        uploaded file (`upload_id`), a persisted query result (`result_id`), or data
        sent inline in this request (`data`). Provide exactly one. `mode` selects
        whether the data replaces the table's contents or is appended on top of them.
      required:
        - mode
      properties:
        async:
          type: boolean
          description: |-
            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:
          type:
            - integer
            - 'null'
          format: int32
          description: |-
            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.
          minimum: 1000
        columns:
          type:
            - object
            - 'null'
          description: |-
            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`.
          additionalProperties:
            $ref: '#/components/schemas/ColumnDefinition'
          propertyNames:
            type: string
          example:
            amount:
              precision: 10
              scale: 2
              type: DECIMAL
            customer_id: BIGINT
            order_id: BIGINT
        data:
          type:
            - string
            - 'null'
          description: |-
            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`.
          example: |
            order_id,customer_id,amount
            1001,42,1999
            1002,7,4550
        format:
          type:
            - string
            - 'null'
          description: |-
            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.
          example: csv
        idempotency_key:
          type:
            - string
            - 'null'
          description: |-
            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 returns `409`, and
            loads nothing.

            A retry sent while the first attempt is still running also returns `409`,
            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.
          example: 3f6b1c1e-9a2b-4a5f-9a1e-2f0d6a7c8b91
        key:
          type:
            - array
            - 'null'
          items:
            type: string
          description: |-
            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:
          type: string
          description: |-
            How the data is applied: `"replace"` overwrites the table's contents,
            `"append"` inserts the new rows on top of the existing data.
          example: replace
        result_id:
          type:
            - string
            - 'null'
          description: |-
            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:
          type:
            - string
            - 'null'
          description: |-
            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`.
      additionalProperties: false
    LoadManagedTableResponse:
      type: object
      description: |-
        Result of a managed-table load: row count after the load plus the published
        table schema. Returned inline (`200`) for a synchronous load, and as the
        `GET /v1/jobs/{id}` result payload for a completed background load.
      required:
        - connection_id
        - schema_name
        - table_name
        - row_count
        - arrow_schema_json
      properties:
        arrow_schema_json:
          type: string
          description: Schema of the loaded table, as JSON.
        connection_id:
          type: string
        row_count:
          type: integer
          format: int64
          description: Total number of rows in the table after the load.
          minimum: 0
        schema_name:
          type: string
        table_name:
          type: string
    ManagedSchemaResponse:
      type: object
      description: |-
        Response body for a successful add-schema request. Echoes the normalized
        (lowercased) names so callers see exactly what was persisted.
      required:
        - connection_id
        - schema
        - tables
      properties:
        connection_id:
          type: string
          description: |-
            Connection backing the catalog the schema was added to. For a database
            default catalog this is the database's `default_connection_id`.
        schema:
          type: string
        tables:
          type: array
          items:
            type: string
    ManagedTableResponse:
      type: object
      description: Response body for a successful add-table request.
      required:
        - connection_id
        - schema
        - table
      properties:
        connection_id:
          type: string
        schema:
          type: string
        table:
          type: string
    MarkdownResultBody:
      type: string
      description: |-
        The Markdown returned by `GET /v1/results/{id}` when the negotiated format
        is Markdown. A single GitHub-flavored pipe table with a header row.
    MintUploadPartsRequest:
      type: object
      description: |-
        Request body for `POST /v1/uploads/{upload_id}/parts`: get short-lived upload
        URLs for specific parts of a streaming (unknown-size) multi-part upload.

        Provide the 1-based part numbers you want URLs for. Mint parts as you upload,
        and re-request a part number if its URL expires before you finish — the parts
        you have already uploaded are unaffected.
      required:
        - part_numbers
      properties:
        part_numbers:
          type: array
          items:
            type: integer
            format: int32
          description: |-
            The 1-based part numbers to mint URLs for. Must be non-empty; each number
            must be between 1 and the maximum number of parts allowed.
          example:
            - 1
            - 2
            - 3
    MintUploadPartsResponse:
      type: object
      description: |-
        Response body for `POST /v1/uploads/{upload_id}/parts`: the minted part URLs,
        in ascending part-number order.
      required:
        - parts
      properties:
        parts:
          type: array
          items:
            $ref: '#/components/schemas/MintedUploadPartResponse'
          description: |-
            The minted part URLs, in ascending part-number order. `PUT` each part's
            bytes to its URL and keep the response's `ETag` to pass to finalize.
    MintedUploadPartResponse:
      type: object
      description: One minted part URL.
      required:
        - part_number
        - url
      properties:
        part_number:
          type: integer
          format: int32
          description: The 1-based part number this URL is for.
        url:
          type: string
          description: |-
            Short-lived URL to `PUT` this part's bytes to. Keep the response's `ETag`
            and pass the `{part_number, e_tag}` pair to finalize.
    NumericProfileDetail:
      type: object
      description: High-cardinality numeric column (>200 distinct values).
      required:
        - min
        - max
        - mean
      properties:
        max:
          type: string
          description: Maximum value (string to preserve precision for large integers and decimals)
        mean:
          type: number
          format: double
          description: Arithmetic mean
        min:
          type: string
          description: Minimum value (string to preserve precision for large integers and decimals)
    ParquetResultBody:
      type: string
      format: binary
      description: |-
        The Parquet returned by `GET /v1/results/{id}` when the negotiated format
        is Parquet. The raw bytes of the result's parquet file, served as-is with
        no conversion.
    QueryRequest:
      type: object
      description: Request body for POST /query
      required:
        - sql
      properties:
        async:
          type: boolean
          description: |-
            When true, execute the query asynchronously and return a query run ID for polling
            via GET /query-runs/{id}. The query results can be retrieved via GET /results/{id}
            once the query run status is "succeeded".
          default: false
        async_after_ms:
          type:
            - integer
            - 'null'
          format: int32
          description: |-
            If set (requires `async` = true), first attempt the query synchronously and wait up to
            this many milliseconds: if it finishes in time the full result is returned, otherwise an
            async response (a run id to poll) is returned. Must be at least 1000 and at most the
            server's configured maximum; a value out of that range, or set without `async` = true, is
            rejected with 400.
          minimum: 1000
        database_id:
          type:
            - string
            - 'null'
          description: |-
            Database to scope the query to (its id). Alternative to the
            `X-Database-Id` header — exactly one source must be provided. If both
            this field and the header are set and they disagree, the request is
            rejected with a 400.
          example: dbid6lguax1dxn9y1xj5gxnameyywl
        default_catalog:
          type:
            - string
            - 'null'
          description: |-
            Catalog that unqualified table references resolve against within the
            query's database scope. Must name a catalog visible in the database
            (`default`, an attached catalog alias, or a system catalog). Defaults to
            `default` when omitted.
          example: default
        default_schema:
          type:
            - string
            - 'null'
          description: |-
            Schema that unqualified table references resolve against within the
            query's database scope. Defaults to `main` when omitted. Existence is
            not validated up front — an unknown schema surfaces as a "table not
            found" error at planning time.
          example: main
        dialect:
          type:
            - string
            - 'null'
          description: |-
            SQL dialect the `sql` field is written in. One of `hotsql` (the
            default), `duckdb`, `postgres`, or `snowflake`. When set to anything
            other than `hotsql`, the query is translated to HotSQL before
            it runs, so you can use idioms from that dialect (for example Snowflake
            `IFF(...)` or Postgres `MOD(a, b)`). Only read-only queries are accepted.
            An unrecognized value is rejected with a 400.
          example: hotsql
        sql:
          type: string
          example: SELECT customer_id, sum(amount) AS total FROM orders GROUP BY customer_id ORDER BY total DESC LIMIT 10
    QueryResponse:
      type: object
      description: |-
        Response body for POST /query

        Query results are returned immediately along with a `result_id` for later retrieval.
        Saving the result for later retrieval happens asynchronously in the background.

        To check if a result is ready for SQL queries, poll GET /results/{id} and check `status`:
        - `"processing"`: Persistence is still in progress
        - `"ready"`: Result is available for retrieval and SQL queries
        - `"failed"`: Persistence failed (check `error_message` for details)
      required:
        - query_run_id
        - columns
        - nullable
        - rows
        - row_count
        - preview_row_count
        - truncated
        - execution_time_ms
      properties:
        columns:
          type: array
          items:
            type: string
        execution_time_ms:
          type: integer
          format: int64
          minimum: 0
        nullable:
          type: array
          items:
            type: boolean
          description: |-
            Nullable flags for each column (parallel to columns vec).
            True if the column allows NULL values, false if NOT NULL.
        preview_row_count:
          type: integer
          format: int64
          description: |-
            Number of rows in *this* response body. Always present. For a large
            result this is a bounded preview, not the grand total — see
            `total_row_count` and `truncated`.
        query_run_id:
          type: string
          description: Unique identifier for the query run record (qrun...).
        result_id:
          type:
            - string
            - 'null'
          description: |-
            Unique identifier for retrieving this result via GET /results/{id}.
            When non-null, the result is being persisted asynchronously. Null only
            when the result fit entirely in this response (`truncated: false`) but
            could not be persisted for later retrieval — see the `warning` field. A
            `truncated: true` response ALWAYS carries a non-null, resolvable
            `result_id`: a truncated result that cannot be persisted fails the
            request with a retryable HTTP 503 (`PERSISTENCE_UNAVAILABLE`, with a
            `Retry-After` header) rather than returning a partial body with a dead
            ticket.
        row_count:
          type: integer
          description: |-
            **Deprecated** — use `preview_row_count` (rows in this body) and
            `total_row_count` (grand total) instead. Retained as a back-compat alias
            and always equal to `preview_row_count`; for a truncated result it is the
            preview count, *not* the grand total — read `total_row_count` for that.
            Will be removed in a future release once clients migrate.
          deprecated: true
          minimum: 0
        rows:
          type: array
          items:
            type: array
            items: {}
          description: |-
            Array of rows, where each row is an array of column values.
            Values can be strings, numbers, booleans, or null.
        total_row_count:
          type:
            - integer
            - 'null'
          format: int64
          description: |-
            Grand total rows in the full result. Present (and equal to
            `preview_row_count`) when the whole result fit in this response; `null`
            while a truncated result is still being persisted. When `null`, read the
            authoritative total from `GET /v1/query-runs/{id}` (`row_count`) or the
            `X-Total-Row-Count` header on `GET /v1/results/{id}`.
        truncated:
          type: boolean
          description: |-
            True when `rows` is a bounded preview of a larger result. Fetch the full
            result via `result_id`.
        warning:
          type:
            - string
            - 'null'
          description: |-
            Warning message if result persistence could not be initiated. Present
            only when the full result is returned inline (`truncated: false`) but
            could not be persisted: `result_id` is then null and the result cannot be
            re-fetched later, though every row is in this response. A truncated
            result never carries a warning — if it cannot be persisted the request
            fails with a retryable HTTP 503 (`PERSISTENCE_UNAVAILABLE`, with a
            `Retry-After` header) instead.
    QueryRunInfo:
      type: object
      description: Single query run for listing
      required:
        - id
        - status
        - sql_text
        - sql_hash
        - snapshot_id
        - created_at
      properties:
        bytes_scanned:
          type:
            - integer
            - 'null'
          format: int64
          description: |-
            Total bytes of table data read from storage to run this query. `null`
            when the query touches no table at all (for example a constant
            expression like `SELECT 1`). May be `0` when the query reads a table but
            not its row data — for example a row count served from table statistics.
        completed_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        error_message:
          type:
            - string
            - 'null'
        execution_time_ms:
          type:
            - integer
            - 'null'
          format: int64
        id:
          type: string
        result_id:
          type:
            - string
            - 'null'
        row_count:
          type:
            - integer
            - 'null'
          format: int64
        rows_scanned:
          type:
            - integer
            - 'null'
          format: int64
          description: |-
            Total rows read from storage to run this query, before any filtering or
            aggregation. Distinct from `row_count`, which is how many rows the query
            returned. `null` when the query reads no table data from storage.
        saved_query_id:
          type:
            - string
            - 'null'
        saved_query_version:
          type:
            - integer
            - 'null'
          format: int32
        server_processing_ms:
          type:
            - integer
            - 'null'
          format: int64
          description: |-
            Total server-side processing time for this query (milliseconds).
            Measured from query start to result ready. Includes SQL execution,
            task spawning, and result preparation. Does not include network transit.
            Populated for all completed query runs (sync and async).
        snapshot_id:
          type: string
        sql_hash:
          type: string
        sql_text:
          type: string
        status:
          type: string
        trace_id:
          type:
            - string
            - 'null'
        user_public_id:
          type:
            - string
            - 'null'
          description: |-
            Who ran this query: the account id from the access token the request was
            made with. Use it to group a caller's query history.

            Requests made with a credential that identifies no account instead record
            an opaque `user_`-prefixed identifier, which is stable for that credential
            but cannot be resolved to an account.
        warning_message:
          type:
            - string
            - 'null'
    ResultInfo:
      type: object
      description: Summary of a persisted query result for listing
      required:
        - id
        - status
        - created_at
      properties:
        created_at:
          type: string
          format: date-time
        error_message:
          type:
            - string
            - 'null'
        id:
          type: string
        status:
          type: string
    ResultsFormatQuery:
      type: string
      description: |-
        The `?format=` query parameter on `GET /v1/results/{id}`. One of `arrow`,
        `json`, `csv`, `md`, or `parquet`.
      enum:
        - arrow
        - json
        - csv
        - md
        - parquet
    SavedQueryDetail:
      type: object
      description: Saved query detail (includes latest version's SQL)
      required:
        - id
        - name
        - latest_version
        - sql
        - sql_hash
        - tags
        - description
        - created_at
        - updated_at
      properties:
        category:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        description:
          type: string
        has_aggregation:
          type:
            - boolean
            - 'null'
        has_group_by:
          type:
            - boolean
            - 'null'
        has_join:
          type:
            - boolean
            - 'null'
        has_limit:
          type:
            - boolean
            - 'null'
        has_order_by:
          type:
            - boolean
            - 'null'
        has_predicate:
          type:
            - boolean
            - 'null'
        id:
          type: string
        latest_version:
          type: integer
          format: int32
        name:
          type: string
        num_tables:
          type:
            - integer
            - 'null'
          format: int32
        sql:
          type: string
        sql_hash:
          type: string
        table_size:
          type:
            - string
            - 'null'
        tags:
          type: array
          items:
            type: string
        updated_at:
          type: string
          format: date-time
    SavedQuerySummary:
      type: object
      description: Saved query summary for listing
      required:
        - id
        - name
        - latest_version
        - tags
        - description
        - created_at
        - updated_at
      properties:
        created_at:
          type: string
          format: date-time
        description:
          type: string
        id:
          type: string
        latest_version:
          type: integer
          format: int32
        name:
          type: string
        tags:
          type: array
          items:
            type: string
        updated_at:
          type: string
          format: date-time
    SavedQueryVersionInfo:
      type: object
      description: Single saved query version
      required:
        - version
        - sql
        - sql_hash
        - created_at
      properties:
        category:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        has_aggregation:
          type:
            - boolean
            - 'null'
        has_group_by:
          type:
            - boolean
            - 'null'
        has_join:
          type:
            - boolean
            - 'null'
        has_limit:
          type:
            - boolean
            - 'null'
        has_order_by:
          type:
            - boolean
            - 'null'
        has_predicate:
          type:
            - boolean
            - 'null'
        num_tables:
          type:
            - integer
            - 'null'
          format: int32
        sql:
          type: string
        sql_hash:
          type: string
        table_size:
          type:
            - string
            - 'null'
        version:
          type: integer
          format: int32
    SubmitJobResponse:
      type: object
      description: Response returned by APIs that submit a background job (e.g., async refresh).
      required:
        - id
        - status
        - status_url
      properties:
        id:
          type: string
          description: Job ID for status polling.
        status:
          $ref: '#/components/schemas/JobStatus'
          description: Current status of the submitted job.
        status_url:
          type: string
          description: URL to poll for job status.
    TableInfo:
      type: object
      description: Single table metadata
      required:
        - connection
        - schema
        - table
        - synced
        - partition_by
        - sorted_by
      properties:
        columns:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/ColumnInfo'
        connection:
          type: string
        last_sync:
          type:
            - string
            - 'null'
        partition_by:
          type: array
          items:
            $ref: '#/components/schemas/TablePartitionKey'
          description: |-
            The table's partition keys, in the order they were declared when the
            table was created. Empty when the table is not partitioned.

            A table's storage layout is fixed when the table is created and cannot be
            changed afterwards, so this is how to confirm a table really was created
            with the layout that was asked for. The field is always present: an empty
            array means "no partitioning declared", which is not the same as a
            response that omits the field entirely.

            Reported for tables in a Hotdata instant database, which are the only
            ones whose layout is declared here. A table discovered from an external
            connection always reports an empty array — its layout belongs to the
            upstream system, so an empty array there means "not known from here",
            not "confirmed unpartitioned".
        schema:
          type: string
        sorted_by:
          type: array
          items:
            $ref: '#/components/schemas/TableSortKey'
          description: |-
            The table's sort keys, in the order they were declared when the table was
            created. Empty when no sort order was declared. Always present, and
            limited to tables in a Hotdata instant database, for the same reasons as
            `partition_by`.
        synced:
          type: boolean
        table:
          type: string
    TablePartitionKey:
      type: object
      description: |-
        One partition key of a table's storage layout.

        Partitioning groups rows that share a key value into their own files, so a
        query filtering on that key reads only the matching files. Keys are applied
        in the order given, and several keys may read the same column: to get one
        partition per calendar month, declare `year` and `month` on the timestamp
        column. A single calendar transform on its own is rarely what you want —
        `month` alone puts every March of every year in one partition.
      required:
        - column
        - transform
      properties:
        column:
          type: string
          description: Column the key reads.
          example: created_at
        transform:
          type: string
          description: |-
            How the value is derived from the column. One of `identity` (the column
            value itself), `year`, `month`, `day`, or `hour`.
          example: day
    TableProfileResponse:
      type: object
      description: |-
        Column-level statistics for a synced table. Profiles are computed at sync time
        and include per-column cardinality, null counts, and type-specific details.
      required:
        - connection
        - schema
        - table
        - row_count
        - columns
      properties:
        columns:
          type: array
          items:
            $ref: '#/components/schemas/ColumnProfileInfo'
          description: Per-column profile statistics
        connection:
          type: string
          description: Connection name
        row_count:
          type: integer
          description: Total number of rows in the table
          minimum: 0
        schema:
          type: string
          description: Schema name
        synced_at:
          type:
            - string
            - 'null'
          description: When the table was last synced
        table:
          type: string
          description: Table name
    TableSortKey:
      type: object
      description: |-
        One key of a table's sort order.

        Rows are written in this order, which keeps the values in each file within a
        narrow range and lets queries filtering on those columns skip files
        entirely. Most useful on columns you filter by ranges, such as a timestamp.
      required:
        - column
      properties:
        column:
          type: string
          example: created_at
        direction:
          type:
            - string
            - 'null'
          description: |-
            `asc` (the default) or `desc`. Null when the table was declared without
            an explicit direction for this key.
          example: asc
        nulls:
          type:
            - string
            - 'null'
          description: |-
            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.
          example: last
    TemporalProfileDetail:
      type: object
      description: Date or timestamp column.
      required:
        - min
        - max
      properties:
        max:
          type: string
          description: Latest value as ISO-8601 string
        min:
          type: string
          description: Earliest value as ISO-8601 string
    TextProfileDetail:
      type: object
      description: High-cardinality text column (>200 distinct values).
      required:
        - min_length
        - max_length
        - avg_length
      properties:
        avg_length:
          type: number
          format: double
          description: Average string length
        max_length:
          type: integer
          format: int64
          description: Longest string length in the column
          minimum: 0
        min_length:
          type: integer
          format: int64
          description: Shortest string length in the column
          minimum: 0
    UpdateEmbeddingProviderRequest:
      type: object
      description: Request body for PUT /embedding-providers/{id}
      properties:
        api_key:
          type:
            - string
            - 'null'
          description: Inline API key. If provided, updates (or creates) the auto-managed secret.
        config: {}
        name:
          type:
            - string
            - 'null'
        secret_name:
          type:
            - string
            - 'null'
          description: Secret name containing the API key. Pass null to clear.
    UpdateEmbeddingProviderResponse:
      type: object
      description: Response body for PUT /embedding-providers/{id}
      required:
        - id
        - name
        - updated_at
      properties:
        id:
          type: string
        name:
          type: string
        updated_at:
          type: string
          format: date-time
    UpdateSavedQueryRequest:
      type: object
      description: Request body for PUT /v1/queries/{id}
      properties:
        category_override:
          type:
            - string
            - 'null'
          description: Override the auto-detected category. Send `null` to clear (revert to auto).
        description:
          type:
            - string
            - 'null'
        name:
          type:
            - string
            - 'null'
          description: Optional new name. When omitted the existing name is preserved.
        sql:
          type:
            - string
            - 'null'
          description: Optional new SQL. When omitted the existing SQL is preserved.
        table_size_override:
          type:
            - string
            - 'null'
          description: User annotation for table size. Send `null` to clear.
        tags:
          type:
            - array
            - 'null'
          items:
            type: string
    UploadSessionResponse:
      type: object
      description: |-
        A created upload session: everything needed to upload the file and later
        finalize it.
      required:
        - upload_id
        - mode
        - headers
        - finalize_token
      properties:
        finalize_token:
          type: string
          description: |-
            One-time token that authorizes finalizing this upload. Returned exactly
            once at create time — store it; it cannot be retrieved again.
        headers:
          type: object
          description: |-
            Headers you must send verbatim with each `PUT`. Currently always empty;
            present so a future mode can require signed headers without changing the
            response shape.
          additionalProperties:
            type: string
          propertyNames:
            type: string
        mode:
          type: string
          description: |-
            Upload mode: `single` (upload the whole file with one `PUT` to `url`) or
            `multipart` (upload each part with one `PUT` to the matching entry in
            `part_urls`). Modeled as a string so additional modes can be added later
            without breaking clients.
        part_size:
          type:
            - integer
            - 'null'
          format: int64
          description: |-
            For a `multipart` upload (both known-size and streaming), the size in bytes
            to split the file into: send bytes `[(i-1) * part_size, i * part_size)` as
            part *i*, with the last part carrying the remainder. Slice by this value —
            do **not** divide the file evenly by the number of parts, which can make a
            non-final part smaller than the 5 MiB minimum for a non-final part (the
            upload then fails at finalize). Absent for `single` uploads.
          minimum: 0
        part_urls:
          type:
            - array
            - 'null'
          items:
            type: string
          description: |-
            For a known-size `multipart` upload, the per-part URLs in ascending part
            order: `PUT` your file's part *i* (1-based) to `part_urls[i - 1]` and keep
            each response's `ETag`, then pass the `{part_number, e_tag}` list to
            finalize. Absent for `single` uploads, and also absent for a streaming
            (unknown-size) `multipart` upload — there, mint part URLs on demand via
            `POST /v1/uploads/{upload_id}/parts`.
        upload_id:
          type: string
          description: |-
            Identifier for this upload. Pass it to the finalize endpoint and to the
            managed-table load endpoint once finalized.
        url:
          type:
            - string
            - 'null'
          description: |-
            The URL to `PUT` the raw file bytes to, for a `single` upload. Short-lived
            — upload promptly and finalize. Absent for `multipart` uploads (use
            `part_urls`).
    UpsertDatabaseContextRequest:
      type: object
      description: Request body for POST `/v1/databases/{database_id}/context`.
      required:
        - name
        - content
      properties:
        content:
          type: string
          example: The orders table holds one row per completed purchase. `amount` is in USD cents.
        name:
          type: string
          description: |-
            Upsert key in the catalog. Validated with table-name rules (preserves case):
            ASCII letter or `_` first; then alphanumeric or `_` only; 1–128 chars; not a SQL reserved word.
          example: orders_overview
    UpsertDatabaseContextResponse:
      type: object
      description: Response body for POST `/v1/databases/{database_id}/context`.
      required:
        - context
      properties:
        context:
          $ref: '#/components/schemas/DatabaseContextEntry'
    WorkspaceUsageResponse:
      type: object
      description: Response for GET /v1/usage
      required:
        - since
        - bytes_scanned
        - query_count
        - storage_bytes
      properties:
        bytes_scanned:
          type: integer
          format: int64
          description: |-
            Sum of `bytes_scanned` across all completed/failed query runs since `since`.
            Null bytes (queries that touched no row data) contribute 0.
        query_count:
          type: integer
          format: int64
          description: Number of query runs (succeeded + failed) since `since`.
        since:
          type: string
          format: date-time
          description: The period start used for this response (echoed back for the caller to verify).
        storage_bytes:
          type: integer
          format: int64
          description: |-
            The workspace's current stored-data footprint in bytes, measured at request time:
            instant-database data, plus un-consumed uploads, connection caches, and
            search-index artifacts.
        storage_captured_at:
          type:
            - string
            - 'null'
          format: date-time
          description: When `storage_bytes` was measured (the time this response was produced).
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: Org-scoped API token obtained via CLI login or the dashboard.
    WorkspaceId:
      type: apiKey
      in: header
      name: X-Workspace-Id
      description: Public ID of the target workspace.
