Dynamic queries and query composition

The FQL API natively supports composition by using query interpolation. This eliminates injection attack vulnerability, which is a common database security exploit, and makes the query a powerful composition primitive to use in building applications and libraries.

Driver design is based on string-based query templates, using string interpolation to parameterize the templates. When you define a query template, you add placeholders in the template for query arguments. When the query executes, the template with query variables is sent over the wire, parsed, and the variables evaluated to ensure that injection can’t occur.

Composition in drivers

Fauna drivers expose an ergonomic API for composition and use query interpolation in their implementations.

Template and variable syntax can vary by language, although Fauna tries to make them look and feel similar. For example, the JavaScript, Python, and Go drivers use a common template format for declaring FQL queries, using ${} as variable placeholders, but they have syntactic differences in how to pass variables. Review your driver README file for guidance.

Choose a language to see an example of string substitution with composition for that language:

import { fql } from "fauna";

function userByEmail(email) {
    return fql`Users.byEmail(${email})`;
}

q = userByEmail("foo@fauna.com")
client.query(q)
from fauna import fql

def user_by_email(email: str):
    return fql("Users.byEmail(${email})", email=email)

q = user_by_email("foo@fauna.com")
client.query(q)
import (
    "fauna"
)

func UserByEmail(email string) (*fauna.Query, error) {
    args := map[string]interface{}{"email": email}
    return fauna.FQL("Users.byEmail(${email})", args)
}

func main() {
    q, _ := UserByEmail("foo@fauna.com")
    client.Query(q)
}

Here are some things to consider when thinking about composition:

  • You can and should use the composition API by default, even when writing simple string queries. This ensures that your code is safe and extensible. Most drivers make this easy by accepting only template-constructed queries.

  • You can compose queries with primitives, objects, and other queries. Support for serialization and deserialization of user-defined types depends on the driver, so see the README for your driver for the most up-to-date advice.

  • When writing subqueries to be reused using composition, that subquery isn’t required to be syntactically complete. Only the final query in which it is used needs to be complete. This means you can write snippets that can be composed, although they can’t be run by themselves.

Composition with one or more queries example

These are more advanced examples for each driver, showing multiple queries:

import { fql } from "fauna";

function userByEmail(email) {
    return fql`Users.byEmail(${email}).first()`;
)

function updateUserByEmail(email, data) {
    const user = userByEmail(email);
    return fql`
        let u = ${user}
        u.update(${data})
    `;
}

q = updateUserByEmail("foo@fauna.com", { street: "Polyglot St." })
client.query(q)
from fauna import fql

def user_by_email(email: str):
    return fql("Users.byEmail(${email}).first()", email=email)

def update_user_by_email(email: str, data: dict):
    q = """
let u = ${user}
u.update(${data})
"""
    return fql(q, user=user_by_email(email), data=data)

q = update_user_by_email("foo@fauna.com", {"street": "Polyglot St."})
client.query(q)
import (
    "fauna"
)

type User struct {
    Street string `fauna:"street"`
}

func UserByEmail(email string) (*fauna.Query, error) {
    args := map[string]interface{}{"email": email}
    return fauna.FQL("Users.byEmail(${email}).first()", args)
}

func UpdateUserByEmail(email string, data User) (*fauna.Query, error) {
    args := map[string]interface{}{"user": UserByEmail(email), "data": data}

    q := `
let u = ${user}
u.update(${data})
`
    return fauna.FQL(q, args)
}

func main() {
    data := User{Street: "Polyglot St."}
    q, _ := UpdateUserByEmail("foo@fauna.com", data)
    client.Query(q)
}

A simple example

Take an example e-commerce application where you want to search a product catalog by product type and where the type of product varies with each query. This code snippet shows you how to parameterize the query.

const type = "book"
let query  = fql`Product.where(.type == ${type})`
let result = await client.query(query)
console.log(result)

Notice that the product type is parameterized using ${type} placeholder syntax.

Add flexibility to your query

The following code snippet expands that example to cover the case where you want to also search for products by type, minimum price, and maximum price:

fql`Product
  .where(.type == "book")
  .where(.price <= 10000)
  .where(.price >= 10)
`

The equivalent templating syntax gives you the flexibility of searching on any combination of parameters:

// Set up parameterized inclusive, exclusive,
// or exact matches

const comparators = {
  eq:  (field, value) => fql`x => x[${field}] == ${value}`,
  gt:  (field, value) => fql`x => x[${field}] >  ${value}`,
  gte: (field, value) => fql`x => x[${field}] >= ${value}`,
  lt:  (field, value) => fql`x => x[${field}] <  ${value}`,
  lte: (field, value) => fql`x => x[${field}] <= ${value}`,
}

// Define the product search parameters set in the UI

var predicates = [
  { field: "type",  comparator: "eq",  value: "book" },
  { field: "price", comparator: "lte", value: 10000 },
  { field: "price", comparator: "gte", value: 10 },
];

const base_query = fql`Product`

// chain successive where() methods to the query,
// effectively ANDing the predicates

const dynamicQuery = predicates.reduce(
  (acc, val) =>
    fql`${acc}.where( ${comparators[val.comparator] ( val.field, val.value)})`,
  base_query
)

let result = await client.query(dynamicQuery)
console.log(result)

 

Sometimes you might want to search on all three parameters, and at other times only search on one parameter.

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!