Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

monq

Release Go Reference Go Report Card License Coverage

A composable query-building library for the official MongoDB Go driver.

Why

Hand-written MongoDB queries with the Go driver mean nesting bson.D, bson.A, and bson.E values, and remembering operator strings like $eq, $gte, or $elemMatch, usually with the MongoDB docs open in another tab. monq replaces that with plain functions named after the operators they build, so the function signature tells you what it does and your editor's autocomplete surfaces the operators you have available.

monq is not an ODM. There are no models, no sessions, no query execution, it only builds bson.D values and hands them back. Every function's output plugs directly into Find, Aggregate, UpdateOne, and the rest of the driver's API with no adapter layer in between.

How it works

Each function takes a field path and a value (or, for logical operators, other monq expressions) and returns a bson.D:

monq.Eq("status", "active")
// bson.D{{Key: "status", Value: bson.D{{Key: "$eq", Value: "active"}}}}

Filters compose by nesting function calls, no builder object, no method chaining:

filter := monq.And(
    monq.Eq("status", "active"),
    monq.Or(
        monq.Gte("stats.followers", 10000),
        monq.Exists("verified_at", true),
    ),
)

cursor, err := collection.Find(ctx, filter)

For anything monq doesn't have a function for yet, Raw accepts a hand-written bson.D anywhere a monq expression is expected, so it composes with the rest of a query instead of forcing an all-or-nothing rewrite:

monq.And(
    monq.Eq("status", "active"),
    monq.Raw(bson.D{{Key: "$where", Value: "this.credits == this.debits"}}),
)

Array conditions read the same way. ElemMatch takes the criteria a single array element has to satisfy:

filter := monq.ElemMatch("items",
    monq.Eq("sku", "abc"),
    monq.Gte("qty", 2),
)
// {"items": {"$elemMatch": {"sku": {"$eq": "abc"}, "qty": {"$gte": 2}}}}

Operators

Query operators available today:

Category Functions
Comparison Eq Ne Gt Gte Lt Lte In Nin
Logical And Or Nor Not
Element Exists Type
Array All ElemMatch Size
Evaluation Expr JSONSchema Mod Regex Text
Bitwise BitsAllClear BitsAllSet BitsAnyClear BitsAnySet
Geospatial GeoWithin GeoIntersects Near NearSphere
Escape Raw

Operators with optional parts take them as variadic options named after the MongoDB field they set:

monq.Text("coffee shop", monq.Language("en"), monq.CaseSensitive())
// {"$text": {"$search": "coffee shop", "$language": "en", "$caseSensitive": true}}

Geospatial queries come with geometry constructors, so there is no hand-written GeoJSON. Point, Polygon, and GeoJSON build the shape, Geometry hands it to an operator, and the legacy Box, Center, and CenterSphere shapes are there for 2d data. Positions are [longitude, latitude]:

monq.Near("loc", monq.Geometry(monq.Point(-73.97, 40.77)), monq.MaxDistance(1000))
// {"loc": {"$near": {"$geometry": {"type": "Point", "coordinates": [-73.97, 40.77]}, "$maxDistance": 1000.0}}}

Updates

Update operators work the same way, one field each, and a single one is already a valid update document:

collection.UpdateOne(ctx, filter, monq.Set("status", "active"))
// {"$set": {"status": "active"}}

Several of them go through Update, which merges operators sharing a key. It is to update documents what And is to filters, and it exists because two $set documents concatenated by hand end up as a duplicate key that MongoDB does not merge:

update := monq.Update(
    monq.Set("status", "active"),
    monq.Inc("logins", 1),
    monq.Set("name", "ada"),
)
// {"$set": {"status": "active", "name": "ada"}, "$inc": {"logins": 1}}
Category Functions
Field update Set SetOnInsert Unset Inc Mul Min Max Rename CurrentDate CurrentDateTimestamp
Array update Push PushEach AddToSet AddToSetEach Pull PullAll PopFirst PopLast
Bitwise BitAnd BitOr BitXor
Composition Update

The $each form of $push is its own function, since the $position, $slice, and $sort modifiers only exist there:

monq.PushEach("scores", []any{90, 80}, monq.PushSort(-1), monq.PushSlice(3))
// {"$push": {"scores": {"$each": [90, 80], "$sort": -1, "$slice": 3}}}

Aggregation stages

Pipeline stages live in monq/stage, one function per stage. The package split is what keeps names honest: stage.Set is the $set stage while monq.Set is the $set update operator, and the qualifier says which one is meant.

