{"record":{"id":"3460594c90d980c8","repo":"litedb-org/LiteDB","slug":"extend-expression-must-return-a-document","errorCode":null,"errorMessage":"Extend expression must return a document","messagePattern":"Extend expression must return a document","errorType":"validation","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"LiteDB/Engine/Engine/Update.cs","lineNumber":75,"sourceCode":"            IEnumerable<BsonDocument> transformDocs()\n            {\n                var q = new Query { Select = \"$\", ForUpdate = true };\n\n                if (predicate != null)\n                {\n                    q.Where.Add(predicate);\n                }\n\n                using (var reader = this.Query(collection, q))\n                {\n                    while (reader.Read())\n                    {\n                        var doc = reader.Current.AsDocument;\n\n                        var id = doc[\"_id\"];\n                        var value = transform.ExecuteScalar(doc, _header.Pragmas.Collation);\n\n                        if (!value.IsDocument) throw new ArgumentException(\"Extend expression must return a document\", nameof(transform));\n\n                        var result = BsonExpressionMethods.EXTEND(doc, value.AsDocument).AsDocument;\n\n                        // be sure result document will contain same _id as current doc\n                        if (result.TryGetValue(\"_id\", out var newId))\n                        {\n                            if (newId != id) throw LiteException.InvalidUpdateField(\"_id\");\n                        }\n                        else\n                        {\n                            result[\"_id\"] = id;\n                        }\n\n                        yield return result;\n                    }\n                }\n            }\n        }","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/litedb-org/LiteDB/blob/f906a5f850678719e39a39a006cb66dcae563cfa/LiteDB/Engine/Engine/Update.cs#L57-L93","documentation":"Thrown by UpdateMany when the transform expression passed to it does not evaluate to a BsonDocument for a given source document. UpdateMany treats the transform as an 'extend' operation — it merges the returned document into each existing document — so a scalar/array/null return value has no valid merge semantics. The check happens per-document during the lazy enumeration of the update.","triggerScenarios":"Calling engine.UpdateMany(collection, transform, predicate) where the transform BsonExpression evaluates to a non-document BsonValue (e.g. an expression like \"$.count + 1\" that yields a number, or \"$.name\" that yields a string) for at least one matched document.","commonSituations":"Confusing UpdateMany (document transform) with a field-level update; writing an expression that returns different types for different documents; using a path expression that resolves to a scalar.","solutions":["Ensure the transform expression always returns a BsonDocument (e.g. \"{ count: $.count + 1 }\" rather than \"$.count + 1\").","If you only need to update a single field, use a document-projection expression that constructs the new sub-document.","Test the expression in a SELECT first to verify it yields document values for all matched rows.","Catch ArgumentException from UpdateMany and report which document triggered it."],"exampleFix":"// before — transform returns a scalar\ndb.Execute(\"UPDATE items SET { count: $.count + 1 } WHERE $.active = true\");\n// if expression body is just \"$.count + 1\" it throws\n\n// after — transform returns a document\n// correct SQL/expressions construct a full or partial document:\ndb.GetCollection(\"items\").UpdateMany(\"{ count: $.count + 1 }\", \"$.active = true\");","handlingStrategy":"validation","validationCode":"// Validate the transform expression returns a document for sample inputs before calling UpdateMany.\npublic bool TransformReturnsDocument(LiteCollection<BsonDocument> col, BsonExpression transform, BsonExpression predicate)\n{\n    var sample = col.Query().Where(predicate).Limit(1).FirstOrDefault();\n    if (sample == null) return true; // nothing to check\n    var value = transform.ExecuteScalar(sample, col.Database.GetCollection(\"$\").Equals /*placeholder*/);\n    return value.IsDocument;\n}","typeGuard":"static bool IsDocumentResult(BsonValue v) => v != null && v.IsDocument;","tryCatchPattern":"try\n{\n    col.UpdateMany(transformExpr, predicateExpr);\n}\ncatch (ArgumentException ex) when (ex.Message.Contains(\"Extend expression must return a document\"))\n{\n    throw new InvalidOperationException($\"Transform expression '{transformExpr.Source}' returned a non-document value. Rewrite it to return a BsonDocument.\", ex);\n}","preventionTips":["Design transform expressions as document literals: '{ field: <expr> }'.","Test the expression with a SELECT before using it in UpdateMany.","Avoid bare path/scalar expressions in the transform position.","Ensure the expression returns a document for every matched row, not just the first."],"tags":["update","bson-expression","litedb-engine"],"backgroundTag":null,"analyzedSha":"f906a5f850678719e39a39a006cb66dcae563cfa","analyzedAt":"2026-08-13T21:56:30.148Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}