Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.ditto.live/llms.txt

Use this file to discover all available pages before exploring further.

MongoDB uses a multi-stage pipeline model where documents flow through an ordered sequence of transformation stages. DQL uses a single declarative SELECT statement where filtering, projection, grouping, sorting, and limiting are all expressed at once. There are no stages to compose — the query optimizer decides execution order. This means most MongoDB aggregations collapse to a single DQL SELECT. Operations with no DQL equivalent — joins, window functions, set algebra, advanced math, and several array accumulators — are noted as gaps throughout and collected in the Known Gaps section.

AI Agent Prompt

Use this prompt with any AI coding assistant to convert a MongoDB aggregation pipeline to DQL.
Convert the MongoDB aggregation pipeline I provide into a Ditto DQL query. Follow these rules exactly.

## Mental model
MongoDB pipelines are multi-stage. DQL is a single declarative SELECT. Collapse all stages into one query:
  SELECT <projection> FROM <collection> WHERE <filter> GROUP BY <keys> HAVING <agg-filter> ORDER BY <sort> LIMIT <n> OFFSET <skip>

## Stage mappings

$match        → WHERE clause. Full operator support: =, !=, <, >, <=, >=, BETWEEN, IN, NOT IN, LIKE, ILIKE, IS NULL, IS NOT NULL, IS MISSING, IS NOT MISSING.
$sort         → ORDER BY field [ASC|DESC]
$limit        → LIMIT n
$skip         → OFFSET n
$count        → SELECT COUNT(*) AS <field>

$project (include)  → SELECT field1, field2
$project (exclude)  → SELECT coll.*, MISSING field1, MISSING field2 FROM coll   ← requires table alias; bare SELECT * is a syntax error
$project (compute)  → SELECT expr AS alias
$addFields / $set   → SELECT coll.*, expr AS new_field FROM coll                ← same qualified-wildcard pattern
$unset (top-level)  → SELECT coll.*, MISSING field FROM coll
$unset (nested)     → SELECT object_unset(obj, 'field', true) AS obj FROM coll

$group        → GROUP BY with aggregates: COUNT(*), COUNT(DISTINCT f), SUM(f), AVG(f), MIN(f), MAX(f), MEDIAN(f)
$group $addToSet → COUNT(DISTINCT f) only — no array accumulation
HAVING        → filter on aggregated results after GROUP BY

$bucket / $bucketAuto →
  SELECT CASE WHEN f < b1 THEN 'l1' WHEN f < b2 THEN 'l2' ELSE 'other' END AS bucket, COUNT(*)
  FROM coll
  GROUP BY CASE WHEN f < b1 THEN 'l1' WHEN f < b2 THEN 'l2' ELSE 'other' END

## Expression mappings

$cond         → CASE WHEN cond THEN a ELSE b END
$switch       → CASE WHEN cond1 THEN r1 WHEN cond2 THEN r2 ELSE default END   (searched CASE — each branch is a boolean condition)
              → CASE expr WHEN v1 THEN r1 WHEN v2 THEN r2 ELSE default END     (simple CASE — use when all branches compare the same expression to scalar values)
$ifNull       → ifnull(v1, v2) or coalesce(v1, v2, ...)

$map          → ARRAY expr FOR v IN source END                          (SDK v5+)
$filter       → ARRAY v FOR v IN source WHEN condition END              (SDK v5+)
$arrayElemAt  → array[index]
$size         → array_length(arr)
$in (array membership test — is value in array?)  → array_contains(arr, val)
$in ($match membership test — is field in a list?) → WHERE field IN ('a', 'b', 'c')
ANY/EVERY     → ANY v IN arr SATISFIES condition END / EVERY v IN arr SATISFIES condition END

$mergeObjects → object_concat(obj1, obj2, ...)
$objectToArray → object_content(obj, 'fields')
$arrayToObject → OBJECT name_expr:value_expr FOR v IN source END        (SDK v5+)
$getField     → object_set / object_unset / object_keys / object_values

