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: Any = 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: Any = 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.

Use includes() for smaller sets

For small sets, you can use set.includes() to perform a more literal intersection:

// Get set of `Customer` documents with a cart.
let customersWithCarts = Customer.where(.cart != null)
// Gets set of `Customer` documents with an address in DC.
let customersInDC = Customer.where(.address.state == "DC")

// Use `where()` and `includes()` to find the intersection
// of the two sets. This approach is inefficient for large sets.
customersWithCarts
  .where(customer => customersInDC.includes(customer)) {
    id,
    name,
    email
  }

Using set.includes() for large sets isn’t recommended. For large sets, this approach requires a large number of read operations and doesn’t scale well.

Use filters for large sets

For the best performance with large sets, use filtering to perform intersections in FQL. Start with the most selective expression and filter the resulting sets using set.where():

// Setup: Gets the produce `Category` document.
let produce = Category.byName("produce").first()

// Get `Product` documents that:
// - Are in the produce category.
// - Have a price of $10 or less.
// - Include "organic" in the description.
// Project each product's name and category.
// Use the `byCategory` index to filter products by category.
Product.byCategory(produce)
    // Filter products by price.
  .where(.price < 10_00) // $10 is USD cents
  // Filter products by description
  .where(.description.toLowerCase().includes("organic")) {
    name: .name,
    category: .category!.name
  }

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!