PartiQL Query Basics

A PartiQL query is either a SELECT-FROM-WHERE (SFW) statement or a value expression. Unlike SQL expressions, which are restricted to outputting scalar and null values, PartiQL expressions output arbitrary PartiQL values, i.e. — scalars, absent, and complex values. As a result, PartiQL SFW expressions are fully composable within other SFW expressions as well as value expressions. Indeed, PartiQL allows the top-level query to also be a value expression, not just a SELECT-FROM-WHERE as in SQL.

PartiQL Basic Syntax
query
	::= select-from-where
	  | expression

expression
	::= '(' select-from-where ')'
	  | value-expression

select-from-where
	::= select-clause let-clause? from-clause	// SELECT ... FROM
		where-clause?							// WHERE
		group-by-clause?						// GROUP BY
		having-clause?							// HAVING
		set-op-clause?							// UNION|INTERSECT|EXCEPT
		order-by-clause?						// ORDER BY
		fetch-clause?							// LIMIT ... OFFSET

value-expression
	::= path-expression							// x.a[0] ...
	  | call-expression							// uppercase(x) ...
	  | sql-expression							// SQL expressions and special forms
	  | value-constructor
	  | value-literal

SFW queries are composed of several relation expressions derived from its clauses. Each clause (except for SELECT VALUE and PIVOT) can be understood in isolation from the other clauses because a clause is simply a function that inputs and outputs collections of binding tuples.

SELECT Clauses

Core PartiQL SFW queries have a SELECT VALUE clause (in lieu of SQL’s SELECT clause) that can create outputs that are collections of anything — i.e. collections of tuples, collections of scalars, collections of arrays, collections of mixed type elements.

select-clause
    ::= 'SELECT' set-quantifier? '*'                // SELECT *
      | 'SELECT' set-quantifier? binding-list       // SELECT
      | 'SELECT' set-quantifier? VALUE expression   // SELECT VALUE
      | 'PIVOT' expression AT expression            // PIVOT

binding-list ::= binding (',' binding)*

binding ::= expression 'AS' symbol

SELECT

SQL’s well-known SELECT clause can be described as syntactic sugar over SELECT VALUE. When a SFW query is used as a subquery it is coerced into a scalar or a tuple, in the ways that SQL coerces the results of subqueries.

SELECT VALUE

The SELECT VALUE clause inputs a bag of binding tuples or an array of binding tuples (from the other clauses of the SQL query) and outputs a bag or an array. For example, if the query only has SELECT VALUE, FROM, and WHERE clauses, then the bindings that are output by the WHERE clause are input by SELECT VALUE the clause. Unlike SQL, the output of a SELECT VALUE clause need not be a bag or array of tuples. It is a bag or array of any kind of PartiQL values. For example, it may be a bag of integers, or a bag of arrays, etc. Indeed, the values may be heterogeneous. For example, the output may even be a bag that has both integers and arrays.

FROM Clause

The FROM Clause is responsible for producing a collection of binding tuples from either a single collection or multiple collections via joins. The syntax is as follows:

from-clause ::= 'FROM' collection

collection
	::= collection ',' collection								// CROSS JOIN
	  | collection join-type 'CROSS' 'JOIN'	collection			// CROSS JOIN
	  | collection join-type 'JOIN' collection join-condition	// JOIN ... ON
	  | collection-non-join
	  | '(' collection ')'

collection-non-join
	::= expression symbol					// Foo f
	  | collection-expression				// Foo AS x AT y
	  | 'UNPIVOT' collection-expression		// UNPIVOT Foo as v AT k

join-type ::= INNER | LEFT | RIGHT | FULL

join-condition ::= 'ON' expression

Single-Item FROM Clause

The single-item FROM clause simply ranges over a collection value.

Multi-Item FROM Clause

The multi-item FROM clause is responsible for joining the tuples of multiple relations. The join type can be one of CROSS, LEFT, RIGHT, INNER, OR FULL. All joins except for CROSS JOIN can have a join condition.

Unlike SQL, the expressions of the right-hand-side of a PartiQL CROSS JOIN may use fields of tuples in the left-hand-side. This enables descending into nested data structures and functions like nested loops.

See Joins

WHERE Clause

where-clause ::= 'WHERE' expression

The WHERE clause inputs the bindings that have been produced from the FROM clause and outputs the ones that satisfy its condition. The boolean predicates follow SQL’s 3-valued logic.

