QueryEngine Developer Documentation¶
- QueryEngine Developer Documentation
- Overview
- Core Principles
- API Reference
constructor(config)executeQuery(documents, query)_matchDocument(document, query)_matchField(document, fieldPath, queryValue)_getFieldValue(document, fieldPath)_isOperatorObject(value)_matchOperators(documentValue, operators)- Shared Comparison Utilities
_validateQuery(documents, query)_validateQueryInputs(documents, query)_validateQueryDepth(obj, depth)_validateOperators(query)_findOperators(obj, operators)_validateOperatorValues(query)_validateOperatorValuesRecursive(obj, depth)_matchLogicalAnd(document, conditions)_matchLogicalOr(document, conditions)
- Supported Query Operators
- Usage Examples
- Error Handling
- Query Validation System
- Best Practices
Overview¶
The QueryEngine class is responsible for filtering a list of documents based on a MongoDB-style query object. It evaluates documents against query criteria, supporting a variety of comparison, logical, and element operators.
Key Responsibilities:
- Parsing and validating query objects.
- Matching documents against complex query criteria.
- Supporting nested field queries using dot notation.
- Handling various data types and operators.
Dependencies:
InvalidQueryErrorandInvalidArgumentError: For standardised error reporting.Validate: For input validation and type checking.JDbLogger: For component logging and debugging.
Core Principles¶
- MongoDB-like Syntax: The query language closely mirrors MongoDB's query syntax for familiarity and ease of use.
- Read-Only Operations: The engine only filters documents; it does not modify them.
- Extensibility: Designed to allow for the addition of new query operators.
- Performance Considerations: While aiming for comprehensive functionality, it's mindful of the Google Apps Script environment's limitations.
API Reference¶
constructor(config)¶
Creates a new QueryEngine instance with optional configuration.
Parameters:
config(Object, optional): Configuration object with the following optional properties:maxNestedDepth(Number): Maximum allowed query nesting depth (defaults to 10)supportedOperators(String[]): Operators permitted by the engine (defaults to DatabaseConfig settings)logicalOperators(String[]): Logical operators permitted by the engine (defaults to DatabaseConfig settings)
Example:
// Default configuration (uses DatabaseConfig defaults)
const queryEngine = new QueryEngine();
// Custom configuration
const queryEngine = new QueryEngine({
maxNestedDepth: 5,
supportedOperators: ['$eq', '$gt'],
logicalOperators: ['$and']
});
executeQuery(documents, query)¶
Filters an array of documents based on the provided query object.
Parameters:
documents(Array\query(Object): The MongoDB-style query object.
Returns:
Array<Object>: An array containing only the documents that match the query.
Example:
const queryEngine = new QueryEngine();
const docs = [
{ name: 'Alice', age: 30, city: 'New York' },
{ name: 'Bob', age: 24, city: 'London' },
{ name: 'Charlie', age: 30, city: 'Paris' }
];
const results = queryEngine.executeQuery(docs, { age: 30, city: 'New York' });
// results: [{ name: "Alice", age: 30, city: "New York" }]
_matchDocument(document, query)¶
(Private) Determines if a single document matches the given query. This method iterates through the query conditions and evaluates them against the document, handling both logical operators and field-based queries.
Parameters:
document(Object): The document to evaluate.query(Object): The query object or a sub-query object.
Returns:
Boolean:trueif the document matches the query,falseotherwise.
_matchField(document, fieldPath, queryValue)¶
(Private) Evaluates a specific field's value against a query condition, which can be a simple value for equality or an object containing query operators (e.g., { $gt: 10 }).
Parameters:
document(Object): The document to test.fieldPath(String): Field path (supports dot notation).queryValue(*|Object): Expected value or operator object.
Returns:
Boolean:trueif the field matches the query,falseotherwise.
_getFieldValue(document, fieldPath)¶
(Private) Retrieves a value from a document using a dot-notation path. Handles nested objects.
Parameters:
document(Object): The document to read from.fieldPath(String): The dot-notation path (e.g.,"address.street").
Returns:
*: The value at the specified path, orundefinedif the path does not exist.
_isOperatorObject(value)¶
(Private) Checks if a value represents an operator object (plain object, not Date or Array).
Parameters:
value(*): Value to check.
Returns:
Boolean:trueif value is a plain operator object,falseotherwise.
_matchOperators(documentValue, operators)¶
(Private) Matches operators against a document value, ensuring all operators in the object match.
Parameters:
documentValue(*): Value from document.operators(Object): Operator object (e.g.,{$gt: 5, $lt: 10}).
Returns:
Boolean:trueif all operators match,falseotherwise.
Shared Comparison Utilities¶
QueryEngine delegates all equality and ordering logic to ComparisonUtils through the COMPARISON_EVALUATORS map:
Operator Evaluation Architecture:
The QueryEngineMatcher component uses dedicated evaluator functions mapped to operators:
evaluateEquality($eq): UsesComparisonUtils.equalswitharrayContainsScalar:truefor Mongo-like "array contains" semanticsevaluateGreaterThan($gt): UsesComparisonUtils.compareOrderingto check> 0evaluateLessThan($lt): UsesComparisonUtils.compareOrderingto check< 0
These functions are registered in the COMPARISON_EVALUATORS map, which provides a single source of truth for operator evaluation. The _evaluateOperator method looks up the appropriate evaluator and invokes it; unsupported operators raise InvalidQueryError.
Benefits:
- Single source of truth: All operator logic centralized in evaluator functions
- No duplication: One evaluation path eliminates drift risk
- Consistent behavior: Date handling, type coercion, and comparison rules unified
- Extensibility: New operators can be added by registering evaluator functions
- Maintainability: Changes to operator logic only need one update
_validateQuery(documents, query)¶
(Private) Validates query structure and operators comprehensively for security and robustness.
Parameters:
documents(Array): Documents array to validate.query(Object): Query object to validate.
Throws:
InvalidArgumentError: When inputs are invalid.InvalidQueryError: When query structure or operators are invalid.
_validateQueryInputs(documents, query)¶
(Private) Validates basic input types for executeQuery method.
Parameters:
documents(Array): Documents array to validate.query(Object): Query object to validate.
Throws:
InvalidArgumentError: When inputs are invalid.
_validateQueryDepth(obj, depth)¶
(Private) Validates query depth to prevent excessive nesting attacks.
Parameters:
obj(*): Object to check.depth(Number): Current depth.
Throws:
InvalidQueryError: When depth exceeds configured limit.
_validateOperators(query)¶
(Private) Validates that all operators used in query are supported.
Parameters:
query(Object): Query object.
Throws:
InvalidQueryError: When unsupported operator is found.
_findOperators(obj, operators)¶
(Private) Recursively finds all operators used in a query object.
Parameters:
obj(*): Object to search.operators(Array): Array to collect operators (optional, defaults to empty array).
Returns:
Array: Array of operator strings found.
_validateOperatorValues(query)¶
(Private) Validates operator values in query for correctness.
Parameters:
query(Object): Query object to validate.
Throws:
InvalidQueryError: When operator values are invalid.
_validateOperatorValuesRecursive(obj, depth)¶
(Private) Recursively validates operator values in query with depth protection.
Parameters:
obj(*): Object to validate.depth(Number): Current recursion depth.
Throws:
InvalidQueryError: When operator values are invalid or depth exceeds limit.
_matchLogicalAnd(document, conditions)¶
(Private) Handles $and logical operator evaluation.
Parameters:
document(Object): Document to test.conditions(Array): Array of conditions that must all match.
Returns:
Boolean:trueif all conditions match,falseotherwise.
Throws:
InvalidQueryError: When conditions is not an array.
_matchLogicalOr(document, conditions)¶
(Private) Handles $or logical operator evaluation.
Parameters:
document(Object): Document to test.conditions(Array): Array of conditions where at least one must match.
Returns:
Boolean:trueif any condition matches,falseotherwise.
Throws:
InvalidQueryError: When conditions is not an array.
Supported Query Operators¶
Note: The following table summarises operator support in the current implementation. Only operators marked as "✔ Implemented" are available. Others are planned for future development.
| Operator | Implemented | Notes |
|---|---|---|
$eq |
✔ | Supported |
$gt |
✔ | Supported |
$lt |
✔ | Supported |
$and |
✔ | Supported |
$or |
✔ | Supported |
$ne |
✖ | Planned |
$gte |
✖ | Planned |
$lte |
✖ | Planned |
$in |
✖ | Planned |
$nin |
✖ | Planned |
$not |
✖ | Planned |
$nor |
✖ | Planned |
$exists |
✖ | Planned |
$type |
✖ | Planned |
$all |
✖ | Planned |
$elemMatch |
✖ | Planned |
$size |
✖ | Planned |
Currently Supported Operators¶
$eq: Matches values that are equal to a specified value. (Implicit for simple key-value pairs)$gt: Matches values that are greater than a specified value.$lt: Matches values that are less than a specified value.$and: Joins query clauses with a logical AND. Returns all documents that match the conditions of all clauses. (Implicit when multiple fields are specified at the same level)$or: Joins query clauses with a logical OR. Returns all documents that match the conditions of at least one clause.
Defaults for supported and logical operators are defined in DatabaseConfig and can be overridden via the QueryEngine constructor.
Usage Examples¶
Simple Equality Match¶
const queryEngine = new QueryEngine();
const documents = [
{ item: 'apple', qty: 10 },
{ item: 'banana', qty: 20 }
];
// Equivalent to: { item: { $eq: "apple" } }
const result = queryEngine.executeQuery(documents, { item: 'apple' });
// result: [{ item: "apple", qty: 10 }]
Using Comparison Operators¶
const queryEngine = new QueryEngine();
const documents = [
{ product: 'A', price: 10 },
{ product: 'B', price: 20 },
{ product: 'C', price: 30 }
];
// Price greater than 15
const expensive = queryEngine.executeQuery(documents, { price: { $gt: 15 } });
// expensive: [{ product: "B", price: 20 }, { product: "C", price: 30 }]
// Price less than 25
const affordable = queryEngine.executeQuery(documents, { price: { $lt: 25 } });
// affordable: [{ product: "A", price: 10 }, { product: "B", price: 20 }]
Using Logical Operators¶
const queryEngine = new QueryEngine();
const documents = [
{ name: 'Shirt', colour: 'blue', stock: 5 },
{ name: 'Pants', colour: 'blue', stock: 0 },
{ name: 'Shirt', colour: 'red', stock: 10 }
];
// Blue items that are in stock
const blueAndInStock = queryEngine.executeQuery(documents, {
$and: [{ colour: 'blue' }, { stock: { $gt: 0 } }]
});
// blueAndInStock: [{ name: "Shirt", colour: "blue", stock: 5 }]
// Implicit AND
const blueAndInStockImplicit = queryEngine.executeQuery(documents, {
colour: 'blue',
stock: { $gt: 0 }
});
// blueAndInStockImplicit: [{ name: "Shirt", colour: "blue", stock: 5 }]
// Red items OR items with no stock
const redOrNoStock = queryEngine.executeQuery(documents, {
$or: [{ colour: 'red' }, { stock: 0 }]
});
// redOrNoStock: [
// { name: "Pants", colour: "blue", stock: 0 },
// { name: "Shirt", colour: "red", stock: 10 }
// ]
Querying Nested Fields¶
Use dot notation to query fields within embedded documents.
const queryEngine = new QueryEngine();
const documents = [
{ item: 'journal', details: { supplier: 'X', pages: 200 } },
{ item: 'pen', details: { supplier: 'Y', colour: 'blue' } }
];
const journalsFromX = queryEngine.executeQuery(documents, { 'details.supplier': 'X' });
// journalsFromX: [{ item: "journal", details: { supplier: "X", pages: 200 } }]
Querying Array Fields¶
const queryEngine = new QueryEngine();
const documents = [
{ item: 'A', tags: ['red', 'round'], ratings: [5, 8, 9] },
{ item: 'B', tags: ['blue', 'square'], ratings: [7, 8] },
{ item: 'C', tags: ['red', 'square'], ratings: [6] }
];
// Items tagged "red" (simple match in array)
const redItems = queryEngine.executeQuery(documents, { tags: 'red' });
// redItems: [
// { item: "A", tags: ["red", "round"], ratings: [5, 8, 9] },
// { item: "C", tags: ["red", "square"], ratings: [6] }
// ]
// Items with a rating of 9
const highRated = queryEngine.executeQuery(documents, { ratings: 9 });
// highRated: [{ item: "A", tags: ["red", "round"], ratings: [5, 8, 9] }]
Future Array Operators (Not Yet Implemented)¶
The following examples show planned functionality for future releases:
// PLANNED: Using $all - item must have both "red" and "square" tags
// const redSquareItems = queryEngine.executeQuery(documents, { tags: { $all: ["red", "square"] } });
// PLANNED: Using $elemMatch - find documents where at least one rating is between 7 and 8 inclusive
// const specificRatingRange = queryEngine.executeQuery(documents, {
// ratings: { $elemMatch: { $gte: 7, $lte: 8 } }
// });
// PLANNED: Using $size - find documents where tags array has exactly 2 elements
// const twoTagsItems = queryEngine.executeQuery(documents, { tags: { $size: 2 } });
Error Handling¶
The QueryEngine uses the following error types for different issues:
InvalidQueryError: For query structure problems such as:- Unrecognised query operators
- Invalid operator syntax or values (e.g.,
$andwithout an array value) - Query nesting depth exceeded
InvalidArgumentError: For input validation problems such as:- Non-array documents parameter
- Null, undefined, string, or array query parameters
Example error handling:
try {
const results = queryEngine.executeQuery(documents, query);
} catch (error) {
if (error instanceof InvalidQueryError) {
console.error('Invalid query:', error.message);
} else if (error instanceof InvalidArgumentError) {
console.error('Invalid argument:', error.message);
}
}
Query Validation System¶
The QueryEngine includes a comprehensive validation system to ensure query security and robustness:
Input Validation¶
- Type Checking: Ensures
documentsis an array andqueryis a valid object - Null Safety: Prevents null, undefined, string, or array query parameters
- Fail-Fast: Basic input validation occurs before expensive query processing
Structure Validation¶
- Depth Protection: Prevents excessive query nesting (configurable via
maxNestedDepth) - Operator Validation: Ensures only supported operators are used
- Recursive Validation: Validates nested query structures thoroughly
Security Features¶
- Malicious Query Prevention: Deep validation prevents potential security exploits
- Resource Protection: Depth limits prevent stack overflow or excessive processing
- Comprehensive Error Reporting: Clear error messages for debugging
Validation Process¶
- Input Types: Validates basic parameter types (documents array, query object)
- Query Depth: Recursively checks nesting doesn't exceed configured limit
- Operator Support: Verifies all operators in query are supported
- Operator Values: Validates operator values are correctly structured (e.g.,
$andrequires arrays)
This multi-layered approach ensures queries are safe, valid, and performant before execution begins.
Best Practices¶
- Specificity: Make queries as specific as possible for better performance, especially with large datasets.
- Operator Knowledge: Understand the behaviour of each operator, particularly how they interact (e.g., implicit AND vs. explicit
$andor$or). - Data Types: Be mindful of data types when performing comparisons. The engine attempts type coercion in some cases (e.g., comparing numbers and strings that represent numbers) but strict type matching is generally safer.
- Nested Queries: While dot notation is powerful, overly deep nesting or complex queries on deeply nested structures can impact readability and potentially performance.
- Array Queries: Array operators like
$allor$elemMatchcan be powerful but may have performance implications on very large arrays or complex element matching conditions. - Index Utilisation (Conceptual): While this
QueryEngineoperates on in-memory arrays, in a full database system, query structure significantly impacts index utilisation. Designing queries with this in mind is good practice.