Mongo queries framework

The queries framework is used to build Mongo queries as JsonObject with a fluent interface.

Using the queries framework we can get from this:

var exists = new JsonObject
exists["$exists"] = true

var query = new JsonObject
query["login"] = "Morriar"
query["email"] = exists

collection.find(query)

To this:

collection.find((new MongoMatch).eq("login", "Morriar").exists("email", true))

The framework provides three classes used to map the MongoDB query API:

More on this features can be found in the official MongoDB documentation: https://docs.mongodb.com/manual/reference/operator/

Introduced classes

class MongoGroup

mongodb :: MongoGroup

Mongo pipeline group stage
class MongoMatch

mongodb :: MongoMatch

A basic match query
class MongoPipeline

mongodb :: MongoPipeline

Mongo pipelines are arrays of aggregation stages

All class definitions

class MongoGroup

mongodb $ MongoGroup

Mongo pipeline group stage
class MongoMatch

mongodb $ MongoMatch

A basic match query
class MongoPipeline

mongodb $ MongoPipeline

Mongo pipelines are arrays of aggregation stages
package_diagram mongodb::queries queries mongodb mongodb mongodb::queries->mongodb json json mongodb->json c c mongodb->c ...json ... ...json->json ...c ... ...c->c popcorn::pop_repos pop_repos popcorn::pop_repos->mongodb::queries github::loader loader github::loader->popcorn::pop_repos popcorn::pop_tracker pop_tracker popcorn::pop_tracker->popcorn::pop_repos github::loader... ... github::loader...->github::loader popcorn::pop_tracker... ... popcorn::pop_tracker...->popcorn::pop_tracker

Ancestors

module abstract_collection

core :: abstract_collection

Abstract collection classes and services.
module abstract_text

core :: abstract_text

Abstract class for manipulation of sequences of characters
module array

core :: array

This module introduces the standard array structure.
module bitset

core :: bitset

Services to handle BitSet
module bytes

core :: bytes

Services for byte streams and arrays
module c

c :: c

Structures and services for compatibility with the C language
module caching

serialization :: caching

Services for caching serialization engines
module circular_array

core :: circular_array

Efficient data structure to access both end of the sequence.
module codec_base

core :: codec_base

Base for codecs to use with streams
module codecs

core :: codecs

Group module for all codec-related manipulations
module collection

core :: collection

This module define several collection classes.
module core

core :: core

Standard classes and methods used by default by Nit programs and libraries.
module engine_tools

serialization :: engine_tools

Advanced services for serialization engines
module environ

core :: environ

Access to the environment variables of the process
module error

core :: error

Standard error-management infrastructure.
module error

json :: error

Intro JsonParseError which is exposed by all JSON reading APIs
module exec

core :: exec

Invocation and management of operating system sub-processes.
module file

core :: file

File manipulations (create, read, write, etc.)
module fixed_ints

core :: fixed_ints

Basic integers of fixed-precision
module fixed_ints_text

core :: fixed_ints_text

Text services to complement fixed_ints
module flat

core :: flat

All the array-based text representations
module gc

core :: gc

Access to the Nit internal garbage collection mechanism
module hash_collection

core :: hash_collection

Introduce HashMap and HashSet.
module inspect

serialization :: inspect

Refine Serializable::inspect to show more useful information
module iso8859_1

core :: iso8859_1

Codec for ISO8859-1 I/O
module json

json :: json

Read and write JSON formatted text using the standard serialization services
module kernel

core :: kernel

Most basic classes and methods.
module list

core :: list

This module handle double linked lists
module math

core :: math

Mathematical operations
module meta

meta :: meta

Simple user-defined meta-level to manipulate types of instances as object.
module native

core :: native

Native structures for text and bytes
module native_mongodb

mongodb :: native_mongodb

Native wrapper for the MongoDB C Driver
module numeric

core :: numeric

Advanced services for Numeric types
module parser_base

parser_base :: parser_base

Simple base for hand-made parsers of all kinds
module poset

poset :: poset

Pre order sets and partial order set (ie hierarchies)
module protocol

