Comparing number values as strings in MongoDb - mongodb

I am currently trying to compare two strings which are numbers in a find query. I need to use strings because the numbers would cause an overflow in Javascript if I save them as such.
The to compared value comes via an API call and the value is inside an array:
const auction = await this.findOne({
$and: [{
$or: [
{ 'bids.amount': amount },
{ 'bids.signature': signature },
{ 'bids.amount': { $gte: amount } }
]
}, { 'tokenId': tokenId }, { 'isActive': true }]
});
How would I change the query in order to handle the strings as numbers, so my comparison would actually be correct?

The bellow query assumes that bids is an array like
bids=[{"amount" : "1224245" , "signature" : "234454523"} ...]
If signature is not a number remove the $toLong from signature.
aggregate(
[{"$match":
{"$expr":
{"$and":
[{"$eq": ["$tokenId", "tokenId_VAR"]},
{"$eq": ["$isActive", true]},
{"$reduce":
{"input": "$bids",
"initialValue": false,
"in":
{"$or":
["$$value",
{"$gte": [{"$toLong": "$$this.amount"}, "amount_VAR"]},
{"$eq": [{"$toLong": "$$this.signature"}, "signature_VAR"]}]}}}]}}}])

Related

Set and concat to string field with arrayFilters in MongoDB

My MongoDB document looks like this.
{
_id: '556ebf103a8b4c3d067bb6466df7c0651c53a1ff79fa348edff656586c24ad0c',
Log: [
{
ScriptName: 'UpdateScript',
ScriptLogs: [
{
ID: 'f5bd1fefb2a5c4712ea1d35eb5bd309074984c5932db3723cc29f8d92258a3fa',
StdOut: 'Hello'
}
]
}
]}
I have the MongoDb Query
db.getCollection("ServerEntitys").update(
{_id: '556ebf103a8b4c3d067bb6466df7c0651c53a1ff79fa348edff656586c24ad0c'},
{
$set: {
'Log.$[i].ScriptLogs.$[j].StdOut': { "$concat": ["$Log.$[i].ScriptLogs.$[j].StdOut", "World"]}
}
},
{ arrayFilters: [ { "i.ScriptName": "UpdateScript" } , { "j.ID": 'f5bd1fefb2a5c4712ea1d35eb5bd309074984c5932db3723cc29f8d92258a3fa'} ] }
)
After running the query I get
StdOut: {
$concat: [
'$Log.$[i].ScriptLogs.$[j].StdOut',
'World'
]
},
instead of
StdOut: 'HelloWorld'
Can someone tell me why mongoDB uses $concat as value and not as a $concat instruction
When you are using normal update, you should use ONLY update operators
After MongoDB 4.2, we can also update using a pipeline update, using ONLY aggregate operators (no update ones, no arrayfilters etc)
We dont have an update operator that concat strings, so looks like you need pipeline update like the bellow.
Query
nested $map
outer to match the updateScript
inner to match the ID
in both cases if no match is found we keep the old array member
Playmongo
coll.updateMany({},
[{"$set":
{"Log":
{"$map":
{"input": "$Log",
"as": "outer",
"in":
{"$cond":
[{"$ne": ["$$outer.ScriptName", "UpdateScript"]}, "$$outer",
{"$mergeObjects":
["$$outer",
{"ScriptLogs":
{"$map":
{"input": "$$outer.ScriptLogs",
"as": "inner",
"in":
{"$cond":
[{"$ne": ["$$inner.ID", "2"]}, "$$inner",
{"$mergeObjects":
["$$inner",
{"StdOut":
{"$concat":
["$$inner.StdOut"," World"]}}]}]}}}}]}]}}}}}])

mongo - accessing a key-value field while filtering

