Skip to main content
Arrow Flight SQL is the protocol Parable’s SQL endpoint speaks. Parable authenticates and authorizes each gRPC call, then streams the Arrow results back unchanged. To connect DBeaver or Python step by step, start with Query Provider Pools with SQL. For language clients, see Query with SDKs. Use this page when you configure an ADBC, JDBC, or custom Flight SQL client.

Connection contract

x-tenant-slug is accepted as an alias for x-tenant. Send the authorization metadata on every call. The Flight Handshake RPC is accepted and does nothing, so a client cannot authenticate through it.

Pools each role reaches

A query that names a pool the caller’s role does not reach fails with INVALID_ARGUMENT, the message FROM or JOIN names a source this Workspace cannot read. Choose an available source, or ask a Workspace Administrator to check your data access., and error code QL-PL-002. Catalog discovery leaves that pool out. Inside the pools a role reaches, the Workspace’s Provider Policies decide which rows of each Provider table it reads. Table metadata leaves out the row count and validation rates of a table they narrow for the caller. A caller whose Workspace roles hold All data access (tenant.data.unrestricted), as Workspace Administrator does, reads every row of every Provider table, and each such call is recorded in the Workspace’s audit events.

Tokens

A SQL tool authenticates with a pat_ token (API tokens). A SQL token a member creates in Protection → Access Tokens reaches that Workspace only and queries with its owner’s Workspace roles at the time of each query; a staff role never counts. A Parable token queries any Workspace its owner can, with the owner’s roles, staff roles included, at the time of each query. Flight SQL refuses a call with:

Connect with ADBC

Set PARABLE_FLIGHT_URL to grpc+tls://sql.parable.work:443. The public endpoints use TLS.

Request lifecycle

For a statement query:
  1. GetFlightInfo validates and plans the SQL, then returns an opaque ticket.
  2. DoGet executes from that ticket and streams Arrow record batches.
  3. The client releases the result stream when it has consumed or abandoned it.
Prepared statements follow the same read path with a server-issued handle. See supported operations for the complete command matrix.

Limits

A query may run for up to one hour. The clock starts when Parable admits the query and covers planning and the whole result stream, including the time your client spends reading it. A query that runs longer fails with DEADLINE_EXCEEDED, Parable error code QL-TO-002 (QUERY_TIMEOUT), and the message query <id> exceeded timeout of 3600s. The server sets this limit. A longer deadline on your client does not extend it, and a shorter one ends the call early, so set your driver’s timeout to at least an hour before running a long query.

Row limit

A query may return up to 2,000,000 rows. DoGet streams them batch by batch, without collecting the result first. A query whose result has more rows fails. Nothing is truncated: the stream ends with RESOURCE_EXHAUSTED, Parable error code QL-SQ-005 (RESULT_ROW_CAP_EXCEEDED), and this message:
The error can arrive after some record batches. Your driver raises it when you read past them, so discard the rows it returned before the error: they are not the result. To stay under the limit, add LIMIT, filter with WHERE, or aggregate in SQL. To read more rows than the limit, run one query per range of a column, such as one month at a time.

AI call limits

ponder(), embed() and the Jev functions (ask, noul, choice and score) send paid calls to an AI Provider. By default, one query may send up to 1,000,000 of these calls and spend up to $1,000 on them. What counts as one call:
  • ponder() and a Jev function send one call for each row.
  • embed() sends up to 32 rows in one call.
  • A Jev call that TypeSafe or a decision model throttles and Parable sends again counts again. A decision model answers one Jev call with one chat request for each question, and the call counts once. The cost limit counts each question that still has no answer.
Parable checks the limits before each call. When the next call would pass a limit, Parable does not send it, and the stream ends with RESOURCE_EXHAUSTED and one of these error codes:
  • QL-SQ-010 (AI_CALL_LIMIT_EXCEEDED): the query would send more calls than the limit.
  • QL-SQ-011 (AI_COST_LIMIT_EXCEEDED): the query’s calls would cost more than the limit. Parable knows the cost of a call only after the call answers, so the calls already sent can pass the limit by a small amount.