core :: protocol

module queue

core :: queue

Queuing data structures and wrappers
module range

core :: range

Module for range of discrete objects.
module re

core :: re

Regular expression support for all services based on Pattern
module ropes

core :: ropes

Tree-based representation of a String.
module safe

serialization :: safe

Services for safer deserialization engines
module serialization

serialization :: serialization

General serialization services
module serialization_core

serialization :: serialization_core

Abstract services to serialize Nit objects to different formats
module serialization_read

json :: serialization_read

Services to read JSON: deserialize_json and JsonDeserializer
module serialization_write

json :: serialization_write

Services to write Nit objects to JSON strings: serialize_to_json and JsonSerializer
module sorter

core :: sorter

This module contains classes used to compare things and sorts arrays.
module static

json :: static

Static interface to read Nit objects from JSON strings
module stream

core :: stream

Input and output streams of characters
module text

core :: text

All the classes and methods related to the manipulation of text entities
module time

core :: time

Management of time and dates
module union_find

core :: union_find

union–find algorithm using an efficient disjoint-set data structure
module utf8

core :: utf8

Codec for UTF-8 I/O

Parents

module mongodb

mongodb :: mongodb

MongoDB Nit Driver.

Children

module pop_repos

popcorn :: pop_repos

Repositories for data management.

Descendants

# Mongo queries framework
#
# The `queries` framework is used to build Mongo queries as JsonObject with
# a fluent interface.
#
# Using the `queries` framework we can get from this:
#
# ~~~nitish
# var exists = new JsonObject
# exists["$exists"] = true
#
# var query = new JsonObject
# query["login"] = "Morriar"
# query["email"] = exists
#
# collection.find(query)
# ~~~
#
# To this:
#
# ~~~nitish
# collection.find((new MongoMatch).eq("login", "Morriar").exists("email", true))
# ~~~
#
# The framework provides three classes used to map the MongoDB query API:
# * `MongoMatch` the base query that can be used with most Mongo services
# * `MongoPipeline` the array of queries that is expected by `MongoCollection::aggregate`
# * `MongoGroup` the group query for a `MongoPipeline`
#
# More on this features can be found in the official MongoDB documentation:
# https://docs.mongodb.com/manual/reference/operator/
module queries

import mongodb

# A basic match query
#
# `MongoMatch` is used with most of the Mongo services like `find`, `find_all`,
# `remove` etc.
#
# Building a query can be done with the fluent interface:
#
# ~~~
# var query = (new MongoMatch).
#	eq("login", "Morriar").
#	gt("age", 18).
#	exists("email", true).
#	is_in("status", [1, 2, 3, 4])
# ~~~
#
# Fore more help on how to use the query operators of MongoDB please
# refer to the official MongoDB documentation:
# https://docs.mongodb.com/manual/reference/operator/query/
class MongoMatch
	super JsonObject

	# Define a custom operaton for `field`
	#
	# If no `field` is specified, append the operator the the root object:
	# ~~~json
	# {$<name>: <value>}
	# ~~~
	#
	# Else, append the operator to the field:
	# ~~~json
	# {field: {$<name>: <value>} }
	# ~~~
	fun op(name: String, field: nullable String, value: nullable Serializable): MongoMatch do
		if field != null then
			var q = new JsonObject
			q["${name}"] = value
			self[field] = q
		else
			self["${name}"] = value
		end
		return self
	end

	# Match documents where `field` equals `value`
	#
	# https://docs.mongodb.com/manual/reference/operator/query/eq/#op._S_eq
	#
	# ~~~json
	# {field: {$eq: value} }
	# ~~~
	fun eq(field: String, value: nullable Serializable): MongoMatch do
		self[field] = value
		return self
	end

	# Match documents where `field` not equals `value`
	#
	# https://docs.mongodb.com/manual/reference/operator/query/ne/#op._S_ne
	#
	# ~~~json
	# {field: {$ne: value} }
	# ~~~
	fun ne(field: String, value: nullable Serializable): MongoMatch do
		op("ne", field, value)
		return self
	end

	# Match documents where `field` is greater than `value`
	#
	# https://docs.mongodb.com/manual/reference/operator/query/gt/#op._S_gt
	#
	# ~~~json
	# {field: {$gt: value} }
	# ~~~
	fun gt(field: String, value: nullable Serializable): MongoMatch do
		op("gt", field, value)
		return self
	end

	# Match documents where `field` is greater or equal to `value`
	#
	# https://docs.mongodb.com/manual/reference/operator/query/gte/#op._S_gte
	#
	# ~~~json
	# {field: {$gte: value} }
	# ~~~
	fun gte(field: String, value: nullable Serializable): MongoMatch do
		op("gte", field, value)
		return self
	end

	# Match documents where `field` is less than `value`
	#
	# https://docs.mongodb.com/manual/reference/operator/query/lt/#op._S_lt
	#
	# ~~~json
	# {field: {$lt: value} }
	# ~~~
	fun lt(field: String, value: nullable Serializable): MongoMatch do
		op("lt", field, value)
		return self
	end

	# Match documents where `field` is less or equal to `value`
	#
	# https://docs.mongodb.com/manual/reference/operator/query/lte/
	#
	# ~~~json
	# {field: {$lte: value} }
	# ~~~
	fun lte(field: String, value: nullable Serializable): MongoMatch do
		op("lte", field, value)
		return self
	end

	# Match documents where `field` exists or not
	#
	# https://docs.mongodb.com/manual/reference/operator/query/exists/#op._S_exists
	#
	# ~~~json
	# {field: {$exists: boolean} }
	# ~~~
	#
	# When `exists` is true, `$exists` matches the documents that contain the
	# field, including documents where the field value is null.
	# If <boolean> is false, the query returns only the documents that do not
	# contain the field.
	fun exists(field: String, exists: Bool): MongoMatch do
		op("exists", field, exists)
		return self
	end

	# Match documents where `field` matches `pattern`
	#
	# To read more about the available options, see:
	# https://docs.mongodb.com/manual/reference/operator/query/regex/#op._S_regex
	#
	# ~~~json
	# {field: {$regex: 'pattern', $options: '<options>'} }
	# ~~~
	#
	# Provides regular expression capabilities for pattern matching strings in queries.
	# MongoDB uses Perl compatible regular expressions (i.e. "PCRE" ).
	fun regex(field: String, pattern: String, options: nullable String): MongoMatch do
		var q = new JsonObject
		q["$regex"] = pattern
		if options != null then q["$options"] = options
		self[field] = q
		return self
	end

	# Match documents where `field` is in `values`
	#
	# https://docs.mongodb.com/manual/reference/operator/query/in/
	#
	# ~~~json
	# { field: { $in: [<value1>, <value2>, ... <valueN> ] } }
	# ~~~
	#
	# `$in` selects the documents where the value of a field equals any value
	# in the specified array.
	fun is_in(field: String, values: Array[nullable Serializable]): MongoMatch do
		op("in", field, new JsonArray.from(values))
		return self
	end

	# Match documents where `field` is not in `values`
	#
	# https://docs.mongodb.com/manual/reference/operator/query/nin/
	#
	# ~~~json
	# { field: { $nin: [<value1>, <value2>, ... <valueN> ] } }
	# ~~~
	#
	# `$nin` selects the documents where:
	# * the field value is not in the specified array or
	# * the field does not exist.
	fun is_nin(field: String, values: Array[nullable Serializable]): MongoMatch do
		op("nin", field, new JsonArray.from(values))
		return self
	end

	# Logical `or`
	#
	# https://docs.mongodb.com/manual/reference/operator/query/or/#op._S_or
	#
	# The `$or` operator performs a logical OR operation on an array of two or
	# more `expressions` and selects the documents that satisfy at least one of
	# the `expressions`.
	#
	# The `$or` has the following syntax:
	#
	# ~~~json
	# { field: { $or: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] } }
	# ~~~
	fun lor(field: nullable String, expressions: Array[Serializable]): MongoMatch do
		op("or", field, new JsonArray.from(expressions))
		return self
	end

	# Logical `and`
	#
	# https://docs.mongodb.com/manual/reference/operator/query/and/#op._S_and
	#
	# The `$and` operator performs a logical AND operation on an array of two or
	# more `expressions` and selects the documents that satisfy all of the `expressions`.
	#
	# The `$and` has the following syntax:
	#
	# ~~~json
	# { field: { $and: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] } }
	# ~~~
	fun land(field: nullable String, expressions: Array[Serializable]): MongoMatch do
		op("and", field, new JsonArray.from(expressions))
		return self
	end

	# Logical `not`
	#
	# https://docs.mongodb.com/manual/reference/operator/query/not/#op._S_not
	#
	# `$not` performs a logical NOT operation on the specified `expression` and
	# selects the documents that do not match the `expression`.
	# This includes documents that do not contain the field.
	#
	# The $not has the following syntax:
	#
	# ~~~json
	# { field: { $not: { <expression> } } }
	# ~~~
	fun lnot(field: nullable String, expression: Serializable): MongoMatch do
		op("not", field, expression)
		return self
	end

	# Logical `nor`
	#
	# https://docs.mongodb.com/manual/reference/operator/query/nor/#op._S_nor
	#
	# `$nor` performs a logical NOR operation on an array of one or more query
	# expression and selects the documents that fail all the query expressions
	# in the array.
	#
	# The $nor has the following syntax:
	#
	# ~~~json
	# { field: { $nor: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] } }
	# ~~~
	fun lnor(field: nullable String, expressions: Array[Serializable]): MongoMatch do
		op("nor", field, new JsonArray.from(expressions))
		return self
	end

	# Array contains all
	#
	# https://docs.mongodb.com/manual/reference/operator/query/all/#op._S_all
	#
	# `$all` selects the documents where the value of a field is an array that
	# contains all the specified elements.
	#
	# ~~~json
	# { field: { $all: [ <value1>, <value2>, ... ] } }
	# ~~~
	fun all(field: nullable String, values: Array[Serializable]): MongoMatch do
		op("all", field, new JsonArray.from(values))
		return self
	end

	# Array element match
	#
	# https://docs.mongodb.com/manual/reference/operator/query/elemMatch/#op._S_elemMatch
	#
	# `$elemMatch` matches documents that contain an array field with at least
	# one element that matches all the specified query criteria.
	#
	# ~~~json
	# { field: { $elemMatch: <query> } }
	# ~~~
	fun elem_match(field: nullable String, query: Serializable): MongoMatch do
		op("elemMatch", field, query)
		return self
	end

	# Array size match
	#
	# https://docs.mongodb.com/manual/reference/operator/query/size/#op._S_size
	#
	# `$size` matches any array with the number of elements specified by the argument
	#
	# ~~~json
	# { field: { $size: <size> } }
	# ~~~
	fun size(field: nullable String, size: Int): MongoMatch do
		op("size", field, size)
		return self
	end