I have these data:
myMap = {
"2": "facing",
"3": "not-facing"
"1": "hidden"
}
stages [
{
"k": 1,
"v": "hidden"
},
{
"k": 2,
"v": "facing"
},
{
"k": 3,
"v": "not-facing"
}
]
and a aggregate query but, I'm missing a syntax to dynamically fetch the map data:
db.MyCollection.aggregate()
.addFields({
myMaps: myMap
})
.addFields({
stages: stages
})
.addFields({
process: {
$filter: {
input: stages,
as: stageData,
cond: {$eq: [$$stageData.v, $myMaps[$$stageData.k]]}
}
}
})
As you may already note, this syntax: $myMaps[$$stageData.k] doesn't work, how should I access the myMaps based on the value of the k in stageData ?
Query
like your query set mymap and stages as extra field
mymapKeys is an extra field added (you can $unset is in the end)
filter the stages, and if stage.k is contained in mymapKeys we keep that member
*not sure if this is what you need, but looks like from your query
in mongodb query language we dont have getField(doc,$$k) we only have getField(doc,constant_string) which cant be used in your case,
so it costs here more than a hashmap lookup, here its like linear cost (check if member in the array). For arrays we have $getElementAt(array,$$k) if those numbers are always in sequence, 1,2,3 etc you might be able to use arrays instead of objects
Playmongo
aggregate(
[{"$set": {"mymap": {"2": "facing", "3": "not-facing", "1": "hidden"}}},
{"$set":
{"mymapKeys":
{"$map": {"input": {"$objectToArray": "$mymap"}, "in": "$$this.k"}}}},
{"$set":
{"stages":
[{"k": 1, "v": "hidden"}, {"k": 2, "v": "facing"},
{"k": 3, "v": "not-facing"}]}},
{"$set":
{"process":
{"$filter":
{"input": "$stages",
"cond": {"$in": [{"$toString": "$$this.k"}, "$mymapKeys"]}}}}}])

Convert array of objects, to object

