|
| 1 | +// Package dockey contains logic related to document key determination. |
| 2 | +// Its tests use a cluster and thus are stored in internal/verifier. |
| 3 | + |
| 4 | +package dockey |
| 5 | + |
| 6 | +import ( |
| 7 | + "maps" |
| 8 | + "slices" |
| 9 | + "strconv" |
| 10 | + |
| 11 | + "github.com/samber/lo" |
| 12 | + "go.mongodb.org/mongo-driver/bson" |
| 13 | +) |
| 14 | + |
| 15 | +// ExtractTrueDocKeyAgg returns an aggregation expression that extracts the |
| 16 | +// document key from the document to which the `docExpr` refers. |
| 17 | +// |
| 18 | +// NB: This avoids the problem documented in SERVER-109340; as a result, |
| 19 | +// the returned key may not always match the change stream’s `documentKey` |
| 20 | +// (because the server misreports its own sharding logic). |
| 21 | +func ExtractTrueDocKeyAgg(fieldNames []string, docExpr string) bson.D { |
| 22 | + assertFieldNameUniqueness(fieldNames) |
| 23 | + |
| 24 | + var docKeyNumKeys bson.D |
| 25 | + numToKeyLookup := map[string]string{} |
| 26 | + |
| 27 | + for n, name := range fieldNames { |
| 28 | + var valExpr = docExpr + "." + name |
| 29 | + |
| 30 | + // Aggregation forbids direct creation of an object with dotted keys. |
| 31 | + // So here we create an object with numeric keys, then below we’ll |
| 32 | + // map the numeric keys back to the real ones. |
| 33 | + |
| 34 | + nStr := strconv.Itoa(n) |
| 35 | + docKeyNumKeys = append(docKeyNumKeys, bson.E{nStr, valExpr}) |
| 36 | + numToKeyLookup[nStr] = name |
| 37 | + } |
| 38 | + |
| 39 | + // Now convert the numeric keys back to the real ones. |
| 40 | + return mapObjectKeysAgg(docKeyNumKeys, numToKeyLookup) |
| 41 | +} |
| 42 | + |
| 43 | +// Potentially reusable: |
| 44 | +func mapObjectKeysAgg(expr any, mapping map[string]string) bson.D { |
| 45 | + // We would ideally pass mapping into the aggregation and $getField |
| 46 | + // to get the mapped key, but pre-v8 server versions required $getField’s |
| 47 | + // field parameter to be a constant. (And pre-v5 didn’t have $getField |
| 48 | + // at all.) So we use a $switch instead. |
| 49 | + mapAgg := bson.D{ |
| 50 | + {"$switch", bson.D{ |
| 51 | + {"branches", lo.Map( |
| 52 | + slices.Collect(maps.Keys(mapping)), |
| 53 | + func(key string, _ int) bson.D { |
| 54 | + return bson.D{ |
| 55 | + {"case", bson.D{ |
| 56 | + {"$eq", bson.A{ |
| 57 | + key, |
| 58 | + "$$numericKey", |
| 59 | + }}, |
| 60 | + }}, |
| 61 | + {"then", mapping[key]}, |
| 62 | + } |
| 63 | + }, |
| 64 | + )}, |
| 65 | + }}, |
| 66 | + } |
| 67 | + |
| 68 | + return bson.D{ |
| 69 | + {"$arrayToObject", bson.D{ |
| 70 | + {"$map", bson.D{ |
| 71 | + {"input", bson.D{ |
| 72 | + {"$objectToArray", expr}, |
| 73 | + }}, |
| 74 | + {"in", bson.D{ |
| 75 | + {"$let", bson.D{ |
| 76 | + {"vars", bson.D{ |
| 77 | + {"numericKey", "$$this.k"}, |
| 78 | + {"value", "$$this.v"}, |
| 79 | + }}, |
| 80 | + {"in", bson.D{ |
| 81 | + {"k", mapAgg}, |
| 82 | + {"v", "$$value"}, |
| 83 | + }}, |
| 84 | + }}, |
| 85 | + }}, |
| 86 | + }}, |
| 87 | + }}, |
| 88 | + } |
| 89 | +} |
0 commit comments