Skip to content

Database Core

lostcause edited this page Aug 24, 2026 · 1 revision

Database core

Beyond CRUD, PolyGraph has database-grade write-path guarantees: transactions, revisions, conditional writes, resource limits, secondary indexes, and adapter capability checks.

Transactions

await graph.transaction(async (tx) => {
  tx.addNode({ id: 'a', type: 't', data: {}, insertedAt: Date.now(), updatedAt: Date.now() })
  tx.patchNode('b', { increment: { views: 1 } })
}, { operationId: 'req-123' })

transaction(callback, options?) gives atomic read-your-own-writes: mutations inside the callback are visible to later reads in the same callback, but only committed as one unit if the callback resolves without throwing — a thrown error rolls the whole transaction back. Nested transactions are rejected. options may include operationId (a stable logical identity for retries/audit), actor, baseRevision, and metadata — retained in the durable mutation record when the adapter supports a change feed (see Persistence). Requires an adapter that declares both atomicBatches and transactions capabilities, or it throws AdapterCapabilityError.

Revisions & conditional writes

Every node carries a revision, starting at 0 and incrementing on each write. Pass expectedRevision on a write to make it conditional (optimistic concurrency):

try {
  graph.updateNode('doc_1', { title: 'v2' }, undefined, undefined, { expectedRevision: 3 })
} catch (err) {
  // ConflictError — someone else wrote doc_1 since revision 3
}

A mismatch throws ConflictError(id, expectedRevision, actualRevision).

Patches

patchNode(id, patch, options?) applies structured, atomic operations against a loaded node in one call:

graph.patchNode('doc_1', {
  set: { 'data.status': 'published' },
  unset: ['data.draftNote'],
  increment: { 'data.viewCount': 1 },
  compareAndSet: { 'data.status': { expected: 'draft', value: 'review' } },
})

set/unset/increment target data.* (or unprefixed) paths, plus the directly-patchable top-level paths updatedAt and the provenance fields (memoryClass, confidence, source, observedAt, derivedFrom, supersedes, contradicts — see Property graph). compareAndSet throws ConflictError if the path's current value doesn't deep-equal expected.

Resource limits

graph.setResourceLimits({ maxVectorDimensions?, maxNodePayloadBytes?, maxBatchSize? }) rejects writes that exceed configured bounds — maxVectorDimensions on node vectors, maxNodePayloadBytes on serialized node data, maxBatchSize on addNodes/transaction mutation counts. graph.getResourceLimits() reads the current configuration back. Violations throw ResourceLimitError.

Secondary indexes

graph.defineIndex({ name, nodeType?, fields, unique?, sparse? }) builds a compound (multi-field) or unique index over node data, optionally scoped to one node type. sparse skips nodes missing an indexed field rather than indexing them under undefined. registerNodeType's convenience indexes option calls this for you at registration time (see Property graph). Persisted index metadata round-trips through adapters that support it, including the Python bindings.

Explainable queries

Persisted and hot-query plans expose index selection, estimated cost, and operational metrics, so you can see why a query chose (or skipped) an index — useful when a query isn't hitting the index you expect.

Capability checks

graph.adapterCapabilities reads what the active adapter declares (atomicBatches, transactions, fsync, secondaryIndexes, snapshots, changeFeed, concurrentWriters, vectorSearch). graph.requireAdapterCapabilities({ name: expected, ... }) throws AdapterCapabilityError unless every listed capability matches. Use this to fail fast when a feature (transactions, a change feed, fsync durability) needs a capability the configured adapter doesn't provide. See Persistence for the adapters themselves.

Schema migrations

Application-level record migrations are a related but separate concern — see Schema migrations.


Back to Home.

Clone this wiki locally