## Date functions
$dateFromString   → date_cast(str, format)          e.g. date_cast('2024-01-15', 'YYYY-MM-DD')
$dateToString     → date_format(date, format)        e.g. date_format(ts, 'YYYY-MM-DD')
$dateAdd          → date_add(date, 'day', n)
$dateSubtract     → date_sub(date, 'day', n)
$dateDiff         → date_diff(date1, date2, 'day')
$year/$month/$day → date_part(date, 'year') / date_part(date, 'mon') / date_part(date, 'day')
$hour/$minute     → date_part(date, 'hour') / date_part(date, 'min')

## String functions
$concat           → concat(s1, s2, ...) or s1 || s2
$toLower/$toUpper → lower(s) / upper(s)
$substr           → substr(s, start, len)           zero-based index
$trim/$ltrim/$rtrim → `trim`(s) / ltrim(s) / rtrim(s)   note: trim requires backtick quoting
$indexOfCP        → pos(s, substr)                  returns -1 if not found
$strLenCP         → char_length(s)
$strLenBytes      → byte_length(s)
$split            → split(s, delim)
$regexMatch       → regexp_like(s, pattern[, flags]) or LIKE / ILIKE / SIMILAR TO

## Type and cast
$type          → type(x)        returns: boolean, string, integer, float, object, array, binary, null, missing
$toString etc. → cast(v, 'string') / cast(v, 'integer') / cast(v, 'float') / cast(v, 'boolean')
               also: CAST(v AS type) syntax

## Arithmetic operators
MongoDB expression operators map to native DQL operators — use them directly in SELECT projections and WHERE clauses:
$add        → +
$subtract   → -
$multiply   → *
$divide     → /
$mod        → %
$abs        → abs(x)
$ceil       → ceil(x)
$floor      → floor(x)

Comparison expression operators ($eq, $ne, $gt, $gte, $lt, $lte) used inside $project or $addFields map to DQL's native =, !=, >, >=, <, <= operators.

$expr (using aggregation expressions inside $match) is not needed in DQL — expressions work natively anywhere in a WHERE clause.

## Unsupported — if the pipeline uses any of these, do NOT guess a DQL translation. Instead:
1. State which operator is unsupported.
2. Describe what it does in the MongoDB pipeline.
3. Suggest the closest workaround.

Unsupported operators and their workarounds:
- $lookup / $graphLookup  → No join support. Use ditto.store.transaction() to query both collections at a consistent snapshot, then join in application code.
- $unwind (row-multiplication) → No row-multiplying unwind. Use ARRAY transform for in-place reshaping or ANY/EVERY for filtering.
- $group $push / $addToSet (array) → No array-accumulating aggregate. Query ungrouped and collect values in application code.
- $group $first / $last / $top / $topN → No ordered-first/last-in-group aggregate.
- $group $stdDevPop / $stdDevSamp / $percentile → No statistical aggregates.
- $accumulator / $function → No user-defined functions.
- $facet  → No single-query multi-facet. Use a read-only ditto.store.transaction() to run multiple aggregations against a consistent snapshot.
- $merge / $out → Queries are read-only. Use ditto.store.transaction() to aggregate, transform in application code, and write back atomically.
- $replaceRoot / $replaceWith → No direct equivalent. Select nested fields explicitly.
- $sample → No random sampling.
- $unionWith → No collection union.
- $setWindowFields → No window functions.
- $reduce → No fold/reduce expression.
- $concatArrays / $slice / $reverseArray / $sortArray / $zip → No direct array equivalents.
- $setUnion / $setIntersection / $setDifference → No set algebra on arrays.
- $replaceOne / $replaceAll → No string replace function.
- $round / $trunc / $sqrt / $pow / $exp / $ln / $log → No advanced math functions.
- $sin / $cos / $tan (and other trig) → No trigonometric functions.
- $geoNear → No geospatial support.
- $densify / $fill → No time-series gap filling.

