Skip to main content

QueryDefinition

Struct QueryDefinition 

Source
pub struct QueryDefinition {
Show 26 fields pub name: String, pub return_type: String, pub returns_list: bool, pub returns_count: bool, pub nullable: bool, pub arguments: Vec<ArgumentDefinition>, pub sql_source: Option<String>, pub function: Option<String>, pub description: Option<String>, pub auto_params: AutoParams, pub deprecation: Option<DeprecationInfo>, pub jsonb_column: String, pub relay: bool, pub relay_cursor_column: Option<String>, pub relay_cursor_type: CursorType, pub pagination_order: Option<PaginationOrder>, pub inject_params: IndexMap<String, InjectedParamSource>, pub read_routing: ReadRouting, pub cache_ttl_seconds: Option<u64>, pub additional_views: Vec<String>, pub requires_role: Option<String>, pub requires_actor: Vec<ActorType>, pub rest_path: Option<String>, pub rest_method: Option<String>, pub rest_stream: bool, pub native_columns: HashMap<String, String>,
}
Expand description

A query definition compiled from @fraiseql.query.

Queries are declarative bindings to database views/tables. They describe what to fetch, not how to fetch it.

§Example

use fraiseql_core::schema::QueryDefinition;

let query = QueryDefinition::new("users", "User");

Fields§

§name: String

Query name (e.g., “users”).

§return_type: String

Return type name (e.g., “User”).

§returns_list: bool

Does this query return a list?

§returns_count: bool

Does this query return COUNT(*) over sql_source instead of rows?