A call to a model that Parable has no price for yet is refused, because the cost limit could not count it. The stream ends with FAILED_PRECONDITION and QL-SQ-012 (AI_MODEL_UNPRICED). The message names the function, the model and its processor. Most refusals come before any call. On OpenRouter, and for a Jev alias, the price shows in the answer, so the call is refused after that answer and no further call is sent. Do not retry: name a model that has a price. The message names the limit and the function. The error can arrive after some record batches: discard them, because they are not the result. Do not retry the same query. Call the function over fewer rows: filter with WHERE, sample or LIMIT the rows before the function, or aggregate fewer rows. To call a function over every row of a large table, publish the SQL as a Plot. A Refresh keeps each answer and runs under its own limits. Each IP address may send 600 calls a minute. A query takes at least two calls, GetFlightInfo and DoGet. Calls over the limit are refused with HTTP status 429 at Parable’s edge, which gRPC clients report as UNAVAILABLE.

Metadata discovery

Flight SQL supports catalog, schema, table, table-type, SQL capability, and logical key discovery. GetTables can include an Arrow IPC schema for every matching table, including optional Parable semantic metadata. Discover tables and columns shows how to use those operations.

Semantic types in comparisons

A column can carry a semantic scalar, such as Contact.Email or Contact.PhoneNumber, in its parable.scalar.canonical_name field metadata. Parable uses it when a query plans:
  • Two columns with different scalars can be compared only when scalar-lib declares them comparable, such as Temporal.Date and Temporal.DateTime. contacts.email = people.phone fails with INVALID_ARGUMENT and error code QL-PL-007 (INCOMPARABLE_SCALARS). The message names both columns and both scalars. The check covers comparison operators, BETWEEN, IN, subquery comparisons, join keys, and UNION, INTERSECT and EXCEPT columns.
  • To compare across scalars on purpose, cast one side where it is compared: CAST(contacts.email AS VARCHAR) = people.phone. A column without a scalar compares with any column.
  • A value you supply is never refused and needs no cast. A string literal or bound parameter compared with an Identity.UUID or Contact.Email column is converted to that scalar’s canonical form, so email = 'Jane@Example.com' matches the stored jane@example.com. A value that is not valid for the scalar is compared as written: email = '' returns no rows, and email <> '' returns every row with an email.

Embedding vectors in comparisons

embed() stamps its model identity (provider, name, variant, version, dimensions, normalization) in its output field’s parable.model_* metadata. Vectors are comparable only within one identity, so array_distance over two vectors stamped with different identities fails with INVALID_ARGUMENT and error code QL-PL-008 (INCOMPARABLE_EMBEDDING_MODELS), even when their dimensions match. The message names the function and both models. A Plot’s embed() column carries the identity of the model that wrote it. A vector without an identity, such as a literal vector, is compared on its dimensions alone; see Embed.

Recursive queries

WITH RECURSIVE walks a hierarchy, such as everyone below each manager at any depth. Join its two terms with UNION: it stops when an iteration adds no new row, so a loop in the data, such as a person listed as their own manager, ends. UNION ALL fails with INVALID_ARGUMENT and error code QL-PL-014 (RECURSIVE_UNION_ALL), because nothing else would stop it.
A recursive query works the same way in SQL over Flight, in a Plot, in a Pipeline and in SQL over a Parable’s Plots. A large hierarchy is cheaper as a Pipeline: it is computed once per Refresh, and readers read its stored rows.

Errors and retries