end

# Mongo pipelines are arrays of aggregation stages
#
# With the `MongoCollection::aggregate` method, pipeline stages appear in a array.
# Documents pass through the stages in sequence.
#
# ~~~json
# db.collection.aggregate( [ { <stage> }, ... ] )
# ~~~
#
# The MongoPipeline fluent interface can be used to bluid a pipeline:
# ~~~
# var pipeline = (new MongoPipeline).
#	match((new MongoMatch).eq("game", "nit")).
#	group((new MongoGroup("$game._id")).sum("nitcoins", "$game.nitcoins")).
#	sort((new MongoMatch).eq("nitcoins", -1)).
#	limit(10)
# ~~~
#
# The pipeline can then be used in an aggregation query:
# ~~~nitish
# collection.aggregate(pipeline)
# ~~~
#
# For more information read about MongoDB pipeline operators from the MongoDB
# official documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/
class MongoPipeline
	super JsonArray

	# Add a stage to the pipeline
	#
	# https://docs.mongodb.com/manual/reference/operator/aggregation/#stage-operators
	#
	# Each stage is registered as:
	# ~~~json
	# { $<stage>: <json> }
	# ~~~
	fun add_stage(stage: String, json: Serializable): MongoPipeline do
		var obj = new JsonObject
		obj["${stage}"] = json
		add obj
		return self
	end

	# Apply projection
	#
	# https://docs.mongodb.com/manual/reference/operator/aggregation/project/#pipe._S_project
	#
	# Passes along the documents with only the specified fields to the next stage
	# in the pipeline.
	#
	# ~~~json
	# { $project: { <specifications> } }
	# ~~~
	#
	# The specified fields can be existing fields from the input documents or
	# newly computed fields.
	fun project(projection: JsonObject): MongoPipeline do return add_stage("project", projection)

	# Apply match
	#
	# https://docs.mongodb.com/manual/reference/operator/aggregation/match/
	#
	# Filters the documents to pass only the documents that match the specified
	# condition(s) to the next pipeline stage.
	#
	# ~~~json
	# { $match: { <query> } }
	# ~~~
	fun match(query: MongoMatch): MongoPipeline do return add_stage("match", query)

	# Apply sort
	#
	# https://docs.mongodb.com/manual/reference/operator/aggregation/sort/
	#
	# Sorts all input documents and returns them to the pipeline in sorted order.
	#
	# ~~~json
	# { $sort: { <projection> } }
	# ~~~
	fun sort(projection: JsonObject): MongoPipeline do return add_stage("sort", projection)

	# Apply skip
	#
	# https://docs.mongodb.com/manual/reference/operator/aggregation/skip/
	#
	# Skips over the specified number of documents that pass into the stage and
	# passes the remaining documents to the next stage in the pipeline.
	#
	# ~~~json
	# { $skip: { <number> } }
	# ~~~
	#
	# If `number == null` then no skip stage is generated
	fun skip(number: nullable Int): MongoPipeline do
		if number == null then return self
		return add_stage("skip", number)
	end

	# Apply limit
	#
	# https://docs.mongodb.com/manual/reference/operator/aggregation/limit/
	#
	# Limits the number of documents passed to the next stage in the pipeline.
	#
	# ~~~json
	# { $limit: { <number> } }
	# ~~~
	#
	# If `number == null` then no limit stage is generated
	fun limit(number: nullable Int): MongoPipeline do
		if number == null then return self
		return add_stage("limit", number)
	end

	# Apply group
	#
	# https://docs.mongodb.com/manual/reference/operator/aggregation/group/
	#
	# Groups documents by some specified expression and outputs to the next stage
	# a document for each distinct grouping.
	#
	# The output documents contain an `_id` field which contains the distinct
	# group by key.
	#
	# The output documents can also contain computed fields that hold the values
	# of some accumulator expression grouped by the `$group`'s `_id` field.
	# `$group` does not order its output documents.
	#
	# ~~~json
	# { $group: { <group> } }
	# ~~~
	fun group(group: MongoGroup): MongoPipeline do return add_stage("group", group)

	# Apply unwind
	#
	# https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/
	#
	# Deconstructs an array field from the input documents to output a document
	# for each element.
	# Each output document is the input document with the value of the array
	# field replaced by the element.
	#
	# ~~~json
	# { $unwind: <field path> }
	# ~~~
	fun unwind(path: String): MongoPipeline do return add_stage("unwind", path)
