xx yy
xx yy

Reputation: 614

Join fields when not all have values

I want to modify a field through a projection stage in the aggregation pipeline, this field is combination of other fields values separated by (-)

if the field is null or empty of missing it will not be added to the cocatenated string

{$project:{

//trial-1:
finalField:{
 $concat["$field1",'-','$field2','-','$field3',...] 
//problem1: $concat will return null if any of it's arguments is null or missing
//problem2: if all the fields are exist with non-null values, the delimiter will exists even if the field dosen't 
}


//trial-2:
finalField:{
 $concat:[
  {$cond:[{field1:null},'',{$concat:['$field1','-']},..]
//the problem: {field1:null} fails if the field dosen't exixt (i.e the expression gives true)

//trial-3
finalField:{
 $concat:[
  {$cond:[{$or:[{field1:null},{field:{$exists:true}},'',
   {$concat:['$field1','-']}
]}]}
]
}

] 
}

//trial-4 -> using $reduce instead of $concate (same issues)

}

Upvotes: 3

Views: 3654

Answers (1)

Neil Lunn
Neil Lunn

Reputation: 151092

You basically want $ifNull. It's "sort of" like $exists but for aggregation statements, where it returns a default value when the field expression returns null, meaning "not there":

{ "$project": {
  "finalField": {
    "$concat": [
      { "$ifNull": [ "$field1", "" ] },
      "-",
      { "$ifNull": [ "$field2", "" ] },
      "-",
      { "$ifNull": [ "$field3", "" ] }
    ]
  }
}}

For example with data like:

{ "field1": "a", "field2": "b", "field3": "c" },
{ "field1": "a", "field2": "b" },
{ "field1": "a", "field3": "c" }

You get, without any error producing of course:

{ "finalField" : "a-b-c" }
{ "finalField" : "a-b-" }
{ "finalField" : "a--c" }

If you want something fancier, then you would instead dynamically work with the names, as in:

  { "$project": {
    "finalField": {
      "$reduce": {
        "input": {
          "$filter": {
            "input": { "$objectToArray": "$$ROOT" },
            "cond": { "$ne": [ "$$this.k", "_id" ] }
          }
        },
        "initialValue": "",
        "in": { 
          "$cond": {
            "if": { "$eq": [ "$$value", "" ] },
            "then": { "$concat": [ "$$value", "$$this.v" ] },
            "else": { "$concat": [ "$$value", "-", "$$this.v" ] }
          }
        }
      }
    }
  }}

Which can be aware of what fields were actually present and only attempt to join those:

{ "finalField" : "a-b-c" }
{ "finalField" : "a-b" }
{ "finalField" : "a-c" }

You can even manually specify the list of fields if you don't want the $objectToArray over the document or sub-document:

  { "$project": {
    "finalField": {
      "$reduce": {
        "input": {
          "$filter": {
            "input": ["$field1", "$field2", "$field3"],
            "cond": { "$ne": [ "$$this", null ] }
          }
        },
        "initialValue": "",
        "in": { 
          "$cond": {
            "if": { "$eq": [ "$$value", "" ] },
            "then": { "$concat": [ "$$value", "$$this" ] },
            "else": { "$concat": [ "$$value", "-", "$$this" ] }
          }
        }
      }
    }
  }}

Upvotes: 12

Related Questions