Delta Operations¶
The Knot/Edge and Tree operations are useful for granular independent operations. However, a more practical way to make changes, would be through a series of related operations that need to be applied together. Imagine this happening through a UI where all changes are first drafted and applied in one go. Delta operations allow you to make batch changes to the Bonsai tree structure in a single operation. This is useful when you need to make multiple related changes and want to ensure consistency. This guide explains how to use delta operations effectively.
Note
Delta operations are not Atomic. The operations get applied one by one across the three storage interfaces. As you might have realized, atomicity depends on type of databases used. If an error occurs during the application of delta operations, the tree may be left in an inconsistent state. It is up to the application to handle errors and revert changes if needed.
Understanding Delta Operations¶
Delta operations are a way to apply a series of changes to a tree structure as a single unit of work. They provide several benefits:
- Drafting Changes: Allows you to draft a series of changes before applying them
- Auditability: Changes can be tracked and potentially reverted
- Efficiency: Multiple changes can be applied in a single operation
Types of Delta Operations¶
Bonsai supports three types of delta operations:
- KEY_MAPPING_DELTA: Operations related to key mappings
- KNOT_DELTA: Operations related to Knots
- EDGE_DELTA: Operations related to Edges
Each operation type has its own specific class and builder pattern.
Creating Delta Operations¶
You can create delta operations using the specific operation builders or constructors:
// Create a knot delta operation using builder
Knot knot = Knot.builder()
.id("knotId")
.knotData(ValuedKnotData.stringValue("Sample value"))
.build();
KnotDeltaOperation knotOperation = KnotDeltaOperation.builder()
.knot(knot)
.build();
// Or using constructor
KnotDeltaOperation knotOp = new KnotDeltaOperation(knot);
// Create a key mapping delta operation using builder
KeyMappingDeltaOperation mappingOperation = KeyMappingDeltaOperation.builder()
.keyId("newKey")
.knotId("generatedKnotId")
.build();
// Or using constructor
KeyMappingDeltaOperation mappingOp = new KeyMappingDeltaOperation("newKey", "generatedKnotId");
// Create an edge delta operation using builder
Edge edge = Edge.builder()
.edgeIdentifier(new EdgeIdentifier("edgeId", 1, 1))
.knotId("targetKnotId")
.filter(EqualsFilter.builder().field("userId").value("U1").build())
.build();
EdgeDeltaOperation edgeOperation = EdgeDeltaOperation.builder()
.edge(edge)
.build();
// Or using constructor
EdgeDeltaOperation edgeOp = new EdgeDeltaOperation(edge);
Applying Delta Operations¶
To apply a list of delta operations, use the applyDeltaOperations
method:
// Create a list of delta operations
List<DeltaOperation> operations = new ArrayList<>();
operations.add(createKnotOp);
operations.add(createMappingOp);
// Apply the delta operations
TreeKnotState result = bonsai.applyDeltaOperations("rootKey", operations);
// The result contains the updated tree and revert operations
TreeKnot updatedTree = result.getTreeKnot();
List<DeltaOperation> revertOperations = result.getRevertDeltaOperations();
The applyDeltaOperations
method returns a TreeKnotState
object that contains:
- The updated tree structure (
TreeKnot
) - A list of operations that can be used to revert the changes (
getDeltaOperationsToPreviousState()
)
Preview vs Apply Operations¶
Bonsai provides two methods for working with delta operations:
getCompleteTreeWithDeltaOperations()
: Preview changes without persisting them to storageapplyDeltaOperations()
: Apply changes and persist them to storage
// Preview changes without persisting
TreeKnotState preview = bonsai.getCompleteTreeWithDeltaOperations("rootKey", operations);
// Apply and persist changes
TreeKnotState result = bonsai.applyDeltaOperations("rootKey", operations);
Example: Creating a Complete Tree with Delta Operations¶
Here's an example of using delta operations to create a complete tree structure:
// Create a list of delta operations
List<DeltaOperation> operations = new ArrayList<>();
// Create the leaf knots
Knot eligibleKnot = Knot.builder()
.id("eligibleKnot")
.knotData(ValuedKnotData.builder().booleanValue(true).build())
.properties(Map.of("description", "User is eligible"))
.build();
operations.add(KnotDeltaOperation.builder()
.knot(eligibleKnot)
.build());
Knot ineligibleKnot = Knot.builder()
.id("ineligibleKnot")
.knotData(ValuedKnotData.builder().booleanValue(false).build())
.properties(Map.of("description", "User is ineligible"))
.build();
operations.add(KnotDeltaOperation.builder()
.knot(ineligibleKnot)
.build());
// Create the root knot
Knot rootKnot = Knot.builder()
.id("rootKnot")
.knotData(ValuedKnotData.builder().build())
.properties(Map.of("description", "User eligibility decision point"))
.build();
operations.add(KnotDeltaOperation.builder()
.knot(rootKnot)
.build());
// Create the edges
Edge edge1 = Edge.builder()
.id("edge1")
.sourceKnotId("rootKnot")
.targetKnotId("eligibleKnot")
.filters(List.of(
Filter.builder()
.path("$.user.age")
.operator(Operator.GREATER_THAN_EQUAL)
.value(18)
.build(),
Filter.builder()
.path("$.user.country")
.operator(Operator.IN)
.value(List.of("US", "CA", "UK"))
.build()
))
.build();
operations.add(EdgeDeltaOperation.builder()
.edge(edge1)
.build());
Edge edge2 = Edge.builder()
.id("edge2")
.sourceKnotId("rootKnot")
.targetKnotId("ineligibleKnot")
.filters(List.of())
.build();
operations.add(EdgeDeltaOperation.builder()
.edge(edge2)
.build());
// Create the key mapping
operations.add(KeyMappingDeltaOperation.builder()
.keyId("userEligibility")
.knotId("rootKnot")
.build());
// Apply the delta operations
TreeKnotState result = bonsai.applyDeltaOperations(null, operations);
Example: Updating an Existing Tree with Delta Operations¶
Here's an example of using delta operations to update an existing tree structure:
// Create a list of delta operations
List<DeltaOperation> operations = new ArrayList<>();
// Update a knot's data
Knot updatedKnot = Knot.builder()
.id("eligibleKnot")
.knotData(ValuedKnotData.builder().booleanValue(true).build())
.properties(Map.of("description", "User is eligible", "version", 1L))
.build();
operations.add(KnotDeltaOperation.builder()
.knot(updatedKnot)
.build());
// Update an edge's filters
Edge updatedEdge = Edge.builder()
.id("edge1")
.sourceKnotId("rootKnot")
.targetKnotId("eligibleKnot")
.filters(List.of(
Filter.builder()
.path("$.user.age")
.operator(Operator.GREATER_THAN_EQUAL)
.value(21)
.build(),
Filter.builder()
.path("$.user.country")
.operator(Operator.IN)
.value(List.of("US", "CA"))
.build()
))
.properties(Map.of("version", 1L))
.build();
operations.add(EdgeDeltaOperation.builder()
.edge(updatedEdge)
.build());
// Apply the delta operations
TreeKnotState result = bonsai.applyDeltaOperations("userEligibility", operations);
Reverting Delta Operations¶
The applyDeltaOperations
method returns a list of operations that can be used to revert the changes:
// Apply delta operations
TreeKnotState result = bonsai.applyDeltaOperations("userEligibility", operations);
// Get the revert operations
List<DeltaOperation> revertOperations = result.getDeltaOperationsToPreviousState();
// If needed, apply the revert operations to undo the changes
TreeKnotState revertResult = bonsai.applyDeltaOperations("userEligibility", revertOperations);
This allows you to implement undo functionality or rollback changes if needed.
Error Handling¶
Delta operations can throw various exceptions:
try {
TreeKnotState result = bonsai.applyDeltaOperations("userEligibility", operations);
} catch (BonsaiError e) {
if (e.getErrorCode() == BonsaiErrorCode.VERSION_MISMATCH) {
// Handle version mismatch error
System.err.println("Version mismatch: " + e.getMessage());
} else if (e.getErrorCode() == BonsaiErrorCode.CYCLE_DETECTED) {
// Handle cycle detected error
System.err.println("Cycle detected: " + e.getMessage());
} else {
// Handle other errors
throw e;
}
}
Best Practices¶
- Group related changes: Include all related changes in a single delta operation
- Consider versioning: Include version information in update operations to prevent conflicts
- Handle errors appropriately: Be prepared to handle errors and potentially retry operations
- Use meaningful IDs: Use meaningful IDs for created Knots and Edges
- Test thoroughly: Test delta operations thoroughly to ensure they work as expected
- Consider transaction boundaries: Think about what constitutes a logical transaction in your application
- Use revert operations: Store revert operations for important changes to enable undo functionality