## Output format
- Provide the DQL query in a sql code block.
- If the pipeline collapses cleanly to DQL, show the query with a one-line explanation of each clause.
- If any part is unsupported, show the DQL for the supported portions (if any) and then list each unsupported operator with its workaround.
- Do not invent DQL syntax. If you are uncertain whether a function exists, say so.

Quick Reference

The table below covers the most common MongoDB aggregation pipeline stages and expression operators. Where a direct equivalent exists, a DQL example is provided. Where one doesn’t exist yet, the notes column says so — if any of those are important to your use case, reach out to support and let us know.
MongoDBDQL EquivalentNotes
$matchWHEREFull operator parity: =, !=, <, >, BETWEEN, IN, LIKE, IS NULL, IS MISSING, etc.
$project (include fields)SELECT field1, field2Direct equivalent
$project (exclude fields)SELECT products.*, MISSING field1 FROM productsWildcard must be qualified — SELECT *, MISSING field1 is a syntax error
$project (computed fields)SELECT expr AS aliasFull expression support
$addFields / $setSELECT products.*, expr AS new_field FROM productsSame pattern — qualified wildcard plus computed fields
$unset (top-level)SELECT products.*, MISSING field FROM productsSame as exclude projection
$unset (nested field)object_unset(obj, 'field')Use in SELECT projection for nested exclusion
$groupGROUP BYWorks with COUNT, SUM, AVG, MIN, MAX, MEDIAN
$group with $pushNo direct equivalent. No array-accumulating aggregate.
$group with $addToSetCOUNT(DISTINCT ...)Counts distinct values only; no array-building equivalent
$group with $first / $lastNo direct equivalent.
$group with $stdDevPop / $stdDevSampNo direct equivalent.
$group with $percentileNo direct equivalent.
$accumulator / $functionNo direct equivalent.
$sortORDER BY ... [ASC|DESC]Direct equivalent
$limitLIMIT nDirect equivalent
$skipOFFSET nDirect equivalent
$countSELECT COUNT(*)Direct equivalent
$unwindARRAY ... FOR ... IN ... END / ANY ... IN ... SATISFIESNo row-multiplying unwind. Use array transform or search expressions.
$lookupUse a transaction to query multiple collections at a consistent snapshot — see example.
$facetMultiple separate queriesUse a read-only transaction to run multiple aggregations against the same snapshot — see example.
$bucket / $bucketAutoCASE WHEN ... END + GROUP BYManual bucketing via CASE expressions
$replaceRoot / $replaceWithNo direct equivalent.
$merge / $outUse a transaction for atomic read-modify-write — see example.
$sampleNo random document sampling.
$unionWithNo collection union.
$geoNearNo geospatial support.
$densify / $fillNo time-series gap filling.
$redactNo document-level access control.
$setWindowFieldsNo window functions.
$condCASE WHEN ... THEN ... ELSE ... ENDDirect equivalent; DQL CASE is more expressive
$ifNullifnull(v1, v2, ...) / coalesce(...)Direct equivalent
$switchSimple or searched CASEDirect equivalent
$mapARRAY expr FOR v IN source ENDSDK v5+
$filterARRAY v FOR v IN source WHEN condition ENDSDK v5+
$reduceNo direct equivalent.
$arrayElemAtarray[index]Direct subscript access
$size (array)array_length(arr)Direct equivalent
$in (array membership)array_contains(arr, val)Direct equivalent
$concatArraysNo documented direct equivalent.
$sliceNo documented direct equivalent.
$reverseArrayNo array reversal function.
$sortArrayNo in-expression array sort.
$zipNo array zip.
$setUnion / $setIntersection / $setDifferenceNo set algebra operators on arrays.
$mergeObjectsobject_concat(obj1, obj2, ...)Direct equivalent
$objectToArrayobject_content(obj, 'fields')Available in SDK v4.11+
$arrayToObjectOBJECT name_expr:value_expr FOR ... IN ... ENDObject transformation expression, SDK v5+
$dateFromStringdate_cast(str, format)Direct equivalent
$dateToStringdate_format(date, format)Direct equivalent
$dateAdd / $dateSubtractdate_add(...) / date_sub(...)Direct equivalent
$dateDiffdate_diff(date1, date2, part)Direct equivalent
$year, $month, $day, etc.date_part(date, 'year')Unified single function
$regexMatchregexp_like(...), LIKE, ILIKE, SIMILAR TOMultiple pattern-matching options
$substr / $substrCPsubstr(str, start, len)Direct equivalent
$concatconcat(...) or || operatorDirect equivalent
$toLower / $toUpperlower(str) / upper(str)Direct equivalent
$trim / $ltrim / $rtrim`trim`(str) / ltrim() / rtrim()Direct equivalent; trim requires backtick quoting
$typetype(x) / json_type(x)Direct equivalent
$toString, $toInt, etc.cast(v, 'string') / CAST(v AS type)Unified cast function
$absabs(x)Direct equivalent
$ceilceil(x)Direct equivalent
$floorfloor(x)Direct equivalent
$round / $truncNo rounding or truncation functions.
$sqrt / $pow / $exp / $ln / $logNo advanced math functions.
$sin, $cos, $tan, etc.No trigonometric functions.
Pipeline composition (multi-stage)Single declarative SELECTMental model shift, not a feature gap

