Subdocuments in TerminusDB are documents that exist only as part of a parent document. Unlike regular documents, subdocuments cannot exist independently and must always be linked to a parent document through a property. This guide consolidates all subdocument operations using WOQL.
Understanding the @linked-by Annotation
When working with subdocuments through the document interface (WOQL's insert_document, update_document, etc.), you must include the @linked-by annotation. This annotation serves a critical purpose for schema validation.
Why @linked-by is Required
The @linked-by annotation tells the schema checker which parent document the subdocument belongs to and through which property it is linked. This is essential because:
- Schema validation context: The schema checker needs to understand the document graph to validate that the subdocument is correctly typed for the property it's assigned to
- Graph traversal: During document operations, TerminusDB builds an understanding of the document structure to ensure referential integrity
- Type checking: The annotation allows verification that the subdocument type matches the expected type defined in the parent's schema
Important: @linked-by is Not Stored
The @linked-by annotation is not persisted in the triple store. It is active only during document handling operations to provide the schema checker with the necessary context about the current graph structure. Once the document operation completes, only the actual data and the linking triple are stored.
The @linked-by Structure
{
"@linked-by": {
"@id": "ParentDocument/id", // The parent document's ID
"@property": "propertyName" // The property linking to this subdocument
}
}Create a Subdocument
Creating a subdocument requires two operations:
- Insert the subdocument with
@linked-byannotation - Add the triple linking the parent to the subdocument
let v = Vars("subdocId");
and(
insert_document(
doc({
"@type": "PersonRole",
"@linked-by": {
"@id": "Person/John",
"@property": "role"
},
title: "Manager",
department: "Engineering"
}),
v.subdocId
),
add_triple("Person/John", "role", v.subdocId)
)The insert_document creates the subdocument with its properties, and add_triple establishes the link from the parent document to the subdocument.
Read a Subdocument
Reading a subdocument works the same as reading any document once you have its ID:
let v = Vars("subdoc", "subdocId");
and(
triple("Person/John", "role", v.subdocId),
read_document(v.subdocId, v.subdoc)
)This query:
- Finds the subdocument ID linked from
Person/Johnvia theroleproperty - Reads the complete subdocument into the
subdocvariable
Update a Subdocument
Subdocuments cannot be updated in place using update_document (which only works for top-level documents). Instead, you must delete the old subdocument and create a new one with the updated content.
This approach works best with random-keyed identifiers on subdocuments:
let v = Vars("parentDoc", "oldSubdoc", "newSubdoc");
select(v.oldSubdoc, v.newSubdoc).and(
eq(v.oldSubdoc, "Person/John/role/PersonRole/cxW1Egirxm8-QYrq"),
triple(v.parentDoc, "role", v.oldSubdoc),
delete_document(v.oldSubdoc),
insert_document(
doc({
"@type": "PersonRole",
"@linked-by": {
"@id": v.parentDoc,
"@property": "role"
},
title: "Senior Manager",
department: "Engineering"
}),
v.newSubdoc
),
update_triple(v.parentDoc, "role", v.newSubdoc, v.oldSubdoc)
)This query:
- Finds the parent document that links to the subdocument
- Deletes the old subdocument
- Creates a new subdocument with updated values (including the required
@linked-by) - Updates the linking triple to point to the new subdocument
The select at the beginning ensures the old and new subdocument IDs are returned in the bindings.
Delete a Subdocument
Deleting a subdocument requires removing both the subdocument and the triple that links it from the parent:
let v = Vars("parentDoc", "subdocId");
and(
eq(v.subdocId, "Person/John/role/PersonRole/cxW1Egirxm8-QYrq"),
triple(v.parentDoc, "role", v.subdocId),
delete_document(v.subdocId),
delete_triple(v.parentDoc, "role", v.subdocId)
)If you only delete the subdocument without removing the linking triple, you will have a dangling reference in your data.
Working with Multiple Subdocuments
When a property can hold multiple subdocuments (Set or List types), you can iterate over them:
let v = Vars("person", "roleId", "roleDoc");
and(
isa(v.person, "Person"),
triple(v.person, "roles", v.roleId),
read_document(v.roleId, v.roleDoc)
)To add an additional subdocument to a Set or List property:
let v = Vars("newRoleId");
and(
insert_document(
doc({
"@type": "PersonRole",
"@linked-by": {
"@id": "Person/John",
"@property": "roles"
},
title: "Consultant"
}),
v.newRoleId
),
add_triple("Person/John", "roles", v.newRoleId)
)Best Practices
Always include @linked-by: When using document interface operations (
insert_document, etc.) with subdocuments, always include the@linked-byannotation to ensure proper schema validationUse random keys for updatable subdocuments: If you need to update subdocuments frequently, use
@key: { "@type": "Random" }in your schema to generate unique IDs that don't depend on contentMaintain referential integrity: Always delete the linking triple when deleting a subdocument, and always create the linking triple when creating a subdocument
Consider using top-level document updates: For complex subdocument updates, it may be simpler to read the entire parent document, modify it in your application, and use
update_documenton the parent
See Also
- Add Documents with WOQL - General document insertion
- Edit Documents with WOQL - Document updates
- Delete Documents with WOQL - Document deletion
- Schema Reference - Subdocument schema definitions