Search code examples
nosqlrethinkdb

How to atomically update a document in RethinkDB based on old value?


Let's say I have a document schema like the following:

{
  users: [{userid: 123, username: "foo"}, {userid: 234, username: "bar"}]
}

I want to add an item to users with username equal to a "uniquified" version of the given username. For example, if I try to add {userid: 456, username: "baz"} to the users list above it should succeed, but if I try to add {userid: 456, username: "foo"} to the above then {userid: 456, username: "foo (1)"} should be added instead.

Is there any way to do this with an atomic update in RethinkDB? This is a deterministic operation, so theoretically it should be possible right? If not, is there some way that I can at least detect a username conflict during the insert and simply reject the update?

I know that I can use subqueries, but it seems like the result would not be an atomic operation? All of the examples of subqueries I've seen in the documentation show subqueries on a separate table.


Solution

  • According to the consistency guarantees, you can combine get (not getAll or filter though) with update and have an atomic chain. Then, inside update, it's described that if you use subqueries or something that is not deterministic, then you're not atomic and have to explicitly declare the nonAtomic flag.

    The most verbose part in the query becomes then the way to increment the count, since you don't want to end up with multiple bar (1).

    The following should behave atomically, assuming you already provide:

    • the id of the document, here did = '3a297bc8-9fda-4c57-8bcf-510f51158f7f'
    • the username, here uname = 'bar'
    • the userid, here uid = 345
    var uname = 'baz';
    var did = "3a297bc8-9fda-4c57-8bcf-510f51158f7f";
    var uid = 345;
    // not sure readMode is necessary here, it's described in consistency guarantees
    //  but not shown in the example with get/update/branch
    r.db('db').table('table', { readMode: 'majority' })
    // use only get to ensure atomicity between get and update
    .get(did)
    // update will throw if you're not deterministic, i.e. it can't behave atomically
    //  here we never address anything but the current document so it's OK
    .update(function(doc) {
      // this retrieves the highest index in parentheses if any
      //  put in a var because 1/ we use the result twice 2/ it's kind of verbose...
      var matched = doc('users').map(function(user) {
        // regex is /^bar(?: \(([0-9]+)\))?$/
        //  with the only capturing group on the index itself
        return user('username').match(r.expr('').add('^', uname, '(?: \\(([0-9]+)\\))?$'))
      }).filter(function(match) {
        // remove all user items that didn't match (i.e. they're null)
        return match.typeOf().ne('NULL');
      }).map(function(match) {
        // check whether we are processing 'bar' or 'bar (N)'
        //  (no captured group = no '(N)' = pure 'bar' = set index at zero)
        return r.branch(
          match('groups').filter(function(group) {
            return group.typeOf().ne('NULL');
          }).count().gt(0),
          // wrap in { index } for the following orderBy
          { index: match('groups').nth(0)('str').coerceTo('number') },
          { index: 0 }
        );
      })
      // ensure the first item in the list is the highest index
      .orderBy(r.desc('index'));
      // now we can decide on what to add
      return r.branch(
        // if there were some matches, 'bar' exists already
        //  and we now have the highest index in the list
        matched.count().gt(0),
        // add 'bar' appended with ' (N)', having N = highest index + 1
        {
          users: doc('users').add([{
            userid: uid,
            username: r.expr(uname).add(
              ' (',
              matched.nth(0)('index').add(1).coerceTo('string'),
              ')'
            )
          }])
        },
        // else, just add the user as is
        { users: doc('users').add([{ userid: uid, username: uname }]) }
      );
    });
    

    Hope this helps!