Filtering

$matchWHERE

MongoDB
{ $match: { status: "active", price: { $gt: 50 } } }
DQL
SELECT * FROM orders WHERE status = 'active' AND price > 50
DQL supports the full comparison operator set: =, !=, <, >, <=, >=, BETWEEN, IN, NOT IN, LIKE, ILIKE, SIMILAR TO, IS NULL, IS NOT NULL, IS MISSING, IS NOT MISSING.
DQL
-- Range filter
SELECT * FROM products WHERE price BETWEEN 10 AND 100

-- Membership test
SELECT * FROM orders WHERE status IN ('active', 'pending')

-- Pattern match (case-insensitive)
SELECT * FROM users WHERE name ILIKE 'john%'

-- Null / missing checks
SELECT * FROM documents WHERE archived IS NULL
SELECT * FROM documents WHERE tags IS NOT MISSING

Projection

$project (include fields)

MongoDB
{ $project: { name: 1, price: 1 } }
DQL
SELECT name, price FROM products

$project (computed fields)

MongoDB
{ $project: { name: 1, discounted: { $multiply: ["$price", 0.9] } } }
DQL
SELECT name, price * 0.9 AS discounted FROM products

$addFields / $set — add fields while keeping all existing ones

MongoDB
{ $addFields: { adjusted_price: { $multiply: ["$price", 1.1] } } }
DQL
SELECT products.*, price * 1.1 AS adjusted_price FROM products
SELECT *, price * 1.1 AS adjusted_price FROM products is a syntax error. The wildcard must be qualified with the collection name: SELECT products.*, ....

$project (exclude fields) / $unset

MongoDB
{ $unset: ["internal_notes", "cost"] }
DQL
SELECT products.*, MISSING internal_notes, MISSING cost FROM products
SELECT *, MISSING field1 FROM products is a syntax error. The wildcard must be qualified with the collection name: SELECT products.*, MISSING field1 FROM products. See Excluding Fields.
Fields listed as MISSING override the wildcard expansion — products.* includes the field, but the explicit MISSING projection wins and the key is absent from output. For excluding a nested field within an object value, use object_unset:
DQL
SELECT object_unset(metadata, 'internal', true) AS metadata FROM products

Grouping and Aggregation

$group