pipeline := stage.Pipeline(
    stage.Match(monq.Eq("status", "active")),
    stage.Sort(bson.D{{Key: "created_at", Value: -1}}),
    stage.Limit(20),
)

cursor, err := collection.Aggregate(ctx, pipeline)

Pipeline returns a []bson.D, which is what the driver's mongo.Pipeline is defined as, so it goes into Aggregate as is. A plain slice literal works too.

Grouping stages take their output fields as Accumulator pieces, merged into one document by the stage:

stage.Group("$category",
    stage.Accumulator("total", bson.D{{Key: "$sum", Value: "$amount"}}),
)
// {"$group": {"_id": "$category", "total": {"$sum": "$amount"}}}
Category Functions
Filtering Match Limit Skip Sample Count Sort
Grouping Group Bucket BucketAuto SortByCount Facet Unwind
Joining Lookup LookupPipeline GraphLookup UnionWith
Reshaping Project AddFields Set Unset ReplaceRoot ReplaceWith
Output Out Merge Documents
Geospatial GeoNear
Building Field Accumulator FacetPipeline Namespace
Assembly Pipeline

GeoNear takes the same geometry constructors the query operators do, which is why they return bare GeoJSON:

stage.GeoNear(monq.Point(-73.97, 40.77), "distance", stage.MaxDistance(1000), stage.Spherical())
// {"$geoNear": {"near": {"type": "Point", "coordinates": [-73.97, 40.77]}, "distanceField": "distance", ...}}

Aggregation expressions

monq/expr builds the expressions stages compute with. Expression operators compare values rather than naming a field, so a field goes in as a reference:

stage.Project(
    stage.Field("name", 1),
    stage.Field("grade", expr.Switch(
        expr.Branch(expr.Gte(expr.Field("score"), 90), "A"),
        expr.Branch(expr.Gte(expr.Field("score"), 80), "B"),
        expr.DefaultCase("F"),
    )),
)

expr.Field("score") is the string "$score", which is how the aggregation framework tells a field from a constant. The rule runs both ways: any string starting with $ is read as a reference, so a literal one goes through expr.Literal. That is also why expr.Eq(a, b) and monq.Eq(field, value) are different functions rather than one name; expr.Eq("status", "active") compares two constants and is false everywhere.

Accumulators are the same functions, since MongoDB spells them the same way. Sum reads its argument count: one argument totals a field across a group, several add them up inside each document.

stage.Group("$category",
    stage.Accumulator("total", expr.Sum(expr.Field("amount"))),
    stage.Accumulator("best", expr.Top(bson.D{{Key: "score", Value: -1}}, expr.Field("name"))),
)
Category Functions
References Field Literal
Comparison Cmp Eq Ne Gt Gte Lt Lte
Boolean And Or Not
Conditional Cond IfNull Switch Branch DefaultCase
Arithmetic Abs Add Ceil Divide Exp Floor Ln Log Log10 Mod Multiply Pow Round Sqrt Subtract Trunc
String Concat Split SubstrBytes SubstrCP StrLenBytes StrLenCP Strcasecmp ToLower ToUpper
Trimming Trim Ltrim Rtrim TrimChars
Searching IndexOfBytes IndexOfCP RegexFind RegexFindAll RegexMatch ReplaceOne ReplaceAll
Array ArrayElemAt ConcatArrays First Last FirstN LastN MaxN MinN In IndexOfArray IsArray Size
Array shape Filter Map Reduce Range ReverseArray Slice SliceFrom SortArray Zip
Object ArrayToObject ObjectToArray MergeObjects GetField SetField UnsetField
Set AllElementsTrue AnyElementTrue SetDifference SetEquals SetIntersection SetIsSubset SetUnion
Date DateAdd DateSubtract DateDiff DateFromParts DateToParts DateFromString DateToString
Date parts Year Month DayOfMonth DayOfWeek DayOfYear Hour Minute Second Millisecond Week IsoDayOfWeek IsoWeek IsoWeekYear
Conversion Convert IsNumber Type ToBool ToDate ToDecimal ToDouble ToInt ToLong ToObjectID ToString
Accumulator Sum Avg Max Min Push AddToSet Count StdDevPop StdDevSamp Top TopN Bottom BottomN Median Percentile

Install

go get github.com/behzadsh/monq

License

MIT

About

MongoDB queries in Go you can actually read. Composable builders for filters, updates, and pipelines, with the docs in your godoc.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages