2

Background:

A customer is an object that has a name field.

A line is an object that has the following fields:

  • inLine - an array of customers
  • currentCustomer - a customer
  • processed - an array of customers

The collection 'line' contains documents that are line objects.


Problem:

I'm trying to implement a procedure which would do the following:

  1. Push currentCustomer to processed
  2. Set currentCustomer to the 1st element in inLine
  3. Pop the 1st element of inLine

Since the new value of a field depends on the previous value of another, atomicity is important here.

What I tried so far:

Naive approach

db.collection('line').findOneAndUpdate({
    _id: new ObjectId(lineId),
}, {
    $set: {
        currentCustomer: '$inLine.0',
    },
    $pop: {
        inLine: -1,
    },
    $push: {
        processed: '$currentCustomer',
    },
});

However, currentCustomer is set to a string which is literally "$inLine.0" and processed has a string which is literally "$currentCustomer".

Aggregation approach

db.collection('line').findOneAndUpdate({
    _id: new ObjectId(lineId),
}, [{
    $set: {
        currentCustomer: '$inLine.0',
    },
    $pop: {
        inLine: -1,
    },
    $push: {
        processed: '$currentCustomer',
    },
}]);

However, I got the following error:

MongoError: A pipeline stage specification object must contain exactly one field.

Multi-stage aggregation approach

db.collection('line').findOneAndUpdate({
    _id: new ObjectId(lineId),
}, [{
    $set: {
        currentCustomer: '$inLine.0',
    },
}, {
    $pop: {
        inLine: -1,
    },
}, {
    $push: {
        processed: '$currentCustomer',
    },
}]);

However, $pop and $push are Unrecognized pipeline stage names.

I tried making it using only $set stages, but it ended up very ugly and I still couldn't get it to work.

Ivan Rubinson
  • 3,001
  • 4
  • 19
  • 48
  • [Update MongoDB field using value of another field](https://stackoverflow.com/questions/3974985/update-mongodb-field-using-value-of-another-field) – Ivan Rubinson Nov 16 '20 at 16:19

2 Answers2

2

Based on turivishal's answer, it was solved like so:

db.collection('line').findOneAndUpdate({
    _id: new ObjectId(lineId),
}, [{
    $set: {
        // currentCustomer = inLine.length === 0 ? null : inLine[0]
        currentCustomer: {
            $cond: [
                { $eq: [{ $size: '$inLine' }, 0] },
                null,
                { $first: '$inLine' },
            ],
        },
        // inLine = inLine.slice(1)
        inLine: {
            $cond: [
                { $eq: [{ $size: '$inLine' }, 0] },
                [],
                { $slice: ['$inLine', 1, { $size: '$inLine' }] },
            ],
        },
        // if currentCustomer !== null then processed.push(currentCustomer)
        processed: {
            $cond: [
                {
                    $eq: ['$currentCustomer', null],
                },
                '$processed',
                {
                    $concatArrays: [
                        '$processed', ['$currentCustomer'],
                    ],
                }
            ],
        },
    },
}]);
Ivan Rubinson
  • 3,001
  • 4
  • 19
  • 48
  • Worth noting: This currently doesn't work on MongoDB M0 Sandbox (which is the free one) because it runs version 4.2, but [`$first` is new in 4.4](https://stackoverflow.com/a/64937528/6517320). – Ivan Rubinson Dec 15 '20 at 23:31
1

I don't think its possible with simple update using $push or $pop.

As per your experiment, the aggregation can not support direct $push, $pop stage in root level, so I have corrected your query,

  • currentCustomer check condition if size of inLine is 0 then return null otherwise get first element from inLine array using $arrayElemAt,
  • inLine check condition if size of inLine is 0 then return [] otherwise remove first element from inLine array using $slice and $size
  • processed concat both arrays using $concatArrays, $ifNull to check if field is null then return blank array, check condition if currentCustomer null then return [] otherwise return currentCustomer
db.collection('line').findOneAndUpdate(
  { _id: new ObjectId(lineId), }, 
  [{
    $set: {
      currentCustomer: {
        $cond: [
          { $eq: [{ $size: "$inLine" }, 0] },
          null,
          { $arrayElemAt: ["$inLine", 0] }
        ]
      },
      inLine: {
        $cond: [
          { $eq: [{ $size: "$inLine" }, 0] },
          [],
          { $slice: ["$inLine", 1, { $size: "$inLine" }] }
        ]
      },
      processed: {
        $concatArrays: [
          { $ifNull: ["$processed", []] },
          {
            $cond: [
              { $eq: ["$currentCustomer", null] },
              [],
              ["$currentCustomer"]
            ]
          }
        ]
      }
    }
  }]
);

Playground

turivishal
  • 34,368
  • 7
  • 36
  • 59