Compiled as the sibling of a list query that opted in (count = true), named <query>Count and exposed as Int!. It exists because a bare [T] list has nowhere to hang a total, so an offset-paginated client had no way to learn how many rows match its filter — totalCount was reachable only through a Relay connection, and a Relay connection is keyset-only, so obtaining the count cost random access (#938).

return_type stays the entity type rather than Int: the filter machinery is keyed on it (where input types, native column casts, RLS policy lookup), and re-pointing it at a scalar would silently detach the count from the filters whose rows it is counting. Only the rendered GraphQL return type differs.

The count reflects the full filtered set, independent of limit/offset — which is the entire point — so the sibling carries where alone and no pagination arguments.

§nullable: bool

Is the return value nullable?

§arguments: Vec<ArgumentDefinition>

Query arguments.

§sql_source: Option<String>

SQL source table/view (for direct table queries).

§function: Option<String>

The declared function that answers this root field, in place of reading a relation (#1329).

Names an entry in the compiled functions section whose trigger is request:query. The engine invokes it with this field’s arguments, as the requesting principal, and projects its result through this query’s return_type — so a function-backed field is introspectable, typed, and governed by the same requires_role / requires_actor / field-RBAC rules as any other root field.

§Root fields only

There is no nested-field equivalent, deliberately. A function attached to a field inside a type would be invoked once per row, which is an N+1 in isolates rather than in queries — measurably worse. Bounding it to root fields makes “one invocation per query” a property of the shape rather than a guideline, and the compiler refuses the nested spelling rather than documenting against it.

§Mutually exclusive with sql_source

A field resolves one way. The compiler refuses a query declaring both, and refuses one declaring neither where the SQL path needs a relation — there is no precedence rule to remember and no silent winner.

§Cost

An invocation costs ~5–8 ms on top of the work the function itself does, which is the same order as this repository’s documented cold read and roughly 5× a cache hit. That is a defensible price for computation SQL cannot express and a poor one for anything a view could answer; it is stated here, and in docs/architecture/functions.md, so the choice is made knowingly.

A function-backed field is not cached: the result cache keys and invalidates rows of a relation, and this field reads none, so every request runs the function. cache_ttl_seconds and additional_views are compile errors beside a function rather than settings that are accepted and ignored.

§description: Option<String>

Description.

§auto_params: AutoParams

Auto-wired parameters (where, orderBy, limit, offset).

§deprecation: Option<DeprecationInfo>

Deprecation information (from @deprecated directive). When set, this query is marked as deprecated in the schema.

§jsonb_column: String

JSONB column name (e.g., “data”). Used to extract data from JSONB columns in query results.

§relay: bool

Whether this query is a Relay connection query.

When true, the compiler wraps the result in XxxConnection with edges { cursor node { ... } } and pageInfo fields, using keyset pagination on pk_{snake_case(return_type)} (BIGINT).

§relay_cursor_column: Option<String>

Keyset pagination column for relay queries.

Derived from the return type name: User → pk_user. This BIGINT column lives in the view (sql_source) and is used as the stable sort key for cursor-based keyset pagination:

  • Forward: WHERE {col} > $cursor ORDER BY {col} ASC LIMIT $first
  • Backward: WHERE {col} < $cursor ORDER BY {col} DESC LIMIT $last

Only set when relay = true.

§relay_cursor_type: CursorType

Type of the keyset cursor column.

Defaults to Int64 for backward compatibility with schemas that use pk_{type} BIGINT columns. Set to Uuid when the cursor column has a UUID type.

Only meaningful when relay = true.

§pagination_order: Option<PaginationOrder>

The order an offset-paginated read of this query falls back to when the client requests none (#1303).

Resolved at compile time for every list query that paginates, and None for every query that does not — plus the one that does and whose author declared pagination_order = "none", which is how a view carrying its own ORDER BY keeps it. That opt-out is declared rather than inferred: the compiler cannot see a view’s body, and ordering unconditionally would silently destroy the escape hatch its own auto_param_warnings tells authors to use.

See PaginationOrder for why this is decided here and not by the SQL layer, and for what each variant costs.

§inject_params: IndexMap<String, InjectedParamSource>

Server-side parameters injected from JWT claims at runtime.

Keys are SQL column names. Values describe where to source the runtime value. These params are NOT exposed as GraphQL arguments.

For queries: adds a WHERE key = $value condition per entry using the same WhereClause mechanism as TenantEnforcer. Works on all adapters.

Clients cannot override these values.

§read_routing: ReadRouting

Where this query’s reads may be served from (#957).

Read-replica routing is otherwise a whole-server decision: the pin window and the staleness budget apply to every query alike, so an operator sizing them for the strictest query gives up the offload on all the others, and sizing them for the common case silently serves the strict one stale.

FraiseQL defines and enforces this shape; an authoring language emits it — A @reads_from(...) directive is one spelling of it. Replica topology deliberately stays out of the compiled artifact: URLs are server configuration and secrets.

See ReadRouting for what each answer guarantees, including why primary also bypasses the result cache.

§cache_ttl_seconds: Option<u64>

Per-query result cache TTL in seconds.

Overrides the global CacheConfig::ttl_seconds for this query’s view. Common use-cases:

  • Reference data (countries, currencies): 3600 (1 h)
  • Live / real-time data: 0 (bypass cache entirely)

None → use the global cache TTL.

§additional_views: Vec<String>

Additional database views this query reads beyond the primary sql_source.

When this query JOINs or queries multiple views, list all secondary views here so that mutations touching those views correctly invalidate this query’s cache entries.

Without this list, only sql_source is registered for invalidation. Any mutation that modifies a secondary view will NOT invalidate this query’s cache — silently serving stale data.

Each entry must be a valid SQL identifier (letters, digits, _) validated by the CLI compiler at schema compile time.

§Example

@fraiseql.query(
    sql_source="v_user_with_posts",
    additional_views=["v_post"],
)
def users_with_posts() -> list[UserWithPosts]: ...
§requires_role: Option<String>

Role required to execute this query and see it in introspection.

When set, only users with this role can discover and execute this query. Users without the role receive "Unknown query" (not FORBIDDEN) to prevent role enumeration.

§requires_actor: Vec<ActorType>

Actor classes permitted to execute this query — an allow-list (#966).

Empty (the default) means unrestricted. Non-empty means the request’s derived ActorType must be one of these, or the operation is refused — regardless of what roles the caller holds. That independence is the point: “autonomous agents may not read this” is a statement about the kind of principal, not about its permissions.

§An allow-list, not a deny-list

A deny-list would admit every actor class added after it was written. An allow-list refuses them, which is the fail-closed direction: a new ActorType variant is in nobody’s list until an author puts it there.

§Delegation is deliberately not consulted

For a delegated request (RFC 8693: an agent acting for a human), the classification is AiAgent and the check does not fall back to the permissions of the human in acting_for. Composite authorization already happens through roles — a delegated token carries the human’s roles, so requires_role consults them — and having requires_actor do the same would make it a no-op for exactly the case it exists for.

§Anonymous

A request with no security context has no classification, so a non-empty list refuses it. Fail-closed, and it does not depend on ActorType::default being the benign variant.

§rest_path: Option<String>

Custom REST path override (e.g., "/users/{id}/posts").

§rest_method: Option<String>

REST HTTP method override (e.g., "GET").

§rest_stream: bool

Whether this query may be exported as a stream over REST (#958).

Default false. Accept: application/x-ndjson, text/csv and the XLSX type are refused with 406 on a route that does not opt in — the JSON representation is unaffected.

§Why a per-route opt-in rather than a transport-wide switch

A streamed export is not a bigger page. It reads the whole filtered relation, bypasses max_page_size by construction (an export total is not a page), and holds a pooled database connection for as long as the client takes to read it. Those are reasonable properties for a route meant to hand over a dataset and unreasonable ones for the other kind of route, which is most of them — so which routes have them is the schema author’s decision, made per route, rather than a consequence of the REST transport being on.

@fraiseql.query(sql_source="v_invoice", rest_stream=True)
def invoices() -> list[Invoice]: ...
§native_columns: HashMap<String, String>

Native columns detected at compile time for direct query arguments.

Maps argument name → PostgreSQL cast suffix (e.g., "uuid", "int4", ""). An empty string means the column exists but needs no type cast (e.g. text).

At runtime, arguments present in this map generate WHERE col = $N (native column lookup) instead of WHERE data->>'col' = $N (JSONB extraction), enabling B-tree index usage for single-entity lookups.

Only populated when fraiseql compile --database <url> is used. Schemas compiled without a database URL omit this field and fall back to JSONB extraction.

Implementations§

Source§

impl QueryDefinition

Source

pub fn new(name: impl Into<String>, return_type: impl Into<String>) -> Self

Create a new query definition.

Source

pub const fn returning_list(self) -> Self

Set this query to return a list.

Source

pub fn with_sql_source(self, source: impl Into<String>) -> Self

Set the SQL source.

Source

pub fn with_function(self, function: impl Into<String>) -> Self

Back this root field with a declared function instead of a relation (#1329).

See function for what the engine does with the name and what it costs.

Source

pub fn count_sibling(&self) -> Self

Derive the <name>Count sibling of this list query (#938).

This is the only way a count query is built. A sibling query that answers “how many rows match?” against the same view is a second door onto the same rows, and a second door that forgets one of the first’s restrictions is an oracle: a count that ignores inject_params reports other tenants’ row totals, and one that ignores requires_role answers callers who cannot see the list at all. Neither leaks a row, which is exactly why it would survive review — so the inheritance is centralised here rather than spelled out at each construction site (the _entities lesson, #1030).

Inherited, deliberately: the entity return_type and sql_source (same rows), inject_params (same tenant scoping), requires_role (same visibility), explicitly declared arguments (they lower into the same WHERE), native_columns (same casts, so the count uses the same indexes), additional_views (same cache invalidation) and deprecation.

Dropped, deliberately: limit/offset/orderBy — a total that moved with the page would answer a question nobody asked; pagination_order with them, since a scalar total has no rows to order; relay, which has its own totalCount; and the REST overrides, since the REST surface already counts through Prefer: count=exact.

function is dropped for a different reason than the rest: a count is SELECT COUNT(*) over a relation, and a function-backed field has none — which is why count = true beside function is a compile error (#1329). Dropping it here means a sibling hand-built through this constructor fails as “no SQL source” rather than invoking the function and projecting its object as a scalar total.

Source

pub fn deprecated(self, reason: Option<String>) -> Self

Mark this query as deprecated.

§Example
use fraiseql_core::schema::QueryDefinition;

let query = QueryDefinition::new("oldUsers", "User")
    .deprecated(Some("Use 'users' instead".to_string()));
assert!(query.is_deprecated());
Source

pub const fn is_deprecated(&self) -> bool

Check if this query is deprecated.

Source

pub fn deprecation_reason(&self) -> Option<&str>

Get the deprecation reason if deprecated.

Source

pub fn graphql_arguments( &self, schema: &CompiledSchema, ) -> Vec<ArgumentDefinition>

The full set of GraphQL arguments this query accepts, for rendering into the federation _service SDL, generated clients, and introspection.

The auto-wired where/orderBy/limit/offset arguments are gated by auto_params and read directly from the argument map at runtime, so they are deliberately not stored in arguments (where the runtime would otherwise mistake a synthesized limit/offset for an explicit column filter). This method materialises them so every consumer that renders from the argument list can surface — and a generated client can actually pass — them.

where/orderBy are typed against the derived filter surface — {Entity}WhereInput and [{Entity}OrderByInput!] — when schema carries those types, and fall back to the JSON scalar when it does not. Either way the runtime parses the raw value via WhereClause::from_graphql_json / OrderByClause::from_graphql_json; the type is what clients see, not what the executor reads.

§Why this takes a schema

The published type of an auto-wired argument is a property of the schema, not of the query alone: derived_inputs::derive emits {Entity}WhereInput only for a return type whose fields the schema can adjudicate, and skips any name the author already declared. Reading the answer from the same place that produced it is what makes a dangling type reference unrepresentable — the argument is typed iff the type exists. A cached answer on QueryDefinition would let a caller publish the name without the schema that justifies it.

An explicit argument always wins: if the query already declares an argument of the same name it is left untouched and no duplicate is synthesized.

Relay connection queries are returned unchanged — their pagination surface (first/after/last/before) is owned by each renderer’s dedicated relay path, not by auto_params.

Source

pub fn accepted_argument_names(&self, schema: &CompiledSchema) -> Vec<String>

The argument names a client document may write on this field (GraphQL § 5.4.1), for validate_argument_names.

graphql_arguments is what introspection publishes; this is what the runtime actually reads, which is wider in two places:

  • A relay connection’s cursor window (first/after/last/before) is owned by each renderer’s relay path rather than by auto_params, and the relay runner also reads where/orderBy from the merged argument map when the matching auto-param is on.
  • nearest (#386, #959) is a runtime-only similarity-search argument. Accepting it here is what lets its own diagnostics — not eligible on relay, needs a list query, unknown metric, wrong dimension — be what the client sees, instead of a blanket “unknown argument”. It is refused on a count sibling, which never reads it.

Trait Implementations§

Source§

impl Clone for QueryDefinition

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for QueryDefinition

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for QueryDefinition

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl<'de> Deserialize<'de> for QueryDefinition

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl PartialEq for QueryDefinition

Source§

fn eq(&self, other: &Self) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl Serialize for QueryDefinition

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl StructuralPartialEq for QueryDefinition

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more