Skip to content

Latest commit

 

History

History
676 lines (572 loc) · 22.5 KB

README.md

File metadata and controls

676 lines (572 loc) · 22.5 KB

Getting started with FQL (Fauna Query Language)

This is not an exhaustive documentation but a series of ideas and examples that can help you approach the official docs more easily. I found FQL to be difficult to grasp until it clicks. Each section builds on the previous ones so if you are just skimming and don't understand something you probably need to go back.

I'm also learning FQL as I go along so don't hesitate to correct me.

To get started with Fauna just open a free account and go to the dashboard.

If you get stuck sign into Fauna's community Slack. There are people there from all timezones willing to help. You can also ask in StackOverflow using the faunadb tag.

Sections:

Using the dashboard shell

Fauna Dashboard Shell

You can input FQL queries directly in the web dashboard using a shell. It's a great way of figuring out how FQL works and it remembers the queries you've made.

Shortcuts:

  • Submit current query scope: CMD + Enter
  • Previous query: CMD + UP
  • Next query: CMD + DOWN
  • Undo: CMD + Z

In Windows just change CMD for Ctrl

Naming conventions

AFAIK there are no naming conventions in Fauna but this is what I've been doing and it has stuck:

  • Collections go in PascalCase. Eg: MyFavoriteThings.
  • Document properties go in camelCase. Eg: musicalInstrument.
  • References in a document always end with Ref or Refs in the case of arrays. Eg: userRef or productsRefs.
  • Indexes go in snake_case with collections and properties as they exist in the database. Eg: JazzArtists_by_musicalIntrument.

Documents and collections

Fauna doesn't have tables like traditional relational databases. The smallest unit of data is a schemaless document. These are organized in collections which are essentially buckets of documents with no order whatsoever. Fauna databases can also have children databases.

This is what a document looks like:

{
  "ref": Ref(Collection("Fruits"), "264471980339626516"),
  "ts": 1588478985090000,
  "data": {
    "name": "Mango"
  }
}

Each document is composed by:

  • A reference (ref in short). More on refs later, but this represents a location in the database. In this case Ref(Collection("Fruits"), "264471980339626516") represents the document with the id 264471980339626516 in the Fruits collection.
  • A timestamp ts in microseconds.
  • Some data that looks and behaves pretty much like JavaScript objects with some special Fauna types like Ref.

More about documents here.

Let's create a collection with CreateCollection:

CreateCollection({ name: "Fruits" })

// Result:

{
  "ref": Collection("Fruits"),
  "ts": 1588478932770000,
  "history_days": 30,
  "name": "Fruits"
}

As you can see collections are similar to documents with some special properties. Let's keep the defaults for now but you can read more about collections here.

Now let's create a document with Create:

Create(
  Collection("Fruits"),
  {
    data: {
      name: "Mango"
    }
  }
)

// Result:

{
  "ref": Ref(Collection("Fruits"), "264471980339626516"),
  "ts": 1588478985090000,
  "data": {
    "name": "Mango"
  }
}
  • Create is the function used to create documents.
  • Collection('Fruits') gets a reference to the collection Fruits.
  • {data: {name: 'Mango'}} is the document you want to create.

See the CRUD tutorial for more info on how to manipulate documents.

Create a document with a predefined id

First you need to retrieve a new unique id in the Fauna cluster with NewId:

NewId()

// Result:

"264517098691101203"

And then you can create your new document with a reference to the provided id instead:

Create(
  Ref(Collection("Fruits"), "264517098691101203"),
  {
    data: {
      name: "Apple"
    }
  }
)

Relationships and references

Relationships in Fauna are modelled with references. A Ref is a type of data in Fauna which points to a location in the database. For documents this reference includes an id property which would be the 264517098691101203 in the previous example.

Remember that a reference is not a thing, it's a pointer to a thing (documents, collections, etc).

If you want to retrieve a document from a reference you need to use the Get function:

Get(
  Ref(
    Collection("Fruits"),
    "264471980339626516"
  )
)

// Result:

{
  "ref": Ref(Collection("Fruits"), "264471980339626516"),
  "ts": 1588478985090000,
  "data": {
    "name": "Mango"
  }
}

So this is how you could model a relationship between documents:

Create(
  Collection("People"),
  {
    data: {
      name: "Pier",
      favoriteFruitRef: Ref(Collection("Fruits"), "264471980339626516")
    }
  }
)

Or:

Create(
  Collection("People"),
  {
    data: {
      name: "Pier",
      favoriteFruitsRefs: [
        Ref(Collection("Fruits"), "264471980339626516"),
        Ref(Collection("Fruits"), "467478980389696500")
      ]
    }
  }
)