MongoDB
{
  $group: {
    _id: "$category",
    total_sales: { $sum: "$price" },
    avg_price: { $avg: "$price" },
    count: { $sum: 1 }
  }
}
DQL
SELECT
  category,
  SUM(price) AS total_sales,
  AVG(price) AS avg_price,
  COUNT(*) AS count
FROM products
GROUP BY category
All standard aggregates are supported: COUNT, SUM, AVG, MIN, MAX, MEDIAN, MID.

HAVING — filter on aggregated results

MongoDB
{ $group: { _id: "$category", count: { $sum: 1 } } },
{ $match: { count: { $gt: 5 } } }
DQL
SELECT category, COUNT(*) AS count
FROM products
GROUP BY category
HAVING COUNT(*) > 5

$addToSet / COUNT(DISTINCT ...)

DQL supports COUNT(DISTINCT ...) for counting unique values but does not accumulate values into an array within a group:
DQL
SELECT category, COUNT(DISTINCT supplier_id) AS unique_suppliers
FROM products
GROUP BY category
$push and $addToSet (as array accumulators in a group) have no DQL equivalent. If you need to collect values into an array per group, restructure the query or handle this in application code.

Sorting, Limiting, and Pagination

$sort, $limit, $skip

MongoDB
[
  { $sort: { created_at: -1 } },
  { $skip: 20 },
  { $limit: 10 }
]
DQL
SELECT * FROM orders
ORDER BY created_at DESC
LIMIT 10
OFFSET 20

Counting

$count

MongoDB
{ $count: "total" }
DQL
SELECT COUNT(*) AS total FROM products
With a filter:
DQL
SELECT COUNT(*) AS total FROM products WHERE category = 'electronics'

Array Operations

$unwind → array transform or search expression

MongoDB’s $unwind duplicates a document for each array element. DQL has no row-multiplying unwind. Instead:
  • Transform array contents in-place with the ARRAY expression
  • Filter documents based on array contents with ANY ... IN ... SATISFIES
MongoDB
{ $unwind: "$tags" }
// then: { $match: { tags: "electronics" } }
DQL
-- Find documents where any tag matches
SELECT * FROM products
WHERE ANY tag IN tags SATISFIES tag = 'electronics' END
DQL
-- Transform: extract only active line items
SELECT ARRAY item FOR item IN line_items WHEN item.status = 'active' END AS active_items
FROM orders

$mapARRAY transformation expression

MongoDB
{ $project: { prices_with_tax: { $map: { input: "$item_prices", as: "p", in: { $multiply: ["$$p", 1.08] } } } } }
DQL
SELECT ARRAY price * 1.08 FOR price IN item_prices END AS prices_with_tax
FROM orders

$filterARRAY ... WHEN expression

MongoDB
{ $project: { active_items: { $filter: { input: "$items", as: "item", cond: { $eq: ["$$item.status", "active"] } } } } }
DQL
SELECT ARRAY item FOR item IN items WHEN item.status = 'active' END AS active_items
FROM orders

$arrayElemAt → subscript operator

MongoDB
{ $project: { first: { $arrayElemAt: ["$items", 0] } } }
DQL
SELECT items[0] AS first_item FROM orders

$sizearray_length()

MongoDB
{ $project: { tag_count: { $size: "$tags" } } }
DQL
SELECT array_length(tags) AS tag_count FROM products

$in (array membership) → array_contains()

MongoDB
{ $project: { has_electronics: { $in: ["electronics", "$tags"] } } }
DQL
SELECT array_contains(tags, 'electronics') AS has_electronics FROM products
$concatArrays and $slice have no documented DQL equivalent at this time.

Object Operations

$mergeObjectsobject_concat()

MongoDB
{ $project: { merged: { $mergeObjects: ["$defaults", "$settings"] } } }
DQL
SELECT object_concat(defaults, settings) AS merged FROM users
Later arguments win on key conflicts.

$objectToArrayobject_content()

