3

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)

}

1 Answer 1

12

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" ] }
          }
        }
      }
    }
  }}
Sign up to request clarification or add additional context in comments.

1 Comment

@xxyy Didn't see you there. Yes I was writing that part anyway and it's added here now.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.