Note: If you're coming from SQL you might be tempted to store raw ids in your documents but it's much better to use the native Ref type as it will make your FQL life simpler.

See the E-commerce tutorial for more info on modeling relational data with Fauna.

Indexes

Retrieving one document at a time with Get won't get you very far. Since collections are just buckets of documents, you need to use indexes to create some order, retreive multiple documents, sort, filter, and more.

The combination of a collection and an index is probably the closest you will get to a table of a relational database. The big difference being that table rows can only belong to a single table. In contrast, Fauna indexes can have multiple collections as sources of data and documents can belong to as many indexes as you need.

Let's create our first index with CreateIndex:

CreateIndex({
  name: "all_Fruits",
  source: Collection("Fruits")
})

// Result:

{
  "ref": Index("all_Fruits"),
  "ts": 1588482162446000,
  "active": true,
  "serialized": true,
  "name": "all_Fruits",
  "source": Collection("Fruits"),
  "partitions": 8
}

As you can see indexes are also a special type of document. Pretty much everything in Fauna is a document.

After adding some more fruits to your Fruits collection let's retreive all the references using the all_Fruits index we just created:

Paginate(
  Match(
    Index("all_Fruits")
  )
)

// Result:

{
  "data": [
    Ref(Collection("Fruits"), "264471980339626516"),
    Ref(Collection("Fruits"), "264476914201133588"),
    Ref(Collection("Fruits"), "264476925331767828"),
    Ref(Collection("Fruits"), "264476941393854996"),
    Ref(Collection("Fruits"), "264476977509958164"),
    Ref(Collection("Fruits"), "264476994575532564"),
    Ref(Collection("Fruits"), "264477007396471316")
  ]
}

Woah woah woah there's a lot going on there.

Let's break it down. I've found sometimes it makes more sense to go backwards with FQL:

  • Index returns a reference to an index.
  • Match combines the index with some optional parameters to feed Paginate with the necessary configuration to actually retrieve the matching data. At this point no actual data has been yet retrieved from Fauna.
  • Paginate takes the output of Match (actually a reference to a Set), retrieves data from Fauna, and finally returns a Page with an array of references.

Later on we'll see how to get actual documents instead of references and how to use Match for filtering.

Page size

By default Paginate returns pages with 64 items. You can define how many items you want to receive with size:

Paginate(
  Match(Index('all_Fruits')),
  {size: 3}
)

// Result:

{
  "after": [
    Ref(Collection("Fruits"), "264476941393854996")
  ],
  "data": [
    Ref(Collection("Fruits"), "264471980339626516"),
    Ref(Collection("Fruits"), "264476914201133588"),
    Ref(Collection("Fruits"), "264476925331767828")
  ]
}

Fauna gives us the next document with after to use as a cursor because in this case there are more results than fit in a single page of 3.

See the Paginate docs for more info on using cursors.

Unique values

You can also use indexes to determine that a property of a document must be unique in the whole collection much like the UNIQUE constraint in SQL. This must be defined when creating the index:

CreateIndex({
  name: "Users_by_email",
  source: Collection("Users"),
  terms: [
    {field: ["data", "email"]}
  ],
  unique: true
})

Even if you never use that index to retrieve documents it will ensure the email property in the data part of your document is unique accross the Users collection. Fauna will return an error if you try to insert a document that breaks that constraint.

We'll see what that terms object is later on.

Note: ["data", "email"] represents a path. We'll see more about paths later but you can think of those as data/email or even data.email.

Gettings multiple documents from references

Ok so we know how to get a list of references from an index. How do we actually get documents?

When programming you might have come across map which executes a function on each item of an array and returns a new array. For example in JavaScript we use myArray.map().

Fauna also has a Map function. We can use it to retrieve an array of documents from an array of references:

Map(
  Paginate(Match(Index("all_Fruits"))),
  Lambda("fruitRef", Get(Var("fruitRef")))
)

// Result:

{
  "data": [
    {
      "ref": Ref(Collection("Fruits"), "264471980339626516"),
      "ts": 1588478985090000,
      "data": {
        "name": "Mango"
      }
    },
    {
      "ref": Ref(Collection("Fruits"), "264476914201133588"),
      "ts": 1588483690410000,
      "data": {
        "name": "Apple"
      }
    },
    // etc
  ]
}

Don't panic, let's break this down.

We already understand from a previous example that Paginate(Match(Index('all_Fruits'))) returns an array of references, right? So Map iterates over this array, executes Lambda on each item, and returns a new array with documents.

