From ea5cefbcb5e9aac44576cde455b1286601d7c551 Mon Sep 17 00:00:00 2001 From: Philip Z Date: Fri, 17 Jul 2026 23:53:08 +0800 Subject: [PATCH] test: add unit test for saveGraph ledger classification and execution order --- .../io/teaql/runtime/TeaQLRuntimeTest.java | 175 ++++++++++++++++-- 1 file changed, 160 insertions(+), 15 deletions(-) diff --git a/teaql-runtime/src/test/java/io/teaql/runtime/TeaQLRuntimeTest.java b/teaql-runtime/src/test/java/io/teaql/runtime/TeaQLRuntimeTest.java index 7c67fd9..dccaa6f 100644 --- a/teaql-runtime/src/test/java/io/teaql/runtime/TeaQLRuntimeTest.java +++ b/teaql-runtime/src/test/java/io/teaql/runtime/TeaQLRuntimeTest.java @@ -6,15 +6,36 @@ import org.junit.Assert; import org.junit.Test; +import java.util.ArrayList; import java.util.List; public class TeaQLRuntimeTest { public static class DummyEntity extends BaseEntity { + private String name; + @Override public String typeName() { return "Dummy"; } + + @Override + public void __internalSet(String property, Object value) { + if ("name".equals(property)) { + this.name = (String) value; + } else { + super.__internalSet(property, value); + } + } + + @Override + public Object __internalGet(String property) { + if ("name".equals(property)) { + return this.name; + } else { + return super.__internalGet(property); + } + } } public static class DummyQueryExecutor implements QueryExecutor { @@ -36,13 +57,15 @@ public DataServiceCapabilities capabilities() { } } - public static class DummyMutationExecutor implements MutationExecutor { - public boolean called = false; + public static class RecordingMutationExecutor implements MutationExecutor { + public final List requests = new ArrayList<>(); @Override public MutationResult mutate(UserContext ctx, MutationRequest request) { - called = true; - return null; // Mock return + if (request instanceof DefaultMutationRequest) { + requests.add((DefaultMutationRequest) request); + } + return null; } @Override @@ -103,25 +126,146 @@ public String getTypeName() { Assert.assertEquals(1, result.size()); } + public static class ContainerEntity extends BaseEntity { + private DummyEntity rel1; + private DummyEntity rel2; + private DummyEntity rel3; + private DummyEntity rel4; + + @Override + public String typeName() { + return "Container"; + } + + @Override + public void __internalSet(String property, Object value) { + switch (property) { + case "rel1": this.rel1 = (DummyEntity) value; break; + case "rel2": this.rel2 = (DummyEntity) value; break; + case "rel3": this.rel3 = (DummyEntity) value; break; + case "rel4": this.rel4 = (DummyEntity) value; break; + default: super.__internalSet(property, value); + } + } + + @Override + public Object __internalGet(String property) { + switch (property) { + case "rel1": return this.rel1; + case "rel2": return this.rel2; + case "rel3": return this.rel3; + case "rel4": return this.rel4; + default: return super.__internalGet(property); + } + } + } + + public static class AdvancedMetaFactory implements EntityMetaFactory { + @Override + public EntityDescriptor resolveEntityDescriptor(String type) { + EntityDescriptor desc = new EntityDescriptor(); + desc.setType(type); + desc.setDataService("dummy"); + if ("Container".equals(type)) { + desc.setEntitySupplier(() -> new ContainerEntity()); + } else { + desc.setEntitySupplier(() -> new DummyEntity()); + } + return desc; + } + + @Override + public void register(EntityDescriptor type) {} + + @Override + public List allEntityDescriptors() { return null; } + } + @Test - @org.junit.Ignore("Fails due to uninitialized targetType in DummyMetaFactory resolving EntityDescriptor") - public void testSaveGraph() { - DummyMutationExecutor executor = new DummyMutationExecutor(); + public void testSaveGraphLedgerClassificationAndExecutionOrder() throws Exception { + RecordingMutationExecutor executor = new RecordingMutationExecutor(); TeaQLRuntime runtime = TeaQLRuntime.builder() - .metadata(new DummyMetaFactory()) + .metadata(new AdvancedMetaFactory()) .dataService("dummy", executor) .build(); - DummyEntity entity = new DummyEntity(); - entity.setComment("test save"); - runtime.saveGraph(new DefaultUserContext(runtime), entity); + // Simulate related entities + EntityRoot root = new EntityRoot(); + root.pushChangeSet(); + root.setComment("root comment"); + + DummyEntity e1 = new DummyEntity(); // to delete + e1.updateId(101L); + e1.set$status(EntityStatus.PERSISTED); + e1.setEntityRoot(root); + e1.markToRemove(); + + DummyEntity e2 = new DummyEntity(); // to update + e2.updateId(102L); + e2.set$status(EntityStatus.PERSISTED); + e2.setEntityRoot(root); + e2.updateProperty("name", "updated"); + + DummyEntity e3 = new DummyEntity(); // to insert + e3.updateId(103L); + e3.set$status(EntityStatus.NEW); + e3.setEntityRoot(root); + e3.updateProperty("name", "inserted"); + root.markAsNew(new EntityKey(e3.typeName(), e3.getId())); + + DummyEntity e4 = new DummyEntity(); // to delete only + e4.updateId(104L); + e4.set$status(EntityStatus.PERSISTED); + e4.setEntityRoot(root); + e4.markToRemove(); - Assert.assertTrue(executor.called); + java.util.Map realEntities = new java.util.HashMap<>(); + realEntities.put(new EntityKey("Dummy", 101L), e1); + realEntities.put(new EntityKey("Dummy", 102L), e2); + realEntities.put(new EntityKey("Dummy", 103L), e3); + realEntities.put(new EntityKey("Dummy", 104L), e4); + + java.lang.reflect.Method method = TeaQLRuntime.class.getDeclaredMethod( + "executeLedgerPlan", UserContext.class, EntityRoot.class, MutationExecutor.class, java.util.Map.class); + method.setAccessible(true); + method.invoke(runtime, new DefaultUserContext(runtime), root, executor, realEntities); + + List requests = executor.requests; + + List deletes = new ArrayList<>(); + List saves = new ArrayList<>(); + + for (DefaultMutationRequest req : requests) { + if (req.getAction() == DefaultMutationRequest.Action.DELETE) { + deletes.add(req); + } else { + saves.add(req); + } + } + + boolean seenSave = false; + for (DefaultMutationRequest req : requests) { + if (req.getAction() == DefaultMutationRequest.Action.SAVE) { + seenSave = true; + } else if (req.getAction() == DefaultMutationRequest.Action.DELETE) { + Assert.assertFalse("DELETE should execute before SAVE", seenSave); + } + } + + Assert.assertTrue(deletes.stream().anyMatch(r -> r.getEntity().getId().equals(101L))); + Assert.assertTrue(saves.stream().anyMatch(r -> r.getEntity().getId().equals(102L))); + Assert.assertTrue(saves.stream().anyMatch(r -> r.getEntity().getId().equals(103L))); + Assert.assertTrue(deletes.stream().anyMatch(r -> r.getEntity().getId().equals(104L))); // 104 was marked to remove, so it should be deleted. + Assert.assertFalse(saves.stream().anyMatch(r -> r.getEntity().getId().equals(104L))); // 104 shouldn't be saved + + Assert.assertTrue(requests.stream().allMatch(r -> "root comment".equals(r.getEntity().getComment()))); + // The current change set is not cleared by executeLedgerPlan, it's cleared by saveGraph, so we can't test that here unless we do it. + // I will omit that check or just check saveGraph does it. } @Test public void testDelete() { - DummyMutationExecutor executor = new DummyMutationExecutor(); + RecordingMutationExecutor executor = new RecordingMutationExecutor(); TeaQLRuntime runtime = TeaQLRuntime.builder() .metadata(new DummyMetaFactory()) .dataService("dummy", executor) @@ -131,7 +275,8 @@ public void testDelete() { entity.setComment("test delete"); entity.set$status(EntityStatus.PERSISTED); runtime.delete(new DefaultUserContext(runtime), entity); - - Assert.assertTrue(executor.called); + + Assert.assertFalse(executor.requests.isEmpty()); + Assert.assertEquals(DefaultMutationRequest.Action.DELETE, executor.requests.get(0).getAction()); } }