Unsupported commands return gRPC UNIMPLEMENTED. Invalid authentication and Workspace selection return gRPC authentication, permission, or argument statuses rather than HTTP JSON errors. A Parable error code, such as QL-PL-002, arrives in the parable-error-code response metadata beside the gRPC status. Facts a refusal names for you, when it names any, arrive as a JSON object of strings in the binary parable-error-details-bin metadata. When query concurrency is saturated, Parable returns RESOURCE_EXHAUSTED with error code QL-IN-008. A client can retry a read-only query with bounded exponential backoff. Do not retry invalid SQL, authentication failures, or permission failures without changing the request. QL-PL-007 (INCOMPARABLE_SCALARS) is deterministic: change the comparison or add a CAST rather than retrying. QL-PL-008 (INCOMPARABLE_EMBEDDING_MODELS) is too: embed both sides with the same model. So is QL-PL-015 (FILTER_POLICY_COLUMN_MISSING, FAILED_PRECONDITION): a Plot that reads Provider data lacks a column a Parable filter Policy that binds you reads, usually identity_account_id; the Plot’s author adds the column. QL-TN-009 (SOURCE_PERMISSIONS_UNAVAILABLE) arrives with FAILED_PRECONDITION. Source permissions decide which rows of a table you read, and Parable can’t tell who may open its rows. Nothing is read: Parable never returns every row instead. The message says why, and the table detail names the table. When the message says that the access projection has not been built for its Provider, Parable has not read that Provider’s sharing yet. Parable reads a Provider’s sharing after each sync of it, so run the query again after the Provider’s next sync. SQL the query engine cannot run is the query’s to fix. It returns INVALID_ARGUMENT with one of these codes, unless a row names another status. The message names at most the columns, functions and types involved, never the rest of the SQL or its values. Change the query rather than retrying: GetFlightInfo and GetSchema run the engine’s type checks on the statement they plan, so QL-PL-019 arrives there, and a DoGet of the same statement answers it too. A prepared statement is checked at the GetFlightInfo after its parameters are bound, not at CreatePreparedStatement. Casts and some unsupported expressions are checked when the result is read, so QL-PL-009 and QL-PL-012 can arrive from DoGet after GetFlightInfo succeeded. Other engine failures keep INTERNAL with QL-IN-001 (DATAFUSION_ERROR) and the message Query execution failed. That includes an error raised by a row while the query runs, such as a string value that does not cast to a number, because its description carries the value. A Jev argument read from a column, rather than written in the query, is checked on each row, so a row it refuses fails this way under the default on_error = fail, unless its request is too large to send (QL-SQ-006, below). A row a Plot returns that its declared contract does not allow, read through parables.*, fails with FAILED_PRECONDITION and error code PA-AU-047. The message names the Plot, the column that holds the value and what is wrong with it, never the value. A Jev function’s request (ask, noul, choice or score) that gets no answer fails the query with the limit it hit. The request goes to TypeSafe, or, when the Workspace’s AI policy resolves the call to one, to a decision model that Google Vertex AI serves. A Workspace that switches TypeSafe off runs its Jev calls on the decision model. The message names the function, the service, and the limit or the service’s HTTP status, never the state or the questions. They can arrive from DoGet, after GetFlightInfo succeeded: Under on_error = null, a row whose request fails with QL-SQ-006 or QL-SQ-007 reads NULL instead, as a row with an invalid argument or an unusable answer does. That failure belongs to the row. A QL-TO-003 or QL-IN-010 failure belongs to the endpoint, so it always fails the query, including under on_error = null. When a workspace table is briefly unavailable, Parable returns UNAVAILABLE with error code QL-IN-005 (UPSTREAM_UNAVAILABLE), and the message names the table. Retry the query with the same backoff. Five error codes arrive with RESOURCE_EXHAUSTED, and each one needs a different response. Read the error code before you retry: RESOURCE_EXHAUSTED can also arrive with no error code, for example when a workspace table returns more rows than one query may read: do not retry it, because the message says why the query was refused. Like QL-SQ-005, QL-IN-009, QL-SQ-010 and QL-SQ-011 can arrive after some record batches. Discard the rows your driver returned before the error: they are not the result. QL-TO-002 (QUERY_TIMEOUT) is a query past the time limit. Filter, aggregate, or split it before running it again. QL-TO-004 (ROW_SEARCH_REREAD_TIMEOUT) arrives with DEADLINE_EXCEEDED when search_everything could not read its matching records again from any source table in time. The message names the time limit. Run the search again. If it fails again, narrow its scope to fewer tables.