Check out v4 of the Fauna CLI
v4 of the Fauna CLI is now GA. The new version introduces enhancements to the developer experience, including an improved authentication workflow. To get started, check out the CLI v4 quick start. Migrating from v3 of the CLI? See the CLI migration guide. |
Work with multiple Sets
This guide covers common patterns for performing SQL-like set operations, such as unions, joins, and intersections, in FQL. You can use these operations to combine and filter Sets of collection documents.
Unions
Use set.concat()
to combine two
Sets into a single Set. This is similar to a UNION
clause in SQL.
// Setup: Get frozen and produce `Category` documents.
let frozenCat = Category.byName("frozen").first()
let produceCat = Category.byName("produce").first()
// Get Sets of related `Product` documents for each category.
let frozenDocs = Product.byCategory(frozenCat)
let produceDocs = Product.byCategory(produceCat)
// Use `concat()` to combine the previous
// `Product` document Sets. Project each
// product's name and category.
frozenDocs.concat(produceDocs) {
name: .name,
category: .category!.name
}
// The combined Set contains all `Product` documents
// in the frozen and produce categories.
{
data: [
{
name: "pizza",
category: "frozen"
},
...
{
name: "cilantro",
category: "produce"
}
]
}
Chain set.concat()
You can chain set.concat()
to combine three or more Sets. The following example expands on
the previous one:
// Setup: Get frozen, produce, and party `Category` documents.
let frozenCat = Category.byName("frozen").first()
let produceCat = Category.byName("produce").first()
let partyCat = Category.byName("party").first()
// Get Sets of related `Product` documents for each category.
let frozenDocs = Product.byCategory(frozenCat)
let produceDocs = Product.byCategory(produceCat)
let partyDocs = Product.byCategory(partyCat)
// Chain `concat()` expressions to combine the previous
// `Product` document Sets. Project each
// product's name and category.
frozenDocs
.concat(produceDocs)
.concat(partyDocs) {
name: .name,
category: .category!.name
}
// The combined Set contains all `Product` documents
// in the frozen, produce, and party categories.
{
data: [
{
name: "pizza",
category: "frozen"
},
{
name: "avocados",
category: "produce"
},
...
{
name: "taco pinata",
category: "party"
}
]
}
Use set.flatMap()
When combining several Sets, you can improve readability
by using set.flatMap()
to iterate through the Sets:
// Start with an Array of category names.
["frozen", "produce", "party"]
// Convert the Array to a Set. `flatMap()` operates on Sets.
.toSet()
// Use `flatMap()` to process each category name
// and combine the resulting Sets. `flatMap()` will:
// 1. Apply the given function to each element of the Set.
// 2. Collect the results in a Set,
// with a nested child Set for each element.
// 3. Flatten the resulting Set by one level,
// resulting in a single, flat Set.
.flatMap(name => {
// Get the `Category` document for each category name.
let category = Category.byName(name).first()
// Get Sets of related `Product` documents for each category.
// Each Set is included in the flattened results, creating
// a single Set of products across all listed categories.
// Project each product's name and category.
Product.byCategory(category)
}) {
name: .name,
category: .category!.name
}
// The combined Set contains all `Product` documents
// in the listed categories.
{
data: [
{
name: "pizza",
category: "frozen"
},
{
name: "avocados",
category: "produce"
},
...
{
name: "taco pinata",
category: "party"
}
]
}
Joins
In Fauna, you can use document
references to model relationships between documents in different collections.
You can use projection to traverse the
references. This is similar to performing a JOIN
in SQL.
// Get a Set of `Order` documents with a `status` of `cart`.
// In production, use an index instead of `where()` for
// better performance.
let orders = Order.where(.status == "cart")
// Projects fields in the `Order` documents.
orders {
// Traverses the `Customer` document reference in
// the `customer` field.
customer {
id,
name,
email
}
}
{
data: [
{
customer: {
id: "111",
name: "Alice Appleseed",
email: "alice.appleseed@example.com"
}
},
{
customer: {
id: "222",
name: "Bob Brown",
email: "bob.brown@example.com"
}
}
]
}
Use set.map()
for transformations
You can also use set.map()
to
extract and transform field values from referenced documents.
// Get a Set of `Order` documents with a `status` of `cart`.
// In production, use an index instead of `where()` for
// better performance.
let orders: Any = Order.where(.status == "cart")
// Uses `map()` to iterate through each document in the Set
// and extract the `customer` field from each `Order` document.
// Then project fields from the `customer` field's referenced
// `Customer` document.
orders.map(order => order.customer) {
id,
name,
email
}
{
data: [
{
id: "111",
name: "Alice Appleseed",
email: "alice.appleseed@example.com"
},
{
id: "222",
name: "Bob Brown",
email: "bob.brown@example.com"
}
]
}
Use set.flatMap()
for *-to-many relationships
For one-to-many or many-to-many relationships, you can use
set.flatMap()
to extract field
values from referenced documents and flatten the resulting Set by one level:
// Get a Set of all `Customer` documents.
Customer.all()
// Use `flatMap()` to iterate through each document in the Set
// and extract fields from:
// - The `Order` document referenced in the `order` field.
// - The `Customer` document reference nested in the `order` field.
// Then flatten the resulting Set.
.flatMap(customer => {
let orders = customer.orders
orders {
id,
total,
status,
customer {
id,
name
}
}
})
{
data: [
{
id: "408922916844994633",
total: 5392,
status: "cart",
customer: {
id: "111",
name: "Alice Appleseed"
}
},
{
id: "408922916872257609",
total: 1880,
status: "cart",
customer: {
id: "222",
name: "Bob Brown"
}
}
]
}
Intersections
An intersection returns elements that exist in multiple Sets,
similar to an INTERSECT
clause in SQL.
Filter using where()
To perform intersections in FQL, start with the most selective expression and
filter the resulting Sets using set.where()
.
For the best performance, use a covered index call as the first expression and only filter on covered values.
// Get the produce category.
let produce = Category.byName("produce").first()
// Use the most selective index, `byName()`, first.
// Then filter with covered values.
// In this example, `category`, `price`, and `name`
// should be covered by the `byName()` index.
Product.byName("limes")
.where(.category == produce)
.where(.price < 500) {
name,
category: .category!.name,
price
}
Filter using query composition
The Fauna client drivers compose queries using FQL template strings. You can interpolate variables, including other FQL template strings, into the template strings to compose dynamic queries.
You can use FQL string templates to reuse filters across queries. For example, using the JavaScript driver:
// Start with base query
let query = fql`Product.byName("limes")`
// Define filters to apply
const filters = [
fql`.where(doc => doc.category == Category.byName("produce").first()!)`,
fql`.where(doc => doc.price < 500)`
]
// Compose the final query
filters.forEach(filter => {
query = fql`${query}${filter}`
})
For more complex use cases, you can build a priority map of indexes and filters to automatically select the most performant approach:
const indexMap = {
by_name: (name: string) => fql`Product.byName(${name})`,
by_category: (cat: string) =>
fql`Product.byCategory(Category.byName(${cat}).first()!)`
}
const filterMap = {
by_name: (name: string) => fql`.where(.name == ${name})`,
by_category: (cat: string) =>
fql`.where(.category == Category.byName(${cat}).first()!)`
}
Check for the existence of document references
When checking for the existence of
document references, use
set.take()
to avoid unneeded
document reads:
// Get a Set of `Customer` documents that have:
// - An address in "DC".
// - At least one order.
Customer
// Filter customers by state.
.where(.address.state == "DC")
// Filter customers by `orders` field.
// The `orders` field is a computed field containing a Set of
// `Order` documents for the customer. `take(1)` ensures we only
// read the first document to check for existence.
.where(.orders.take(1).isEmpty() == false) {
id,
name,
email
}
Is this article helpful?
Tell Fauna how the article can be improved:
Visit Fauna's forums
or email docs@fauna.com
Thank you for your feedback!