Cardinality

The number of items in a set is known as its cardinality. A set with a cardinality of zero is referred to as an empty set. A set with a cardinality of one is known as a singleton.

Terminology

The term cardinality is used to refer to both the exact number of elements in a given set or a range of possible values. Internally, EdgeDB tracks 5 different cardinality ranges: Empty (zero elements), One (a singleton set), AtMostOne (zero or one elements), AtLeastOne (one or more elements), and Many (any number of elements).

EdgeDB uses this information to statically check queries for validity. For instance, when assigning to a required multi link, the value being assigned in question must have a cardinality of One or AtLeastOne (as empty sets are not permitted).

Functions and operators

It’s often useful to think of EdgeDB functions/operators as either element-wise or aggregate. Element-wise operations are applied to each item in a set. Aggregate operations operate on sets as a whole.

This is a simplification, but it’s a useful mental model when getting started with EdgeDB.

Aggregate operations

An example of an aggregate function is count(). It returns the number of elements in a given set. Regardless of the size of the input set, the result is a singleton integer.

  1. db>
  1. select count('hello');
  1. {1}
  1. db>
  1. select count({'this', 'is', 'a', 'set'});
  1. {4}
  1. db>
  1. select count(<str>{});
  1. {0}

Another example is array_agg(), which converts a set of elements into a singleton array.

  1. db>
  1. select array_agg({1,2,3});
  1. {[1, 2, 3]}

Element-wise operations

By contrast, the len() function is element-wise; it computes the length of each string inside a set of strings; as such, it converts a set of str into an equally-sized set of int64.

  1. db>
  1. select len('hello');
  1. {5}
  1. db>
  1. select len({'hello', 'world'});
  1. {5, 5}

Cartesian products

In case of element-wise operations that accept multiple arguments, the operation is applied to a cartesian product of all the input sets.

  1. db>
  1. select {'aaa', 'bbb'} ++ {'ccc', 'ddd'};
  1. {'aaaccc', 'aaaddd', 'bbbccc', 'bbbddd'}
  1. db>
  1. select {true, false} or {true, false};
  1. {true, true, true, false}

By extension, if any of the input sets are empty, the result of applying an element-wise function is also empty. In effect, when EdgeDB detects an empty set, it “short-circuits” and returns an empty set without applying the operation.

  1. db>
  1. select {} ++ {'ccc', 'ddd'};
  1. {}
  1. db>
  1. select {} or {true, false};
  1. {}

Certain functions and operators avoid this “short-circuit” behavior by marking their inputs as optional. A notable example of an operator with optional inputs is the ?? operator.

  1. db>
  1. select <str>{} ?? 'default';
  1. {'default'}

Per-input cardinality

Ultimately, the distinction between “aggregate vs element-wise” operations is a false one. Consider the in operation.

  1. db>
  1. select {1, 4} in {1, 2, 3};
  1. {true, false}

This operator takes two inputs. If it was “element-wise” we would expect the cardinality of the above operation to the cartesian product of the input cardinalities: 2 x 3 = 6. It it was aggregate, we’d expect a singleton output.

Instead, the cardinality is 2. This operator is element-wise with respect to the first input and aggregate with respect to the second. The “element-wise vs aggregate” concept isn’t determined on a per-function/per-operator basis; it determined on a per-input basis.

Type qualifiers

When defining functions, all inputs are element-wise by default. The set of type qualifier is used to designate an input as aggregate. Currently this modifier is not supported for user-defined functions, but it is used by certain standard library functions.

Similarly the optional qualifier marks the input as optional; an operation will be executed is an optional input is empty, whereas passing an empty set for a “standard” (non-optional) element-wise input will always result in an empty set.

Similarly, the output of a function can be annotated with set of and optional qualifiers.

Cardinality computation

To compute the number of times a function/operator will be invoked, take the cardinality of each input and apply the following transformations, based on the type qualifier (or lack thereof) for each:

  1. element-wise: N -> N
  2. optional: N -> max(1, N)
  3. aggregate: N -> 1

The ultimate cardinality of the result is the union of the results of each invokation; as such, it depends on the values returned by each invokation.