Rui Pedro
Rui Pedro

Reputation: 97

Mongoose unique if not null and if state

I have a unique index like this

    code: {
        type: String,
        index: {
            unique: true,
            partialFilterExpression: {
                code: { $type: 'string' }
            }
        },
        default: null
    },
    state: { type: Number, default: 0 },

but When the state is 2 (archived) I want to keep the code, but it should be able to reuse the code, so it cannot be unique if state is 2. Is there any away that I could accomplish this?

Upvotes: 0

Views: 300

Answers (2)

GitGitBoom
GitGitBoom

Reputation: 1912

This is possible, though it's through a work around documented here https://jira.mongodb.org/browse/SERVER-25023.

In MongoDB 4.7 you will be able to apply different index options to the same field but for now you can add a non-existent field to separate the two indexes.

Here's an example using the work around.

(async () => {
  const ItemSchema = mongoose.Schema({
    code: {
      type: String,
      default: null
    },
    state: {
      type: Number,
      default: 0,
    },
  });

  // Define a unique index for active items
  ItemSchema.index({code: 1}, {
    name: 'code_1_unique',
    partialFilterExpression: {
      $and: [
        {code: {$type: 'string'}},
        {state: {$eq: 0}}
      ]
    },
    unique: true
  })

  // Defined a non-unique index for non-active items
  ItemSchema.index({code: 1, nonExistantField: 1}, {
    name: 'code_1_nonunique',
    partialFilterExpression: {
      $and: [
        {code: {$type: 'string'}},
        {state: {$eq: 2}}
      ]
    },
  })

  const Item = mongoose.model('Item', ItemSchema)

  await mongoose.connect('mongodb://localhost:27017/so-unique-compound-indexes')
  
  // Drop the collection for test to run correctly
  await Item.deleteMany({})

  // Successfully create an item
  console.log('\nCreating a unique item')
  const itemA = await Item.create({code: 'abc'});


  // Throws error when trying to create with the same code
  await Item.create({code: 'abc'})
    .catch(err => {console.log('\nThrowing a duplicate error when creating with the same code')})


  // Change the active code
  console.log('\nChanging item state to 2')
  itemA.state = 2; 
  await itemA.save();


  // Successfully created a new doc with sama code
  await Item.create({code: 'abc'})
    .then(() => console.log('\nSuccessfully created a new doc with sama code'))
    .catch(() => console.log('\nThrowing a duplicate error'));
  

  // Throws error when trying to create with the same code 
  Item.create({code: 'abc'})
  .catch(err => {console.log('\nThrowing a duplicate error when creating with the same code again')})
})();

Upvotes: 1

harshil1507
harshil1507

Reputation: 79

This is not possible with using indexes. Even if you use a compound index for code and state there will still be a case where

new document

{
  code: 'abc',
  state: 0
}

archived document

{
  code: 'abc',
  state: 2

}

Now although you have the same code you will not be able to archive the new document or unarchive the archived document.

You can do something like this

const checkCode = await this.Model.findOne({code:'abc', active:0})
if(checkCode){
  throw new Error('Code has to be unique')
}
else{
 .....do something
}

Upvotes: 0

Related Questions