0

I have a collection with objects like this:

{
  "_id": "test",
  "rows": [
    {
      "id": "r1",
      "blocks": [
        {
          "id": "b1"
        },
        {
          "id": "b2"
        }
      ]
    },
    {
      "id": "r2",
      "blocks": [
        {
          "id": "b3"
        }
      ]
    }
  ]
}

I would like to add collection validation to enforce unique block ids within rows.blocks so that there are never 2 blocks with the same id in a single document.

This answer on Mongo's JIRA board explains how to set up a query validator to check array property uniqueness using a $size == $setUnion check like this:

db.runCommand({collMod:"coll", validator: {$expr:{$eq:[{$size:"$a.b"},{$size:{$setUnion:"$a.b"}}]}}})

However, it doesn't seem to work for nested arrays like in my example. I tried using this but it didn't block me from saving an invalid document:

{validator: {$expr: {$eq: [{$size: "$rows.blocks.id"}, {$size: {$setUnion: "$rows.blocks.id"}}]}}}

So what's the right way to set up a validator to enforce uniqueness among multiple nested arrays like the example?

2
  • If your query pattern is on the block level, you should flatten the document like this. A simple unique index on_id, row_id will enforce the uniqueness. Commented Aug 25 at 23:26
  • True, splitting up the doc in the question into 3 with that format works too. I guess the tradeoff is complexity since debugging a missing block in the original doc would require querying multiple other docs to reassemble the original. If you want to post your comment as an answer, that could be useful to people evaluating their schema design Commented Aug 28 at 14:19

1 Answer 1

0

For calculating uniqueness across multiple nested arrays, you have to use the $sum and $reduce operators in your query validator to combine the elements in the arrays before calculating the final size and setUnion size.

{
  $expr: {
    $eq: [
      {
        $sum: {
          $map: {
            input: '$rows',
            as: 'row',
            'in': {
              $size: {
                $ifNull: [
                  '$$row.blocks',
                  []
                ]
              }
            }
          }
        }
      },
      {
        $size: {
          $setUnion: {
            $reduce: {
              input: '$rows',
              initialValue: [],
              'in': {
                $concatArrays: [
                  '$$value',
                  {
                    $map: {
                      input: {
                        $ifNull: [
                          '$$this.blocks',
                          []
                        ]
                      },
                      as: 'block',
                      'in': '$$block.id'
                    }
                  }
                ]
              }
            }
          }
        }
      }
    ]
  }
}

The 1st $sum gets the total number of blocks in all your rows, while the 2nd $reduce gets the total number of unique block ids in all your rows combined. The final $eq check at the top will verify that they're equal.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.