GROUP BY Clause

group-by-clause ::= 'GROUP BY' binding-list ('AS' symbol)?

The PartiQL GROUP BY clause expands SQL’s grouping. Unlike SQL, the PartiQL GROUP BY can be thought of as a standalone operator that inputs a collection of binding tuples and outputs a collection of binding tuples. A GROUP BY Clause has a binding list which is list of aggregate expressions ei and an associate binding name ni. Additionally, the GROUP BY Clause may contain a grouping variable g known as the group as alias.

HAVING Clause

having-clause ::= 'HAVING' expression

The HAVING Clause is effectively the same as the WHERE Clause; however, the HAVING Clause is applied to the aggregate values output by a GROUP BY Clauses. SQL allows for aggregate expressions in the HAVING Clause, but that behavior is currently not supported by PartiQL.

UNION / INTERSECT / EXCEPT

set-op-clause
	::= 'UNION' 	'ALL'? select-from-where
	  | 'INTERSECT' 'ALL'? select-from-where
	  | 'EXCEPT' 	'ALL'? select-from-where

In addition to the SQL set operators, PartiQL provides OUTER UNION, OUTER INTERSECT, and OUTER EXCEPT which enable combining arbitrary values by coercing to bags. For example, << 1 >> OUTER UNION 2 results in << 1, 2 >>.

Each bag operator has the form LHS Opbag RHS where LHS and RHS are SFW queries and Opbag is the bag (or set) operator. Additionally, the operator may be suffixed with ALL which indicates the output may have duplicate elements. In its absence, DISTINCT is implicit and duplicates are eliminated from the final result.

The standard SQL bag operators are defined as:

LHS UNION ALL RHS     = MULTISET_UNION(LHS, RHS)
LHS INTERSECT ALL RHS = MULTISET_INTERSECT(LHS, RHS)
LHS EXCEPT ALL RHS    = MULTISET_DIFFERENCE(LHS, RHS)

LHS UNION DISTINCT RHS     = DISTINCT(MULTISET_UNION(LHS, RHS))
LHS INTERSECT DISTINCT RHS = DISTINCT(MULTISET_INTERSECT(LHS, RHS))
LHS EXCEPT DISTINCT RHS    = DISTINCT(MULTISET_DIFFERENCE(LHS, RHS))

Let V1 and V2 be arbitrary values, and let C be a function which coerces a value to a bag. The OUTER operators are defined as

V1 OUTER UNION ALL V2     = MULTISET_UNION(C(V1), C(V2))
V1 OUTER INTERSECT ALL V2 = MULTISET_INTERSECT(C(V1), C(V2))
V1 OUTER EXCEPT ALL V2    = MULTISET_DIFFERENCE(C(V1), C(V2))

V1 OUTER UNION DISTINCT V2     = DISTINCT(MULTISET_UNION(C(V1), C(V2)))
V1 OUTER INTERSECT DISTINCT V2 = DISTINCT(MULTISET_INTERSECT(C(V1), C(V2)))
V1 OUTER EXCEPT DISTINCT V2    = DISTINCT(MULTISET_DIFFERENCE(C(V1), C(V2)))

The coercion function C is defined for all PartiQL Values by:

C(absent_value) -> << >>
C(scalar_value) -> << scalar_value >> # singleton bag
C(tuple_value)  -> << tuple_value >>  # singleton bag, see future extensions
C(array_value)  -> bag_value          # discard ordering
C(bag_value)    -> bag_value          # identity

ORDER BY Clause

order-by-clause ::= 'ORDER' 'BY' sort-spec-list

sort-spec-list ::= sort-spec (',' sort-spec)*

sort-spec ::= expression ('ASC'|'DESC')? ('NULLS' (FIRST|LAST))?

The ORDER BY Clause is responsible for turning its input bag into an array. In the following aspects, PartiQL extends the SQL semantics to resolve issues that are not relevant in SQL but emerge when working on Ion data.

LIMIT …​ OFFSET

fetch-clause ::= 'LIMIT' expression ('OFFSET' expression)?

The LIMIT …​ OFFSET clauses take the form LIMIT <N> OFFSET <M> and returns the first N binding tuples of its input collection, skipping the first M if an OFFSET is specified.

PartiQL allows specifying an OFFSET value without a LIMIT value.

PIVOT

The PIVOT Clause is a special PartiQL SELECT Clause which outputs a single tuple from a collection of bindings.