How do I add properties to subdocument array in aggregation/make $map like $addFields?

Viewed 3700
> db.things.find()
{ "_id" : 1, "a" : [ ] }
{ "_id" : 2, "a" : [ { "x" : 2, "y" : 1 }, { "x" : 0, "y" : 1 } ] }
{ "_id" : 3, "a" : [ { "x" : 2, "y" : 5, "abitraryPropertyWhichShouldBeKept" : true } ] }

I could like to add a calculated property, say magnitude, to each subdocument at a. I don’t want to have to specify x and y manually, and there might be other properties I need to keep. My attempt so far (where I have to manually specify x and y which I shouldn’t have to do):

db.things.aggregate([
  {
    $addFields: {
     a: {
       $map: {
         input: '$a',
         as: 'item',
         'in': {
           x: '$$item.x',
           y: '$$item.y',
           magnitude: {
             $sqrt: {
               $add: [
                 { $pow: ['$$item.x', 2], },
                 { $pow: ['$$item.y', 2] }],
               },
             },
           },
         },
       },
     },
  },
])

results which drop the extra key I wanted to keep:

{ "_id" : 1, "a" : [ ] }
{ "_id" : 2, "a" : [ {
  "x" : 2, "y" : 1, "magnitude" : 2.23606797749979 }, {
  "x" : 0, "y" : 1, "magnitude" : 1 } ] }
{ "_id" : 3, "a" : [ { "x" : 2, "y" : 5, "magnitude" : 5.385164807134504 } ] }

The only other way I could think of is chaining $unwind, $addFields, and $group. But to prevent $unwind from removing document 1, I’d have to add a dummy entry to the end of each a array and then discard it when grouping them together again.

Is there something like $map but that behaves like $addFields so that I can just add a property to the nested document? Do I really have to specify all possible keys of the subdocument just to use $map? If I do not know the keys in the nested subdocuments ahead of time, is my only choice really the $unwind with all the extra pomp and circumstance necessary to use it?

Current Workaround

Here’s my $unwind$group try. But, as you can see, I need a ton of stages that wouldn’t be necessary if I could just get $addFields-like behavior inside of $map or some expression operator which would merge objects together like JavaScript’s Object.assign():

db.things.aggregate([
  { // Add dummy to preserve documents during imminent $unwind
    $addFields: {
      a: {
        $concatArrays: [ '$a', [ null, ], ],
      },
    },
  },
  { $unwind: '$a', },
  { // Do actual calculation on single array element
    $addFields: {
      'a.magnitude': {
        $sqrt: {
          $add: [
            { $pow: ['$a.x', 2] },
            { $pow: ['$a.y', 2] },
          ],
        },
      },
    },
  },
  {
    $group: {
      _id: '$_id',
      a: { $push: '$a', },
      item: { $first: '$$ROOT', },
    },
  },
  { // Remove dummy element
    $addFields: {
      'item.a': {
        $slice: [ '$a', { $add: [ { $size: '$a', }, -1] } ],
      },
    },
  },
  { $replaceRoot: { newRoot: '$item', }, },
])

results which properly preserve any unanticipated keys in the original data:

{ "_id" : 3, "a" : [ {
  "x" : 2, "y" : 5, "abitraryPropertyWhichShouldBeKept" : true,
  "magnitude" : 5.385164807134504 } ] }
{ "_id" : 2, "a" : [ {
  "x" : 2, "y" : 1, "magnitude" : 2.23606797749979 }, {
  "x" : 0, "y" : 1, "magnitude" : 1 } ] }
{ "_id" : 1, "a" : [ ] }
1 Answers
Related