This is the tricky part: Lambda("fruitRef", Get(Var("fruitRef"))):

  • Lambda is used in FQL to define anonymous functions. In this example we are defining a function that will take a reference and return a document.
  • Lambda("fruitRef"... defines a function parameter. It could be named anything: fruit, X, Tarzan, etc. The name is irrelevant. In this case the parameter will receive a single document reference that Map will pass to Lambda from Paginate.
  • Var("fruitRef") evaluates the variable named fruitRef in the context of the function. You couldn't simply use fruitRef or "fruitRef" because FQL wouldn't know what do with it. Var explicitly tells Fauna to find and evaluate a variable in the current context.
  • Finally Get receives a reference from Var and returns an actual document. This document is returned by Lambda to Map to form an array of documents.

Consider this JavaScript example:

const newArray = myArray.map((item) => doSomething(item));
// which is equivalent to:
const newArray = myArray.map(function (item) {
  return doSomething(item);
});

If you were using the JavaScript Fauna driver you could write something like this instead of using Lambda:

const result = await client.query(
    q.Map(
      q.Paginate(q.Match(q.Index('all_Fruits'))),
      (fruitRef) => q.Get(fruitRef)
    )
)

Sorting results

To retrieve a list of ordered documents first we need to create a new index.

CreateIndex({
  name: "all_Fruits_sorted_by_name",
  source: Collection("Fruits"),
  values: [
    { field: ["data", "name"] },
    { field: ["ref"] }
  ]
})

The new bit here is this values array.

We saw earlier that indexes also accept a terms array. The idea is simply that terms are input parameters for our index, and values are the actual values the index will output.

In this case the index will return two values for each result:

  • { field: ["data", "name"] } will return the name in the data part of the document.
  • { field: ["ref"] } will return a reference to the document.

Proof:

Paginate(Match(Index("all_Fruits_sorted_by_name")))

// Result:

{
  "data": [
    [
      "Apple",
      Ref(Collection("Fruits"), "264476914201133588")
    ],
    [
      "Avocado",
      Ref(Collection("Fruits"), "264476977509958164")
    ],
    [
      "Banana",
      Ref(Collection("Fruits"), "264476925331767828")
    ],
    [
      "Lemon",
      Ref(Collection("Fruits"), "264477007396471316")
    ],
    // etc
  ]
}

As you can see the documents are already sorted by name alphabetically. Fauna will automatically sort the results by the fields you add to values.

If you want to reverse the order of the results you'd need to create a new index and use the reverse setting:

CreateIndex({
  name: "all_Fruits_sorted_by_name_reverse",
  source: Collection("Fruits"),
  values: [
    { field: ["data", "name"], reverse: true },
    { field: ["ref"] }
  ]
})

Later on we'll see how to actually get a list of documents from these results.

Filtering results

Filtering is similar to sorting and is done using indexes. Instead of adding values we need to add a terms array.

Remember: terms is input, values is output.

CreateIndex({
  name: "Fruits_by_name",
  source: Collection("Fruits"),
  terms: [
    { field: ["data", "name"]}
  ]
})

So now we can use Match to pass a parameter to our index:

Paginate(
  Match(Index("Fruits_by_name"), "Apple")
)

// Result:

{
  "data": [
    Ref(Collection("Fruits"), "264476914201133588")
  ]
}

Or if we want to get documents instead of references:

Map(
  Paginate(Match(Index("Fruits_by_name"), "Apple")),
  Lambda("fruitRef", Get(Var("fruitRef")))
)

// Result:

{
  "data": [
    {
      "ref": Ref(Collection("Fruits"), "264476914201133588"),
      "ts": 1588483690410000,
      "data": {
        "name": "Apple"
      }
    }
  ]
}

If we know in advance we will get a single result we could simply use Get instead of Paginate:

Get(Match(Index("Fruits_by_name"), "Apple"))

// Result:

{
  "ref": Ref(Collection("Fruits"), "264476914201133588"),
  "ts": 1588483690410000,
  "data": {
    "name": "Apple"
  }
}

If you want to know how to sort and filter at the same time check the Search and sort with indexes tutorial.

About paths and using Select

Remember how we got an array of arrays when sorting our data?

To recap here's the ouput we have from the index:

Paginate(Match(Index("all_Fruits_sorted_by_name")))

// Result:

{
  "data": [
    [
      "Apple",
      Ref(Collection("Fruits"), "264476914201133588")
    ],
    // etc
  ]
}

This is no bueno since we actually want documents and not references.

The problem we have now is that we cannot use Lambda("fruitRef", Get(Var("fruitRef"))) like we did before to get a document. If you do that you will get an error because you wouldn't be passing a Ref to Get, you'd be actually passing this array:

[
  "Apple",
  Ref(Collection("Fruits"), "264476914201133588")
]

Damn!

The solution is to find a way to select the second item of that array, which is a reference, and pass it to Get to retrieve a document.

We're going to use Select for this:

Map(
  Paginate(Match(Index("all_Fruits_sorted_by_name"))),
  Lambda("itemFromIndex", Get(Select([1], Var("itemFromIndex"))))
)

// Result:

{
  "data": [
    {
      "ref": Ref(Collection("Fruits"), "264476914201133588"),
      "ts": 1588483690410000,
      "data": {
        "name": "Apple"
      }
    },
    {
      "ref": Ref(Collection("Fruits"), "264476977509958164"),
      "ts": 1588483750760000,
      "data": {
        "name": "Avocado"
      }
    },
    // etc
  ]
}

This is the interesting bit Select([1], Var("itemFromIndex")).

We already know from a previous section that Var("itemFromIndex") will return whatever Lambda is getting into its parameter which in this case is this:

[
  "Apple",
  // 👇 we want this Ref to be able to get a document!
  Ref(Collection("Fruits"), "264476914201133588")
]

To do that we pass this path [1] to Select. This will return a Ref that we can use with Get and voila! we will have our document.

Note: Arrays in Fauna are zero based so we need to use the index 1 to select the second item from the array.

Selecting document properties

Select can also be used with paths that point to document properties (eg: ["data","name"]) as we saw when sorting and filtering results.

To clarify this point further imagine we had a document with this structure:

{
  data: {
    name: "Pier"
    country: {
      name: "Spain",
      city: {
        name: "Palma de Mallorca"
      }
    }
  }
}

To get the city name we'd use the following path: ["data", "country", "city", "name"].

Getting custom results instead of full documents

Until now we've retrieved full documents but it may not make sense to do that every time we query Fauna.

Imagine we have a Posts collection with hundreds of posts such as this one:

{
  data: {
    title: "Lorem ipsum",
    content: "This is the story about that time I took a latin class and...",
    tags: ["boring"],
    authorRef: Ref(Collection("Authors"), "264471980339626516"),
    status: 'DRAFT'
    // etc
  }
}

It would be a waste of bytes to send the complete post if we only wanted to present a list of posts to the user. You could certainly solve that in your backend logic but it can be solved in FQL too.

To do that we need to use Let to create a custom expression.

By this point you should already know how to create collections, documents, and indexes. Create a Posts collection, add some posts to it, and then create an all_Posts index.

Done? Great.

This is how we use Let to retreive a custom expression from Fauna instead of full documents:

Map(
  Paginate(Match(Index("all_Posts"))),
  Lambda("postRef", Let(
    {
      postDocument: Get(Var("postRef"))
    },
    {
      id: Select(["ref", "id"], Var("postDocument")),
      title: Select(["data", "title"], Var("postDocument"))
    }
  ))
)

// Result:

{
  "data": [
    {
      "id": "264538580516340242",
      "title": "Lorem ipsum"
    },
    {
      "id": "264538588626027026",
      "title": "Some other post"
    },
    {
      "id": "264538600151974418",
      "title": "And yet another boring post"
    },
    {
      "id": "264538616193090066",
      "title": "Lorem Ipsum the second"
    }
  ]
}

Boom shaka laka!

Let's break this down.

We already know what Map, Paginate, and Lambda are doing from previous sections.

This is the interesting bit:

Lambda("postRef", Let(
  {
    postDocument: Get(Var("postRef"))
  },
  {
    id: Select(["ref", "id"], Var("postDocument")),
    title: Select(["data", "title"], Var("postDocument"))
  }
))

We are defining a Lambda function which will return whatever Let returns.

As you can see Let accepts two lists of pair values. These lists are called dictionnaries or objects in some programming languages.

The first list are variables you define to be used later on. In the docs they call this first list bindings:

{
  postDocument: Get(Var("postRef"))
}

These bindings will be available in the context of our Let, and even other nested Let. Again, the name doesn't matter. It could be post, X, whatever, etc.

The value of postDocument is a document. Why? Because it's the result of Get which we know returns a document. Var("postRef") is evaluating the postRef variable which is the input parameter of our Lambda.

The second list of values is the output of Let, or the custom expression that Let will return:

{
  id: Select(["ref", "id"], Var("postDocument")),
  title: Select(["data", "title"], Var("postDocument"))
}

Here we are defining id and title. Again, you can use any name you wish. In both cases we are selecting a property from the postDocument document by using Select as we saw in the previous section.

If you want to see a more complex Let example see this Gist by ptpaterson.