This document specifies a data model and a client-driven retrieval protocol for REST/JSON APIs. The data model defines a JSON-LD-based resource representation that serves as the uniform payload across the standard create, read, update, and delete (CRUD) operations of HTTP. On top of this baseline, the protocol defines a JSON-based template language that allows clients to specify which properties to retrieve from a resource, how deeply to expand linked resources, and, for collections, how to filter, sort, paginate, and aggregate results. The template is transmitted as a URL-encoded JSON object in the query string of a standard HTTP GET request, preserving compatibility with content delivery networks (CDNs) and browser caches.
The data model is grounded in JSON-LD 1.1 [W3C.REC-json-ld11] but constrains JSON-LD to a controlled subset that looks and feels like plain idiomatic JSON, requiring no specialised client libraries, preprocessors, or code generators.
This Internet-Draft, draft-qest-00, is submitted in full conformance with the provisions of BCP 78 and BCP 79. Its
intended status is Experimental.
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."
This Internet-Draft will expire on 8 December 2026.
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.
REST/JSON APIs carry resources through create, read, update, and delete (CRUD) operations over HTTP. Building on that standard baseline, this specification adds what vanilla REST/JSON lacks: a standard, portable mechanism for clients to control the shape and scope of the responses they read back. Without one, clients must either accept fixed server-defined payloads, leading to over-fetching of unwanted fields and under-fetching that requires additional round trips, or rely on ad-hoc, non-portable query parameters.
This specification defines a data model and a template language that together address these limitations while remaining within standard REST/JSON conventions:
The data model is grounded in JSON-LD 1.1 [W3C.REC-json-ld11], constrained to a controlled subset that looks and feels like idiomatic JSON: compacted form, identifier property names, native JSON values, language maps, and IRI references. Its value type system derives from XML Schema Definition (XSD) 1.0 [W3C.REC-xmlschema-2] and XPath 2.0 [W3C.REC-xpath-functions], the same foundation referenced by SPARQL 1.1 [W3C.REC-sparql11-query]. Section 3 specifies this subset normatively.
Resources are exchanged as a uniform application/json payload over the standard REST verbs of HTTP [RFC9110]:
creation (POST), retrieval (GET), update (PUT), and deletion (DELETE).
The default media type is the generic application/json. This specification deliberately defines no media type, profile
parameter, or structured suffix of its own, so standard JSON tooling, content negotiation, and HTTP caches apply
unchanged.
In responses, servers MAY alternatively use the JSON-LD media type application/ld+json [W3C.REC-json-ld11], including
an @context derived from the internal data model. Servers MUST reject JSON-LD request payloads: all processing is
driven by the internal data model, not by client-supplied mappings.
| Method | Payload | Description |
|---|---|---|
| GET | resource | Resource retrieval |
| POST | resource | Resource creation |
| PUT | resource | Complete resource state update |
| DELETE | - | Resource deletion |
Each verb targets a resource by its request URL, with POST targeting the owning collection. HTTP defines the operation semantics, status codes, and content negotiation; this specification adds only how payloads are interpreted (the data model, Section 4) and how responses are shaped (client-driven retrieval templates, Section 5). Processors MUST validate every resource payload they accept against the data model (Section 4).
Response status codes follow HTTP [RFC9110]. Servers SHOULD use the following codes for the conditions this document defines:
| Condition | Status |
|---|---|
| Successful retrieval | 200 OK |
| Malformed request payload | 400 Bad Request |
| Validation failure (Sections 3, 4, and 5) | 422 Unprocessable Content |
| Outside the client's authorisation (Section 8.2) | 403 Forbidden / 404 Not Found |
| Over-long encoded template (Section 5) | 414 URI Too Long |
A validation failure SHOULD carry a problem-details payload [RFC9457] including a machine-readable error trace, subject to the disclosure limits of Section 8.2.
A retrieval request is a standard HTTP GET whose query component carries the template, encoded as described in Section 5. The template is optional: a GET without one returns the server's default representation of the target resource (Section 5). When a template is present, the response is a resource (Section 4) shaped to it: it contains exactly the requested properties, with linked resources expanded and everything else omitted. A requested property resolving to no value is itself omitted, never surfaced as an empty structure (Section 4). For collections, the results are filtered, sorted, paginated, and aggregated as specified. Because the template travels in the query string of a GET, retrieval remains safe, idempotent, and cacheable by CDNs and browser caches [RFC9110].
For example, a collection query that selects four item properties, filters by price, sorts ascending, and limits the page to twenty-five items is issued as a single GET request:
GET /products/?%7B%22items%22%3A%5B%7B%22id%22%3A%22%22%2C%22name%22%3A%22%22%2C%22price%22%3A0
%2C%22vendor%22%3A%7B%22id%22%3A%22%22%2C%22name%22%3A%22%22%7D%7D%2C%7B%22%3E%3Dprice%22%3A50
%2C%22%3C%3Dprice%22%3A150%2C%22%5Eprice%22%3A%22asc%22%2C%22%23%22%3A25%7D%5D%7D
whose query string decodes to the template:
{
"items": [
{
"id": "",
"name": "",
"price": 0,
"vendor": {
"id": "",
"name": ""
}
},
{
">=price": 50,
"<=price": 150,
"^price": "asc",
"#": 25
}
]
}
and yields a response like:
{
"items": [
{
"id": "https://example.com/products/456",
"name": "Gadget",
"price": 59.99,
"vendor": {
"id": "https://example.com/vendors/145",
"name": "Acme"
}
},
{
"id": "https://example.com/products/123",
"name": "Widget",
"price": 99.99,
"vendor": {
"id": "https://example.com/vendors/145",
"name": "Acme"
}
},
{
"id": "https://example.com/products/789",
"name": "Gizmo",
"price": 129.99,
"vendor": {
"id": "https://example.com/vendors/236",
"name": "Globex"
}
}
]
}
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.
The data model is defined normatively in CDDL [RFC8610] and the textual micro-syntaxes in ABNF [RFC5234], over JSON values [RFC8259]. These grammars are normative; any accompanying reference-implementation type definitions are not.
The following terms are used throughout this document:
boolean, number, string)undefinedname=expressionREST/JSON payloads (Section 1.3) carry values as JSON [RFC8259] primitives, whose coarse kinds alone cannot drive processing: comparison (Section 5.7.1) and sorting (Section 5.7.5) need a defined ordering, and transforms (Section 5.8.2) need defined input domains and output ranges, neither of which a JSON kind alone supplies.
To meet those needs, this specification defines a minimal processing type system, grounded in the value spaces of XSD 1.0 [W3C.REC-xmlschema-2] and the operators and functions of XPath 2.0 [W3C.REC-xpath-functions]: processors map each payload value to the processing type system before any relevant operation is performed (ingress) and map the result back before returning it (egress).
| Payload (JSON) | Processing (XSD) |
|---|---|
boolean |
xsd:boolean |
number |
xsd:double (ingress default) |
xsd:float |
|
xsd:integer |
|
xsd:decimal |
|
numeric (any XSD numeric datatype) |
|
string |
xsd:string (ingress default) |
temporal (xsd:dateTime, xsd:date, xsd:time) |
References and localised text are not mapped into the processing space: a reference participates only in equality matching (Section 5.7.3), and localised text is matched per tag (Section 5.7.3) or coalesced to a plain string or array of plain strings (Section 6) before any operation applies.
The temporal type comprises the point-in-time datatypes that are component-extractable and totally ordered (the latter
under XPath 2.0's implicit-timezone comparison) over the shared XSD 1.0 / XPath 2.0 basis of the target backends
(Appendix A.1.1). The other XSD 1.0 temporal datatypes, xsd:duration and the Gregorian partials (xsd:gYearMonth,
xsd:gYear, xsd:gMonthDay, xsd:gMonth, xsd:gDay), are not processing types, and neither are the temporal
datatypes added by XSD 1.1, such as xsd:dateTimeStamp, which lie outside this XSD 1.0 basis entirely; such a value, if
carried, is treated as an opaque xsd:string (equality and set matching only).
Processors resolve payloads and templates against the expected type of each property they process. A property's expected type is one or more variants, each:
A property with several variants is union-typed, its variants expected to be disjoint and resolved per branch by matching, not by position (Section 5.4), and further constrained by Section 3.2. A property whose only variant is localised text is a localised property (Section 4.3); a path may also resolve to localised text per branch downstream of a union-typed step (Section 5.8.1).
Alongside its type, each property carries an expected cardinality, single- or multi-valued.
Two regimes resolve a value against the variants, according to whether it carries content:
Expected types are supplied out of band, whether declared by a static property schema or derived dynamically by the application; this specification constrains neither their source nor their provisioning, and they add no conformance requirement of their own. They are definitional: the rules that reference an expected or declared characteristic, among them ingress mapping (Section 3.3), placeholder matching (Section 5.2), locale classification (Section 5.3), union retrieval (Section 5.4), collection queries (Section 5.5), selection (Section 5.7), and path resolution (Section 5.8.1), are evaluated against them, and a property without an expected type is unknown (Section 5.8.1).
A union is the one construct in this specification whose cost multiplies rather than adds. A processor compiling a template or an expression carries a candidate set per union, and every construct reached through the union is evaluated against each candidate, so a path of two union-typed steps over three branches apiece already carries nine candidates and each emitted query grows to match. Where a text variant is among the branches, an unconstrained union also breaks serialisation, since a value set has no form holding localised text alongside values of other kinds (Section 4.2).
Three rules bound these two costs, beyond the disjointness the variants are expected to satisfy: coherence bounds the first at declaration; exclusivity and folding bound the second, on ingress and before any construct reads the property. Each settles once what would otherwise be settled per branch at every construct downstream, so that a union costs a processor one resolution rather than one per variant. Of the four constraints, only exclusivity and folding bear conformance requirements: disjointness and coherence are properties of the declared variants, supplied out of band like the variants themselves (Section 3.1), and add none of their own; a processor that observes a multiple match of a data value at runtime rejects it as ambiguous (Section 3.3).
Coherence. A property identifier declared by more than one nested-resource variant of the same union-typed property is expected to denote the same property throughout that union, so that a step naming it downstream of the union yields one answer rather than one per variant, and processors MAY resolve it once instead of per branch (Section 5.8.1). The variants MAY declare it with differing expected types, cardinalities, or value domains; those declarations merge into the effective type and cardinality of a path crossing the union (Section 5.8.1).
Without coherence, creator.email over a union of Person and Organisation compiles to one predicate per branch, and
one per pair of branches across a second union-typed step; with it, email is one predicate and the branches differ
only in what it is declared to hold. The rule is local to a single union: it constrains neither identifiers declared by
variants of different unions nor identifiers declared by resource types that never meet in one.
Exclusivity. Where a property declares a text variant alongside other variants, its value set on any one resource (Section 4.2) MUST hold either localised text or values of the other variants, never both, and processors MUST reject on ingress (Section 3.3) a payload mixing them.
Exclusivity is per resource, not per property, and that is what lets a two-natured property be represented at all: an
abstract type whose instances split between a translatable name and an untranslatable proper name declares both arms,
and each resource carries whichever suits it, the proper name travelling as a plain string and the translatable name as
a dictionary, each a value set form of Section 4.2. Section 4.3 sets out when to split arms in this way and when a
dictionary carrying und entries models the absence of a localised form instead.
Folding. Except where the text variant is addressed structurally (Section 6, through the constructs of Sections 5.3
and 5.4), processors MUST fold it into the property's other variants, yielding a single value set, before any construct
reads them. The text variant contributes its coalesced value (Section 6.2), an ordinary xsd:string of the variant's
per-tag cardinality holding the strings the map coalesces to as if the property stored them, and contributes nothing
where coalescing yields undefined. The folded set carries no localised text, and every construct that reads the
property, among them placeholder matching (Section 5.2), selection (Section 5.7), and path resolution (Section 5.8.1),
reads the folded set.
Folding keeps the text variant out of the union a processor compiles against, so localised content that is never
addressed structurally adds neither a branch to a union nor a mixed type to a value set: a property declaring a text
variant and a string variant folds to a uniform xsd:string set, its two variants indistinguishable to any construct
reading it. Appendix A.5 shows the coalescing compiling to a single expression on each target backend.
A boolean maps to xsd:boolean. A number maps to xsd:double and a string to xsd:string, unless a more
specific processing type is expected for the value (Section 3.1).
A value that cannot be represented as the processing type expected for it, such as a literal whose JSON kind does not match or a string that is not a valid lexical form of the expected temporal type, is malformed and MUST be rejected by processors.
Over a union-typed property (Section 3.1), the value is matched against the variants and mapped to the one it singles out. Since the variants are expected disjoint, the value matches at most one: a value matching no variant is unsatisfiable and a value matching several is ambiguous, and processors MUST reject either (Section 3.1). The variant the value singles out fixes its processing type, so the same matching that admits the value also casts it.
JSON [RFC8259] guarantees number interoperability only within IEEE 754 double precision, and this specification adopts
that interoperable range as the transport number space, so mapping a number to xsd:double preserves every
interoperable value. This mapping is defined by this specification and is independent of JSON-LD value conversion, which
assigns xsd:integer to integer-valued numbers; the two agree on ordering over that range, because such an integer and
its double promotion compare equal.
Every processing type maps to the transport type heading its group (xsd:boolean, xsd:double, or xsd:string). The
mapping is many-to-one; the processing-space distinctions are not preserved on the wire.
Processors MAY recognise processing types beyond the defaults of this section. Both mappings account for such an extension: ingress (Section 3.3) selects the extended type for an incoming value when one is expected (Section 3.1), and egress (Section 3.4) returns it to the transport type heading its group. The same mechanism MAY serve beyond the retrieval operations defined here, for example, to validate a request payload against the expected type of each property, or to persist values in a natively typed store. These are implementation capabilities; they impose no additional conformance requirement and do not change the surfaced transport type set.
The data model defines the JSON representation of a resource: its property structure, value types, and linking, together with the JSON-LD subset and IRI conventions that constrain it. The same JSON surface syntax underlies both the resource payloads of the REST operations (Section 1.3) and the retrieval templates of the query layer (Section 5): field keys are ECMAScript identifiers; values are JSON primitives, nested objects, or arrays thereof.
The following CDDL [RFC8610] grammar is the data model's normative definition; the subsections below elaborate it in prose.
resource = { * identifier => values }
values = value / text / [* value]
value = literal / reference / resource
literal = bool / number / tstr
reference = tstr ; IRI reference [RFC3987] (Section 4.2)
text = { * tag => tstr } / { * tag => [* tstr] }
identifier = tstr ; ECMAScript IdentifierName [ECMA-262]
tag = tstr ; BCP 47 language tag [RFC5646]
Within a single text map the values are uniformly tstr or uniformly [* tstr].
An empty object ({}) carries no content, whether an empty nested resource or an empty text map; likewise an empty
array. Processors MUST ignore such a value: drop it where it appears as an array element or as a tag's value in a
text map, and otherwise treat the owning field as omitted (set semantics). Encoders MUST NOT emit one: a value set
resolving to no content, whatever its form, is never surfaced as an empty array, text map, or object; the owning field
is omitted from the document instead.
This data model is a controlled subset of JSON-LD 1.1 [W3C.REC-json-ld11], constraining JSON-LD to patterns that read as plain idiomatic JSON, so no JSON-LD processor, preprocessor, or code generator is required. Conforming documents MUST satisfy the following constraints:
@id, @type, etc.) MUST NOT appear directly; they MUST be mapped to identifiers via an
application-provided @context (for example, "id": "@id")@context"@container": "@language" in @context; the
@none language MUST NOT be used; und or zxx is used instead (Section 4.3)A field mapped to @type carries class references, so its expected type (Section 3.1) is reference. Such a field is
commonly system-managed, derived from the expected model rather than supplied by clients; this provenance does not alter
its retrieval semantics. It is an ordinary reference-typed field and MAY be targeted by the equality-based selection
constraints, set matching (Section 5.7.3) and sort focus (Section 5.7.4), like any other reference.
A resource is a field map where each field holds a value set. Resources MAY include a field mapped to @id
in the application-defined JSON-LD @context, identifying the resource globally; a resource without such a field is
anonymous.
{
"id": "https://example.com/products/42",
"name": "Widget",
"category": "Electronics",
"tags": [
"gadget",
"featured"
],
"vendor": "https://example.com/vendors/456",
"price": 99.99,
"inStock": true
}
Each field holds a value set: a single value, a dictionary (Section 4.3), or an array of values. Arrays follow set semantics: duplicate values are ignored, ordering is immaterial, and empty arrays are treated as absent values. Element types within an array MAY be mixed.
A value is one of:
boolean, number, string)A reference carries only the linked resource's IRI; a nested resource carries its state inline. The two are
interchangeable ways to link. Resource identifiers and cross-resource links are absolute IRIs [RFC3987]; they MAY be
transmitted in relative form, and decoders MUST resolve them against a base IRI that defaults to app:/, so a decoded
reference is always absolute. Encoders MAY in turn relativise the IRIs of a response payload against the same base; in
that case they SHOULD prefer the root-relative form.
IRI reference (compact form):
{
"id": "https://example.com/products/42",
"vendor": "https://example.com/vendors/456"
}
Nested description (expanded form):
{
"id": "https://example.com/products/42",
"vendor": {
"id": "https://example.com/vendors/456",
"name": "Acme Corp"
}
}
Resource properties MAY hold localised text in a dictionary: a language map associating BCP 47 [RFC5646] language tags with text values. Within a single dictionary, all values MUST be uniformly scalar or uniformly array; processors MUST reject mixed content.
The @none key for non-localised values MUST NOT be used. Use the und (Undetermined) tag [ISO639-3.und] when the
language is unspecified; use the zxx (No linguistic content) tag [ISO639-3.zxx] for values that carry no language at
all, such as identifiers, codes, or formulae.
Content that has no localised form, a proper name among it, is modelled either way, and the choice turns on whether the
absence is a property of the value or of the resource. A value sitting among translations, as in a taxonomy whose
entries are localised on the interface and one of which happens to have no determinate language, stays in the dictionary
under und: the property remains localised text throughout, and structural access still yields a dictionary. A resource
whose name is a proper name in its own right, carrying no translations to sit among, is better served by a text variant
alongside a string variant (Section 3.1): the property is then two-natured, each resource carries whichever arm suits
it, and the two fold into one xsd:string set on retrieval. The two compose, a text arm still admitting und entries
of its own.
A localised property can be addressed in two ways: structurally, preserving its language tags, or coalesced to a plain string, or array of plain strings of corresponding cardinality, under language negotiation (Section 6).
{
"name": {
"en": "Widget",
"fr": "Bidule"
},
"description": {
"en": [
"Compact",
"Durable"
],
"fr": [
"Compact",
"Resistant"
]
}
}
Clients control the shape and scope of what they read back through JSON templates: which properties to retrieve, how deeply to expand linked resources, and, for collections, how to filter, sort, paginate, and aggregate.
Client-driven retrieval is fully optional. Servers MUST provide defaults, typically derived from the expected types (Section 3.1), preserving standard REST/JSON behaviour while enabling advanced capabilities when needed.
The query component of the GET request URL carries either a retrieval template (Section 5.1) or a selection (Section 5.7). A template MUST use a URL-safe JSON encoding, either URL-encoded or base64url-encoded [RFC4648] JSON; the plain JSON form is reserved for transmission off the query string, such as a POST body. A selection uses the form-urlencoded [WHATWG.URL] shorthand detailed below. The decoder MUST auto-detect both which variant is present and, for a template, its input encoding. The decoded result, like every payload, MUST be validated. Reference values, in resource payloads and selections alike, MAY be transmitted in relative form; decoders MUST resolve them against the base IRI (Section 4.2), so a decoded reference is always an absolute IRI. Encoders MAY in turn relativise the references of a response payload, in which case the root-relative form SHOULD be preferred (Section 4.2).
A localised text property (Section 4.3) coalesces to a plain string, or array of plain strings of corresponding cardinality, under language negotiation (Section 6). A plain string MUST therefore be accepted wherever such a property is targeted, in a retrieval template (Section 5.1) as the placeholder for its coalesced value (Section 5.3), and in a selection (Section 5.7) as an operand, including as an option value (Sections 5.7.3 and 5.7.4); the coalesced value is then matched under ordinary string semantics, a multi-valued one existentially. A sort key (Section 5.7.5) targeting a single-valued coalesced property MUST likewise be accepted, ordering by the coalesced string; localised text is never ordered in any other form.
URL-encoded templates are subject to practical URL length limits; servers SHOULD document their maximum accepted query
string length and return 414 URI Too Long when it is exceeded, and MAY accept an over-long template via POST with an
appropriate content type instead.
When a request query component carries a selection, the server synthesises a retrieval template for it:
{ <collection-property>: [ <item-template>, <selection> ] }.The following elision rules then apply to the synthesised template. This fallback applies only at the request target; nested templates have none.
An empty object ({}) as a template element carries no retrieval instructions and MUST be ignored, as if the owning
property were omitted: this elides an empty template (Section 5.1), an empty union (Section 5.4; no variants, or
all reducing to empty templates), an empty locale (Section 5.3; no tag ranges), and an empty selection
(Section 5.7; no constraints). A selection has nothing to apply to once its element is empty, so a tuple carrying only a
selection discards that selection as well.
The query string's formal syntax is defined in ABNF [RFC5234]:
; query string (the request entry point; Section 5)
query = template / selection ; the decoder auto-detects the variant
; template variant: a `template` (CDDL below) serialised as JSON [RFC8259],
; then made URL-safe by percent- or base64url-encoding
template = <URL-safe JSON encoding of template, Section 5>
; selection variant: form-urlencoded [WHATWG.URL] constraints
selection = [ entry ] *( "&" [ entry ] )
entry = lt / gt / lte / gte
/ like / any / all
/ focus / order
/ offset / limit
lt = "<" expression "=" literal
gt = ">" expression "=" literal
lte = "<=" expression "=" literal / expression "<=" literal
gte = ">=" expression "=" literal / expression ">=" literal
like = "~" expression "=" string
any = "?" expression "=" value / expression "=" value
all = "!" expression "=" value
focus = "+" expression "=" value
order = "^" expression "=" ( "asc" / "desc" / [ "-" ] 1*DIGIT )
offset = "@" "=" 1*DIGIT
limit = "#" "=" 1*DIGIT
; value forms, classifying the text after "=" in each "&"-separated entry:
value = option / tagged
option = "null" / literal / reference
literal = boolean / number / string
boolean = <JSON boolean, [RFC8259]>
number = <JSON number, [RFC8259]>
string = quoted / unquoted
quoted = <double-quoted JSON string, [RFC8259]>
unquoted = <an unquoted text that is not "null", a boolean, or a number; its text is the string>
reference = string ; IRI reference [RFC3987], resolved against the base IRI (Section 4.2)
tagged = string "@" tag ; tag split from the right; quote the base to embed a literal "@"
tag = <BCP 47 language tag, [RFC5646]>
Both variants decode into the template structures, defined in CDDL [RFC8610] (reusing literal, reference,
text, and identifier from the data model, Section 4):
template = { * identifier => placeholders }
placeholders = model / query
placeholder = literal / reference / template
model = union / placeholder / locale
query = [ union, ? selection ] / [ placeholder, ? selection ] / [ projection, ? selection ]
locale = { * tag-range => tstr } / { * tag-range => [ tstr ] }
union = { * slot => placeholder / locale }
projection = { * binding => model }
selection = {
* lt => literal, ; <
* gt => literal, ; >
* lte => literal, ; <=
* gte => literal, ; >=
* like => tstr, ; ~
* any => options, ; ?
* all => options, ; !
* focus => options, ; +
* order => "asc" / "desc" / int, ; ^
? offset => number, ; @
? limit => number ; #
}
; the *-key syntaxes are defined in the ABNF below
lt = tstr ; lt-key
gt = tstr ; gt-key
lte = tstr ; lte-key
gte = tstr ; gte-key
like = tstr ; like-key
any = tstr ; any-key
all = tstr ; all-key
focus = tstr ; focus-key
order = tstr ; order-key
offset = "@" ; literal
limit = "#" ; literal
options = option / text / [* option]
option = null / literal / reference
tag-range = tstr ; RFC 4647 basic language range [RFC4647] (Section 5.3)
slot = tstr ; opaque Union key: a non-negative integer string (Section 5.4)
binding = tstr ; see ABNF below
A selector key is an operator prefix followed by an expression, the prefix fixing the value type per the selection
group above; the pagination keys @ and # are literals, not selectors. The textual micro-syntaxes are defined in
ABNF:
; selector keys (selection map keys)
lt-key = "<" expression
gt-key = ">" expression
lte-key = "<=" expression
gte-key = ">=" expression
like-key = "~" expression
any-key = "?" expression
all-key = "!" expression
focus-key = "+" expression
order-key = "^" expression
; bindings and expressions (projection keys; Section 5.6, Section 5.8)
binding = name "=" expression
name = identifier
expression = pipe path
pipe = *( transform ":" )
path = [ property *( "." property ) ]
property = identifier
transform = identifier
identifier = <ECMAScript IdentifierName, [ECMA-262], Section 12.7>
A selection's form-urlencoded shorthand serialises the grammar above as label=value pairs, where labels are the
prefixed selector keys (Section 5.7). This is a convenience for readable URLs, covering the common selection constraints
rather than the full template grammar; a selection it cannot express is carried in the JSON template form instead. The
shorthand observes the following rules:
?, !, +) MAY be repeated, collecting its values into a set; any other operator MUST be
rejected if repeated.expression=value for
?expression=value, and expression<=value / expression>=value for <=expression / >=expression. Strict <
and > are prefixed-only.~, ?, !, @); the
rest are percent-encoded (^ %5E, < %3C, > %3E, # %23, and + %2B, which form-urlencoding would
otherwise decode as a space), as are the reserved value characters &, =, +, %; this encoding is for transport
safety only, and a decoder SHOULD parse leniently, accepting them unencoded too.The example below filters items where category is "electronics" or "home", name contains "widget", price is
between 50 and 150 inclusive, sorts by price ascending, and returns the first 25 items (label operators shown in
decoded form for readability):
category=electronics
&category=home
&~name=widget
&price>=50
&price<=150
&^price=asc
&@=0
&#=25
A template is a JSON object specifying which properties to retrieve from a resource and how deeply to expand linked resources.
Template properties use placeholder values (Section 5.2) that indicate the expected type. The value of a literal placeholder is never returned and is immaterial: it need not be a legal value of the property's type, only its kind matters, matching a single-type property of that kind and, for a union-typed property (Section 5.4), every variant of that kind. A nested object is instead a template in its own right, whose structure does matter: it selects the properties of the linked resource to expand and, over a union-typed property, matches the variants it structurally fits (Section 5.4); an empty one is elided (Section 5).
GET /products/42?{url-encoded-template}
Template:
{
"id": "",
"name": "",
"price": 0,
"vendor": {
"id": "",
"name": ""
}
}
Response:
{
"id": "https://example.com/products/42",
"name": "Widget",
"price": 99.99,
"vendor": {
"id": "https://example.com/vendors/456",
"name": "Acme Corp"
}
}
The response includes only the requested properties, with the linked vendor expanded to show only id and name.
A placeholder stands in for one property value:
boolean, number, or string, requesting a primitive valueA placeholder's value is immaterial and need not lie within the expected value domain (Section 3.1); only its kind matters. A placeholder MUST, by kind, match at least one variant of its property; one matching none can return nothing and is unsatisfiable, and MUST be rejected, as a data value is (Sections 3.3 and 5.7).
A placeholder stands for data and never carries it back: its value is never returned and is immaterial. A literal
placeholder need not be a legal value of its property's type; only its kind matters, and over a union-typed property
that kind is what matches it to a variant (Section 5.4), a single-type property admitting any value of the kind. A
reference or template placeholder likewise conveys only its kind. A string matches a reference variant only when it
satisfies the IRI-reference production of [RFC3987], which admits the empty string together with the relative,
root-relative, and absolute forms, excluding only a string that could not reference a resource; a string outside it
matches no reference variant. Reference values proper, the options and operands of a selection
(Section 5.7), are instead resolved on decoding (Section 5) and are absolute thereafter.
The placeholder for a multi-valued property is a tuple, whose array form signals multi-valued cardinality. The first element is the per-item template; an optional second element is a collection-wide selection (Section 5.7) that filters, sorts, and paginates the property's values. Runtime validators MUST accept a one- or two-element tuple and reject arrays of any other length.
{
"tags": [
""
],
"categories": [
{
"id": "",
"name": ""
}
]
}
Tag-range keys [RFC4647] select which locales to retrieve. A tag-range key MUST be a basic language range
[RFC4647] (Section 2.1): a sequence of subtags, or the standalone * wildcard. Extended language ranges
[RFC4647] (Section 2.2), carrying * in a leading, interior, or trailing subtag position (for example de-*
or *-CH), MUST be rejected; under the basic filtering used here they add no matching power over their basic prefix,
and a processor MUST NOT attempt to interpret them.
The placeholder returns the subset of the property's dictionary (Section 4.3) matching the ranges by RFC 4647 basic filtering (Section 3.3.1; all matching tags) rather than lookup (a single best match), as a structured map. Each tag-range value is itself a placeholder typed to the expected result: a string where the property holds one value per tag, or a single-element array where it holds several (Section 4.3). Only the type matters, so the array carries exactly one element.
A localised property MAY also be retrieved through a plain string placeholder, yielding its coalesced label (Section 6): the value or values resolved by the request's negotiated language priority (Section 6.1). The placeholder takes the ordinary string shape for the property's per-tag cardinality (Sections 5.2 and 4.3); processors MUST reject a mismatch in either direction (Section 5.2). The tag-range map yields the full structure instead.
A locale is not syntactically disjoint from a nested template, since a tag-range such as en is also a valid property
identifier; processors classify the object by the targeted property's expected type (Section 3.1): a locale over a
property declaring a text variant (Section 4.3) and a template (Section 5.1) otherwise. A property declaring both a text
variant and a nested-resource variant leaves the object form genuinely ambiguous and MUST be addressed through the keyed
union form (Section 5.4), which tells the alternatives apart by key.
{
"title": {
"*": ""
},
"description": {
"en": "",
"fr": ""
},
"keywords": {
"en": [
""
],
"fr": [
""
]
}
}
For union-typed properties (Section 3.1), per-branch retrieval is expressed through a keyed object form whose values are the per-branch placeholders, each a plain placeholder or, only within a projection binding (Section 5.6), a locale placeholder (Section 5.3). Save for the folded text variant below, this keyed form is the only way to address such a property: a plain placeholder over one is mismatched and MUST be rejected (Section 5.2), whichever single branch it may resemble.
Folding (Section 3.2) recasts a text variant before retrieval reaches it: its coalesced value (Section 6.2) stands in as
an ordinary xsd:string branch. A text variant paired with a string variant folds into that one branch, so the property
is not union-typed for retrieval and is addressed by a plain placeholder like any string-valued property, yielding the
coalesced label (Section 5.3) for the values the text variant carries. Where folding leaves two or more branches, a text
variant paired with a reference or a nested resource among them, the keyed form applies as usual and a plain string
alternative addresses the folded branch.
A variant carries a single value, never a collection: cardinality is defined for the property as a whole and applies to the union slot, not independently per branch.
A variant MAY be a locale placeholder (Section 5.3) only within a projection binding (Section 5.6), addressing a branch that resolves to a localised property (Section 4.3), as a path through a union-typed step can (Section 5.8.1); the branch then occupies its own cell (Section 5.6) as a dictionary. A dictionary is not a value and cannot be combined into a value set (Section 4.2) alongside the literals, references, and resources of sibling branches, so a union retrieving a resource property directly admits no locale variant; only the per-cell decomposition of a projection (Section 5.6) accommodates one. A locale variant carries its own per-tag cardinality (Section 5.3) and is therefore never wrapped in a collection.
The keys are opaque: they label the alternatives but carry no positional or nominal meaning. Keys MUST be non-negative integer strings (no decimals, negatives, or exponential forms), a namespace disjoint from property identifiers, and a processor MUST NOT read positional meaning into them. Each value is an alternative placeholder, and the branches it retrieves are fixed by matching the placeholder against the property's variants (Section 5.2), not by the key. Matching is by kind, not by value: a literal alternative matches every variant of its processing kind, a reference alternative every reference variant, and a template alternative every nested-resource variant whose type its properties are valid on (Section 5.2). A placeholder's value is immaterial and need not be a legal value of any variant. An alternative MAY match more than one variant, retrieving each, but like any placeholder MUST match at least one: one matching no variant can return nothing and is unsatisfiable, and MUST be rejected (Sections 3.1 and 5.2). A literal or reference alternative does not tell same-kind variants apart, while a template's structure discriminates the resource variants it fits. Variants left unmatched are skipped at retrieval, contributing no values.
{
"id": "",
"creator": {
"0": {
"id": "",
"name": ""
},
"1": {
"id": "",
"legalName": ""
}
}
}
Where variants share a processing type, a placeholder matches them all by kind, its value immaterial: here region has
two string branches, an ISO 3166 alpha-2 country code and an internal macro-zone code. A single string alternative
already matches both, so retrieval returns whichever branch the stored value belongs to; the values "US" and "EMEA"
below are inert placeholders that select nothing, the two slots being equivalent:
{
"id": "",
"region": {
"0": "US",
"1": "EMEA"
}
}
Numeric keys suit the machine-generated templates that tooling, code generation, and schema-driven translation produce and keep the surface syntax sigil-free; being disjoint from the identifier, binding, and operator-prefixed key spaces, they leave a union structurally unambiguous while remaining opaque labels with no positional force.
A query retrieves a collection, a multi-valued property (Section 3.1) of the enclosing resource. In the template, that property maps to a tuple of two parts: a per-item element that shapes each item, and an optional selection (Section 5.7) that filters, sorts, and paginates the collection as a whole. A single-valued property cannot take such a tuple; a query targeting one MUST be rejected.
The per-item element takes one of three forms, giving three query variants:
A projection is a collection template whose keys are bindings: each pairs an expression with a result name rather than naming an individual property, evaluated per item or, when an aggregate binding is present, per group (Section 5.8.2.1). Binding result names MUST be unique within a projection.
{
"items": [
{
"vendor=vendor": {
"id": "",
"name": ""
},
"items=count:": 0,
"avgPrice=avg:price": 0
}
]
}
Each binding's value is a model (Section 5.1), taking one of three forms:
Each binding yields one cell per output row, holding a single value: a literal, a reference (optionally expanded to a resource), or a dictionary. A union never appears in a cell; a union binding's cell holds one of its matching branch's values. Where the matching branch resolves to a localised property (Section 4.3), that cell is a dictionary. This per-cell decomposition is what keeps a binding representable when its expression reaches localised text downstream of a union (Section 5.8.1): the mixed effective type is split across cells, the localised value occupying its own dictionary cell rather than mixing into a value set (Section 4.2), which has no shape for that combination.
A projection emits one row per combination of its bindings' resolved values: a multi-valued binding fans out into a row per value, the result being the cross-product across bindings (a structural locale binding excepted, counting as a single value; a coalesced multi-valued binding fans out per value, like any multi-valued placeholder). A binding that resolves to no value preserves the row rather than collapsing the cross-product to zero; its label MUST be omitted from that row.
The rows of a projection are distinct: rows sharing the same combination of cell values MUST collapse into one, so a
projection yields the set of distinct binding tuples, not a multiset. Two cells are equal when they hold equal literals,
references to the same resource (whether or not expanded), or equal dictionaries; a pair of omitted labels (an absent
binding on both rows) counts as equal, matching the undefined-key rule of grouping (Section 5.8.2.1). Distinctness
spans the whole collection, collapsing both cross-product fan-out duplicates and equal tuples contributed by different
items; a projection that must keep otherwise-equal items apart includes an identifying binding such as id, which makes
each item's row unique.
A structural locale binding counts as a single value and does not fan out rows, however many tags it holds. The label
binding below yields the full { <tag>: <value>, … } map for the matching tags as one cell:
{
"items": [
{
"id=id": "",
"label=title": {
"*": ""
}
}
]
}
Faceted search is one of the major use cases supported by aggregate projections: the following examples express three common facet patterns.
A category breakdown groups items by a property and counts each group. The projection pairs a non-aggregate binding,
category=category, which becomes the grouping key (Section 5.8.2.1), with the aggregate count=count:, which counts
the items in each group; the sibling selection's ^count: then orders the groups by descending count (Section 5.7.5).
The query returns one row per distinct category value, each carrying its item count:
{
"items": [
{
"category=category": "",
"count=count:": 0
},
{
"^count:": "desc"
}
]
}
A value range reduces the whole collection to a single row carrying the minimum and maximum of a comparable property. With no non-aggregate binding there is no grouping key, so the two aggregates range over every matching item:
{
"items": [
{
"min=min:price": 0,
"max=max:price": 0
}
]
}
A result total reduces the collection to a single row holding the number of matching items. The empty-path aggregate
count=count: counts rows rather than property values (Section 5.8.2.1):
{
"items": [
{
"count=count:": 0
}
]
}
A selection is a collection query's optional second element (Section 5.5): a map of constraints that filters, sorts,
and paginates the collection as a whole. Each constraint key uses the "{operator}{expression}" syntax, where the
operator determines the constraint type and the expression (Section 5.8) identifies the target property or computed
value.
A constraint expression MAY include an aggregate transform (Section 5.8.2.1). In a grouped query (Section 5.8.2.1), aggregate constraints filter groups and non-aggregate ones filter items; in an ungrouped query, an aggregate constraint reduces over each item's own values (Section 5.8.1), filtering, sorting, or ranking the items by the reduced value.
Each operator below tabulates the target types and cardinalities it supports. Over a single-type target, a constraint whose target type or cardinality is not listed for its operator, or whose bound or option type does not match the target's resolved type, is unsupported and processors MUST reject it.
Over a union-typed target (Section 3.1), a bound or option is resolved against the declared variants exactly as a state
value is on ingress (Section 3.3): being expected disjoint (Section 3.1), the variants admit it on at most one branch,
and the processor maps it to that branch's type. A bound or option matching no variant is unsatisfiable and MUST be
rejected, and one matching several is ambiguous and MUST be rejected (a null option is typeless and exempt, Section
5.7.3). The matched branch fixes the regime: a value matches when it lies on that branch and the operator's regime holds
there, per the tables below; values on other branches are ignored (Section 5.8.2) and contribute no match. Since a
comparison bound is a literal (Section 5), comparison (Section 5.7.1) selects the literal branch its bound maps to; text
search (Section 5.7.2) is the exception, its operand a plain search string rather than a typed bound, so it selects no
single branch but applies to every xsd:string branch at once (Section 5.7.2); set matching and sort focus (Sections
5.7.3 and 5.7.4) carry options, so the option's kind selects the branch: a literal option a literal branch, a reference
option a reference branch, and a tagged option the localised text branch, with a null option selecting none and
matching absence (Section 5.7.3); sort (Section 5.7.5), which carries no bound, instead orders all branches within the
total order's tiers, selecting none. Appendix A.4.1's type guards realise this branch selection on the target backends.
A constraint that no resource can satisfy (for example null beside a present value under !, Section 5.7.3) MAY be
short-circuited to an empty result set rather than evaluated.
The operators follow the division drawn by the type system (Section 3), which maps values to processing types for
ordering and computation but leaves references and structurally addressed localised text outside that space, matched by
equality alone; a coalesced localised target (Section 6) is instead constrained as an ordinary xsd:string of
corresponding cardinality, a multi-valued one admitted existentially:
xsd:string.reference and
text (Section 4), matched by equality on their raw form, the only regime available to values outside the processing
space; a literal target is matched in its processing type, as for the ordered operators.Cardinality (single- or multi-valued) constrains the target property, not its type, and each table states it separately.
| Constraint | Semantics |
|---|---|
"<expression": literal |
at least one expression value is strictly less than the bound |
">expression": literal |
at least one expression value is strictly greater than the bound |
"<=expression": literal |
at least one expression value is less than or equal to the bound |
">=expression": literal |
at least one expression value is greater than or equal to the bound |
| Target Type | Cardinality | Notes |
|---|---|---|
xsd:boolean |
single- or multi-valued | ordered false < true |
numeric |
single- or multi-valued | standard numeric ordering |
xsd:string |
single- or multi-valued | Unicode codepoint collation |
temporal |
single- or multi-valued | chronological ordering |
Note that a query literal carries only IEEE-754 double precision (Section 3), so numeric comparison and equality
(Section 5.7.3) resolve only to that precision. On a backend storing finer-than-double numerics, a boundary match MAY
differ. This gap is left open by design, since forcing comparison into xsd:double would cast the stored column and
defeat numeric indexes, contrary to the native-alignment principle (Appendix A.1.2).
| Constraint | Semantics |
|---|---|
"~expression": string |
at least one expression value contains every token as a case-insensitive substring |
| Target Type | Cardinality | Notes |
|---|---|---|
xsd:string |
single- or multi-valued | diacritics-sensitive |
xsd:string branch rather than selecting one.| Constraint | Semantics |
|---|---|
"?expression": options |
at least one expression value equals an option |
"!expression": options |
every option equals at least one expression value |
| Target Type | Cardinality | Notes |
|---|---|---|
| literal | single- or multi-valued | literal equality |
| reference | single- or multi-valued | reference equality |
| text | single- or multi-valued | tagged-value equality |
null, a literal, or a reference.? and ! alike, an
empty set carries no options to match and MUST be ignored, leaving the collection unconstrained.! suits multi-valued targets; a single-valued one satisfies only a single-element set.null option matches an absent value, so the operator's quantifier does the rest: under ? it adds an "or unset"
alternative; under ! every option must hold, so [null] selects unset properties and null beside a present value
matches nothing.| Constraint | Semantics |
|---|---|
"+expression": options |
resources whose expression value is in the option set rank first |
| Target Type | Cardinality | Notes |
|---|---|---|
| literal | single-valued | literal equality |
| reference | single-valued | reference equality |
| text | single-valued | tagged-value equality |
null option ranks resources whose value is absent first, mirroring the absent-value match of set matching
(Section 5.7.3).A common use of sort focus is to prioritise a user's selections within a discrete facet: the chosen option values rank first while the rest of the listing keeps its order, keeping selected items visible without filtering the others out.
| Constraint | Semantics |
|---|---|
"^expression": order |
sorts results by the expression value, in the direction and precedence given by order |
| Target Type | Cardinality | Notes |
|---|---|---|
xsd:boolean |
single-valued | ordered by the value-ordering rules |
numeric |
single-valued | ordered by the value-ordering rules |
temporal |
single-valued | ordered by the value-ordering rules |
xsd:string |
single-valued | ordered by the value-ordering rules |
order sorts ascending, a negative one descending; its absolute value gives 1-based precedence (1 is
highest) among multiple sort keys; "asc" and "desc" abbreviate +1 and -1; zero is ignored.undefined first, then by processing type (xsd:boolean <
numeric < temporal < xsd:string), then within each type by the comparison rules (Section 5.7.1). The order
sign reverses this entire order, the undefined tier and the processing-type ranking included, so a descending key
places undefined last. Ranking by processing type keeps
temporal a tier distinct from xsd:string, ordering comparable temporal values ahead of plain strings, though
egress surfaces both as JSON strings (Section 3), so a union-typed key sorts deterministically across mixed-type
values.| Constraint | Semantics |
|---|---|
"@": integer |
skip the first N results |
"#": integer |
return at most N results |
@: 0 applies no offset and #: 0 imposes no limit.^ criteria with an implementation-defined
deterministic tiebreaker so the result order is total, whatever the projection or grouping (Section 5.8.2.1) in
effect; page boundaries are then stable across requests for the same query over unchanged data, while concurrent
modification between page fetches is not covered (inherent to offset pagination).An expression is a property path optionally preceded by a pipeline of transforms, targeted by projection (Section 5.6) and selection (Section 5.7) keys.
name simple property
user.profile.email nested property path
sum:items.price computed sum
round:avg:scores pipeline: inner transform applied first
count: aggregate over collection (empty path)
A path is a possibly empty, dot-separated sequence of property identifiers (for example, vendor.name) that
navigates from a resource through its properties to a set of values. Processors MUST reject paths that reference unknown
properties, those without an expected type (Section 3.1).
A path is resolved by walking its steps from left to right over a working set of values. The set starts as the
collection's items (or the single target item). Each step expands it: for every value in the set, the step resolves
the named property and adds the resulting values to the next set, omitting undefined. An empty path has no steps, so
the set stays unchanged.
Each step contributes values per input value according to the property it resolves:
xsd:string of corresponding cardinality; under structural access (Section 6), through a locale placeholder or
binding (Sections 5.3 and 5.6) or a tagged option set (Section 5.7.3), contributes the dictionary whole, tags
preserved.The set remaining after the last step is the path's result: an empty set resolves to undefined, a single value to that
value, and several to an array. Appendix A.3 maps path resolution onto the target backends.
A path's effective type is that of its final step's property, or, for an empty path, the item type of the collection
it ranges over; a union-typed step yields a mixed-type set. Where a step downstream of a union-typed step resolves one
property under a distinct declaration per branch, union coherence keeping it a single property (Section 3.2), the
effective type is the disjunction of those per-branch types. That disjunction can include localised text, where a
branch's resolved property is localised (Section 4.3) and addressed structurally
(Section 6); such a branch is expressed by a locale variant (Section 5.4). A localised step yields xsd:string instead
under coalesced access (Section 6.2), at the property's per-tag cardinality.
An effective type is a derived type, not an expected type (Section 3.1), and is never materialised as a value set (Section 4.2): a mixed effective type that includes localised text is consumed per branch by a selection (Section 5.7) and per cell by a projection (Section 5.6), each value keeping its own type, so it never forms a single serialised value set. Processors MUST NOT reject a path on the ground that its effective type mixes localised text with other branch types.
A path's effective cardinality follows statically from its steps:
A pipe is a possibly empty sequence of colon-terminated transform identifiers (for example, round:avg:) that maps
the values a path produces (Section 5.8.1) through named functions. Processors MUST reject pipes that reference unknown
transforms.
A pipe is evaluated by applying its transforms right to left (functional composition) to the path's result: each transform consumes the previous output and produces the next. An empty pipe leaves the result unchanged.
Each transform contributes to the result according to its kind:
undefined and the values of any incompatible
branch (well-typedness, below) before computing (see Appendix A.4.2 for filtering on target backends); if none remain,
the empty-set rules of Section 5.8.2.1 applyundefined input, yields undefined, which propagates through the rest of the pipe (see Appendix A.4.1 for
type guards on target backends).The set left after the last transform is the pipe's result, mapped to undefined, a value, or an array as for a path
(Section 5.8.1).
The supported transform set is defined so that each transform has a well-defined counterpart across the target backends (Appendix A), keeping it portable. Type semantics follow the processing type system (Section 3).
A pipe is well-formed only if it applies at most one aggregate transform; processors MUST reject ill-formed pipes (see Appendix A.4.4 for build-time rejection on target backends).
Transforms MUST be well-typed: a transform MAY be applied only to a value compatible with its declared domain, the compatibility being resolved against the type its input step produces (Section 5.8.1). A transform whose domain admits no value of that type is incompatible, and processors MUST report the incompatibility and reject the pipe (see Appendix A.4.4 for build-time rejection on target backends). Where the input type is a union (Section 5.4), at least one branch MUST be compatible with the domain for the transform to be well-typed; the transform then applies to the values of the compatible branches and ignores those of the incompatible ones, which contribute no value (scalar) or drop from the reduction (aggregate). Appendix A.4.5 maps this branch selection onto the target backends.
A pipe's effective type is the range of its outermost transform. Each transform maps an in-domain value to the range
declared in the following sections; an incompatible-branch value, or an already undefined input, maps to undefined.
A pipe's effective cardinality follows statically from its transforms:
count and sum, which always yield a value (0 on the
empty set) and so have minimum one.| Name | Semantics | Domain | Range | Empty set |
|---|---|---|---|---|
count |
Count the values | any value | xsd:integer |
0 |
min |
Select the minimum value | xsd:boolean |
xsd:boolean |
undefined |
numeric |
same as domain | undefined |
||
xsd:string |
xsd:string |
undefined |
||
temporal |
same as domain | undefined |
||
max |
Select the maximum value | xsd:boolean |
xsd:boolean |
undefined |
numeric |
same as domain | undefined |
||
xsd:string |
xsd:string |
undefined |
||
temporal |
same as domain | undefined |
||
sum |
Sum the values | numeric |
same as domain | 0 |
avg |
Average the values | xsd:float |
xsd:float |
undefined |
xsd:double |
xsd:double |
undefined |
||
other numeric |
xsd:decimal |
undefined |
Unlike the other aggregates, count counts its values by presence, references included, and does not map its input into
the processing space (Section 3); hence its domain is any value rather than a processing type.
Over a mixed numeric input set, an aggregate computes in the least common type under XPath 2.0 numeric promotion
[W3C.REC-xpath-functions], which also fixes its range; the promotion aligns natively across the target backends
(Appendix A.1.2).
Bag semantics. An aggregate counts every contributing value, with no implicit deduplication. Set semantics (Section 4.2) dedupes only within a single property's stored array, not across the aggregate's input, so equal values count separately, whether from different rows or from a multi-valued fan-out within one row.
When an aggregate expression appears among the projection's bindings, the query is evaluated under **grouped semantics **; otherwise the query is ungrouped: every item is evaluated independently, and an aggregate expression in the selection reduces over the values the path gathers from the item under evaluation (Section 5.8.1), so the constraint filters, sorts, or ranks the items by that reduction.
The ungrouped reduction supports cardinality constraints over plain templates; the query below retrieves the vendors carrying at least three products:
{
"vendors": [
{
"id": "",
"name": ""
},
{
">=count:products": 3
}
]
}
Under grouped semantics, each operator's role is determined by whether its expression references an aggregate transform:
Grouping is fixed by the projection alone and is never inferred from a sort key: a non-aggregate ordering or focus expression MUST match, verbatim, the expression of an existing grouping-key binding; binding names are not expressions, and processors MUST reject an expression that matches no key.
Grouping applies to the fanned-out rows of Section 5.6: a multi-valued grouping-key binding fans an item into one row,
and hence one group, per value. Rows sharing the same grouping-key values collapse into a single group. An undefined
grouping-key value (an absent or unresolved binding) groups with itself: all rows missing that key form one group, whose
output row omits the key, extending the no-value rule of the ungrouped case (Section 5.6). Every target backend realises
this natively through
GROUP BY null-grouping (Appendix A.4.6).
Aggregate filter and ordering constraints are independent of any projected bindings: an aggregate MAY appear in a constraint without being projected, and a projected aggregate MAY appear without being constrained. Appendix A.4.6 summarises how this partition maps onto the reference query backends.
| Name | Semantics | Domain | Range |
|---|---|---|---|
abs |
Take the absolute value | numeric |
same as domain |
floor |
Round down to an integral value | numeric |
same as domain |
ceil |
Round up to an integral value | numeric |
same as domain |
round |
Round to the nearest integral value | numeric |
same as domain |
| Name | Semantics | Domain | Range |
|---|---|---|---|
lower |
Convert to lowercase | xsd:string |
xsd:string |
upper |
Convert to uppercase | xsd:string |
xsd:string |
length |
Count the characters | xsd:string |
xsd:integer |
| Name | Semantics | Domain | Range |
|---|---|---|---|
year |
Extract the year | temporal |
xsd:integer |
month |
Extract the month | temporal |
xsd:integer |
day |
Extract the day | temporal |
xsd:integer |
hours |
Extract the hours | temporal |
xsd:integer |
minutes |
Extract the minutes | temporal |
xsd:integer |
seconds |
Extract the seconds | temporal |
xsd:decimal |
A temporal transform yields undefined when the input value lacks the component it extracts.
A localised property (Section 4.3) is retrieved or constrained according to the form of the value used to address it:
The same property MAY be accessed either way. Structural access is unaffected by language negotiation; coalesced access depends on it (Section 6.3).
Note that the two forms call for different tagging granularities, so content SHOULD be tagged according to its usual access pattern:
de-AT is therefore never reached.Dictionary coalescing (Section 6.2) is driven by a language priority: an ordered list of exact language tags [RFC5646] against which each localised value is resolved. The priority is drawn from two sources:
Accept-Language header field of the request [RFC9110], a list of
quality-weighted basic language ranges [RFC4647];Processors MUST derive it, treating an absent or empty Accept-Language header as *:
de-AT becomes de; sr-Cyrl-RS becomes sr-Cyrl);lang-* becomes the language-and-script prefix of lang (fr-* becomes fr);* becomes the fallback chain;und as the final, language-neutral fallback;Each range thus resolves to a single tag, so the list remains short and contains no wildcards. The lang-* and
other-wildcard cases in step 4 are defensive: a conformant Accept-Language carries only basic ranges [RFC4647], which
never take those forms, but reducing them gracefully keeps a lenient or non-HTTP input well-defined. The fallback chain,
being empty by default, imposes no language bias; servers SHOULD be configured with a chain of their primary languages,
so that content in them remains reachable when a request matches nothing else.
Accept-Language: sr-Cyrl-RS, de-AT;q=0.9, *;q=0.5, it;q=0 (with fallback chain: fr, es)
1 parse (sr-Cyrl-RS,1) (de-AT,.9) (*,.5) (it,0)
2 drop q=0 (sr-Cyrl-RS,1) (de-AT,.9) (*,.5)
3 order sr-Cyrl-RS, de-AT, * (already quality-ordered)
4 reduce sr-Cyrl | de | fr, es (region dropped, script kept; `*` replaced by fallback)
5 append und
6 dedupe [sr-Cyrl, de, fr, es, und]
Coalescing resolves a property's dictionary (Section 4.3) against a client-defined language priority list
(Section 6.1): it selects the first priority tag present in the dictionary, then gathers the value or values bound to
that tag, or undefined if the dictionary holds none of the priority's tags. The coalesced result carries the
property's per-tag cardinality and is an ordinary xsd:string value or set thereafter, so coalescing adds no boundary
of its own (Appendix A.5).
Coalescing may run without a negotiated priority (Section 6.1), for example outside an HTTP request. A priority supplied
out of band is taken literally: the trailing und belongs to the negotiation derivation (Section 6.1), not to
coalescing, so processors MUST NOT append it, and a caller wanting the language-neutral fallback appends und itself.
Where no priority is supplied at all, processors MUST default the whole priority to the single tag und, so that an
unscoped request still resolves language-neutral values, such as a proper name stored under und. This whole-priority
default is distinct from the trailing und that Section 6.1 appends to each negotiated priority.
Processors MUST substitute the coalesced value or values for the property and apply the ordinary string semantics of the targeting construct, exactly as for any other string property of corresponding cardinality.
Servers MUST include a Vary header field naming Accept-Language in any response that depends on a coalesced value
(Section 6.2), whether through a coalesced label in the response body (Section 5.3) or through the result set (filtering
or ordering), so that such responses are cached per language.
Coalescing resolves each cell independently, so a single response MAY mix language tags across its cells. Servers MUST
NOT describe such a response with a response-wide Content-Language header field: the field describes the language of
the representation as a whole [RFC9110] and cannot convey which language each cell resolved to.
This document has no IANA actions.
Servers SHOULD impose limits on template nesting and query paths depth, the number of expanded properties, and collection result sizes to prevent denial of service through excessively complex queries.
Aggregate transforms (Section 5.8.2.1) can be particularly expensive on large collections; servers SHOULD support disabling them on a per-endpoint or per-role basis when the computational cost is unacceptable.
Servers MUST NOT let a query read or traverse any property path beyond what the requesting client is authorised to access. Template-driven retrieval does not bypass access control; it constrains the query within the bounds of the client's permissions, whether a value is returned directly, expanded from a reference, or only summarised by an aggregate (Section 5.8.2.1), which MUST therefore be computed solely over values the client may read.
Rejection responses can themselves disclose structure. The validation rules of this document reject unknown properties (Section 5.8.1), type mismatches (Section 3.3), type-incompatible transform pipes (Section 5.8.2), and malformed templates (Section 5.2); verbose errors confirm the existence and types of properties a client is not authorised to know. Servers SHOULD limit the detail of a rejection to what the client is authorised to learn.
Implementations that resolve internal IRIs against a base IRI MUST validate that the resolved IRI remains within the expected authority scope. Malformed or adversarial IRI values MUST be rejected.
The target backends are the query engines against which conforming processors are expected to emit queries:
A query builder is the component of a processor (Section 2) that translates a validated retrieval request into queries for the target backends, as specified in Section 5; the subsections below are written from its perspective.
This appendix maps each protocol feature to its realisation across the target backends, documenting how each natively handles the cases covered by the adopted semantics (Section 5.8.2) and where query-level normalisation is required to reconcile their differences.
Property paths, transform pipes, type promotion, invalid value handling, and substring search align natively across all target backends without normalisation; the remaining cases are detailed in the subsections below.
The temporal type (Section 3) admits the point-in-time XSD datatypes xsd:dateTime, xsd:date, and xsd:time, the
temporal datatypes that are component-extractable and totally ordered (the latter under XPath 2.0's implicit-timezone
comparison) across the shared XSD 1.0 / XPath 2.0 basis of the target backends.
| Aspect | SQL:2011 | GQL:2024 | SPARQL 1.1 |
|---|---|---|---|
| Ordering | native DATE/TIME/TIMESTAMP |
native temporal order | XPath operators |
| Extraction | EXTRACT per type |
.year per type |
YEAR() on xsd:dateTime |
Ordering aligns across the target backends under the XPath 2.0 reference semantics (Section 5.7.1).
Component extraction is uniform across the target backends, with two specifics:
seconds as an integer plus separate sub-second fields rather than xsd:decimal; the query builder
recombines them into one fractional value, which egress collapses to JSON number (Section 3) as for any XSD numeric,
so the surfaced result agrees.xsd:dateTime only. To extract from an xsd:date or xsd:time, the query
builder synthesises an xsd:dateTime, supplying default values for the absent components (a zero time-of-day for a
date, a fixed reference date for a time), then applies the xsd:dateTime accessor. The synthesised components are
never surfaced: an accessor for a component the original type lacks (for example hours of a date) is undefined
(Section 5.8.2.4), so the choice of default is immaterial.The other XSD 1.0 temporal datatypes are excluded from the temporal type: xsd:duration is only partially ordered
(XPath defines equality only), and the Gregorian partials (xsd:gYearMonth, xsd:gYear, xsd:gMonthDay, xsd:gMonth,
xsd:gDay) are equality-only with no accessors. Neither lies in the portable ordered-and-extractable intersection, so
such values, if carried, are opaque xsd:string (Section 3). Temporal datatypes added by XSD 1.1 are excluded as well:
xsd:dateTimeStamp, though a totally ordered subtype of xsd:dateTime, is not portable across the target backends, as
SPARQL 1.1's operator mapping is defined over xsd:dateTime without subtype substitution, so the literal is an
unrecognised datatype on some engines: its comparison a type error, its ordering implementation-defined, and
datatype() yields the subtype IRI rather than xsd:dateTime. Such values are likewise opaque xsd:string.
| Scenario | SQL:2011 | GQL:2024 | SPARQL 1.1 |
|---|---|---|---|
| Integer + decimal | implicit → decimal | implicit → decimal | implicit → decimal |
| Decimal → float | implicit → float | implicit → float | implicit → float |
| String → number | type error | type error | type error |
integer promotes to decimal in arithmetic and aggregates, with further promotion to float or double following
the same hierarchy. Temporal types (date, time, dateTime) are distinct with no implicit promotion, and
string/number conversions are never implicit; all backends reject implicit conversion consistently, so no normalisation
is required beyond numeric promotion, which aligns natively.
| Aspect | SQL:2011 | GQL:2024 | SPARQL 1.1 |
|---|---|---|---|
| Default collation | column/database-defined | lexicographic | not specified |
| Codepoint order | requires COLLATE |
not documented | references XPath |
lower / upper |
follows collation | locale-free | locale-free |
min / max |
follows collation | lexicographic | follows XPath rules |
The adopted codepoint collation (Section 5.7.1) is not the default on most backends, so the query builder must select it explicitly; see Codepoint Collation (Appendix A.2.4). GQL:2024 uses lexicographic ordering, which aligns with codepoint order for ASCII content but is not formally documented for the full Unicode range.
A comparison against an absent target satisfies no operator: every backend yields a non-true result and drops the resource, independent of where the total ordering (Appendix A.2.3) would place an absent value.
| Backend | Comparison with an absent value |
|---|---|
| SQL:2011 | col <op> bound over NULL is UNKNOWN; WHERE excludes the row |
| GQL:2024 | comparison with null is null; WHERE excludes the row |
| SPARQL 1.1 | comparison with an unbound variable errors; FILTER drops the row |
The rule that an absent target satisfies no comparison (Section 5.7.1) thus holds natively across all target backends, with no normalisation.
The total ordering places undefined in the lowest tier (Section 5.7.5): it sorts first under an ascending key and last
under a descending one. This direction-sensitive placement is the only sort divergence among backends:
| Backend | undefined (null / empty) placement |
|---|---|
| SQL:2011 | NULLS FIRST ascending, NULLS LAST descending (default is implementation-defined) |
| GQL:2024 | none: null sorts lowest natively, tracking direction |
| SPARQL 1.1 | none: an unbound value sorts lowest natively, tracking direction |
A union-typed sort key may resolve to values of different types across resources. No backend mandates any order between
value types: SPARQL 1.1 leaves cross-type comparison a type error and its ORDER BY placement implementation-defined,
and SQL:2011 and GQL:2024 fix no portable cross-type order either. The query builder therefore imposes one, emitting a
synthetic discriminator, ORDER BY <undefined-flag>, <type-rank>, <value>, with the type rank xsd:boolean <
numeric < temporal < xsd:string (Section 5.7.5). This ranking is a deliberate qest convention, not a backend
requirement: it places comparable temporal values (Appendix A.1.1) ahead of plain strings, including the opaque
temporals (xsd:duration, the Gregorian partials) that fall in the string tier.
The query builder assigns <type-rank> from each value's native type, which separates temporal from xsd:string
before egress collapses both to a JSON string (Appendix A.1.1). Each backend exposes the native type:
datatype() in SPARQL 1.1.The temporal tier is thus realisable identically to the boolean, number, and string tiers, with no backend unable to separate the two.
The adopted codepoint collation (Section 5.7.1) is not the default on most backends. The query builder must explicitly select Unicode codepoint collation for string comparison, ordering, and case mapping.
| Backend | Selecting codepoint collation |
|---|---|
| SQL | specify COLLATE clause or use codepoint-ordered column collation |
| GQL:2024 | none: lexicographic ordering aligns for common cases |
| SPARQL 1.1 | specify codepoint collation: default is implementation-defined |
| Aspect | SQL:2011 | GQL:2024 | SPARQL 1.1 |
|---|---|---|---|
| Substring | LIKE '%tok%' |
CONTAINS |
CONTAINS |
| Case folding | LOWER() |
lower() |
LCASE() |
A search token matches when the value contains it as a substring, compared over a lower-cased operand for case-insensitivity (Section 5.7.2). For each token the query builder emits one substring test and conjoins the per-token results; token order is not significant. No regular expression, word boundary, or whitespace handling is required, so the filter is realisable over the raw stored value across all target backends with the native substring predicates above, assuming no ingest normalisation:
LOWER(v) LIKE '%tok%'.lower(v) CONTAINS 'tok'.CONTAINS(LCASE(v), 'tok').Substring rather than word-prefix matching is the portable intersection: detecting a word boundary needs a
whitespace-class metacharacter that LIKE, CONTAINS, and STRSTARTS / CONTAINS lack, and GQL:2024 has no
regular-expression facility, so word-prefix semantics are not realisable cross-backend without mandated indexing.
Diacritics-insensitive matching, namely Normalization Form D (NFD) decomposition plus stripping combining marks, is not feasible in a cross-backend way: no in-query approach works uniformly across the target backends without application-level pre-processing at storage time:
| Backend | In-query NFD + strip combining marks? |
|---|---|
| SQL:2011 | no standard NFD function: requires pre-computation |
| GQL:2024 | NORMALIZE(v, NFD) decomposes, but no standard combining-mark strip |
| SPARQL 1.1 | no standard NFD function: requires extension or pre-computation |
Application-level normalisation would require dual storage (original plus normalised form), which is not acceptable. Diacritics-sensitive matching aligns with the cross-backend intersection principle.
| Aspect | SQL:2011 | GQL:2024 | SPARQL 1.1 |
|---|---|---|---|
| Navigation | JOIN + column ref | relationship traversal | triple pattern chains |
| Missing value | NULL (outer join) |
null |
unbound (no binding row) |
| Unknown property | compile-time error | null |
unbound |
All backends propagate "no value" through path steps → undefined. Except for SQL, no backend distinguishes a missing
property from a null-valued one → undefined. SQL rejects unknown columns at compile time; see Unknown Property Guards
(Appendix A.3.4). Because processors reject paths that reference unknown properties (Section 5.8.1), no backend code is
emitted for the offending path.
| Aspect | SQL:2011 | GQL:2024 | SPARQL 1.1 |
|---|---|---|---|
| Multi-valued result | joined row set | collected list / rows | multiple binding rows |
| Ordering guarantees | undefined without ORDER BY | undefined | undefined |
| Empty collection | zero joined rows | empty list / zero rows | zero binding rows |
All backends naturally produce multi-valued results → JSON arrays.
Union properties are transparent at the backend level: path resolution collects whatever values are available across all
branches, each value belonging to exactly one branch (the variants being expected disjoint, Section 3.1), and missing
branches contribute no values. The "no property / no value → undefined" rule applies per branch.
Processors reject paths that reference unknown properties (Section 5.8.1) at query-building time, so no backend code is emitted for them and the native behaviours each backend would otherwise apply do not arise.
| Backend | Handling |
|---|---|
| SQL | rejected at query-building time; never emitted |
| GQL:2024 | rejected at query-building time; never emitted |
| SPARQL 1.1 | rejected at query-building time; never emitted |
| Aspect | SQL:2011 | GQL:2024 | SPARQL 1.1 |
|---|---|---|---|
| Temporal | EXTRACT per type |
.year per type |
YEAR() on dateTime |
| Wrong type | error (not NULL) | type error (→ null) | type error (→ unbound) |
| Null input | NULL propagated |
null propagated |
error (→ unbound) |
A scalar transform is applied only to compatible values (Section 5.8.2): a wholly incompatible pipe is reported and
rejected at query-building time (Appendix A.4.4), while over a union-typed path the query builder admits the compatible
branches and resolves the incompatible-branch values to undefined. The builder secures this rather than relying on
native behaviour, since most backends would otherwise raise the type errors tabulated below:
| Scenario | SQL:2011 | GQL:2024 | SPARQL 1.1 |
|---|---|---|---|
abs(string) |
type error | runtime type error | type error (→ unbound) |
floor(string) |
type error | runtime type error | type error (→ unbound) |
lower(number) |
type error | runtime type error | type error (→ unbound) |
length(number) |
type error | type error | type error (→ unbound) |
year from time |
error | type error | type error (→ unbound) |
hours from date |
error | type error | type error (→ unbound) |
| any scalar on null | NULL (propagated) |
null (propagated) |
error (→ unbound) |
The query builder admits only compatible values to the transform, by one of two means:
undefined (for example SQL NULL) at query-building time
instead of the offending call;undefined:| Backend | Domain guard |
|---|---|
| SQL:2011 | select the variant's typed column (union held per type) |
| GQL:2024 | v IS TYPED <type> guard |
| SPARQL 1.1 | FILTER / IF on datatype(?v) / isNumeric(?v) |
GQL scalar functions throw on a mismatch, so the guard is mandatory there to obtain undefined rather than a query
error; SPARQL 1.1, by contrast, resolves a scalar mismatch to unbound natively, so a guard is needed there only for
aggregates. The same per-value guards realise constraint matching over union-typed targets (Section 5.7), admitting the
values of the single branch the bound or option is mapped to and excluding the rest.
The year from time and hours from date rows above are a finer, subtype-level case: the value is temporal and so
compatible with the transform's domain, but lacks the component extracted (Section 5.8.2.4). The query builder
short-circuits such a pipe to undefined rather than emitting the component call, which also sidesteps engines that
coerce instead of erroring (for example, SQL EXTRACT(HOUR FROM <date>) returning 0).
Before computing, the query builder restricts each aggregate's input to its compatible values: undefined and nulls
drop natively, while incompatible-branch entries (Section 5.8.2) are removed by the same branch guard as for scalar
transforms (Appendix A.4.1), since SUM over a typed column (SQL:2011) and SPARQL SUM would otherwise raise a type
error rather than skip. Aggregation uses bag semantics (Section 5.8.2.1): no aggregate applies a DISTINCT quantifier
to its own input implicitly (COUNT(DISTINCT) and SUM(DISTINCT) are never emitted), unlike the row-level
SELECT DISTINCT that realises the distinct-row rule for ungrouped projections (Appendix A.4.6). The target backends
match the protocol's bag semantics natively through COUNT / SUM without DISTINCT; clients needing distinct-value
aggregates obtain them through grouping (Section 5.8.2.1).
After invalid values are excluded (Section 5.8.2), aggregates over an empty input set produce:
| Aggregate | Protocol | SQL:2011 | GQL:2024 | SPARQL 1.1 |
|---|---|---|---|---|
count |
0 |
0 |
0 |
0 |
sum |
0 |
NULL (differs) |
0 |
0 |
avg |
undefined |
NULL |
null |
0 (differs) |
min / max |
undefined |
NULL |
null |
error |
count / sum → 0 and avg / min / max → undefined match the target backends natively, except SQL SUM and
SPARQL AVG; see Empty-Set Aggregates (Appendix A.4.3) for the patches.
Two backends deviate from the protocol's empty-set semantics (Appendix A.4.2) and require client-side normalisation: SQL
SUM returns NULL instead of 0, and SPARQL AVG returns 0 instead of an unbound binding.
For SQL sum:
| Backend | Normalisation |
|---|---|
| SQL | COALESCE(SUM(col), 0): standard SQL construct |
| GQL:2024 | none: sum() returns 0 natively |
| SPARQL 1.1 | none: SUM returns 0 natively |
For SPARQL avg, Section 18.5.1.4 of [W3C.REC-sparql11-query] defines Avg piecewise with
Avg(M) = "0"^^xsd:integer when Count(M) = 0, a literal 0
rather than a derivation from Sum/Count. The protocol requires undefined. Wrap the binding to force an unbound
projection on the empty case:
| Backend | Normalisation |
|---|---|
| SQL | none: AVG returns NULL natively |
| GQL:2024 | none: avg() returns null natively |
| SPARQL 1.1 | IF(COUNT(?x) > 0, AVG(?x), <http://www.w3.org/2001/XMLSchema#integer>("unbound")) AS ?avg |
The cast of a non-numeric string to xsd:integer fails, raising an error that propagates as an unbound binding. The
more obvious 1/0 divisor is brittle: some engines (for example, RDF4J) eagerly evaluate both IF branches and surface
the BigInteger divide by zero exception as an HTTP 500 rather than as an unbound projection.
Structural composition is resolved entirely at query-building time. The query builder validates the structural rules ( scalar after scalar, aggregate after scalar, scalar after aggregate) and rejects structurally invalid pipes (aggregate after aggregate) outright, before emitting any backend-specific code. No backend-level normalisation is required for structural composition.
Type compatibility is a separate well-typedness rule (Section 5.8.2), checked in the same query-building pass. The query builder walks the pipe from its innermost transform outward, and for each transform intersects its declared domain with the effective type of its input step (the path's type for the innermost transform, the preceding transform's range otherwise). It rejects the pipe when an intersection is empty, reporting the incompatibility before emitting any backend-specific code. A non-empty intersection admits the transform: covering the whole input type needs no guard, while covering only some branches of a union-typed input (Section 5.4) admits the transform on those branches and marks the rest for guarding.
The builder then uses that shape information to place the branch guards (scalar, Appendix A.4.1; aggregate, Appendix
A.4.2) that filter the incompatible-branch values to undefined or exclude them from the reduction. No backend-level
normalisation is required for type compatibility itself: the rejection is emitted by the builder, and the guards are the
only backend-specific code the check produces.
A wholly incompatible transform is reported and rejected before query building (Appendix A.4.4). A transform compatible
on at least one branch of a union-typed input is admitted, and the query builder secures the incompatible branches from
its shape information, never emitting a call a backend would reject: a scalar yields undefined for their values and an
aggregate drops them (Section 5.8.2).
| Backend | Handling |
|---|---|
| SQL:2011 | static short-circuit to NULL, or typed-column selection |
| GQL:2024 | IS TYPED guard (functions throw otherwise) |
| SPARQL 1.1 | mismatch resolves to unbound natively; guard for aggregates |
See Appendix A.4.1 (scalar) and A.4.2 (aggregate) for the filtering mechanism.
Selection constraints (Section 5.7) partition by aggregate-awareness into pre-grouping filters, grouping keys, post-grouping (aggregate) filters, and, in ungrouped queries, per-item reductions (Section 5.8.2.1):
| Rule | SQL:2011 | GQL:2024 | SPARQL 1.1 |
|---|---|---|---|
| Non-aggregate filter (pre-grouping) | WHERE |
FILTER |
FILTER |
| Non-aggregate projection binding (grouping key) | GROUP BY |
GROUP BY |
GROUP BY |
| Aggregate filter / ordering (post-grouping) | HAVING |
FILTER |
HAVING |
| Aggregate filter / ordering (ungrouped) | correlated subquery | subquery aggregate | grouped subselect |
When the projection includes an aggregate binding, the non-aggregate bindings alone define the grouping key
(Section 5.8.2.1); ordering operators may only sort by an existing grouping key and never extend it. Each backend
enforces this through an explicit GROUP BY clause, which folds a null or unbound grouping key into a single group on
every backend, realising the undefined-key rule (Section 5.8.2.1) natively.
An ungrouped projection (no aggregate binding, Section 5.8.2.1) carries no GROUP BY; each backend realises the
distinct-row rule (Section 5.6) with a set quantifier on the projected tuple: SELECT DISTINCT (SQL:2011),
RETURN DISTINCT (GQL:2024), and SELECT DISTINCT (SPARQL 1.1). A grouped projection needs no such quantifier: its
GROUP BY already emits one row per distinct grouping-key tuple, so the quantifier is redundant and is omitted.
Coalescing (Section 6.2) resolves a dictionary (Section 4.3) by selecting the first tag of the priority (Section 6.1) present in the map, then gathering the value or values bound to that tag. The priority is pre-expanded to exact tags, so tag selection is first-present equality and needs no in-query language-range lookup [RFC4647]. One construct covers both per-tag cardinalities inside the cross-backend intersection: a single-string-per-tag map gathers one value, an array-per-tag map the winning tag's set.
The construct binds the winning tag by presence alone, then gathers that tag's values, decomposing into five primitives,
each inside the intersection and needing no window function, LATERAL, aggregate, or array value type:
| Primitive | SQL:2011 | GQL:2024 | SPARQL 1.1 |
|---|---|---|---|
| Seed from target set | JOIN (VALUES …) |
keyed node lookup | VALUES |
| Presence probe | EXISTS (SELECT 1 …) |
EXISTS { … } |
EXISTS { … } |
| Winning-tag scalar (cascade) | searched CASE WHEN … END |
searched CASE WHEN … END |
nested IF |
| Exact tag equality | lang = 'de' |
t.lang = 'de' |
lang(?v) = "de" |
| Gather winning set | JOIN … ON lang = <CASE> |
MATCH … WHERE t.lang = lang |
BIND(…) + FILTER |
The presence probe and gather are both correlated to the seeded resource. The winning-tag CASE sits directly in the
gather's join or filter predicate, so no derived-column nesting or LATERAL is forced. GQL binds the winning tag with
LET and probes with a graph-pattern EXISTS { (v)-[:label]->(t) WHERE t.lang = 'de' }; SPARQL binds ?lang with
BIND before the gather's FILTER(lang(?label) = ?lang). A scalar COALESCE(v_t1, v_t2, …) over per-tag values
remains a valid optimiser rewrite of the single-valued path, invisible at the protocol level.
The cascade's searched CASE WHEN EXISTS is grammatical under ISO/IEC 39075: the searched case's WHEN expression
admits the EXISTS predicate, whereas the simple case (CASE expr WHEN operand) excludes it, so the cascade MUST use the
searched form. An engine that omits EXISTS from its documented searched CASE is an engine-coverage gap, not a grammar
gap.
In SPARQL, the gather accesses the tag as if(lang(?x) = "", "und", lang(?x)), folding the language-neutral encodings
so that a value stored as "name"@und and a plain literal both match the und branch; the de/fr probes use exact
lang(?x) = "…". SQL and GQL store the literal tag und in a column or property and need no folding.
Storage imposes no further gate: because language tags are not constrained in advance (Section 4.3), a conformant store cannot encode them as per-tag columns or fixed properties and MUST hold the tag as queryable data, so the tag the cascade probes is always available as a column, property, or language-tagged literal. The per-tag-column shortcut applied only to a closed, pre-declared tag set, which a localised map is not.
Alessandro Bollini
Metreeca srl
Email: info@metreeca.com
URI: https://www.metreeca.com/contact