MongoDB
{ $project: { pairs: { $objectToArray: "$doc" } } }
DQL
SELECT object_content(doc, 'fields') AS pairs FROM collection1
object_content() returns an array of single-key objects. Available in SDK v4.11+. See Object Operators for full configuration options.

$arrayToObjectOBJECT transformation expression

MongoDB
{ $project: { id_map: { $arrayToObject: { $map: { input: "$catalog", as: "v", in: { k: "$$v._id", v: "$$v.name" } } } } } }
DQL
SELECT OBJECT v._id:v.name FOR v IN catalog END AS id_map
FROM product_catalog
Available in SDK v5+.

Conditional Expressions

$condCASE WHEN

MongoDB
{ $project: { tier: { $cond: { if: { $gt: ["$price", 100] }, then: "premium", else: "standard" } } } }
DQL
SELECT
  price,
  CASE WHEN price > 100 THEN 'premium' ELSE 'standard' END AS tier
FROM products
Multi-branch:
DQL
SELECT
  price,
  CASE
    WHEN price > 100 THEN 'premium'
    WHEN price > 50  THEN 'mid'
    ELSE 'budget'
  END AS tier
FROM products

$ifNullifnull() / coalesce()

MongoDB
{ $project: { display: { $ifNull: ["$nickname", "$name"] } } }
DQL
SELECT ifnull(nickname, name) AS display_name FROM users
coalesce(v1, v2, ...) is a synonym that returns the first non-null, non-missing value.

$switch → simple CASE

MongoDB
{ $project: { code: { $switch: { branches: [ { case: { $eq: ["$color","red"] }, then: 1 }, { case: { $eq: ["$color","blue"] }, then: 2 } ], default: 0 } } } }
DQL
SELECT
  CASE color
    WHEN 'red'  THEN 1
    WHEN 'blue' THEN 2
    ELSE 0
  END AS color_code
FROM cars

Bucketing

$bucket / $bucketAutoCASE + GROUP BY

MongoDB
{ $bucket: { groupBy: "$price", boundaries: [0, 25, 75, 200], default: "other" } }
DQL
SELECT
  CASE
    WHEN price < 25  THEN 'budget'
    WHEN price < 75  THEN 'mid'
    WHEN price < 200 THEN 'premium'
    ELSE 'other'
  END AS bucket,
  COUNT(*) AS count
FROM products
GROUP BY
  CASE
    WHEN price < 25  THEN 'budget'
    WHEN price < 75  THEN 'mid'
    WHEN price < 200 THEN 'premium'
    ELSE 'other'
  END

Date Functions

MongoDBDQLNotes
$dateFromStringdate_cast(str, format)Converts string to epoch milliseconds
$dateToStringdate_format(date, format)Converts epoch ms to formatted string
$dateAdddate_add(date, part, count)Parts: year, mon, day, hour, min, sec, ms
$dateSubtractdate_sub(date, part, count)Negative count on date_add is equivalent
$dateDiffdate_diff(date1, date2, part)Returns integer count of parts between dates
$year / $month / $day / etc.date_part(date, 'year')Single function; parts: year, mon, day, hour, min, sec, weekday, monthname
$dateTruncdate_trunc(date, part)Truncates to the given part
DQL
-- Parse a date string
SELECT date_cast('2024-01-15', 'YYYY-MM-DD') AS ts FROM system:dual

-- Format a stored timestamp
SELECT date_format(created_at, 'YYYY-MM-DD') AS date_str FROM orders

-- Add 7 days
SELECT date_add(created_at, 'day', 7) AS due_date FROM orders

-- Difference between dates in days
SELECT date_diff(due_date, created_at, 'day') AS days_open FROM tasks

-- Extract the year
SELECT date_part(created_at, 'year') AS yr FROM orders

String Functions

