Optic Data Manipulation Starter Pack
Shape XML, run expression libraries at runtime, and execute update plans cleanly
The source shape, the transformation logic, and the output stay together so the before and after are never a mystery.
This pack covers the Optic manipulation surface you reach for once a source is not enough on its own. That means turning XML into rows, using expression helper libraries inside plans, and understanding when a plan stops being read-only.
The examples in this article assume the llamaverse (v2.0+) is deployed. The llamaverse sample data is freely available from github.com/cleverllamas/llamaverse - see the llamaverse article for full setup instructions.
Language and Scope
These examples use the same sample-document structure as the cts, xdmp, and other Optic starter packs.
One naming detail matters here: the documented Optic update function is op:execute(), not op:exec().
Most sections in this pack are read-only. The final op:execute() section is update-oriented and should be treated as a content-changing pattern.
The code assets in this pack were validated against the local MarkLogic 11 runtime used by this repository via tooling/marklogic/validate-optic-starter-packs.sh.
API Context for This Pack
| Context Item | What this pack uses |
|---|---|
| Primary sources | XML seed documents, literal row inputs, and update payload rows |
| Primary functions | op:xpath, op:select, op:execute, op:write, op:join-cross-product |
| Expression helpers | ofn, oxs, and oxdmp helper libraries in Optic expressions |
| Output style | Small row sets for read examples; explicit mutation note for update examples |
Expression Libraries in Optic Plans
The Optic API is not limited to op:* functions. You can also import Optic expression helper libraries that wrap familiar function families for use inside plans, and those expressions are evaluated when the plan executes.
In XQuery you import these helpers explicitly (ofn, oxs, oxdmp). In JavaScript the same families are available as op.fn, op.xs, and op.xdmp.
This example proves runtime evaluation directly:
- Capture a timestamp outside the plan.
- Sleep for five seconds.
- Run a plan that computes
ofn:current-dateTime(),oxs:dateTime(...), andoxdmp:random(...)inside Optic expressions.
If those expressions run at plan execution time, the runtime values must be later than the captured value.
The core libraries used here are:
| Library | Purpose | Typical use in a plan |
|---|---|---|
ofn | Optic wrapper for W3C fn functions | Runtime timestamp and string conversion inside the plan |
oxs | Optic wrapper for xs constructors | Typed xs:dateTime construction inside a plan expression |
oxdmp | Optic wrapper for useful xdmp functions | Runtime random value generation within Optic |
| Option / Argument | What it controls | Used here |
|---|---|---|
ofn:current-dateTime() | Computes runtime timestamp during plan execution | Shows runtime value after a 5-second sleep |
oxs:dateTime(...) | Constructs a typed dateTime expression in-plan | Shows explicit type construction in the plan |
oxdmp:random(...) | Computes server-side random value inside plan expression | Shows execution-time helper behaviour |
{
"demoId": "runtime-proof",
"capturedAt": "set before sleep()",
"runtimeAt": "computed inside the plan"
}
xquery version "1.0-ml";
import module namespace op = "http://marklogic.com/optic" at "/MarkLogic/optic.xqy";
import module namespace ofn = "http://marklogic.com/optic/expression/fn" at "/MarkLogic/optic/optic-fn.xqy";
import module namespace oxs = "http://marklogic.com/optic/expression/xs" at "/MarkLogic/optic/optic-xs.xqy";
import module namespace oxdmp = "http://marklogic.com/optic/expression/xdmp" at "/MarkLogic/optic/optic-xdmp.xqy";
let $captured-at := fn:current-dateTime()
let $_ := xdmp:sleep(5000)
let $captured := op:from-literals((
map:entry("demoId", "runtime-proof")
=> map:with("capturedAt", $captured-at)
))
let $runtime := op:from-literals((
map:entry("runtimeId", "runtime-proof")
))
return
$captured
=> op:join-cross-product(
$runtime,
op:eq(op:col("demoId"), op:col("runtimeId"))
)
=> op:select((
"demoId",
"capturedAt",
op:as("runtimeAt", ofn:current-dateTime()),
op:as("runtimeAsDateTime", oxs:dateTime(ofn:string(ofn:current-dateTime()))),
op:as("runtimeAfterCaptured", op:gt(ofn:current-dateTime(), op:col("capturedAt"))),
op:as("runtimeRandom", oxdmp:random(1000000))
), "clockCheck")
=> op:result()
'use strict';
const op = require('/MarkLogic/optic');
const capturedAt = fn.currentDateTime();
xdmp.sleep(5000);
const captured = op.fromLiterals([
{
demoId: 'runtime-proof',
capturedAt
}
]);
const runtime = op.fromLiterals([
{
runtimeId: 'runtime-proof'
}
]);
const results = captured
.joinCrossProduct(runtime, op.eq(op.col('demoId'), op.col('runtimeId')))
.select([
'demoId',
'capturedAt',
op.as('runtimeAt', op.fn.currentDateTime()),
op.as('runtimeAsDateTime', op.xs.dateTime(op.fn.string(op.fn.currentDateTime()))),
op.as('runtimeAfterCaptured', op.gt(op.fn.currentDateTime(), op.col('capturedAt'))),
op.as('runtimeRandom', op.xdmp.random(1000000))
], 'clockCheck')
.result();
({
sample: 'optic/data-manipulation-starter-pack/assets/expression-libraries.sjs',
kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
count: Array.isArray(results) ? results.length : 0,
data: results
});
{"clockCheck.demoId":"runtime-proof", "clockCheck.capturedAt":"2026-07-23T12:23:50.163682Z", "clockCheck.runtimeAt":"2026-07-23T12:23:55.524298Z", "clockCheck.runtimeAsDateTime":"2026-07-23T12:23:55.524298Z", "clockCheck.runtimeAfterCaptured":true, "clockCheck.runtimeRandom":328961}
| clockCheck.demoId | clockCheck.capturedAt | clockCheck.runtimeAt | clockCheck.runtimeAsDateTime | clockCheck.runtimeAfterCaptured | clockCheck.runtimeRandom |
|---|---|---|---|---|---|
| runtime-proof | 2026-07-23T12:23:50.163682Z | 2026-07-23T12:23:55.524298Z | 2026-07-23T12:23:55.524298Z | true | 328961 |
What to notice: runtimeAfterCaptured is true after a deliberate 5-second pause, which confirms these expression-library calls execute at runtime inside the plan, not just when the script is assembled.
op:xpath()
op:xpath() is the bridge from XML structure into Optic rows. Use it when the source is hierarchical but the next question you need to answer is tabular.
This is the practical answer to the earlier shorthand op:from-xpath: the source is usually op:from-doc-descriptors() or another document-bearing plan, and op:xpath() is the operation that projects XML into columns.
| Option / Argument | What it controls | Used here |
|---|---|---|
column | Output column name for extracted values | Forest status projection columns |
path | XPath expression evaluated against each source document | XPath paths for ID, name, and state |
<all-forests>
<forest-status>
<forest-id>1</forest-id>
<forest-name>Documents-1</forest-name>
<state>online</state>
</forest-status>
</all-forests>
xquery version "1.0-ml";
import module namespace op = "http://marklogic.com/optic" at "/MarkLogic/optic.xqy";
import module namespace ofn = "http://marklogic.com/optic/expression/fn" at "/MarkLogic/optic/optic-fn.xqy";
let $forestDoc := document {
element all-forests {
for $forest in subsequence(xdmp:forest-status(xdmp:forests()), 1, 3)
return $forest
}
}
let $descriptors :=
for $forest at $position in $forestDoc/*:all-forests/*:forest-status
return
map:entry("uri", "/cleverllamas/llamaverse/scratch/reports/forest-status-" || $position || ".xml")
=> map:with("doc", document { $forest })
return
op:from-doc-descriptors($descriptors)
=> op:select((
"uri",
op:as("forestName", ofn:string(op:xpath("doc", "/*:forest-status/*:forest-name"))),
op:as("availability", ofn:string(op:xpath("doc", "/*:forest-status/*:availability"))),
op:as("state", ofn:string(op:xpath("doc", "/*:forest-status/*:state")))
))
=> op:result()
'use strict';
const op = require('/MarkLogic/optic');
const descriptors = xdmp.forests().toArray().map((forestId) =>
{
const status = JSON.parse(xdmp.quote(xdmp.forestStatus(forestId)));
return {
uri: `/forest-status/${forestId}.xml`,
doc: xdmp.unquote(
`<status>
<forest-id>${status.forestId}</forest-id>
<forest-name>${status.forestName}</forest-name>
<state>${status.state}</state>
<availability>${status.availability}</availability>
</status>`
)
};
}
);
const results = op.fromDocDescriptors(descriptors)
.select([
op.as('forestId', op.fn.string(op.xpath('doc', '/status/forest-id'))),
op.as('forestName', op.fn.string(op.xpath('doc', '/status/forest-name')))
])
.result();
({
sample: 'optic/data-manipulation-starter-pack/assets/op-xpath-forest-status.sjs',
kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
count: Array.isArray(results) ? results.length : 0,
data: results
});
{"uri":"/cleverllamas/llamaverse/scratch/reports/forest-status-1.xml", "forestName":"Last-Login", "availability":"online", "state":"open"}
{"uri":"/cleverllamas/llamaverse/scratch/reports/forest-status-2.xml", "forestName":"cleverllamas-content-1", "availability":"online", "state":"open"}
{"uri":"/cleverllamas/llamaverse/scratch/reports/forest-status-3.xml", "forestName":"Schemas", "availability":"online", "state":"open"}
| uri | forestName | availability | state |
|---|---|---|---|
/cleverllamas/llamaverse/scratch/reports/forest-status-1.xml | Last-Login | online | open |
/cleverllamas/llamaverse/scratch/reports/forest-status-2.xml | cleverllamas-content-1 | online | open |
/cleverllamas/llamaverse/scratch/reports/forest-status-3.xml | Schemas | online | open |
What to notice: the interesting transformation is not the descriptor source by itself. It is the moment op:xpath() turns the seeded XML documents into stable report columns.
op:xpath() sequence expansion with op:unnest-inner()
Yes, this pattern is covered now explicitly: op:xpath() can extract a multi-value sequence (for example, repeated <skill> nodes), and op:unnest-inner() expands that sequence into one row per value.
Source docs: https://docs.marklogic.com/op:xpathhttps://docs.marklogic.com/op:unnest-inner
| Option / Argument | What it controls | Used here |
|---|---|---|
op:xpath("doc", "/llama/skills/skill/text()") | Extracts repeated skill values as a sequence | Creates skillSeq from XML nodes |
op:unnest-inner(input, value, ordinal) | Flattens sequence into rows and keeps position | Produces skill + skillOrdinality rows |
<llama>
<name>Cameron</name>
<skills>
<skill>xpath</skill>
<skill>security</skill>
<skill>optic</skill>
</skills>
</llama>
xquery version "1.0-ml";
import module namespace op = "http://marklogic.com/optic" at "/MarkLogic/optic.xqy";
import module namespace ofn = "http://marklogic.com/optic/expression/fn" at "/MarkLogic/optic/optic-fn.xqy";
let $descriptors := (
map:entry("uri", "/llama/skills/aaron.xml")
=> map:with("doc", xdmp:unquote('<llama><name>Aaron</name><skills><skill>xpath</skill><skill>optic</skill></skills></llama>')),
map:entry("uri", "/llama/skills/cameron.xml")
=> map:with("doc", xdmp:unquote('<llama><name>Cameron</name><skills><skill>xpath</skill><skill>security</skill><skill>optic</skill></skills></llama>'))
)
return
op:from-doc-descriptors($descriptors)
=> op:select((
"uri",
op:as("llamaName", ofn:string(op:xpath("doc", "/llama/name"))),
op:as("skillSeq", op:xpath("doc", "/llama/skills/skill/text()"))
))
=> op:unnest-inner("skillSeq", "skill", "skillOrdinality")
=> op:order-by((op:asc("llamaName"), op:asc("skillOrdinality")))
=> op:result()
'use strict';
const op = require('/MarkLogic/optic');
const descriptors = [
{
uri: '/llama/skills/aaron.xml',
doc: xdmp.unquote('<llama><name>Aaron</name><skills><skill>xpath</skill><skill>optic</skill></skills></llama>')
},
{
uri: '/llama/skills/cameron.xml',
doc: xdmp.unquote('<llama><name>Cameron</name><skills><skill>xpath</skill><skill>security</skill><skill>optic</skill></skills></llama>')
}
];
const results = op.fromDocDescriptors(descriptors)
.select([
'uri',
op.as('llamaName', op.fn.string(op.xpath('doc', '/llama/name'))),
op.as('skillSeq', op.xpath('doc', '/llama/skills/skill/text()'))
])
.unnestInner('skillSeq', 'skill', 'skillOrdinality')
.orderBy([op.asc('llamaName'), op.asc('skillOrdinality')])
.result();
({
sample: 'optic/data-manipulation-starter-pack/assets/op-xpath-sequence-expand.sjs',
kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
count: Array.isArray(results) ? results.length : 0,
data: results
});
{"uri":"/llama/skills/aaron.xml", "llamaName":"Aaron", "skill":"xpath", "skillSeq":["xpath", "optic"], "skillOrdinality":1}
{"uri":"/llama/skills/aaron.xml", "llamaName":"Aaron", "skill":"optic", "skillSeq":["xpath", "optic"], "skillOrdinality":2}
{"uri":"/llama/skills/cameron.xml", "llamaName":"Cameron", "skill":"xpath", "skillSeq":["xpath", "security", "optic"], "skillOrdinality":1}
{"uri":"/llama/skills/cameron.xml", "llamaName":"Cameron", "skill":"security", "skillSeq":["xpath", "security", "optic"], "skillOrdinality":2}
{"uri":"/llama/skills/cameron.xml", "llamaName":"Cameron", "skill":"optic", "skillSeq":["xpath", "security", "optic"], "skillOrdinality":3}
| uri | llamaName | skill | skillSeq | skillOrdinality |
|---|---|---|---|---|
/llama/skills/aaron.xml | Aaron | xpath | xpath,optic | 1 |
/llama/skills/aaron.xml | Aaron | optic | xpath,optic | 2 |
/llama/skills/cameron.xml | Cameron | xpath | xpath,security,optic | 1 |
/llama/skills/cameron.xml | Cameron | security | xpath,security,optic | 2 |
/llama/skills/cameron.xml | Cameron | optic | xpath,security,optic | 3 |
What to notice: this is the concrete "XML into rows" transition for repeating nodes. op:xpath() extracts the sequence, and op:unnest-inner() gives you row-level values that can be joined, filtered, grouped, or written.
op:select()
op:select() is the projection and shaping stage for Optic rows. It decides exactly which columns leave the plan step, what they are called, and whether they are raw values or derived expressions.
In practice, this is where your output contract becomes explicit.
Source docs: https://docs.marklogic.com/op:select
| Option / Argument | What it controls | Used here |
|---|---|---|
columns | Which output columns are projected and how each is expressed | Mix of pass-through fields and derived labels/flags |
qualifier | Output qualifier for projected columns | shape qualifier for readable result structure |
{
"name": "Aaron",
"breed": "Huacaya",
"heightCm": 172,
"placeOfBirth": "Cusco"
}
xquery version "1.0-ml";
import module namespace op = "http://marklogic.com/optic" at "/MarkLogic/optic.xqy";
import module namespace ofn = "http://marklogic.com/optic/expression/fn" at "/MarkLogic/optic/optic-fn.xqy";
op:from-literals((
map:entry("name", "Aaron")
=> map:with("breed", "Huacaya")
=> map:with("heightCm", 172)
=> map:with("placeOfBirth", "Cusco"),
map:entry("name", "Cameron")
=> map:with("breed", "Suri")
=> map:with("heightCm", 167)
=> map:with("placeOfBirth", "Quito"),
map:entry("name", "Richard")
=> map:with("breed", "Huacaya")
=> map:with("heightCm", 176)
=> map:with("placeOfBirth", "Arequipa")
))
=> op:select((
op:as("llamaName", op:col("name")),
op:as("profileLabel", ofn:concat((op:col("name"), " (", op:col("breed"), ") from ", op:col("placeOfBirth")))),
op:as("heightCm", op:col("heightCm")),
op:as("isTall", op:gt(op:col("heightCm"), 170))
), "shape")
=> op:order-by(op:asc("llamaName"))
=> op:result()
'use strict';
const op = require('/MarkLogic/optic');
const results = op.fromLiterals([
{ name: 'Aaron', breed: 'Huacaya', heightCm: 172, placeOfBirth: 'Cusco' },
{ name: 'Cameron', breed: 'Suri', heightCm: 167, placeOfBirth: 'Quito' },
{ name: 'Richard', breed: 'Huacaya', heightCm: 176, placeOfBirth: 'Arequipa' }
])
.select([
op.as('llamaName', op.col('name')),
op.as('profileLabel', op.fn.concat(op.col('name'), ' (', op.col('breed'), ') from ', op.col('placeOfBirth'))),
op.as('heightCm', op.col('heightCm')),
op.as('isTall', op.gt(op.col('heightCm'), 170))
], 'shape')
.orderBy(op.asc('llamaName'))
.result();
({
sample: 'optic/data-manipulation-starter-pack/assets/op-select-shaping.sjs',
kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
count: Array.isArray(results) ? results.length : 0,
data: results
});
{"shape.llamaName":"Aaron", "shape.profileLabel":"Aaron (Huacaya) from Cusco", "shape.heightCm":172, "shape.isTall":true}
{"shape.llamaName":"Cameron", "shape.profileLabel":"Cameron (Suri) from Quito", "shape.heightCm":167, "shape.isTall":false}
{"shape.llamaName":"Richard", "shape.profileLabel":"Richard (Huacaya) from Arequipa", "shape.heightCm":176, "shape.isTall":true}
| shape.llamaName | shape.profileLabel | shape.heightCm | shape.isTall |
|---|---|---|---|
| Aaron | Aaron (Huacaya) from Cusco | 172 | true |
| Cameron | Cameron (Suri) from Quito | 167 | false |
| Richard | Richard (Huacaya) from Arequipa | 176 | true |
What to notice: op:select() is where result semantics become deliberate. The input rows carry several fields, but the selected output becomes a concise contract with clear names (llamaName, profileLabel) and computed meaning (isTall).
Privilege Snapshot Pipeline with op:execute()
Once a plan becomes update-oriented, the control point changes. Instead of asking for rows back, you often want to generate write rows, call op:write(), and then finish with op:execute().
This example uses a real admin-side use case: snapshot all privileges for cleverllamas-mother, resolve each privilege ID to its real name, and persist one document per privilege through an Optic update plan.
The flattening step uses op:unnest-inner(): a single row containing a privilege sequence becomes one row per privilege with an ordinal column. Those unnested rows then feed the write pipeline.
| Option / Argument | What it controls | Used here |
|---|---|---|
xdmp:user-privileges() | Returns privilege IDs for the target user | Source list for row expansion |
op:unnest-inner() | Flattens sequence/array values into one row per value | Turns privilege sequence into write-ready rows with ordinality |
xdmp:privilege-name() | Resolves privilege ID to real privilege name | Persists meaningful privilege details |
op:write(...) | Defines write operation for the bound payload | Document insert/update operation |
op:execute() | Executes the update plan | Runs the write plan and commits changes |
{
"user": "cleverllamas-mother",
"privilegeIds": ["..."],
"writeRows": "one row per privilege"
}
xquery version "1.0-ml";
import module namespace op = "http://marklogic.com/optic" at "/MarkLogic/optic.xqy";
import module namespace ofn = "http://marklogic.com/optic/expression/fn" at "/MarkLogic/optic/optic-fn.xqy";
declare option xdmp:update "true";
let $user-name := "cleverllamas-mother"
let $privilege-csv := fn:string-join(
for $privilege-id in xdmp:user-privileges($user-name)
return fn:string($privilege-id),
","
)
let $privilege-rows :=
op:from-literals((
map:entry("userName", $user-name)
=> map:with("privilegeCsv", $privilege-csv)
))
=> op:bind((
op:as("privilegeSeq", ofn:tokenize(op:col("privilegeCsv"), ","))
))
=> op:unnest-inner("privilegeSeq", "privilegeId", "ordinality")
=> op:select(("userName", "privilegeId", "ordinality"))
=> op:result()
let $rows :=
for $privilege-row in $privilege-rows
let $privilege-id := map:get($privilege-row, "privilegeId")
let $ordinality := map:get($privilege-row, "ordinality")
let $privilege-name := xdmp:privilege-name(xs:unsignedLong($privilege-id))
let $uri := "/cleverllamas/llamaverse/scratch/optic-update/privileges/" || $user-name || "-" || fn:string($ordinality) || ".json"
return
map:entry("uri", $uri)
=> map:with("doc", object-node {
"user": $user-name,
"privilegeId": fn:string($privilege-id),
"ordinality": $ordinality,
"privilegeName": $privilege-name,
"source": "op-execute-privilege-snapshot"
})
=> map:with("collections", ("optic-execute-demo", "optic-privilege-snapshot"))
let $_ :=
op:from-param("bindingParam", (), op:doc-col-types())
=> op:write()
=> op:execute(
map:entry("bindingParam", $rows),
("trace=opticPrivilegeSnapshot", "optimize=1")
)
return
<result>
<user>{ $user-name }</user>
<written>{ count($rows) }</written>
<samples>{
for $row in subsequence($rows, 1, 4)
return
<sample>
<uri>{ map:get($row, "uri") }</uri>
<doc>{ xdmp:to-json-string(map:get($row, "doc")) }</doc>
</sample>
}</samples>
</result>
'use strict';
declareUpdate();
const op = require('/MarkLogic/optic');
const userName = 'cleverllamas-mother';
const privilegeCsv = xdmp.userPrivileges(userName).toArray().map(String).join(',');
const privilegeRows = op.fromLiterals([
{
userName,
privilegeCsv
}
])
.bind([
op.as('privilegeSeq', op.fn.tokenize(op.col('privilegeCsv'), ','))
])
.unnestInner('privilegeSeq', 'privilegeId', 'ordinality')
.select(['userName', 'privilegeId', 'ordinality'])
.result()
.toArray();
const rows = privilegeRows.map((row) => ({
uri: `/cleverllamas/llamaverse/scratch/optic-update/privileges/${userName}-${row.ordinality}.json`,
doc: {
user: userName,
privilegeId: String(row.privilegeId),
ordinality: row.ordinality,
privilegeName: xdmp.privilegeName(row.privilegeId),
source: 'op-execute-privilege-snapshot'
},
collections: ['optic-execute-demo', 'optic-privilege-snapshot']
}));
op.fromLiterals(rows).write().execute();
const results = {
user: userName,
written: rows.length,
samples: rows.slice(0, 4)
};
({
sample: 'optic/data-manipulation-starter-pack/assets/op-execute-from-param.sjs',
kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
count: Array.isArray(results) ? results.length : 0,
data: results
});
X-Path: /result
<result><user>cleverllamas-mother</user><written>4</written><samples><sample><uri>/cleverllamas/llamaverse/scratch/optic-update/privileges/cleverllamas-mother-1.json</uri><doc>{"user":"cleverllamas-mother", "privilegeId":"6226129278634019154", "ordinality":1, "privilegeName":"xdmp:eval", "source":"op-execute-privilege-snapshot"}</doc></sample><sample><uri>/cleverllamas/llamaverse/scratch/optic-update/privileges/cleverllamas-mother-2.json</uri><doc>{"user":"cleverllamas-mother", "privilegeId":"2867400659384236357", "ordinality":2, "privilegeName":"cleverllamas-llamaverse", "source":"op-execute-privilege-snapshot"}</doc></sample><sample><uri>/cleverllamas/llamaverse/scratch/optic-update/privileges/cleverllamas-mother-3.json</uri><doc>{"user":"cleverllamas-mother", "privilegeId":"9230786184163395570", "ordinality":3, "privilegeName":"xdmp:eval-in", "source":"op-execute-privilege-snapshot"}</doc></sample><sample><uri>/cleverllamas/llamaverse/scratch/optic-update/privileges/cleverllamas-mother-4.json</uri><doc>{"user":"cleverllamas-mother", "privilegeId":"6866748295311171023", "ordinality":4, "privilegeName":"unprotected-collections", "source":"op-execute-privilege-snapshot"}</doc></sample></samples></result>
| Ordinality | Privilege name | Written URI |
|---|---|---|
| 1 | xdmp:eval | /cleverllamas/llamaverse/scratch/optic-update/privileges/cleverllamas-mother-1.json |
| 2 | cleverllamas-llamaverse | /cleverllamas/llamaverse/scratch/optic-update/privileges/cleverllamas-mother-2.json |
| 3 | xdmp:eval-in | /cleverllamas/llamaverse/scratch/optic-update/privileges/cleverllamas-mother-3.json |
| 4 | unprotected-collections | /cleverllamas/llamaverse/scratch/optic-update/privileges/cleverllamas-mother-4.json |
The formatted tab shows the same write output as a compact table. The written documents contain user, privilegeId, ordinality, privilegeName, and source. The key thing to notice is the shape of the update plan: op:unnest-inner() makes the privilege sequence row-wise, and op:execute() persists those rows as documents.
What to notice: op:execute() is intentionally different from op:result(). This plan mutates content by writing a privilege snapshot set, and op:unnest-inner() gives each privilege row an ordinality so you can see the flattening order directly in persisted output.
Mutation Note
The examples above split into two categories:
op:xpath()plus expression libraries are read-oriented shaping tools.op:write()plusop:execute()are content-changing tools.
Keep those two modes separate in your mental model. Most Optic articles are about row logic; this section is about when row logic becomes an update program.
Building Pipelines?
Decision rule: keep read-shaping and mutation plans mentally separate; it prevents subtle side effects during maintenance.
Manipulation gets even more useful when you know where your data comes from and where it's headed:
- Optic Data Source Starter Pack — Master the source layer that feeds into these transformation patterns.
- Optic Data Analysis Starter Pack — Group and aggregate the transformed rows to answer business questions.
- Optic Joins Starter Pack — Combine multiple transformed sources for richer analysis.
Need Some Help?
Looking for more information on this subject or any other topic related to MarkLogic? Contact Us (info@cleverllamas.com) to find out how we can assist you with consulting or training!
- Language and Scope
- API Context for This Pack
- Expression Libraries in Optic Plans
- op:xpath()
- op:xpath() sequence expansion with op:unnest-inner()
- op:select()
- Privilege Snapshot Pipeline with op:execute()
- Mutation Note
- Building Pipelines?