openapi: 3.0.3
info:
  title: 0sql API
  version: "1"
  description: |
    0sql turns a query spec into one SQL statement for your warehouse. You deploy a project (tables, dimensions, measures, joins and security policies in YAML) to a project and branch, then POST a spec, or a one-line shorthand `expr`, together with a security context, and get back the statement, the datasource it is for, and the adapter to run it with. 0sql never executes SQL: your application runs the statement against its own warehouse. Discovery routes list the fields and tables of a deployed branch so a client can build specs without reading the YAML.

    Every request except `GET /healthz`, `POST /signup`, `POST /login` and `POST /invites/accept` carries `Authorization: Bearer <key>`. A personal key (`zsk_…`) does whatever its user may do: read, deploy, and administer the projects and keys the user has access to. A query key (`zqk_…`) is read only and answers for the projects and branches it is granted: `GET /me`, `GET /projects`, and the planning and discovery routes under `/projects/{uid}/branches/{branch}`. Project access has three levels: read (sql, explain, explore, fields, tables, branch summary), write (deploy, validate, test, delete branch) and owner (access grants, query-key grants, project settings and deletion). The base URL is `https://app.0sql.io`.

    JSON bodies need `Content-Type: application/json`; deploy and validate take a gzipped tar of the project directory. Bodies are limited to 256 MiB. Every error is `{"error": {"class": "<class>", "message": "<text>"}}` with a matching HTTP status; the `Error` schema lists every class. Planning writes nothing: the spec, the context and the SQL are not stored, and the only side effect of a request is the key's `last_used_at`.

servers:
  - url: https://app.0sql.io

security:
  - bearerAuth: []

tags:
  - name: Planning
    description: |
      Turn a spec or a shorthand `expr` into SQL. All three routes take the same body: `spec` or `expr` (spec wins when both are given), plus an optional `context`. A branch with security policies refuses to plan without a context (400 `ContextRequired`). Spec and planner errors come back as 422 with the class that raised them. These routes need read access.
  - name: Discovery
    description: |
      What a deployed branch holds: its summary, fields and tables, and the projects the caller can see. Read access; query keys may call them on the branches they are granted.
  - name: Deployment
    description: |
      Deploy, validate and test a project on a branch, and remove a branch. Write access, personal keys only; query keys are refused with 403. The first deploy of a uid creates the project with the caller as owner.
  - name: Account
    description: |
      Sign up, log in, manage the members of an account, personal keys, query keys and their grants, and read the audit log. Personal keys answer for the user's first account. Routes marked admin need the `admin` role.
  - name: Projects
    description: |
      Project settings, deletion and per-user access. Owner level, except reading access which needs read.
  - name: Health
    description: Liveness, no auth.