MongoDBDQLNotes
$regexMatchregexp_like(str, pattern[, flags])Also: LIKE, ILIKE, SIMILAR TO
$regexFindregexp_like(...)Match test only; no capture group extraction
$substr / $substrCPsubstr(str, start[, len])Zero-based index
$concatconcat(str, ...) or str1 || str2Direct equivalent
$toLowerlower(str)Direct equivalent
$toUpperupper(str)Direct equivalent
$trim`trim`(str)Backtick quoting required; ltrim, rtrim also available
$ltrimltrim(str[, chars])Direct equivalent
$rtrimrtrim(str[, chars])Direct equivalent
$indexOfCPpos(str, substr)Returns zero-based index; -1 if not found
$strLenCPchar_length(str)Direct equivalent
$strLenBytesbyte_length(str)Direct equivalent
$splitsplit(str, delim)Returns array of parts
$replaceOne / $replaceAllNo string replace function.
DQL
-- Regex match (case-insensitive flag)
SELECT regexp_like(name, '^[A-Z]') AS starts_capital FROM users

-- Substring
SELECT substr(name, 0, 3) AS prefix FROM users

-- Concatenation
SELECT concat(first_name, ' ', last_name) AS full_name FROM users

-- Case conversion
SELECT lower(name) AS name_lower, upper(name) AS name_upper FROM users

-- Trim whitespace (note backtick quoting)
SELECT `trim`(name) AS name_trimmed FROM users

Type Inspection and Casting

$typetype() / json_type()

MongoDB
{ $project: { val_type: { $type: "$value" } } }
DQL
SELECT type(value) AS val_type FROM data
type() returns DQL type names (boolean, string, integer, float, object, array, binary, null, missing). json_type() returns JSON type names (boolean, string, number, object, array, binary, null), returning "null" for MISSING values.

$toString, $toInt, $toDouble, etc. → cast()

MongoDB
{ $project: { price_str: { $toString: "$price" } } }
DQL
SELECT cast(price, 'string') AS price_str FROM products
Supported target types: 'string', 'integer', 'float', 'boolean', 'binary'. Also accepts CAST(v AS type) SQL syntax. Returns MISSING if conversion is not possible.

Multi-Stage Pipelines → Single SELECT

A MongoDB pipeline composing multiple stages maps to a single DQL SELECT:
MongoDB
[
  { $match: { status: "active" } },
  { $project: { name: 1, category: 1, price: 1 } },
  { $group: { _id: "$category", avg_price: { $avg: "$price" }, count: { $sum: 1 } } },
  { $sort: { avg_price: -1 } },
  { $limit: 5 }
]
DQL
SELECT
  category,
  AVG(price) AS avg_price,
  COUNT(*) AS count
FROM products
WHERE status = 'active'
GROUP BY category
ORDER BY avg_price DESC
LIMIT 5

Transaction Workarounds

Ditto’s transaction() API runs multiple DQL statements in a single atomic block at serializable isolation — either all statements commit or none do. This covers several patterns that have no direct DQL equivalent. Examples below use JavaScript; the same pattern applies in all supported SDKs.

Simulating Joins

DQL has no join syntax, but a read-only transaction guarantees both queries see the same consistent snapshot of the store. Combine the results in application code:
JavaScript
const { orders, customer } = await ditto.store.transaction(
  async (tx) => {
    const orders = await tx.execute(
      "SELECT * FROM orders WHERE customer_id = :id",
      { id: customerId }
    );
    const customer = await tx.execute(
      "SELECT * FROM customers WHERE _id = :id",
      { id: customerId }
    );
    return { orders: orders.items, customer: customer.items[0] };
  },
  { hint: "join-simulation", isReadOnly: true }
);
// Join in application code
const enriched = orders.map((o) => ({ ...o, customer }));

Consistent Multi-Query Snapshot

