Temporality and indexes

Overview

Fauna documents are immutable, meaning that once created they are fixed and unchangeable. When a document is updated, a new version is created with a new timestamp and updated data. Old versions of documents are retained in accordance with the history_days field of the collection they belong to. Individual documents and collections may also have a ttl (time to live) field. When a ttl period expires, all affected documents are deleted along with their document histories as if they never existed, and they cannot be retrieved with temporal queries.

Every Fauna document includes a ts field to hold its timestamp. You can search an index for documents whose timestamp is at or before a specified time with the At function, and you can create an index to make a collection searchable by the ts field.

Example

For demonstration purposes, let’s create a collection named temporal:

client.query(
  q.CreateCollection({ name: 'temporal' })
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ref: Collection("temporal"),
  ts: 1649181192120000,
  history_days: 30,
  name: 'temporal'
}
Query metrics:
  •    bytesIn:  52

  •   bytesOut: 148

  • computeOps:   1

  •    readOps:   0

  •   writeOps:   1

  •  readBytes: 810

  • writeBytes: 334

  •  queryTime: 7ms

  •    retries:   0

Next, add a document:

client.query(
  q.Create(
    q.Ref(q.Collection('temporal'), '1'),
    {
      data: {
        test: 'temporality',
      },
    }
  )
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ref: Ref(Collection("temporal"), "1"),
  ts: 1648840223890000,
  data: { test: 'temporality' }
}
Query metrics:
  •    bytesIn:  116

  •   bytesOut:  175

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:   14

  • writeBytes:  202

  •  queryTime: 12ms

  •    retries:    0

Next, create an index which includes the ts field in its values:

client.query(
  q.CreateIndex({
    name: 'temporal',
    source: q.Collection('temporal'),
    values: [
      { field: ['data', 'test'] },
      { field: ['ts'] },
      { field: ['ref'] },
    ],
  })
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ref: Index("temporal"),
  ts: 1649201845600000,
  active: true,
  serialized: true,
  name: 'temporal',
  source: Collection("temporal"),
  values: [
    { field: [ 'data', 'test' ] },
    { field: [ 'ts' ] },
    { field: [ 'ref' ] }
  ],
  partitions: 8
}
Query metrics:
  •    bytesIn:   187

  •   bytesOut:   324

  • computeOps:     1

  •    readOps:     0

  •   writeOps:     2

  •  readBytes: 1,332

  • writeBytes:   613

  •  queryTime:  17ms

  •    retries:     0

You can use the Paginate function to read out the contents of the temporal index:

client.query(
  q.Map(
    q.Paginate(q.Match(q.Index('temporal'))),
    q.Lambda(
      ['test', 'ts', 'ref'],
      {
        test: q.Var('test'),
        ts: q.Var('ts'),
        ref: q.Var('ref'),
      }
    )
  )
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  data: [
    {
      test: 'temporality',
      ts: 1649201894950000,
      ref: Ref(Collection("temporal"), "1")
    }
  ]
}
Query metrics:
  •    bytesIn: 186

  •   bytesOut: 177

  • computeOps:   1

  •    readOps:   8

  •   writeOps:   0

  •  readBytes: 211

  • writeBytes:   0

  •  queryTime: 2ms

  •    retries:   0

In this case, we’ve used a custom result object to label each field in the index’s values definition.

Now let’s insert an event into the document’s history with a specific timestamp. The following example uses the Epoch function to construct a timestamp 1000 seconds after the Unix epoch date (1970-01-01T00:00:00Z) and the Insert function to add a create event to the document’s history.

client.query(
  q.Insert(
    q.Ref(q.Collection('temporal'), '1'),
    q.Epoch(1000, 'seconds'),
    'create',
    {
      data: {
        test: 'time travel',
      },
    }
  )
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ts: 1000000000,
  action: 'create',
  document: Ref(Collection("temporal"), "1"),
  data: { test: 'time travel' }
}
Query metrics:
  •    bytesIn:  171

  •   bytesOut:  192

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:  160

  • writeBytes:  787

  •  queryTime: 12ms

  •    retries:    0

If you read out the contents of the index again you can see that the original version of the document still exists, with its original timestamp. The Match function, which reads documents from the temporal index, retrieves the most recent version of the document; when we added a create event to the document we gave it an earlier timestamp, so its the original version we see now:

client.query(
  q.Map(
    q.Paginate(q.Match(q.Index('temporal'))),
    q.Lambda(
      ['test', 'ts', 'ref'],
      {
        test: q.Var('test'),
        ts: q.Var('ts'),
        ref: q.Var('ref'),
      }
    )
  )
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  data: [
    {
      test: 'temporality',
      ts: 1649201894950000,
      ref: Ref(Collection("temporal"), "1")
    }
  ]
}
Query metrics:
  •    bytesIn: 186

  •   bytesOut: 177

  • computeOps:   1

  •    readOps:   8

  •   writeOps:   0

  •  readBytes: 353

  • writeBytes:   0

  •  queryTime: 2ms

  •    retries:   0

You can use the At function to retrieve the document with the earlier timestamp. The following example looks for documents created at or before one day after the Unix epoch date plus 1,000 seconds:

client.query(
  q.At(
    q.TimeAdd(q.Epoch(1000, 'seconds'), 1, 'day'),
    q.Paginate(q.Match(q.Index('temporal')))
  )
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  data: [ [ 'time travel', 1000000000, Ref(Collection("temporal"), "1") ] ]
}
Query metrics:
  •    bytesIn: 137

  •   bytesOut: 153

  • computeOps:   1

  •    readOps:   8

  •   writeOps:   0

  •  readBytes: 353

  • writeBytes:   0

  •  queryTime: 2ms

  •    retries:   0

The document’s event history is a record of all events in the life of the document. The following example updates our document, adding an update event to the document’s history:

client.query(
  q.Update(
    q.Ref(q.Collection('temporal'), '1'),
    {
      data: {
        test: 'current time',
      },
    }
  )
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ref: Ref(Collection("temporal"), "1"),
  ts: 1649181444450000,
  data: { test: 'current time' }
}
Query metrics:
  •    bytesIn: 117

  •   bytesOut: 176

  • computeOps:   1

  •    readOps:   0

  •   writeOps:   1

  •  readBytes: 149

  • writeBytes: 458

  •  queryTime: 8ms

  •    retries:   0

The document now has three events in its history. The following example uses the Events function to display them in order by timestamp:

client.query(
  q.Paginate(q.Events(q.Ref(q.Collection('temporal'), '1')))
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  data: [
    {
      ts: 1000000000,
      action: 'create',
      document: Ref(Collection("temporal"), "1"),
      data: { test: 'time travel' }
    },
    {
      ts: 1649181443630000,
      action: 'update',
      document: Ref(Collection("temporal"), "1"),
      data: { test: 'temporality' }
    },
    {
      ts: 1649181444450000,
      action: 'update',
      document: Ref(Collection("temporal"), "1"),
      data: { test: 'current time' }
    }
  ]
}
Query metrics:
  •    bytesIn:  66

  •   bytesOut: 576

  • computeOps:   1

  •    readOps:   1

  •   writeOps:   0

  •  readBytes: 237

  • writeBytes:   0

  •  queryTime: 1ms

  •    retries:   0

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!