end

# Mongo pipeline group stage
#
# https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group
#
# Groups documents by some specified expression and outputs to the next stage a
# document for each distinct grouping.
#
# ~~~
# var group = (new MongoGroup("$game._id")).sum("nitcoins", "$game.nitcoins")
#
# var pipeline = (new MongoPipeline).group(group)
# ~~~
#
# The output documents contain an `_id` field which contains the distinct group by key.
# The output documents can also contain computed fields that hold the values of
# some accumulator expression grouped by the `$group`‘s `_id` field.
# `$group` does not order its output documents.
#
# The `$group` stage has the following prototype form:
#
# ~~~json
# { $group: { _id: <expression>, <field1>: { <accumulator1> : <expression1> }, ... } }
# ~~~
#
# The `_id` field is mandatory; however, you can specify an `_id` value of null
# to calculate accumulated values for all the input documents as a whole.
#
# The remaining computed fields are optional and computed using the `<accumulator>`
# operators.
class MongoGroup
	super JsonObject

	# Group `_id`
	#
	# See `MongoGroup::group`.
	var id: String

	init do self["_id"] = id

	# Add an accumulator
	#
	# Each accumulator is registered as:
	# ~~~json
	# <field>: { <accumulator> : <expression> }
	# ~~~
	private fun acc(name: String, field: String, expression: nullable Serializable): MongoGroup do
		var q = new JsonObject
		q["${name}"] = expression
		self[field] = q
		return self
	end

	# Calculates and returns the sum of numeric values
	#
	# https://docs.mongodb.com/manual/reference/operator/aggregation/sum/#grp._S_sum
	#
	# ~~~json
	# { $sum: <expression> }
	# ~~~
	#
	# `$sum` ignores non-numeric values.
	fun sum(field: String, expression: Serializable): MongoGroup do
		return acc("sum", field, expression)
	end

	# Returns the average value of the numeric values
	#
	# https://docs.mongodb.com/manual/reference/operator/aggregation/avg/
	#
	# ~~~json
	# { $avg: <expression> }
	# ~~~
	#
	# `$avg` ignores non-numeric values.
	fun avg(field: String, expression: Serializable): MongoGroup do
		return acc("avg", field, expression)
	end

	# Returns the maximum value
	#
	# https://docs.mongodb.com/manual/reference/operator/aggregation/max/
	#
	# ~~~json
	# { $max: <expression> }
	# ~~~
	#
	# `$max` compares both value and type, using the specified BSON comparison
	# order for values of different types.
	fun max(field: String, expression: Serializable): MongoGroup do
		return acc("max", field, expression)
	end

	# Returns the minimum value
	#
	# https://docs.mongodb.com/manual/reference/operator/aggregation/min/
	#
	# ~~~json
	# { $min: <expression> }
	# ~~~
	#
	# `$min` compares both value and type, using the specified BSON comparison
	# order for values of different types.
	fun min(field: String, expression: Serializable): MongoGroup do
		return acc("min", field, expression)
	end

	# Return the first value
	#
	# https://docs.mongodb.com/manual/reference/operator/aggregation/first/
	#
	# ~~~json
	# { $first: <expression> }
	# ~~~
	#
	# Returns the value that results from applying an expression to the first
	# document in a group of documents that share the same group by key.
	#
	# Only meaningful when documents are in a defined order.
	fun first(field: String, expression: Serializable): MongoGroup do
		return acc("first", field, expression)
	end

	# Return the last value
	#
	# https://docs.mongodb.com/manual/reference/operator/aggregation/last/
	#
	# ~~~json
	# { $last: <expression> }
	# ~~~
	#
	# Returns the value that results from applying an expression to the last
	# document in a group of documents that share the same group by key.
	#
	# Only meaningful when documents are in a defined order.
	fun last(field: String, expression: Serializable): MongoGroup do
		return acc("last", field, expression)
	end

	# Push to an array
	#
	# https://docs.mongodb.com/manual/reference/operator/aggregation/push/
	#
	# ~~~json
	# { $push: <expression> }
	# ~~~
	#
	# Returns an array of all values that result from applying an expression to
	# each document in a group of documents that share the same group by key.
	fun push(field: String, expr: Serializable): MongoGroup do
		return acc("push", field, expr)
	end

	# Push to a unique array
	#
	# https://docs.mongodb.com/manual/reference/operator/aggregation/addToSet/
	#
	# ~~~json
	# { $addToSet: <expression> }
	# ~~~
	#
	# Returns an array of all unique values that results from applying an
	# expression to each document in a group of documents that share the same
	# group by key.
	#
	# Order of the elements in the output array is unspecified.
	fun addToSet(field: String, expr: Serializable): MongoGroup do
		return acc("addToSet", field, expr)
	end
end
lib/mongodb/queries.nit:17,1--666,3