The equivalent of $facet — run independent aggregations that are guaranteed to reflect the same point in time:
JavaScript
const facets = await ditto.store.transaction(
  async (tx) => {
    const byCategory = await tx.execute(
      "SELECT category, COUNT(*) AS count FROM products GROUP BY category"
    );
    const byStatus = await tx.execute(
      "SELECT status, COUNT(*) AS count FROM products GROUP BY status"
    );
    return { byCategory: byCategory.items, byStatus: byStatus.items };
  },
  { hint: "facet-simulation", isReadOnly: true }
);

Atomic Read-Modify-Write

The equivalent of $merge / $out — aggregate, transform in application code, then write the results back in the same atomic operation:
JavaScript
await ditto.store.transaction(
  async (tx) => {
    const summary = await tx.execute(
      "SELECT category, COUNT(*) AS count, AVG(price) AS avg_price FROM products GROUP BY category"
    );
    for (const row of summary.items) {
      await tx.execute(
        "INSERT INTO product_summaries VALUES (:doc) ON ID CONFLICT DO UPDATE",
        { doc: { _id: row.category, count: row.count, avg_price: row.avg_price } }
      );
    }
    return "commit";
  },
  { hint: "aggregate-and-write" }
);

Known Gaps

The following MongoDB aggregation capabilities have no DQL equivalent.

Pipeline Stages

MongoDBWorkaround
$lookupNo joins. Use a transaction to query multiple collections at a consistent snapshot and join in application code.
$graphLookupNo graph traversal. Implement multi-hop lookups in application code.
$unionWithNo collection union. Run separate queries and merge results in application code.
$unwind (row multiplication)No row-multiplying unwind. Use ARRAY transform or ANY/EVERY search expressions for in-place operations.
$facetNo single-query equivalent. Use a read-only transaction to run multiple aggregations against a consistent snapshot.
$replaceRoot / $replaceWithNo direct equivalent. Restructure data at insert time, or select nested fields explicitly.
$merge / $outQueries are read-only. Use a transaction to aggregate, transform, and write back atomically.
$sampleNo random sampling. Fetch a larger result set and sample in application code.
$setWindowFieldsNo window functions ($rank, $denseRank, $expMovingAvg, etc.).
$densify / $fillNo time-series gap filling or null-value backfill.
$geoNearNo geospatial support.
$redactNo document-level access control expressions.

Grouping and Accumulators

MongoDBWorkaround
$push / $addToSet (array accumulator)No array-building aggregate. Collect values in application code after querying.
$first / $last / $firstN / $lastNNo ordered first/last value within a group. Use a subquery approach or handle in application code.
$top / $topN / $bottom / $bottomNNo top-N per group. Sort and limit in application code.
$stdDevPop / $stdDevSampNo standard deviation aggregates.
$percentileNo percentile aggregate.
$accumulator / $functionNo user-defined aggregate or expression functions.

Array Operations

MongoDBWorkaround
$reduceNo fold/reduce expression. Implement in application code.
$concatArraysNo array concatenation expression. Combine in application code.
$sliceUse subscript array[index] for single elements; slice in application code.
$reverseArrayNo array reversal. Reverse in application code.
$sortArrayNo in-expression array sort. Sort in application code.
$zipNo array zip. Implement in application code.
$setUnion / $setIntersection / $setDifference / $setEquals / $setIsSubsetNo set algebra on arrays. Implement using ARRAY transforms and ANY/EVERY expressions.

String Operations

MongoDBWorkaround
$replaceOne / $replaceAllNo string replace function. Use regexp_like to match; replace in application code.

Math Functions

DQL provides abs(), ceil(), and floor(). The following have no equivalent:
MongoDBWorkaround
$round / $truncUse floor(x + 0.5) to approximate rounding; truncate in application code.
$sqrtNo square root. Compute in application code.
$powNo exponentiation. Compute in application code.
$exp / $ln / $log / $log10No exponential or logarithm functions. Compute in application code.
$sin, $cos, $tan, $asin, $acos, $atan, $atan2, etc.No trigonometric functions. Compute in application code.
$degreesToRadians / $radiansToDegreesNo angle conversion functions.