openapi: 3.1.0
info:
  title: sqlai.dev SQL verifier
  version: "1.0.0"
  description: |
    Executes SQL against a real ephemeral in-memory database (SQLite, WASM) and
    returns ground truth: rows, typed errors, query plans, or result diffs.
    Fresh sandbox per call; submitted SQL and data are never stored.

    Payment: x402. 20 free calls/day per client, then HTTP 402 with
    payment requirements; retry with an X-PAYMENT header. Same tools are also
    exposed over MCP (Streamable HTTP) at https://mcp.sqlai.dev/mcp.
  license:
    name: MIT
    identifier: MIT
security: []
servers:
  - url: https://mcp.sqlai.dev
paths:
  /api/run_sql:
    post:
      operationId: runSql
      summary: Execute one SQL statement against a fresh in-memory database
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [schema, query]
              properties:
                schema: { type: string, description: "DDL statements (multiple allowed)" }
                query: { type: string, description: "ONE SQL statement (stacked statements rejected)" }
                seed: { $ref: "#/components/schemas/Seed" }
                engine: { $ref: "#/components/schemas/Engine" }
      responses:
        "200":
          description: Execution outcome (ok:true with rows, or ok:false with a typed error — both are successful verifications)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RunResult" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
  /api/validate_sql:
    post:
      operationId: validateSql
      summary: Parse and bind a query against a schema without executing it
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [schema, query]
              properties:
                schema: { type: string }
                query: { type: string }
                engine: { $ref: "#/components/schemas/Engine" }
      responses:
        "200":
          description: ok with referenced tables, or typed error
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  meta: { $ref: "#/components/schemas/Meta" }
                  referenced_tables: { type: array, items: { type: string } }
                  missing_tables: { type: array, items: { type: string } }
                  error: { $ref: "#/components/schemas/SqlError" }
                  dialect_notes: { $ref: "#/components/schemas/DialectNotes" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
  /api/explain_plan:
    post:
      operationId: explainPlan
      summary: Engine-native query plan plus full-scan warnings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [schema, query]
              properties:
                schema: { type: string }
                query: { type: string }
                engine: { $ref: "#/components/schemas/Engine" }
      responses:
        "200":
          description: plan lines or typed error
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  meta: { $ref: "#/components/schemas/Meta" }
                  plan: { type: array, items: { type: string } }
                  full_scan_warnings: { type: array, items: { type: string } }
                  error: { $ref: "#/components/schemas/SqlError" }
                  dialect_notes: { $ref: "#/components/schemas/DialectNotes" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
  /api/run_sql_batch:
    post:
      operationId: runSqlBatch
      summary: Run up to 10 queries, each against its OWN fresh database
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [schema, queries]
              properties:
                schema: { type: string }
                queries: { type: array, maxItems: 10, items: { type: string } }
                seed: { $ref: "#/components/schemas/Seed" }
                engine: { $ref: "#/components/schemas/Engine" }
      responses:
        "200":
          description: array of run_sql results in input order
          content:
            application/json:
              schema:
                type: object
                properties:
                  version: { type: string }
                  results:
                    type: array
                    items: { $ref: "#/components/schemas/RunResult" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
  /api/diff_results:
    post:
      operationId: diffResults
      summary: Execute two queries on identical data and diff the result sets
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [schema, query_a, query_b]
              properties:
                schema: { type: string }
                seed: { $ref: "#/components/schemas/Seed" }
                query_a: { type: string }
                query_b: { type: string }
                engine: { $ref: "#/components/schemas/Engine" }
      responses:
        "200":
          description: equality verdict with row-level diff, or typed error naming the failing query
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  meta: { $ref: "#/components/schemas/Meta" }
                  equal: { type: boolean, description: "same columns AND same row multiset (order-insensitive)" }
                  order_differs: { type: boolean }
                  row_count_a: { type: integer }
                  row_count_b: { type: integer }
                  only_in_a: { type: array, items: { type: array } }
                  only_in_b: { type: array, items: { type: array } }
                  diff_truncated: { type: boolean }
                  failed_query: { type: string, enum: [a, b, schema] }
                  error: { $ref: "#/components/schemas/SqlError" }
                  dialect_notes: { $ref: "#/components/schemas/DialectNotes" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
components:
  schemas:
    Engine:
      type: string
      enum: [sqlite, duckdb]
      description: "'sqlite' (default). 'duckdb' is browser-demo only and returns a typed unsupported error server-side."
    Seed:
      type: object
      description: 'Seed rows: {"table_name": [{"column": value}, ...]}. Max 10MB JSON.'
      additionalProperties:
        type: array
        items: { type: object }
    Meta:
      type: object
      properties:
        version: { type: string, description: "response schema version (currently \"1\")" }
        engine: { type: string }
        now: { type: string, format: date-time, description: "call timestamp (time-dependent SQL varies across calls; flagged via dialect_notes)" }
        execution_ms: { type: integer }
    SqlError:
      type: object
      properties:
        type:
          type: string
          enum: [unknown_column, unknown_table, syntax, type_mismatch, constraint, timeout, resource_limit, unsupported]
        message: { type: string }
        position:
          type: [object, "null"]
          properties:
            line: { type: integer }
            col: { type: integer }
        suggestion: { type: [string, "null"], description: "rule-based (edit distance vs schema); never LLM-generated" }
    DialectNotes:
      type: array
      items:
        type: object
        properties:
          rule: { type: string }
          note: { type: string }
    RunResult:
      type: object
      properties:
        ok: { type: boolean }
        meta: { $ref: "#/components/schemas/Meta" }
        columns:
          type: array
          items:
            type: object
            properties:
              name: { type: string }
              type: { type: [string, "null"] }
        rows: { type: array, items: { type: array } }
        row_count: { type: integer, description: "true count before the 500-row return cap" }
        truncated: { type: boolean }
        empty_dataset: { type: boolean, description: "true when no seed rows were provided — zero rows means no data, not query logic" }
        error: { $ref: "#/components/schemas/SqlError" }
        dialect_notes: { $ref: "#/components/schemas/DialectNotes" }
  responses:
    PaymentRequired:
      description: Free tier exhausted — x402 v1 payment requirements
      content:
        application/json:
          schema:
            type: object
            properties:
              x402Version: { type: integer, const: 1 }
              error: { type: string }
              accepts:
                type: array
                items:
                  type: object
                  description: x402 v1 PaymentRequirements (scheme "exact", USDC atomic units)
