17-Jan-2022 | Like this? Dislike this? Let me know |
The MongoDB query language ("MQL") does not need a parser per se in the same way that is required of SQL. Instead of syntax and whitespace and commas and quotes, MQL boils downs to "maps of maps". For example, the following expression (using "relaxed" Javascript as permitted by mongosh, the MongoDB CLI):
{$match: {$expr: {$eq:[0,{$size: "$someArray"}]} }}
Document d1 = new Document("$size", "$someArray"); List l1 = Arrays.asList(0, d1); Document d2 = new Document("$eq", l1); Document d3 = new Document("$expr", d2); Document d4 = new Document("$match", d3); or merged: Document d4 = new Document("$match", new Document("$expr", new Document("$eq", Arrays.asList(0, new Document("$size", "$someArray")))))
A challenge arises when the the expression is not modestly sized. A popular way to experiment with and construct desired complex aggregation pipelines is to use Compass or a plain text editor to build a Javascript/mongosh expression because readability and general understanding of the purpose of the pipeline is dramatically improved:
{$addFields: { friends: {$reduce: { input: {$range:[0,6]}, initialValue: {aa: "$friends", picks: []}, in: { $let: { vars: {idx: {$floor:{$multiply:[{$rand: {}},{$size:"$$value.aa"}]}}, sz: {$size:"$$value.aa"} }, in: { picks: {$concatArrays: [ "$$value.picks", {$slice:["$$value.aa","$$idx",1]} ]}, aa: {$cond: [{$eq:["$$idx",0]}, {$slice:["$$value.aa",1,{$subtract:["$$sz",1]}]}, {$cond: [ {$eq:["$$idx",{$subtract:["$$sz",1]}]}, {$slice:["$$value.aa",0,{$subtract:["$$sz",1]}]}, {$concatArrays: [ {$slice:["$$value.aa",0,"$$idx"]}, {$slice:["$$value.aa",{$add:["$$idx",1]},{$subtract:["$$sz",2]}]} ]} ]} ]} } }} }} } }
a = Arrays.asList(new Document("$addFields", new Document("friends", new Document("$reduce", new Document("input", new Document("$range", Arrays.asList(0L, 6L))) .append("initialValue", new Document("aa", "$friends") .append("picks", Arrays.asList())) .append("in", new Document("$let", new Document("vars", new Document("idx", new Document("$floor", new Document("$multiply", Arrays.asList(new Document("$rand", new Document()), new Document("$size", "$$value.aa"))))) .append("sz", new Document("$size", "$$value.aa"))) .append("in", new Document("picks", new Document("$concatArrays", Arrays.asList("$$value.picks", new Document("$slice", Arrays.asList("$$value.aa", "$$idx", 1L))))) .append("aa", new Document("$cond", Arrays.asList(new Document("$eq", Arrays.asList("$$idx", 0L)), new Document("$slice", Arrays.asList("$$value.aa", 1L, new Document("$subtract", Arrays.asList("$$sz", 1L)))), new Document("$cond", Arrays.asList(new Document("$eq", Arrays.asList("$$idx", new Document("$subtract", Arrays.asList("$$sz", 1L)))), new Document("$slice", Arrays.asList("$$value.aa", 0L, new Document("$subtract", Arrays.asList("$$sz", 1L)))), new Document("$concatArrays", Arrays.asList(new Document("$slice", Arrays.asList("$$valu\ e.aa", 0L, "$$idx")), new Document("$slice", Arrays.asList("$$value.aa", new Document("$add", Arrays.asList("$$idx", 1L)), new Document("$subtract", Arrays.asList("$$sz", 2L\ ))))))))))))))))))
private static class StageHelper { private StringBuilder txt; public StageHelper() { this.txt = new StringBuilder(); } public void add(String expr, Object ... subs) { expr.replace("'", "\""); // This is the helpful part. if(subs.length > 0) { expr = String.format(expr, subs); // this too } txt.append(expr); } public Document fetch() { return Document.parse(txt.toString()); } }
Listpipeline = new ArrayList (); int n_picks = 6; StageHelper s = new StageHelper(); s.add("{$addFields: {"); s.add(" friends: {$reduce: {"); s.add(" input: {$range:[0,%d]},", n_picks); // varags substitution! s.add(" initialValue: {aa: '$friends', picks: []},"); s.add(" in: {"); s.add(" $let: {"); s.add(" vars: {idx: {$floor:{$multiply:[{$rand: {}},{$size:'$$value.aa'}]}},"); s.add(" sz: {$size:'$$value.aa'}"); s.add(" },"); s.add(" in: {"); s.add(" picks: {$concatArrays: [ '$$value.picks', {$slice:['$$value.aa','$$idx',1]} ]},"); s.add(" aa: {$cond: [{$eq:['$$idx',0]},"); s.add(" {$slice:['$$value.aa',1,{$subtract:['$$sz',1]}]},"); s.add(" {$cond: ["); s.add(" {$eq:['$$idx',{$subtract:['$$sz',1]}]},"); s.add(" {$slice:['$$value.aa',0,{$subtract:['$$sz',1]}]},"); s.add(" {$concatArrays: ["); s.add(" {$slice:['$$value.aa',0,'$$idx']},"); s.add(" {$slice:['$$value.aa',{$add:['$$idx',1]},{$subtract:['$$sz',2]}]}"); s.add(" ]}"); s.add(" ]}"); s.add(" ]}"); s.add(" }"); s.add(" }}"); s.add(" }}"); s.add(" }"); s.add("}"); pipeline.add(s.fetch()); AggregateIterable output = coll.aggregate(pipeline); MongoCursor iterator = output.iterator(); while (iterator.hasNext()) { Document doc = iterator.next(); // ... }
Listpipeline = new ArrayList (); String agg = """ {$addFields: { friends: {$reduce: { input: {$range:[0,%d]}, initialValue: {aa: "$friends", picks: []}, in: { $let: { vars: {idx: {$floor:{$multiply:[{$rand: {}},{$size:"$$value.aa"}]}}, sz: {$size:"$$value.aa"} }, in: { picks: {$concatArrays: [ "$$value.picks", {$slice:["$$value.aa","$$idx",1]} ]}, aa: {$cond: [{$eq:["$$idx",0]}, {$slice:["$$value.aa",1,{$subtract:["$$sz",1]}]}, {$cond: [ {$eq:["$$idx",{$subtract:["$$sz",1]}]}, {$slice:["$$value.aa",0,{$subtract:["$$sz",1]}]}, {$concatArrays: [ {$slice:["$$value.aa",0,"$$idx"]}, {$slice:["$$value.aa",{$add:["$$idx",1]},{$subtract:["$$sz",2]}]} ]} ]} ]} } }} }} } } """; pipeline.add( Document.parse(String.format(agg, 6)) ); AggregateIterable output = coll.aggregate(pipeline); MongoCursor iterator = output.iterator(); while (iterator.hasNext()) { Document doc = iterator.next(); // ... }
... s.add(" }}"); s.add(" }"); s.add("}"); pipeline.add(s.fetch()); // Direct object setup instead of StageHelper: pipeline.add(new Document("$addFields", new Document("friends", "$friends.picks"))); return pipeline;
Java: new Document("d", new Document("$gt", new java.util.Date(Util.toDate("2022-01-01")))) Javascript: {"d":{"$gt": new ISODate("2022-01-01")}}
This will compile and also execute in the query engine without fail but it is wrong because types are mismatched. Field d is a datetime in the database but here it is a string! There is no java.util.Date or ISODate in JSON. s.add("{$match: {d: {$gt: '2022-01-17T21:08:45.820Z'}} } ");
s.add("{$match: {d: {$gt: {$date: '2022-01-17T21:08:45.820Z'}} }} "); or more likely through variable substitution: s.add("{$match: {d: {$gt: {$date: '%s'}} }} ", "2022-01-17T21:08:45.820Z");
StageHelper s = new StageHelper(); // Don't worry about -1 or X; they will be replaced: s.add("{$match: {'idx':-1, d:{'$gt':'X'}}}"); Document doc = s.fetch(); Map match_arg = (Map) DotPath.extractObject(doc, "$match"); match_arg.put("idx",3); Map date_arg = (Map) DotPath.extractObject(doc, "$match.d"); date_arg.put("$gt",new java.util.Date(1642636800000L)); // The doc is now: Document{{$match=Document{{idx=3, d=Document{{$gt=Wed Jan 19 19:00:00 EST 2022}}}}}} // If we were to use this approach on the random slice pipeline // example above to change the argument to $range to change the // number of picks, it would be: List a = (List) Dotpath.extract("$addFields.friends.$reduce.input.$range"); a.put(1, 8); // change idx 1 (second item in $range array) to 8
StageHelper s = new StageHelper(); s.add("{$match: {'idx':'@idx', d:{'$gt':'@targetDate'}}}"); Document doc = s.fetch(); // If parse OK, move on to subs: // Pseudojava: Map vars = {"idx": 3, "targetDate": new java.util.Date("2020-01-01")} StageHelper.substitute(doc, '@', vars);
Like this? Dislike this? Let me know