I have a mongo db record like this
items: [
0: {
key: name,
value: y
},
1: {
key: module,
value: z
}
]
And per record my expected output is this
{
name : y,
module : z
}
What is the best mongo db aggregation i should apply to achieve this. I have tried few ways but i am unable to produce output.
We can't use unwind otherwise it will break the relationship between name and module.
Query
the items array is almost ready to become document {}
$map to rename key->k and value->v (those are the field names mongo needs)
$arrayToObject to make the items [] => {}
and then merge items document with the root document
and project the items {} (we don't need it anymore the fields are part of the root document now)
*you can combine the 2 $set in 1 stage, i wrote it in 2 to be simpler
Test code here
aggregate(
[{"$set":
{"items":
{"$map":
{"input": "$items", "in": {"k": "$$this.key", "v": "$$this.value"}}}}},
{"$set": {"items": {"$arrayToObject": ["$items"]}}},
{"$replaceRoot": {"newRoot": {"$mergeObjects": ["$items", "$$ROOT"]}}},
{"$project": {"items": 0}}])
aggregate(
[{
$project: {
items: {
$map: {
input: "$items",
"in": {
"k": "$$this.key",
"v": "$$this.value"
}
}
}
}
}, {
$project: {
items: {
$arrayToObject: "$items"
}
}
}])
Test Code

Remove Duplicate character from string in Mongodb

I want to remove duplicate characters from strings in MongoDB.
Example:
Input string: xxxyzzxcdv
Output string: xyzcdv
Query
reduce on range(count string)
keep 2 values {"previous": [], "string": ""} (initial value of reduce)
get the cur-char {"$substrCP": ["$mystring", "$$this", 1]}
this is the current index on the string, and i take the next char
if it is in previous kep "string" as it is, else concat to add the new character
heelo
reduce on (0 1 2 3 4) `{"$range": [0, {"$strLenCP": "$mystring"}]}`
we start from `{"previous": [], "string": ""}`
- get 1 character start from index 0
`{"$substrCP": ["$mystring", "$$this", 1]}}` = "h"
- if this character is on previous don't add it
`{"$in": ["$$cur_char", "$$value.previous"]}`
- else add it on previous and on the string the 2 concats in code
Repeat for `index($$this)`= 1
- get 1 character start from index 1
`{"$substrCP": ["$mystring", "$$this", 1]}}` = "e"
.....
PlayMongo
aggregate(
[{"$set":
{"mystring":
{"$getField":
{"field": "string",
"input":
{"$reduce":
{"input": {"$range": [0, {"$strLenCP": "$mystring"}]},
"initialValue": {"previous": [], "string": ""},
"in":
{"$let":
{"vars": {"cur_char": {"$substrCP": ["$mystring", "$$this", 1]}},
"in":
{"$cond":
[{"$in": ["$$cur_char", "$$value.previous"]},
"$$value",
{"previous":
{"$concatArrays": ["$$value.previous", ["$$cur_char"]]},
"string":
{"$concat": ["$$value.string", "$$cur_char"]}}]}}}}}}}}}])
Edit
The second query removed only the duplicates we choose.
Query
removes only the characters in the array, here only ["x"]
i removed the $getField because its only for MongoDB 5 +
aggregate(
[{"$set":
{"mystring":
{"$reduce":
{"input": {"$range": [0, {"$strLenCP": "$mystring"}]},
"initialValue": {"previous": [], "string": ""},
"in":
{"$let":
{"vars": {"cur_char": {"$substrCP": ["$mystring", "$$this", 1]}},
"in":
{"$cond":
[{"$and":
[{"$in": ["$$cur_char", "$$value.previous"]},
{"$in": ["$$cur_char", ["x"]]}]},
"$$value",
{"previous":
{"$concatArrays": ["$$value.previous", ["$$cur_char"]]},
"string":
{"$concat": ["$$value.string", "$$cur_char"]}}]}}}}}}},
{"$set": {"mystring": "$mystring.string"}}])
Edit
If you need to use this aggregation for update, you can use it as pipeline update like.
update({},
[{"$set": ......])
See your driver to find how to do update with pipeline, in Java its like above, alternative run it as database command
Let's assume we have the following collection and the records inside of it:
db.messages.insertMany([
{
"message": "heelllo theeere"
},
{
"message": "may thhhee forrrce be wiithh yyouuu"
},
{
"message": "execute orrrrdder 66"
}
])
Due to uncertainty, I am dropping solutions for both manipulating while querying and updating the records (permanently).
If you want to remove them while running your aggregation query:
In addition to #Takis's solution, using $function pipeline operator can be another option if your MongoDB version is 4.4 or higher.
Further readings on $function operator
// Query via Aggregation Framework
db.messages.aggregate([
{
$match: {
message: {
$ne: null
}
}
},
{
$addFields: {
distinctChars: {
$function: {
body: function (message) {
return message
.split("")
.filter(function (item, pos, self) {
return self.indexOf(item) == pos;
})
.join("");
},
args: ["$message"],
lang: "js"
}
}
}
},
])
If you want to remove them via an update operation:
// Update each document using cursor
db.messages.find({ message: { $ne: null } })
.forEach(doc => {
var distinctChars = doc.message
.split("")
.filter(function (item, pos, self) {
return self.indexOf(item) == pos;
})
.join("");
db.messages.updateOne({ _id: doc._id }, [{ $set: { distinctChars: distinctChars } }]);
});
A quick reminder: Above script just shows an easy way to update the records to reach the goal without focusing on other details. It can be an expensive operation depending on your real world collection's size and configurations, sharding for instance. Consider to improve it with your own way.
Result
For both way, the result should be like the following:
[
{
"_id": {
"$oid": "618d95ccdedc26d80875b75a"
},
"message": "heelllo theeere",
"distinctChars": "helo tr"
},
{
"_id": {
"$oid": "618d95ccdedc26d80875b75b"
},
"message": "may thhhee forrrce be wiithh yyouuu",
"distinctChars": "may theforcbwiu"
},
{
"_id": {
"$oid": "618d95ccdedc26d80875b75c"
},
"message": "execute orrrrdder 66",
"distinctChars": "excut ord6"
}
]

Finding all docs that match minimum amount of array values

First time I am writing MongoDB query and managed up to certain point for now but stuck at the moment. I looked into the match property but not sure if it is relevant.
The query below will return all the user documents that contain at least one given role.
roles := []string{"admin", "super_admin", "manager", "student"}
a.db.Collection("users").Find(ctx, bson.M{"roles": bson.M{"$in": roles}})
// db.users.find({roles: { $in: ["admin", "super_admin", "manager", "student"] }})
What I need now is that, specify the minimum matching criteria. For example, the user document must match at least 2 given roles (doesn't matter which ones they are). I will need to use something like EQ, GTE, GT, LT, LTE operators.
Update
It is ok to just handle minimum match so happy to ignore all the listed operators above.
I am not sure there are any other easy way to achieve this, you can use aggregation operators in find if you are using MongoDB v4.4 or you can use aggregate(), I don't know about go syntax but I can do it in MongoDB driver query,
$reduce to iterate loop of roles array, se initial value to 0, check condition if current role is in you input role then add one in initial value otherwise return existing initial value,
check expression with $gte that return number is greater than 2
db.users.find({
$expr: {
$gte: [
{
$reduce: {
input: "$roles",
initialValue: 0,
in: {
$cond: [
{ $in: [$$this", ["admin","super_admin","manager","student"]] },
{ $add: ["$$value", 1] },
"$$value"
]
}
}
},
2 // input your number
]
}
})
Playground
Using aggregate():
Playground
Follows up to accepted answer, this is the Go version of it. Just for those who might need some reference.
filter := bson.M{
"$expr": bson.M{
"$gte": []interface{}{
bson.M{
"$reduce": bson.M{
"input": "$roles",
"initialValue": 0,
"in": bson.M{
"$cond": []interface{}{
bson.M{
"$in": []interface{}{
"$$this",
roles, // This is your []string slice
},
},
bson.M{
"$add": []interface{}{
"$$value",
1,
},
},
"$$value",
},
},
},
},
minMatch, // This is the limit
},
},
}