paths:
  /projects/{uid}/branches/{branch}/sql:
    parameters:
      - $ref: "#/components/parameters/ProjectUid"
      - $ref: "#/components/parameters/Branch"
    post:
      tags: [Planning]
      summary: Plan SQL
      operationId: planSql
      description: |
        Plan a spec, or a shorthand `expr`, against the branch and return one SQL statement with the datasource it runs on. Give `spec` or `expr`; with neither the answer is 400 `Invalid`. Field references that resolved by near match are listed under `corrections`. When the request used `expr`, the response also carries the `spec` it produced and `items`, one line per item describing how it was read.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SqlRequest"
      responses:
        "200":
          description: The statement and its datasource
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SqlResponse"
              example:
                sql: |-
                  WITH ag7098b0d0901f2eb16d14f9356f0bb2a0 AS (
                  SELECT
                  	T0."ss_sold_date_sk" AS "dimed56b67",
                  	sum(T0."ss_net_paid") AS "msr621f67c"
                  FROM
                  	store_sales T0
                  	JOIN item T1
                  		ON T0.ss_item_sk = T1.i_item_sk
                  WHERE
                  	LOWER(T1."i_category") IN ('men', 'children', '"books,com"')
                  GROUP BY
                  	T0."ss_sold_date_sk"
                  ), ag29130fae5cf6548d6ddb1bd37298b407 AS (
                  SELECT
                  	sum(T0."ws_net_paid") AS "msr501e4a8",
                  	T0."ws_sold_date_sk" AS "dimed56b67"
                  FROM
                  	web_sales T0
                  	JOIN item T1
                  		ON T0.ws_item_sk = T1.i_item_sk
                  WHERE
                  	LOWER(T1."i_category") IN ('men', 'children', '"books,com"')
                  GROUP BY
                  	T0."ws_sold_date_sk"
                  )
                  SELECT
                  	A0.msr501e4a8 AS "Web Net Paid",
                  	COALESCE(A1.dimed56b67, A0.dimed56b67) AS "Web Sold Date",
                  	A1.msr621f67c AS "Net Paid"
                  FROM
                  	ag29130fae5cf6548d6ddb1bd37298b407 A0
                  	FULL OUTER JOIN ag7098b0d0901f2eb16d14f9356f0bb2a0 A1
                  		ON A1.dimed56b67 = A0.dimed56b67
                datasource: Warehouse
                datasource_uid: warehouse
                adapter: postgres
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/PlanError"
        "500":
          $ref: "#/components/responses/ServerError"

  /projects/{uid}/branches/{branch}/explain:
    parameters:
      - $ref: "#/components/parameters/ProjectUid"
      - $ref: "#/components/parameters/Branch"
    post:
      tags: [Planning]
      summary: Explain a plan
      operationId: explainSql
      description: |
        Everything `/sql` returns, plus how long the server took (`timings`), the share of each planning phase (`phases`), and the node graph the planner shaped (`nodes`). Nodes are in post order; the last one is the root. Phase names: `resolve`, `segment_fork`, `complex_measure`, `exclusion`, `inclusion`, `snapshot`, `contribution`, `temporal`, `top_n`, `segment`, `security` (with a context), `reference`, `alias`, `sql`, `final_query`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SqlRequest"
      responses:
        "200":
          description: The statement, timings, phases and nodes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExplainResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/PlanError"
        "500":
          $ref: "#/components/responses/ServerError"

  /projects/{uid}/branches/{branch}/explore:
    parameters:
      - $ref: "#/components/parameters/ProjectUid"
      - $ref: "#/components/parameters/Branch"
    post:
      tags: [Planning]
      summary: Explore what a spec can add
      operationId: explore
      description: |
        Given a spec or `expr`, list the dimensions and measures that can join the query from where it stands. With `q`, keep the fields that contain the term (score 1) or sit near it by trigram similarity (score 0.5 or more), best first. Hidden fields are left out. The security context is accepted and ignored.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExploreRequest"
      responses:
        "200":
          description: Dimensions and measures the query can add
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExploreResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/PlanError"
        "500":
          $ref: "#/components/responses/ServerError"

  /projects/{uid}/branches/{branch}/deploy:
    parameters:
      - $ref: "#/components/parameters/ProjectUid"
      - $ref: "#/components/parameters/Branch"
    post:
      tags: [Deployment]
      summary: Deploy a project
      operationId: deployBranch
      description: |
        Deploy a project directory to the branch. The body is a gzipped tar (`Content-Type: application/gzip`) of the directory: `project.yml` at the root or inside one top-level folder, `datasources.yml`, `models/**/tbl.*.yml`, `models/**/rel.*.yml`, `security.yml` and `tests/*.yml`. The server compiles the model, runs the tests, stores the snapshot, and answers with the summary. **Deploy returns 200 even when tests fail**; check `test_results.failed`.

        An empty body, a bad archive, a missing `project.yml` or a loader error is 400 `DeployError`. An existing project needs write access on the branch; a protected production branch needs an owner. The first deploy of a uid creates the project with the caller as owner, taking `name` and `production_branch` from `project.yml`. The uid must be a slug: lower-case letters, digits, `-` and `_`.
      requestBody:
        required: true
        content:
          application/gzip:
            schema:
              type: string
              format: binary
              description: A tar.gz of the project directory.
      responses:
        "200":
          description: The deployment summary with test results
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeployResponse"
        "400":
          $ref: "#/components/responses/DeployError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/ServerError"

  /projects/{uid}/branches/{branch}/validate:
    parameters:
      - $ref: "#/components/parameters/ProjectUid"
      - $ref: "#/components/parameters/Branch"
    post:
      tags: [Deployment]
      summary: Validate a project
      operationId: validateBranch
      description: |
        Same body and checks as deploy, but nothing is stored: the model is compiled and the tests run against it, then the archive is dropped. The response is the deploy summary with `validated_only` set to `true`. `zsql deploy --dry-run` and `zsql check` call this route.
      requestBody:
        required: true
        content:
          application/gzip:
            schema:
              type: string
              format: binary
              description: A tar.gz of the project directory.
      responses:
        "200":
          description: The summary the deploy would produce
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeployResponse"
        "400":
          $ref: "#/components/responses/DeployError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/ServerError"

  /projects/{uid}/branches/{branch}/test:
    parameters:
      - $ref: "#/components/parameters/ProjectUid"
      - $ref: "#/components/parameters/Branch"
    post:
      tags: [Deployment]
      summary: Run the deployed tests
      operationId: testBranch
      description: |
        Run the `tests/*.yml` deployed on the branch and report each result. No body. Tests plan their projections with no security context and compare the SQL with `assert_sql` (lower-cased, whitespace-normalized) or `assert_regex` (case-insensitive, dotall). Write access.
      responses:
        "200":
          description: Pass and fail counts with one result per test
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TestResults"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /projects/{uid}/branches/{branch}:
    parameters:
      - $ref: "#/components/parameters/ProjectUid"
      - $ref: "#/components/parameters/Branch"
    get:
      tags: [Discovery]
      summary: Branch summary
      operationId: branchSummary
      description: |
        What is deployed on the branch: when, and how many datasources, tables, fields, joins, paths, policies and tests it holds, with any deploy warnings. `zsql status` prints this. Read access.
      responses:
        "200":
          description: The deployment summary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeploymentSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    delete:
      tags: [Deployment]
      summary: Remove a branch
      operationId: removeBranch
      description: |
        Remove the deployment on the branch: its snapshot and tests. The project and its other branches stay. Write access. `zsql remove --yes` calls this route.
      responses:
        "200":
          description: The branch that was removed
          content:
            application/json:
              schema:
                type: object
                required: [removed]
                properties:
                  removed:
                    type: string
                    description: The removed deployment as `uid/branch`.
                    example: tpcds/main
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /projects/{uid}/branches/{branch}/fields:
    parameters:
      - $ref: "#/components/parameters/ProjectUid"
      - $ref: "#/components/parameters/Branch"
    get:
      tags: [Discovery]
      summary: List or search fields
      operationId: listFields
      description: |
        Every dimension and measure on the branch. With `q`, fields whose name contains the term come first with score 1, then fuzzy matches by trigram similarity with their score. Hidden fields are left out unless `hidden=true`. Read access.
      parameters:
        - name: q
          in: query
          required: false
          description: Search term, matched against names, uids and synonyms.
          schema:
            type: string
            example: paid
        - name: hidden
          in: query
          required: false
          description: Include hidden fields.
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: The fields, best match first when searching
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FieldsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /projects/{uid}/branches/{branch}/tables:
    parameters:
      - $ref: "#/components/parameters/ProjectUid"
      - $ref: "#/components/parameters/Branch"
    get:
      tags: [Discovery]
      summary: List tables
      operationId: listTables
      description: Every table on the branch with its physical name, cost, datasource and field uids. Read access.
      responses:
        "200":
          description: The tables
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TablesResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /projects:
    get:
      tags: [Discovery]
      summary: List projects and deployments
      operationId: listProjects
      description: |
        Every deployment the caller can read, one summary per project and branch, plus the projects themselves with the caller's access level. A query key sees the deployments it is granted. `zsql list` prints this.
      responses:
        "200":
          description: Deployments and projects
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/ServerError"

  /signup:
    post:
      tags: [Account]
      summary: Create an account
      operationId: signup
      security: []
      description: |
        Create an account and its first admin user, and return the user's first personal key. The secret is shown once; store it with `zsql auth --api-key`. Passwords are at least 8 characters. 403 `Forbidden` when sign-up is closed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SignupRequest"
      responses:
        "200":
          description: The account, user, key record and its secret
          content:
            application/json:
              schema:
                type: object
                required: [account, user, key, secret]
                properties:
                  account:
                    $ref: "#/components/schemas/Account"
                  user:
                    $ref: "#/components/schemas/User"
                  key:
                    $ref: "#/components/schemas/UserKey"
                  secret:
                    type: string
                    description: The personal key, shown once.
                    example: zsk_3kD9vQm2xT7bLw1pYz8nRf4cHs6gJa0eUi5oNq2tWb
        "400":
          $ref: "#/components/responses/InvalidInput"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/ServerError"

  /login:
    post:
      tags: [Account]
      summary: Log in
      operationId: login
      security: []
      description: Start a browser session for the console at `app.0sql.io`. The response sets the `zsql_session` cookie (30 days). API clients use keys, not sessions.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginRequest"
      responses:
        "200":
          description: The user and account, with a session cookie
          content:
            application/json:
              schema:
                type: object
                required: [user, account]
                properties:
                  user:
                    $ref: "#/components/schemas/User"
                  account:
                    $ref: "#/components/schemas/Account"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/ServerError"

  /logout:
    post:
      tags: [Account]
      summary: Log out
      operationId: logout
      description: >-
        End the browser session and clear its cookie. Session requests carry `x-requested-with: zsql`.
      responses:
        "200":
          description: The session is over
          content:
            application/json:
              schema:
                type: object
                required: [logged_out]
                properties:
                  logged_out:
                    type: boolean
                    example: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/ServerError"

  /me:
    get:
      tags: [Account]
      summary: Who am I
      operationId: me
      description: |
        The principal behind the key. A personal key answers with `kind` `user`, the user, account, role and the id of the key in use. A query key answers with `kind` `query_key`, the key record, the account and its grants.
      responses:
        "200":
          description: The caller's identity
          content:
            application/json:
              schema:
                type: object
                required: [kind, account]
                properties:
                  kind:
                    type: string
                    enum: [user, query_key]
                    description: Which kind of key made the request.
                    example: user
                  account:
                    $ref: "#/components/schemas/Account"
                  user:
                    $ref: "#/components/schemas/User"
                  role:
                    type: string
                    enum: [admin, developer]
                    description: The user's role, with a personal key.
                    example: admin
                  key_id:
                    type: integer
                    description: The id of the personal key in use.
                    example: 7
                  key:
                    $ref: "#/components/schemas/QueryKey"
                  grants:
                    type: array
                    description: The query key's grants.
                    items:
                      $ref: "#/components/schemas/Grant"
              example:
                kind: user
                user: { id: 1, email: ajo@example.com, name: Ajo }
                account: { id: 1, uid: acme, name: Acme }
                role: admin
                key_id: 7
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/ServerError"

  /account/password:
    post:
      tags: [Account]
      summary: Change password
      operationId: changePassword
      description: Set the caller's password (at least 8 characters). Personal keys and sessions.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PasswordRequest"
      responses:
        "200":
          description: The password was changed
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    example: true
        "400":
          $ref: "#/components/responses/InvalidInput"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/ServerError"

  /account/members:
    get:
      tags: [Account]
      summary: List members
      operationId: listMembers
      description: The users of the account and their roles. Any user.
      responses:
        "200":
          description: The members
          content:
            application/json:
              schema:
                type: object
                required: [members]
                properties:
                  members:
                    type: array
                    items:
                      $ref: "#/components/schemas/Member"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/ServerError"
    post:
      tags: [Account]
      summary: Add a member
      operationId: addMember
      description: |
        Add a user to the account as `developer` or `admin`. Without a password the response includes a one-time invite token (`zin_…`) the user redeems with `POST /invites/accept` or at `/invite?token=…`. Admin.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MemberRequest"
      responses:
        "200":
          description: The new member
          content:
            application/json:
              schema:
                type: object
                required: [user, role]
                properties:
                  user:
                    $ref: "#/components/schemas/User"
                  role:
                    type: string
                    enum: [developer, admin]
                    example: developer
                  invite:
                    type: string
                    description: A one-time invite token, when the member was added without a password.
                    example: zin_8fKq2LmZ7vXt4RbNw1cYp9sDh3gJe6aUo5iTn0rWqM
        "400":
          $ref: "#/components/responses/InvalidInput"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/ServerError"

  /account/members/{email}:
    parameters:
      - name: email
        in: path
        required: true
        description: The member's email.
        schema:
          type: string
          example: dev@example.com
    delete:
      tags: [Account]
      summary: Remove a member
      operationId: removeMember
      description: Remove the user from the account. Admin.
      responses:
        "200":
          description: The removed member
          content:
            application/json:
              schema:
                type: object
                required: [removed]
                properties:
                  removed:
                    type: string
                    description: The email of the removed user.
                    example: dev@example.com
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /account/members/{email}/invite:
    parameters:
      - name: email
        in: path
        required: true
        description: The member's email.
        schema:
          type: string
          example: dev@example.com
    post:
      tags: [Account]
      summary: Issue an invite
      operationId: inviteMember
      description: Issue a fresh one-time invite token for an existing member, for example when the first one expired or was lost. Admin.
      responses:
        "200":
          description: The invite token
          content:
            application/json:
              schema:
                type: object
                required: [invite]
                properties:
                  invite:
                    type: string
                    description: A one-time invite token.
                    example: zin_8fKq2LmZ7vXt4RbNw1cYp9sDh3gJe6aUo5iTn0rWqM
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /invites/accept:
    post:
      tags: [Account]
      summary: Accept an invite
      operationId: acceptInvite
      security: []
      description: >-
        Redeem an invite token: set the user's name and password, and receive a first personal key. The token is single use.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InviteAcceptRequest"
      responses:
        "200":
          description: The account, user, key record and its secret
          content:
            application/json:
              schema:
                type: object
                required: [account, user, key, secret]
                properties:
                  account:
                    $ref: "#/components/schemas/Account"
                  user:
                    $ref: "#/components/schemas/User"
                  key:
                    $ref: "#/components/schemas/UserKey"
                  secret:
                    type: string
                    description: The personal key, shown once.
                    example: zsk_3kD9vQm2xT7bLw1pYz8nRf4cHs6gJa0eUi5oNq2tWb
        "400":
          $ref: "#/components/responses/InvalidInput"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /account/keys:
    get:
      tags: [Account]
      summary: List personal keys
      operationId: listKeys
      description: The caller's personal keys, by name and 12-character prefix. Secrets are never listed.
      responses:
        "200":
          description: The caller's keys
          content:
            application/json:
              schema:
                type: object
                required: [keys]
                properties:
                  keys:
                    type: array
                    items:
                      $ref: "#/components/schemas/UserKey"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/ServerError"
    post:
      tags: [Account]
      summary: Create a personal key
      operationId: createKey
      description: Create a named personal key (`zsk_…`). The secret is returned once.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/KeyRequest"
      responses:
        "200":
          description: The key record and its secret
          content:
            application/json:
              schema:
                type: object
                required: [key, secret]
                properties:
                  key:
                    $ref: "#/components/schemas/UserKey"
                  secret:
                    type: string
                    description: The personal key, shown once.
                    example: zsk_3kD9vQm2xT7bLw1pYz8nRf4cHs6gJa0eUi5oNq2tWb
        "400":
          $ref: "#/components/responses/InvalidInput"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/ServerError"

  /account/keys/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: The key id.
        schema:
          type: integer
          example: 7
    delete:
      tags: [Account]
      summary: Revoke a personal key
      operationId: revokeKey
      description: Revoke one of the caller's personal keys. Requests with it fail with 401 from then on.
      responses:
        "200":
          description: The revoked key id
          content:
            application/json:
              schema:
                type: object
                required: [revoked]
                properties:
                  revoked:
                    type: integer
                    example: 7
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /account/query-keys:
    get:
      tags: [Account]
      summary: List query keys
      operationId: listQueryKeys
      description: The account's query keys with their grants. Any user.
      responses:
        "200":
          description: The query keys and their grants
          content:
            application/json:
              schema:
                type: object
                required: [query_keys]
                properties:
                  query_keys:
                    type: array
                    items:
                      type: object
                      required: [key, grants]
                      properties:
                        key:
                          $ref: "#/components/schemas/QueryKey"
                        grants:
                          type: array
                          items:
                            $ref: "#/components/schemas/Grant"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/ServerError"
    post:
      tags: [Account]
      summary: Create a query key
      operationId: createQueryKey
      description: Create a named query key (`zqk_…`) for an application. It is read only and answers for nothing until a project owner grants it a project. The secret is returned once.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/KeyRequest"
      responses:
        "200":
          description: The key record and its secret
          content:
            application/json:
              schema:
                type: object
                required: [key, secret]
                properties:
                  key:
                    $ref: "#/components/schemas/QueryKey"
                  secret:
                    type: string
                    description: The query key, shown once.
                    example: zqk_9mB2xV7tQk4LwP1zRn8fCs3hJd6gYa0eUo5iTn2rWq
        "400":
          $ref: "#/components/responses/InvalidInput"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/ServerError"

  /account/query-keys/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: The query key id.
        schema:
          type: integer
          example: 3
    delete:
      tags: [Account]
      summary: Revoke a query key
      operationId: revokeQueryKey
      description: Revoke a query key and its grants. Admin or the key's creator.
      responses:
        "200":
          description: The revoked key id
          content:
            application/json:
              schema:
                type: object
                required: [revoked]
                properties:
                  revoked:
                    type: integer
                    example: 3
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /account/query-keys/{id}/rotate:
    parameters:
      - name: id
        in: path
        required: true
        description: The query key id.
        schema:
          type: integer
          example: 3
    post:
      tags: [Account]
      summary: Rotate a query key
      operationId: rotateQueryKey
      description: Issue a new secret for the key. Its grants stay; the old secret stops working. Admin or the key's creator.
      responses:
        "200":
          description: The key record and its new secret
          content:
            application/json:
              schema:
                type: object
                required: [key, secret]
                properties:
                  key:
                    $ref: "#/components/schemas/QueryKey"
                  secret:
                    type: string
                    description: The new query key, shown once.
                    example: zqk_4rTq8WnZ2vXk7LbMw1cYp9sDh3gJe6aUo5iPn0fQsE
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /account/query-keys/{id}/grants:
    parameters:
      - name: id
        in: path
        required: true
        description: The query key id.
        schema:
          type: integer
          example: 3
    post:
      tags: [Account]
      summary: Grant a project to a query key
      operationId: grantQueryKey
      description: |
        Let the key read a project. `branch` names one branch, `*` means every branch, and omitting it means the production branch. Project owner.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GrantRequest"
      responses:
        "200":
          description: The key and its new grant
          content:
            application/json:
              schema:
                type: object
                required: [key, grant]
                properties:
                  key:
                    $ref: "#/components/schemas/QueryKey"
                  grant:
                    $ref: "#/components/schemas/Grant"
        "400":
          $ref: "#/components/responses/InvalidInput"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /account/query-keys/{id}/grants/{project}:
    parameters:
      - name: id
        in: path
        required: true
        description: The query key id.
        schema:
          type: integer
          example: 3
      - name: project
        in: path
        required: true
        description: The project uid.
        schema:
          type: string
          example: tpcds
    delete:
      tags: [Account]
      summary: Revoke a grant
      operationId: revokeGrant
      description: Take a project away from a query key. Project owner.
      responses:
        "200":
          description: The revoked grant
          content:
            application/json:
              schema:
                type: object
                required: [revoked]
                properties:
                  revoked:
                    type: object
                    required: [key, project]
                    properties:
                      key:
                        type: integer
                        description: The query key id.
                        example: 3
                      project:
                        type: string
                        description: The project uid.
                        example: tpcds
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /account/audit:
    get:
      tags: [Account]
      summary: Audit log
      operationId: auditLog
      description: The account's audit events, newest first. Deploys, key and grant changes, membership and project changes each write one row. Admin.
      parameters:
        - name: limit
          in: query
          required: false
          description: How many events to return.
          schema:
            type: integer
            default: 50
            maximum: 1000
      responses:
        "200":
          description: The events
          content:
            application/json:
              schema:
                type: object
                required: [events]
                properties:
                  events:
                    type: array
                    items:
                      $ref: "#/components/schemas/AuditEvent"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/ServerError"

  /projects/{uid}:
    parameters:
      - $ref: "#/components/parameters/ProjectUid"
    patch:
      tags: [Projects]
      summary: Update project settings
      operationId: patchProject
      description: |
        Change the project's name, production branch, visibility or production protection. Every field is optional; the response is the whole project. `visibility` `account` gives every developer read access; `restricted` limits access to explicit grants. With `protected_production`, deploying to the production branch needs an owner. Owner.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectPatch"
      responses:
        "200":
          description: The project after the change
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Project"
        "400":
          $ref: "#/components/responses/InvalidInput"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    delete:
      tags: [Projects]
      summary: Delete a project
      operationId: deleteProject
      description: Delete the project, every branch deployed on it, its access grants and its query-key grants. Owner.
      responses:
        "200":
          description: The deleted project uid
          content:
            application/json:
              schema:
                type: object
                required: [deleted]
                properties:
                  deleted:
                    type: string
                    example: tpcds
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /projects/{uid}/access:
    parameters:
      - $ref: "#/components/parameters/ProjectUid"
    get:
      tags: [Projects]
      summary: Who can access a project
      operationId: projectAccess
      description: The project, each user's access level, and the query keys granted on it with their grants. Read access.
      responses:
        "200":
          description: Users and query keys with access
          content:
            application/json:
              schema:
                type: object
                required: [project, access, query_keys]
                properties:
                  project:
                    $ref: "#/components/schemas/Project"
                  access:
                    type: array
                    description: Users with explicit access.
                    items:
                      type: object
                      required: [user, level]
                      properties:
                        user:
                          $ref: "#/components/schemas/User"
                        level:
                          type: string
                          enum: [read, write, owner]
                          example: owner
                  query_keys:
                    type: array
                    description: Query keys granted on the project.
                    items:
                      type: object
                      required: [key, grant]
                      properties:
                        key:
                          $ref: "#/components/schemas/QueryKey"
                        grant:
                          $ref: "#/components/schemas/Grant"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    put:
      tags: [Projects]
      summary: Set a user's access
      operationId: setProjectAccess
      description: Give a member of the account `read`, `write` or `owner` access to the project, replacing any level they had. Owner.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AccessRequest"
      responses:
        "200":
          description: The user and their level
          content:
            application/json:
              schema:
                type: object
                required: [user, level]
                properties:
                  user:
                    $ref: "#/components/schemas/User"
                  level:
                    type: string
                    enum: [read, write, owner]
                    example: write
        "400":
          $ref: "#/components/responses/InvalidInput"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /projects/{uid}/access/{email}:
    parameters:
      - $ref: "#/components/parameters/ProjectUid"
      - name: email
        in: path
        required: true
        description: The user's email.
        schema:
          type: string
          example: dev@example.com
    delete:
      tags: [Projects]
      summary: Remove a user's access
      operationId: removeProjectAccess
      description: Remove the user's explicit access to the project. On an account-visible project a developer keeps read access. Owner.
      responses:
        "200":
          description: The user whose access was removed
          content:
            application/json:
              schema:
                type: object
                required: [removed]
                properties:
                  removed:
                    type: string
                    description: The user's email.
                    example: dev@example.com
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /healthz:
    get:
      tags: [Health]
      summary: Liveness
      operationId: healthz
      security: []
      description: Answers `ok` as plain text. No auth, no JSON.
      responses:
        "200":
          description: The server is up
          content:
            text/plain:
              schema:
                type: string
                example: ok

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        A personal key (`zsk_…`) or a query key (`zqk_…`), as `Authorization: Bearer <key>`. Keys are created in the console at `https://app.0sql.io` or with the Account routes.

  parameters:
    ProjectUid:
      name: uid
      in: path
      required: true
      description: The project uid, a slug of lower-case letters, digits, `-` and `_`.
      schema:
        type: string
        example: tpcds
    Branch:
      name: branch
      in: path
      required: true
      description: The branch name. `zsql` deploys to the checked-out git branch; `main` is the usual production branch.
      schema:
        type: string
        example: main

  responses:
    BadRequest:
      description: "`Invalid` (neither `spec` nor `expr`), `Shorthand` (the `expr` did not parse) or `ContextRequired` (the branch has security policies and the request has no context)"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              class: ContextRequired
              message: this branch has security policies; a context is required to plan
    InvalidInput:
      description: "`Invalid`: the body is missing a field or a value is not allowed"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              class: Invalid
              message: a password needs at least 8 characters
    DeployError:
      description: "`DeployError`: the body is not a tar.gz, holds no `project.yml`, or a file did not load. Loader errors read `Error in <file>: <message>`"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              class: DeployError
              message: the archive holds no project.yml
    Unauthorized:
      description: "`Unauthorized`: the key is missing or not valid"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              class: Unauthorized
              message: "an API key is required: Authorization: Bearer <key>"
    Forbidden:
      description: "`Forbidden`: the caller lacks the access level, the role, or the grant this route needs; query keys on write routes"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              class: Forbidden
              message: dev@example.com has read access to tpcds; this needs write
    NotFound:
      description: "`NotFound`: no deployment for the project and branch, or no such record"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              class: NotFound
              message: no deployment for project tpcds branch main
    PlanError:
      description: "The spec could not be planned. `Query::Spec::InvalidSpecError` for a malformed spec, `ActiveRecord::RecordInvalid` for a validation failure (message prefixed `Validation failed:`), `Semantic::NotFound` or `Semantic::Ambiguous` for a field reference, `Planner::ResolutionError`, `Planner::SecurityPolicyError`, `Planner::SegmentDatasourceError` or `Unimplemented` from the planner"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              class: Semantic::NotFound
              message: "No field named 'net paidd' in this model. Did you mean: Net Paid, Web Net Paid?"
    ServerError:
      description: "`Dialect` (the base dialect could not load) or `Accounts` (the account store failed)"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              class: Accounts
              message: the account store is unavailable

  schemas:
    Error:
      type: object
      description: |
        Every non-2xx answer. `class` says what went wrong and maps to the HTTP status: 400 `Invalid`, `Shorthand`, `ContextRequired`, `DeployError`; 401 `Unauthorized`; 403 `Forbidden`; 404 `NotFound`; 422 `Query::Spec::InvalidSpecError`, `ActiveRecord::RecordInvalid`, `Semantic::NotFound`, `Semantic::Ambiguous`, `Planner::ResolutionError`, `Planner::SegmentDatasourceError`, `Planner::SecurityPolicyError`, `Unimplemented`; 500 `Dialect`, `Accounts`. The `zsql` CLI prints errors as `<class>: <message>`.
      required: [error]
      properties:
        error:
          type: object
          required: [class, message]
          properties:
            class:
              type: string
              description: The error class.
              enum:
                - Invalid
                - Shorthand
                - ContextRequired
                - DeployError
                - Unauthorized
                - Forbidden
                - NotFound
                - Query::Spec::InvalidSpecError
                - ActiveRecord::RecordInvalid
                - Semantic::NotFound
                - Semantic::Ambiguous
                - Planner::ResolutionError
                - Planner::SegmentDatasourceError
                - Planner::SecurityPolicyError
                - Unimplemented
                - Dialect
                - Accounts
              example: NotFound
            message:
              type: string
              description: What went wrong, for a person to read.
              example: no deployment for project tpcds branch main
      example:
        error:
          class: NotFound
          message: no deployment for project tpcds branch main

    Spec:
      type: object
      description: |
        A query: the fields to project, the calculations over them, the filters, and the segments that constrain the population. Unknown keys are rejected (422 `Query::Spec::InvalidSpecError`, `malformed spec: ...`). At least one projection is required. `filters` is either a flat array of [FilterLeaf](#schema-filterleaf) objects (an AND) or a [FilterNode](#schema-filternode) tree of `and`/`or` arrays; see [Filter](#schema-filter).
      properties:
        name:
          type: string
          description: A name for the query, carried through to explain output.
          example: Web paid by month
        description:
          type: string
          description: Free text, carried and never planned.
          example: Monthly web revenue for men's and children's categories
        limit:
          type: integer
          description: Row limit carried with the spec; 5000 when absent. The planner does not emit it into the statement.
          default: 5000
          example: 5000
        projections:
          type: array
          description: The fields, with their decorators, and in-place calculations. Order is kept. At least one is required.
          items:
            $ref: "#/components/schemas/Projection"
        calculations:
          type: array
          description: Calculations kept apart from the projections; they follow them in the output. Each needs `alias` and `sql`.
          items:
            $ref: "#/components/schemas/Projection"
        filters:
          description: A flat array of FilterLeaf objects, ANDed, or a FilterNode tree. See [Filter](#schema-filter).
          oneOf:
            - type: array
              items:
                $ref: "#/components/schemas/FilterLeaf"
            - $ref: "#/components/schemas/FilterNode"
          example:
            - field: category
              predicate: in_list
              value: men, children
        segments:
          type: array
          description: Populations that constrain the whole query or named measures.
          items:
            $ref: "#/components/schemas/Segment"
        hints:
          type: array
          description: Table uids the resolver must route through.
          items:
            type: string
            example: web_sales
          example: [web_sales]
        db_settings:
          type: object
          description: Per-query dialect overrides, for example `final_pass_measure_join_type` or `force_group_by`.
          example:
            final_pass_measure_join_type: inner
        view:
          type: object
          description: Visualization settings, carried and never planned.
          example: {}
        classification:
          type: object
          description: Any JSON, carried and never planned.
          example: {}
      example:
        projections:
          - field: category
          - field: date
            decorators:
              - type: truncate
                grain: month
          - field: ws_net_paid
            alias: Web Net Paid
            order_by: desc
        filters:
          - field: category
            predicate: in_list
            value: men, children
          - field: date
            predicate: greater_than_or_equal_to
            value: 1y

    Projection:
      type: object
      description: |
        One output column. A field projection names a dimension or measure in `field`, by uid, by name (case-insensitive) or by synonym, with an optional `@d`/`@m` suffix to pick the kind when a name is both. A calculation sets `calculation: true` with an `alias` and a `sql` formula over `[Name]@m`, `[Name]@d` and `[Alias]` references; a formula that calls an aggregate is a measure calculation, otherwise a dimension calculation. A field reference that resolves to nothing is 422 `Semantic::NotFound`, with a near match taken silently and reported under `corrections` when it is clear enough.
      properties:
        field:
          type: string
          description: >-
            The field, as `"uid"`, `"Name"`, `"Name@d"`, `"Name@m"` or `{"uid": "..."}`. Required on a field projection; ignored on a calculation.
          example: ws_net_paid
        field_type:
          type: string
          description: A kind hint when the reference is ambiguous. Anything starting with `d` is a dimension; any other value is a measure.
          enum: [dimension, measure]
          example: measure
        alias:
          type: string
          description: The output column name. Required on a calculation; on a field projection it defaults to the name the decorators lend, else the field's name.
          example: Web Net Paid
        axis:
          type: string
          description: Where a client lays the column out; carried, not planned. `tip` holds measures only.
          enum: [row, x, y, series, tip, pivot]
          default: row
          example: row
        order_by:
          type: string
          description: Sort the result by this column.
          enum: [asc, desc]
          example: desc
        format:
          type: object
          description: Display format, carried and never planned.
          example:
            unit: $
        hidden:
          type: boolean
          description: Compute the column but leave it out of the SELECT list.
          default: false
          example: false
        decorators:
          type: array
          description: Date truncation, extraction, windows, contribution, temporal comparison or custom SQL applied to the field.
          items:
            $ref: "#/components/schemas/Decorator"
        calculation:
          type: boolean
          description: Marks the entry as a calculation; then `alias` and `sql` are required.
          default: false
          example: false
        sql:
          type: string
          description: The calculation formula. SQL text with `[Name]@m`, `[Name]@d` and `[Alias]` references; bare column names are rejected.
          example: "[Web Net Paid]@m / nullif([Net Paid]@m, 0)"
        data_type:
          type: string
          description: A calculation's result type; `decimal` when absent.
          enum: [string, integer, decimal, date, date_time, boolean, bigint, binary]
          default: decimal
          example: decimal
      example:
        field: ws_net_paid
        alias: Web Net Paid
        order_by: desc

    Decorator:
      type: object
      description: |
        A transform on a projected field: `{"type": "<kind>", ...attributes}`. `truncate`, `extract` and `customize` apply to dimensions, and truncate and extract need a date or date_time field; `window`, `contribute` and `temporalize` apply to measures. Any attribute outside the vocabulary below is rejected (`unknown attribute 'x' on the <type> decorator of <field>`). Without an explicit alias a decorator lends one, such as `Month(Date)`, `Running Sum(Web Net Paid)`, `% Web Net Paid of Total` or `LM(Web Net Paid)`.
      required: [type]
      properties:
        type:
          type: string
          description: The decorator kind.
          enum: [truncate, extract, window, contribute, temporalize, customize]
          example: truncate
        grain:
          type: string
          description: Truncate a date to this grain. Required by `truncate`.
          enum: [raw, millisecond, second, minute, hour, day, week, month, quarter, year]
          example: month
        extract:
          type: string
          description: The date part to extract. Required by `extract`.
          enum: [minute, hour, day_of_month, day_of_week, day_of_year, week_of_year, month, quarter, year, day_name, month_name, year_month]
          example: month
        mode:
          type: string
          description: The window shape. Required by `window`.
          enum: [moving, running]
          example: running
        function:
          type: string
          description: The window function. Required by `window`.
          enum: [avg, sum, min, max, count, row_number, rank, dense_rank, percent_rank, cume_dist, ntile, lag, lead, first_value, last_value]
          example: sum
        size:
          type: integer
          description: Rows in a `moving` window, at least 1.
          default: 7
          example: 7
        offset:
          type: integer
          description: Rows back or forward for `lag` and `lead`, at least 1.
          default: 0
          example: 1
        buckets:
          type: integer
          description: Buckets for `ntile`, at least 1.
          default: 4
          example: 4
        order_by:
          type: object
          description: Window ordering, a map of projected dimension uid to `asc` or `desc`. The key `-1` means the first projected date. Keys not among the projected dimensions are dropped.
          example:
            date: asc
        partition_refs:
          type: array
          description: Projected dimension uids to partition a `window` or `contribute` by; `-1` means automatic. Dimensions not in the query are dropped.
          items:
            type: string
            example: category
          example: [category]
        ignore_partition_filters:
          type: boolean
          description: For `window` and `contribute`, compute the window or total before the dimension filters apply.
          default: true
          example: true
        transform:
          type: string
          description: The period comparison. Required by `temporalize`.
          enum: [year_over_year, quarter_over_quarter, month_over_month, week_over_week, day_over_day]
          example: year_over_year
        percent_change:
          type: boolean
          description: For `temporalize`, return the percentage change instead of the prior period's value.
          example: false
        abbreviate:
          type: boolean
          description: Carried for display.
          default: true
          example: true
        custom_sql:
          type: string
          description: For `customize`, SQL around `@expression`, which stands for the field. Must contain `@expression` and no aggregate call.
          example: upper(@expression)
      example:
        type: truncate
        grain: month

    Filter:
      description: |
        The shape of `filters` on a spec or a segment. Either an **array** of [FilterLeaf](#schema-filterleaf) objects, a flat AND in which every entry names a `field` (an `and`/`or` object inside the array is rejected), or a **[FilterNode](#schema-filternode)** object whose first key is `and` or `or` holding an array of leaves and nested nodes. Filters keep their listed order. A filter on a measure is a measure filter and lands in `HAVING`; a filter on a dimension lands in `WHERE`.
      oneOf:
        - type: array
          items:
            $ref: "#/components/schemas/FilterLeaf"
        - $ref: "#/components/schemas/FilterNode"
      example:
        - field: category
          predicate: in_list
          value: men, children
        - field: ws_net_paid
          predicate: greater_than
          value: "100"

    FilterLeaf:
      type: object
      description: |
        One condition on a field. Values are scalars; lists are one comma-separated string (commas inside quotes do not split). Strings compare lower-cased on both sides. Dates take `YYYY-MM-DD` and the usual timestamp forms, or a relative value such as `28d`, `3m`, `1y`. For `top_n`, `value` is N and `top_n_measure` the ranking measure, or `value` is `"N:measure"`; without a measure, rows are counted. Allowed predicates depend on the field type: strings take the like predicates, lists, `keyword` and `top_n`; numbers and dates take the comparisons and `between`; `equals`, `does_not_equal`, `is_null`, `is_not_null` and `custom` apply to any field. `custom` is accepted but not planned (422 `Unimplemented`).
      required: [field, predicate]
      properties:
        field:
          type: string
          description: The field, by uid, name or synonym, with an optional `@d`/`@m` suffix.
          example: date
        predicate:
          type: string
          description: The comparison.
          enum:
            - equals
            - does_not_equal
            - greater_than
            - greater_than_or_equal_to
            - less_than
            - less_than_or_equal_to
            - between
            - in_list
            - exclude_list
            - contains
            - does_not_contain
            - starts_with
            - does_not_start_with
            - ends_with
            - does_not_end_with
            - is_null
            - is_not_null
            - top_n
            - keyword
            - custom
          example: between
        value:
          type: string
          description: The value, a number, a date, or a comma-separated list. Required by every predicate except `is_null` and `is_not_null`. `filter_value` is accepted as an alias.
          example: "2024-01-01"
        value_end:
          type: string
          description: The upper bound of `between`. `filter_value_end` is accepted as an alias.
          example: "2024-12-31"
        field_type:
          type: string
          description: A kind hint when the field reference is ambiguous.
          enum: [dimension, measure]
          example: dimension
        top_n_measure:
          type: string
          description: For `top_n`, the measure to rank by, as a uid or a name.
          example: ws_net_paid
      example:
        field: date
        predicate: between
        value: "2024-01-01"
        value_end: "2024-12-31"

    FilterNode:
      type: object
      description: |
        A logic node: exactly one of `and` or `or`, holding an array of [FilterLeaf](#schema-filterleaf) objects and nested nodes. Any other key is rejected (`a filter node needs a field or an and/or key`).
      properties:
        and:
          type: array
          description: Conditions that must all hold. Each item is a FilterLeaf or a FilterNode.
          items:
            oneOf:
              - $ref: "#/components/schemas/FilterLeaf"
              - $ref: "#/components/schemas/FilterNode"
          example:
            - field: category
              predicate: equals
              value: music
            - field: date
              predicate: greater_than_or_equal_to
              value: "2024-01-01"
        or:
          type: array
          description: Conditions of which at least one must hold. Each item is a FilterLeaf or a FilterNode.
          items:
            oneOf:
              - $ref: "#/components/schemas/FilterLeaf"
              - $ref: "#/components/schemas/FilterNode"
          example:
            - field: category
              predicate: equals
              value: books
      example:
        or:
          - field: category
            predicate: equals
            value: books
          - and:
              - field: category
                predicate: equals
                value: music
              - field: date
                predicate: greater_than_or_equal_to
                value: "2024-01-01"

    Segment:
      type: object
      description: |
        A population: the members identified by `keys` that satisfy `filters` and, through `measures`, exist in those measures' fact tables. It joins the query on the keys. Without `apply_to` it constrains the whole query; with `apply_to` it constrains the named measure projections, matched by alias first, then by measure uid. `keys` is required and the segment needs at least one of `keys`, `measures`, `filters` or `expanding` as a definition. An expanding segment carries a dimension of the other members sharing the key into the query (basket and pair analysis); then `mode` and `apply_to` are not allowed.
      required: [keys]
      properties:
        name:
          type: string
          description: A name for the segment; `Segment N` when absent.
          example: Book Products
        mode:
          type: string
          description: Keep members (`include`, an inner join) or drop them (`exclude`, an anti-join). Not for expanding segments.
          enum: [include, exclude]
          default: include
          example: include
        keys:
          type: array
          description: Dimensions identifying the members, the join grain. At least one.
          items:
            type: string
            example: product_name
          example: [product_name]
        measures:
          type: array
          description: Measures whose fact tables define membership. Listing several means membership through any of them.
          items:
            type: string
            example: net_paid
          example: [net_paid]
        filters:
          description: Conditions the members satisfy, in the same array-or-tree shape as the spec's filters. See [Filter](#schema-filter). A measure filter here becomes a HAVING on the segment.
          oneOf:
            - type: array
              items:
                $ref: "#/components/schemas/FilterLeaf"
            - $ref: "#/components/schemas/FilterNode"
          example:
            - field: category
              predicate: equals
              value: books
        apply_to:
          type: array
          description: Measure projections the segment constrains, by alias or measure uid. Empty means the whole query.
          items:
            type: string
            example: ws_net_paid
          example: [ws_net_paid]
        expanding:
          type: array
          description: Dimensions of the other members sharing the key to carry into the query.
          items:
            $ref: "#/components/schemas/Expanding"
        join:
          type: string
          description: For expanding segments, how the carried rows join.
          enum: [inner, left]
          default: inner
          example: inner
        pair_dedupe:
          type: string
          description: For expanding segments, how self-pairs are handled. `neq` drops a member paired with itself, `lt` keeps one ordering of each pair, `none` keeps everything.
          enum: [neq, lt, none]
          default: neq
          example: neq
      example:
        name: Book Products
        mode: include
        keys: [product_name]
        filters:
          - field: category
            predicate: equals
            value: books
        apply_to: [ws_net_paid]

    Expanding:
      type: object
      description: A dimension an expanding segment carries into the query, taken from the other members that share the segment's key.
      required: [field]
      properties:
        field:
          type: string
          description: The dimension to carry, by uid or name.
          example: product_name
        as:
          type: string
          description: The carried column's name. `"<Field> (same <Key>)"` when absent.
          example: Product Name (same Customer ID)
      example:
        field: product_name
        as: Product Name (same Customer ID)

    SecurityContext:
      type: object
      description: |
        Who the query is for. Row-level security policies on the branch read it: a policy that fires resolves the allowed values of its context dimension from the group names, the group tags, the email or the user tags, then filters rows to them or masks the field outside them. A fired policy that resolves nothing denies unless it says `unresolved: allow`. The bypass flags skip policies that allow a bypass. Every key is optional; unknown keys are rejected. A branch with policies refuses to plan without a context (400 `ContextRequired`). Nothing from the context is stored.
      properties:
        email:
          type: string
          description: The user's email, read by policies that resolve permissions from the email.
          example: tank@matrix.com
        system_admin:
          type: boolean
          description: Bypasses policies that let system admins through.
          default: false
          example: false
        project_admin:
          type: boolean
          description: Bypasses policies that let project admins through.
          default: false
          example: false
        tags:
          type: array
          description: The user's own tags as `key:value`, read by user-scoped tag policies.
          items:
            type: string
            example: region:west
          example: []
        groups:
          type: array
          description: The groups the user belongs to, read by group-scoped policies.
          items:
            $ref: "#/components/schemas/Group"
      example:
        email: tank@matrix.com
        system_admin: false
        project_admin: false
        tags: []
        groups:
          - name: CC-TMNT
            tags: ["call_center_id:TMNT"]
          - name: CC-AVCL
            tags: ["call_center_id:AVCL", "cat:men"]

    Group:
      type: object
      description: A group in a security context. A policy resolving from group names reads `name`; one resolving from group tags reads the values of `tags` whose key matches its `tag_key`.
      properties:
        name:
          type: string
          description: The group name.
          example: CC-TMNT
        tags:
          type: array
          description: Tags as `key:value`.
          items:
            type: string
            example: "call_center_id:TMNT"
          example: ["call_center_id:TMNT"]
      example:
        name: CC-TMNT
        tags: ["call_center_id:TMNT"]

    SqlRequest:
      type: object
      description: |
        The body of `/sql` and `/explain`. Give a `spec`, or an `expr` in the shorthand (`category, month(date), ws_net_paid desc, category in (Books, Music)`); the spec wins when both are present and neither is 400 `Invalid`. A shorthand that does not parse is 400 `Shorthand`. Add a `context` whenever the branch has security policies.
      properties:
        spec:
          $ref: "#/components/schemas/Spec"
        expr:
          type: string
          description: A shorthand line, parsed on the server into a spec.
          example: category, month(date), ws_net_paid desc, category in (men, children)
        context:
          $ref: "#/components/schemas/SecurityContext"
      example:
        spec:
          name: Query One
          projections:
            - field: ws_net_paid
              alias: Web Net Paid
            - field: ws_sold_date
              alias: Web Sold Date
            - field: net_paid
              alias: Net Paid
          filters:
            - field: category
              predicate: in_list
              value: men ,children, "books,com"
        context:
          email: tank@matrix.com
          system_admin: false
          project_admin: false
          tags: []
          groups:
            - name: CC-TMNT
              tags: ["call_center_id:TMNT"]

    SqlResponse:
      type: object
      description: What a query needs to run, the statement and the datasource it is for. `corrections` lists references that resolved by near match and is left out when empty. `spec` and `items` appear only when the request used `expr`.
      required: [sql, datasource, datasource_uid, adapter]
      properties:
        sql:
          type: string
          description: The statement, in the datasource's dialect.
          example: |-
            SELECT
            	sum(T0."ws_net_paid") AS "Web Net Paid"
            FROM
            	web_sales T0
            	JOIN item T1
            		ON T0.ws_item_sk = T1.i_item_sk
            WHERE
            	LOWER(T1."i_category") LIKE 'super%'
        datasource:
          type: string
          description: The datasource name from `datasources.yml`.
          example: Warehouse
        datasource_uid:
          type: string
          description: The datasource key from `datasources.yml`.
          example: warehouse
        adapter:
          type: string
          description: The warehouse adapter the statement is written for.
          example: postgres
        corrections:
          type: array
          description: Field references that resolved by near match, present only when there are any.
          items:
            $ref: "#/components/schemas/Correction"
        spec:
          description: The spec the shorthand produced, only for an `expr` request.
          allOf:
            - $ref: "#/components/schemas/Spec"
        items:
          type: array
          description: One line per shorthand item saying how it was read, only for an `expr` request.
          items:
            type: string
            example: "projection   category"
          example: ["projection   category", "filter       category equals Books"]

    Correction:
      type: object
      description: A field reference that did not match exactly and was resolved to the nearest field by trigram similarity. The score is the overlap coefficient, rounded to two places; a correction is taken when it scores at least 0.5 and leads the runner-up by 0.2.
      required: [term, field_uid, field_name, score]
      properties:
        term:
          type: string
          description: The reference as written.
          example: web net payd
        field_uid:
          type: string
          description: The field it resolved to.
          example: ws_net_paid
        field_name:
          type: string
          description: That field's name.
          example: Web Net Paid
        score:
          type: number
          description: The similarity, 0 to 1.
          example: 0.83

    ExplainResponse:
      description: Everything `SqlResponse` carries, plus how long the server took, each planning phase's share, and the node graph the planner shaped. Nodes are in post order; the last is the root.
      allOf:
        - $ref: "#/components/schemas/SqlResponse"
        - type: object
          required: [timings, phases, nodes]
          properties:
            timings:
              $ref: "#/components/schemas/Timings"
            phases:
              type: array
              description: The planning phases in order, with the microseconds each took. `security` appears only when a context was given.
              items:
                $ref: "#/components/schemas/Phase"
            nodes:
              type: array
              description: The plan's nodes in post order; the last is the root.
              items:
                $ref: "#/components/schemas/ExplainNode"

    Timings:
      type: object
      description: Microseconds the server spent parsing and resolving the spec, planning it, and both together.
      required: [parser_us, plan_us, total_us]
      properties:
        parser_us:
          type: integer
          description: Parsing and resolving the spec.
          example: 41
        plan_us:
          type: integer
          description: Planning the resolved query.
          example: 187
        total_us:
          type: integer
          description: Both together.
          example: 228

    Phase:
      type: object
      description: One planning phase and the microseconds it took.
      required: [name, us]
      properties:
        name:
          type: string
          description: The phase.
          enum: [resolve, segment_fork, complex_measure, exclusion, inclusion, snapshot, contribution, temporal, top_n, segment, security, reference, alias, sql, final_query]
          example: resolve
        us:
          type: integer
          description: Microseconds spent.
          example: 23

    ExplainNode:
      type: object
      description: One node of the plan. A `query` node reads a table, an `aggregation` node groups one, and a `merge` node joins the outputs of its inputs. Fields that do not apply to a node are left out.
      required: [id, alias, kind, root, segment, projections, filters, inputs, group_by, identity]
      properties:
        id:
          type: integer
          description: The node's index.
          example: 0
        alias:
          type: string
          description: The CTE name in the SQL.
          example: ag5c9bb8d550197b3a219ae50f0b427905
        kind:
          type: string
          description: The node kind.
          enum: [query, aggregation, merge]
          example: aggregation
        root:
          type: boolean
          description: Whether this node produces the final SELECT.
          example: true
        table:
          type: string
          description: The table the node reads, for query and aggregation nodes.
          example: web_sales
        datasource:
          type: string
          description: The datasource the table lives in.
          example: warehouse
        paths:
          type: array
          description: Join paths the node walks from its table.
          items:
            type: string
            example: web_sales > item
          example: [web_sales > item]
        purpose:
          type: string
          description: Why the node exists when it serves another, such as a top-n ranking or a contribution total.
          example: top_n
        transform:
          type: string
          description: The temporal transform a comparison node serves.
          example: month_over_month
        segment:
          type: boolean
          description: Whether the node was imported from a segment's plan.
          example: false
        projections:
          type: array
          description: What the node projects.
          items:
            type: string
            example: Category
          example: [Category, Web Net Paid]
        filters:
          type: array
          description: The filters the node applies.
          items:
            type: string
            example: category in_list men, children
          example: ["category in_list men, children"]
        inputs:
          type: array
          description: Ids of the nodes read through the FROM clause.
          items:
            type: integer
            example: 0
          example: []
        strategies:
          type: array
          description: Inputs attached by a strategy, such as a segment, an exclusion or an expansion.
          items:
            $ref: "#/components/schemas/Strategy"
        join_type:
          type: string
          description: How a merge node joins its inputs.
          example: full
        group_by:
          type: boolean
          description: Whether the node groups.
          example: true
        security_filters:
          type: array
          description: Row filters a security policy added.
          items:
            type: string
            example: "call_center_id in tmnt, avcl"
          example: []
        security_masks:
          type: array
          description: Fields a security policy masks.
          items:
            type: string
            example: country
          example: []
        identity:
          type: string
          description: The string the alias hashes from.
          example: web_sales|category|ws_net_paid

    Strategy:
      type: object
      description: A node attached to another by a planning strategy rather than through the FROM clause.
      required: [node, kind]
      properties:
        node:
          type: integer
          description: The attached node's id.
          example: 1
        kind:
          type: string
          description: The strategy, such as a segment join, an exclusion or an expansion.
          example: segment

    ExploreRequest:
      type: object
      description: The body of `/explore`. The same `spec` or `expr` as `/sql`, with an optional search term `q`. A `context` is accepted and ignored.
      properties:
        spec:
          $ref: "#/components/schemas/Spec"
        expr:
          type: string
          description: A shorthand line in place of `spec`.
          example: category, ws_net_paid
        context:
          $ref: "#/components/schemas/SecurityContext"
        q:
          type: string
          description: Keep fields containing this term or near it by trigram similarity, best first.
          example: paid
      example:
        spec:
          projections:
            - field: category
            - field: ws_net_paid
        q: paid

    ExploreResponse:
      type: object
      description: The dimensions and measures that can join the query from where it stands, hidden fields excluded. With `q`, each carries a `score` and the lists are ordered best first.
      required: [dimensions, measures, total_us]
      properties:
        dimensions:
          type: array
          description: Dimensions the query can add.
          items:
            $ref: "#/components/schemas/ExploreField"
        measures:
          type: array
          description: Measures the query can add.
          items:
            $ref: "#/components/schemas/ExploreField"
        total_us:
          type: integer
          description: Microseconds the server spent.
          example: 312

    ExploreField:
      type: object
      description: A field `/explore` offers.
      required: [uid, name, data_type, tables]
      properties:
        uid:
          type: string
          example: net_paid
        name:
          type: string
          example: Net Paid
        data_type:
          type: string
          enum: [string, integer, bigint, decimal, date, date_time, boolean]
          example: decimal
        description:
          type: string
          example: Net amount paid on store sales
        tables:
          type: array
          description: Uids of the tables that hold the field.
          items:
            type: string
            example: store_sales
          example: [store_sales]
        score:
          type: number
          description: With `q`, 1 for a containing match or the trigram similarity from 0.5 up.
          example: 1

    FieldsResponse:
      type: object
      description: The fields of a branch, best match first when searching.
      required: [fields]
      properties:
        fields:
          type: array
          items:
            $ref: "#/components/schemas/Field"

    Field:
      type: object
      description: A dimension or measure as deployed.
      required: [uid, name, kind, data_type, synonyms, tags, hidden, tables]
      properties:
        uid:
          type: string
          example: ws_net_paid
        name:
          type: string
          example: Web Net Paid
        kind:
          type: string
          enum: [dimension, measure]
          example: measure
        data_type:
          type: string
          enum: [string, integer, bigint, decimal, date, date_time, boolean]
          example: decimal
        description:
          type: string
          example: Net amount paid on web orders
        synonyms:
          type: array
          description: Other names the field answers to in specs.
          items:
            type: string
            example: web revenue
          example: [web revenue]
        tags:
          type: array
          description: Tags, which security policies trigger on.
          items:
            type: string
            example: pii
          example: []
        hidden:
          type: boolean
          description: Hidden fields are left out of listings but still resolve in specs.
          example: false
        tables:
          type: array
          description: Uids of the tables that hold the field.
          items:
            type: string
            example: web_sales
          example: [web_sales]
        score:
          type: number
          description: With `q`, 1 for a containing match or the trigram similarity.
          example: 1

    TablesResponse:
      type: object
      description: The tables of a branch.
      required: [tables]
      properties:
        tables:
          type: array
          items:
            $ref: "#/components/schemas/Table"

    Table:
      type: object
      description: A table as deployed. The planner prefers the cheapest table that answers a question.
      required: [uid, name, physical_name, cost, datasource, fields]
      properties:
        uid:
          type: string
          example: web_sales
        name:
          type: string
          example: Web Sales
        physical_name:
          type: string
          description: The name in the warehouse.
          example: web_sales
        cost:
          type: integer
          description: The relative cost from the table file.
          example: 100
        datasource:
          type: string
          description: The datasource key.
          example: warehouse
        fields:
          type: array
          description: Uids of the fields the table holds.
          items:
            type: string
            example: ws_net_paid
          example: [ws_net_paid, ws_sold_date, category]

    DeploymentSummary:
      type: object
      description: What is deployed on a branch and when. Counts come from the compiled snapshot.
      required: [project, branch, deployed_at, datasources, tables, fields, joins, paths, policies, values, tests, warnings]
      properties:
        project:
          type: string
          description: The project uid.
          example: tpcds
        branch:
          type: string
          example: main
        deployed_at:
          type: string
          description: UTC to the minute.
          example: "2026-10-01 15:04"
        datasources:
          type: integer
          example: 1
        tables:
          type: integer
          example: 6
        fields:
          type: integer
          example: 42
        joins:
          type: integer
          example: 5
        paths:
          type: integer
          description: Join paths the universe formed.
          example: 9
        policies:
          type: integer
          description: Security policies.
          example: 1
        tests:
          type: integer
          example: 3
        warnings:
          type: array
          description: Non-fatal findings from the last deploy.
          items:
            type: string
            example: "models/core/tbl.store.yml: field 'Store Name' has no description"
          example: []

    DeployResponse:
      description: The answer to deploy and validate, the deployment summary plus the test run. Deploy answers 200 even when tests fail; check `test_results.failed`.
      allOf:
        - $ref: "#/components/schemas/DeploymentSummary"
        - type: object
          required: [test_results, validated_only]
          properties:
            test_results:
              $ref: "#/components/schemas/TestResults"
            validated_only:
              type: boolean
              description: True from `/validate`, where nothing was stored.
              example: false

    TestResults:
      type: object
      description: The outcome of running the branch's `tests/*.yml`.
      required: [passed, failed, results]
      properties:
        passed:
          type: integer
          example: 3
        failed:
          type: integer
          description: Failed and errored tests together.
          example: 0
        results:
          type: array
          items:
            $ref: "#/components/schemas/TestResult"

    TestResult:
      type: object
      description: One test. On a failed `assert_sql` the expected and generated statements are included; on a failed `assert_regex` the pattern and the generated statement. An `error` is a test that could not plan.
      required: [name, status, file]
      properties:
        name:
          type: string
          example: Category revenue
        status:
          type: string
          enum: [passed, failed, error]
          example: passed
        file:
          type: string
          description: The test file's basename.
          example: category_revenue.yml
        message:
          type: string
          description: Why the test failed or errored, when it did.
          example: generated SQL did not match
        expected_sql:
          type: string
          description: The `assert_sql` the test expected, when it failed.
          example: SELECT T1."i_category" AS "Category", sum(T0."ws_net_paid") AS "Web Net Paid" FROM web_sales T0 JOIN item T1 ON T0.ws_item_sk = T1.i_item_sk GROUP BY T1."i_category"
        generated_sql:
          type: string
          description: The statement the planner produced, when the test failed.
          example: SELECT T1."i_category" AS "Category", sum(T0."ws_net_paid") AS "Web Net Paid" FROM web_sales T0 JOIN item T1 ON T0.ws_item_sk = T1.i_item_sk GROUP BY T1."i_category"
        pattern:
          type: string
          description: The `assert_regex` the test expected, when it failed.
          example: SELECT

    ProjectsResponse:
      type: object
      description: Every deployment the caller can read, and the projects with the caller's access level.
      required: [deployments, projects]
      properties:
        deployments:
          type: array
          description: One summary per project and branch.
          items:
            $ref: "#/components/schemas/DeploymentSummary"
        projects:
          type: array
          description: The projects the caller can see.
          items:
            $ref: "#/components/schemas/ProjectSummary"

    ProjectSummary:
      type: object
      description: A project as the caller sees it, with their access level.
      required: [uid, name, production_branch, visibility, protected_production, level]
      properties:
        uid:
          type: string
          example: tpcds
        name:
          type: string
          example: TPC-DS
        production_branch:
          type: string
          example: main
        visibility:
          type: string
          description: "`account` gives every developer read access; `restricted` needs an explicit grant."
          enum: [account, restricted]
          example: account
        protected_production:
          type: boolean
          description: Whether deploying to the production branch needs an owner.
          example: true
        level:
          type: string
          description: The caller's access.
          enum: [read, write, owner]
          example: owner

    Account:
      type: object
      description: An account, the tenant that owns users, projects and query keys.
      required: [id, uid, name]
      properties:
        id:
          type: integer
          example: 1
        uid:
          type: string
          example: acme
        name:
          type: string
          example: Acme

    User:
      type: object
      description: A user of an account.
      required: [id, email, name]
      properties:
        id:
          type: integer
          example: 1
        email:
          type: string
          example: ajo@example.com
        name:
          type: string
          example: Ajo

    UserKey:
      type: object
      description: A personal key record. The secret (`zsk_` plus 40 characters) is shown once at creation; the record keeps its 12-character prefix. A personal key does whatever its user may do.
      required: [id, user_id, name, prefix, created_at]
      properties:
        id:
          type: integer
          example: 7
        user_id:
          type: integer
          example: 1
        name:
          type: string
          example: laptop
        prefix:
          type: string
          description: The first 12 characters of the secret.
          example: zsk_3kD9vQm2
        created_at:
          type: string
          example: "2026-09-30T08:12:44Z"
        last_used_at:
          type: string
          description: Updated at most once a minute; null until first use.
          nullable: true
          example: "2026-10-01T15:04:00Z"

    QueryKey:
      type: object
      description: A query key record. The secret (`zqk_…`) is shown once at creation or rotation. Query keys are read-only and answer for the projects and branches their grants name.
      required: [id, account_id, name, prefix, created_by, created_at]
      properties:
        id:
          type: integer
          example: 3
        account_id:
          type: integer
          example: 1
        name:
          type: string
          example: dashboard-service
        prefix:
          type: string
          description: The first 12 characters of the secret.
          example: zqk_9mB2xV7t
        created_by:
          type: integer
          description: The id of the user who created it.
          example: 1
        created_at:
          type: string
          example: "2026-09-30T08:12:44Z"
        last_used_at:
          type: string
          description: Updated at most once a minute; null until first use.
          nullable: true
          example: "2026-10-01T15:04:00Z"

    Grant:
      type: object
      description: A query key's access to a project. `branch` is a branch name, `*` for every branch, or null for the production branch.
      required: [project_id, project_uid, branch]
      properties:
        project_id:
          type: integer
          example: 1
        project_uid:
          type: string
          example: tpcds
        branch:
          type: string
          description: A branch name, `*`, or null for the production branch.
          nullable: true
          example: "*"

    Project:
      type: object
      description: A project. The first deploy of a uid creates it with the caller as owner.
      required: [id, account_id, uid, name, production_branch, visibility, protected_production, created_by]
      properties:
        id:
          type: integer
          example: 1
        account_id:
          type: integer
          example: 1
        uid:
          type: string
          example: tpcds
        name:
          type: string
          example: TPC-DS
        production_branch:
          type: string
          default: main
          example: main
        visibility:
          type: string
          description: "`account` gives every developer read access; `restricted` needs an explicit grant."
          enum: [account, restricted]
          example: account
        protected_production:
          type: boolean
          description: Whether deploying to the production branch needs an owner.
          example: true
        created_by:
          type: integer
          description: The id of the user who created it.
          example: 1

    Member:
      type: object
      description: A user of the account and their role.
      required: [user, role]
      properties:
        user:
          $ref: "#/components/schemas/User"
        role:
          type: string
          enum: [admin, developer]
          example: developer

    AuditEvent:
      type: object
      description: One audit row. Deploys, key and grant changes, membership and project changes each write one.
      required: [actor, action, subject, at]
      properties:
        actor:
          type: string
          description: Who did it, by email or key name.
          example: ajo@example.com
        action:
          type: string
          example: deploy
        subject:
          type: string
          description: What it was done to.
          example: tpcds/main
        at:
          type: string
          example: "2026-10-01T15:04:00Z"

    SignupRequest:
      type: object
      description: Create an account and its first admin.
      required: [account, email]
      properties:
        account:
          type: string
          description: The account name.
          example: Acme
        email:
          type: string
          example: ajo@example.com
        name:
          type: string
          description: The user's name.
          example: Ajo
        password:
          type: string
          description: At least 8 characters.
          example: correct horse battery
      example:
        account: Acme
        email: ajo@example.com
        name: Ajo
        password: correct horse battery

    LoginRequest:
      type: object
      description: Start a console session.
      required: [email, password]
      properties:
        email:
          type: string
          example: ajo@example.com
        password:
          type: string
          example: correct horse battery

    MemberRequest:
      type: object
      description: Add a user to the account.
      required: [email, role]
      properties:
        email:
          type: string
          example: dev@example.com
        name:
          type: string
          example: Dev
        role:
          type: string
          enum: [developer, admin]
          example: developer

    InviteAcceptRequest:
      type: object
      description: Redeem an invite token.
      required: [token]
      properties:
        token:
          type: string
          description: The one-time invite token.
          example: zin_8fKq2LmZ7vXt4RbNw1cYp9sDh3gJe6aUo5iTn0rWqM
        name:
          type: string
          description: The user's name.
          example: Dev
        key_name:
          type: string
          description: A name for the first personal key.
          example: laptop
        password:
          type: string
          description: At least 8 characters.
          example: correct horse battery

    KeyRequest:
      type: object
      description: Create a named key.
      required: [name]
      properties:
        name:
          type: string
          example: dashboard-service

    GrantRequest:
      type: object
      description: Grant a project to a query key.
      required: [project]
      properties:
        project:
          type: string
          description: The project uid.
          example: tpcds
        branch:
          type: string
          description: A branch name, or `*` for every branch. Omit for the production branch.
          example: "*"

    ProjectPatch:
      type: object
      description: Settings to change on a project; every field is optional.
      properties:
        name:
          type: string
          example: TPC-DS
        production_branch:
          type: string
          example: main
        visibility:
          type: string
          enum: [account, restricted]
          example: restricted
        protected_production:
          type: boolean
          example: true

    AccessRequest:
      type: object
      description: Set a member's access level on a project.
      required: [email, level]
      properties:
        email:
          type: string
          example: dev@example.com
        level:
          type: string
          enum: [read, write, owner]
          example: write

    PasswordRequest:
      type: object
      description: Set the caller's password.
      required: [password]
      properties:
        password:
          type: string
          description: At least 8 characters.
          example: correct horse battery
