Skip to content

Request lifecycle

Every query passes the same stages, over REST and over MCP. Each stage can stop the request with an error code.

sequenceDiagram
    autonumber
    participant T as Transport (REST / MCP)
    participant S as queryservice
    participant P as parser
    participant E as policy engine
    participant B as approval broker
    participant R as rewriter
    participant D as DuckDB executor
    participant A as audit dispatcher
    T->>S: query (authenticated subject)
    S->>S: slot, rate limit, budget, input caps
    S->>P: parse (pg_query)
    S->>E: evaluate (or rewrite-cache hit)
    alt deny / reject
        S->>A: decision record
        S-->>T: ACL_DENIED / ACL_REJECTED
    else approval required
        S->>B: consume grant, or park + sync wait
    end
    S->>R: rewrite (filters, masks, LIMIT, sample)
    S->>D: execute (read-only, streaming)
    S->>S: attach post-query masks
    S->>A: access record (fail-closed gate)
    S-->>T: stream rows
    S->>A: completion record + budget usage (at close)

Diagram numbers count messages, not stages.

Stages

  1. Transport limits and login. The REST API and MCP over HTTP check their rate limits. Sluice maps the API key or token to a subject.
  2. Admission. Sluice checks the concurrency limit, the subject's rate limit and budget and the SQL size. It caps the requested timeout and row count.
  3. Parse. Sluice parses the statement with the PostgreSQL grammar and finds its tables.
  4. Statement checks. Sluice checks function calls and table names.
  5. Policy. The engine allows, denies or rejects the query.
  6. Approval. An approval policy can hold the query until a person accepts it.
  7. Rewrite. Sluice refuses writes and DDL. It adds row filters, column masks and limits to the SQL.
  8. Execute. DuckDB runs the rewritten SQL.
  9. Post-query masks. Some mask types change values in the result rows.
  10. Audit, then rows. Sluice queues the audit record, then sends the rows.
  11. Close. Sluice writes a second audit record with the row count and updates the budget.

Example

Goal: see which stage stops a request. One policy allows the group analytics to read shop.main.customers.

apiVersion: sluice.bino.bi/v1alpha1
kind: SqlAccessPolicy
metadata:
  name: allow-customers
spec:
  effect: allow
  match:
    any:
      - subjects:
          groups: ["analytics"]
        resources:
          catalogs: ["shop"]
          schemas: ["main"]
          tables: ["customers"]

No policy allows shop.main.payroll. The policy stage denies the query:

curl -s http://localhost:8080/v1/query \
  -H "X-Api-Key: sl_demo_ana.supersecret" \
  -H "Content-Type: application/json" \
  -d '{"sql": "SELECT name, salary FROM shop.main.payroll"}'
{
  "code": "ACL_DENIED",
  "message": "no SqlAccessPolicy matched (default-deny)",
  "query_id": "<query-id>"
}

A write on the allowed table passes the policy stage. The rewrite stage rejects it:

curl -s http://localhost:8080/v1/query \
  -H "X-Api-Key: sl_demo_ana.supersecret" \
  -H "Content-Type: application/json" \
  -d '{"sql": "DELETE FROM shop.main.customers WHERE id = 1"}'
{
  "code": "ACL_REJECTED",
  "message": "write operations are not permitted",
  "query_id": "<query-id>"
}

A typing error gives a parse error:

curl -s http://localhost:8080/v1/query \
  -H "X-Api-Key: sl_demo_ana.supersecret" \
  -H "Content-Type: application/json" \
  -d '{"sql": "SELEC id, name FROM shop.main.customers"}'
{
  "code": "ERR_SYNTAX",
  "message": "SQL parse error",
  "query_id": "<query-id>"
}

What happened: each request stopped at a different stage. Several stages share the code ACL_REJECTED.

Statements Sluice runs

Statement Result
SELECT Runs
EXPLAIN of a SELECT Runs. ERR_UNSUPPORTED_SYNTAX if a row filter or column mask applies
INSERT, UPDATE, DELETE, also inside a WITH ACL_REJECTED
DDL, COPY, SELECT ... INTO, SET, RESET, SHOW ERR_UNSUPPORTED_SYNTAX
ATTACH ERR_SYNTAX

Policies see no table in SET and SHOW. An allow policy that names a table does not match them, so the answer is ACL_DENIED. The code in the table shows under an allow policy without resources.

Where a request stops

Stage What stops the request Error code
Transport limits limits.globalRps or limits.perIpRps reached ERR_RATE_LIMITED
Login No valid API key or token ERR_UNAUTHORIZED
Admission limits.maxConcurrent or subject rate limit reached ERR_RATE_LIMITED
Daily budget used up ERR_BUDGET_EXCEEDED
SQL over limits.maxSqlBytes ERR_PAYLOAD_TOO_LARGE
Parse Parser cannot read the statement ERR_SYNTAX
Two statements in one request ERR_MULTIPLE_STATEMENTS
Identifier over 63 bytes, nesting too deep ERR_UNSUPPORTED_SYNTAX
Statement checks Function not on the allow list, or written as schema.f(x) or x.f() ACL_REJECTED
Function in FROM other than generate_series, range, unnest ACL_REJECTED
TABLESAMPLE ACL_REJECTED
Table name with /, \, :, *, ? or [ ACL_REJECTED
Table written as catalog.table ACL_REJECTED
Second catalog with limits.disableCrossCatalog ACL_REJECTED
Parameter such as $1 ERR_UNSUPPORTED_SYNTAX
Policy No allow, or a deny policy ACL_DENIED or the policy's code
Reject rule ACL_REJECTED or the rule's code
Engine error ERR_POLICY_UNAVAILABLE, ERR_TIMEOUT
Approval Waiting for a person ERR_APPROVAL_PENDING
Rejected or expired ERR_APPROVAL_REJECTED, ERR_APPROVAL_EXPIRED
Queue full ERR_RATE_LIMITED
ApprovalPolicy added by a reload, approval.publicBaseUrl not set ERR_INTERNAL
Rewrite Write statement ACL_REJECTED
DDL, COPY, SET, RESET, SHOW ERR_UNSUPPORTED_SYNTAX
Form refused on a filtered or masked table ERR_UNSUPPORTED_SYNTAX, ERR_SYNTAX, ERR_MASK_UNSUPPORTED_CONTEXT
Masked table missing in the schema cache ERR_REWRITE_FAILED
Schema cannot be loaded ERR_DATASOURCE_UNAVAILABLE
Template value the subject lacks ACL_DENIED
Statement too large for the DuckDB parse check ERR_PAYLOAD_TOO_LARGE
Rewrite or execute Timeout, client gone ERR_TIMEOUT, ERR_CANCELED
Execute DuckDB refuses the statement ERR_QUERY_FAILED
DuckDB cannot parse a statement that Sluice did not rewrite ERR_SYNTAX
Data source not reachable ERR_DATASOURCE_UNAVAILABLE
Other error ERR_INTERNAL
Post-query masks Result columns do not match, mask cannot be built ERR_MASK_UNSUPPORTED_CONTEXT, ERR_INTERNAL
Audit Audit queue full ERR_AUDIT_UNAVAILABLE

Limits

  • Parse and rewrite errors show only if the policies allow the statement. A deny or reject comes first.
  • ERR_SYNTAX also covers a table alias that is a DuckDB keyword and an unquoted identifier with a non-letter Unicode character.
  • A request stopped by the transport limits or the login leaves no audit record.
  • With cache.rewrite.enabled, a cached rewrite keeps the old column list of a masked table until cache.rewrite.ttl ends or a reload.

See also