diff --git a/teaql-core/src/main/java/io/teaql/core/BaseEntity.java b/teaql-core/src/main/java/io/teaql/core/BaseEntity.java index d2fa2f4f..1a8fa4dc 100644 --- a/teaql-core/src/main/java/io/teaql/core/BaseEntity.java +++ b/teaql-core/src/main/java/io/teaql/core/BaseEntity.java @@ -104,7 +104,7 @@ public Long getId() { return id; } - public BaseEntity updateId(Long id) { + BaseEntity updateId(Long id) { markPropertyLoaded(ID_PROPERTY); if (ObjectUtil.equals(this.id, id)) return this; handleUpdate(ID_PROPERTY, getId(), id); @@ -112,13 +112,13 @@ public BaseEntity updateId(Long id) { return this; } - @FrameworkInternal("Generated schema bootstrap fixed-ID creation only") + @FrameworkInternal("New entity ID initialization for Q creation and generated bootstrap") public void __internalInitializeNewEntityId(Long fixedId) { if (fixedId == null || fixedId <= 0) { - throw new IllegalArgumentException("Generated bootstrap ID must be positive"); + throw new IllegalArgumentException("Creation ID must be positive"); } if (id != null || !newItem()) { - throw new IllegalStateException("Fixed bootstrap ID can only initialize a new entity"); + throw new IllegalStateException("Creation ID can only initialize a new entity"); } id = fixedId; markPropertyLoaded(ID_PROPERTY); @@ -130,7 +130,7 @@ public Long getVersion() { return version; } - public BaseEntity updateVersion(Long version) { + BaseEntity updateVersion(Long version) { markPropertyLoaded(VERSION_PROPERTY); if (ObjectUtil.equals(this.version, version)) return this; handleUpdate(VERSION_PROPERTY, getVersion(), version); diff --git a/teaql-core/src/main/java/io/teaql/core/ExecutableRequest.java b/teaql-core/src/main/java/io/teaql/core/ExecutableRequest.java index cdcce725..263950d5 100644 --- a/teaql-core/src/main/java/io/teaql/core/ExecutableRequest.java +++ b/teaql-core/src/main/java/io/teaql/core/ExecutableRequest.java @@ -34,6 +34,23 @@ public T newEntity(UserContext context) { return context.initializeEntity(request.getTypeName(), request.internalNewEntity()); } + /** Creates a new aggregate with an explicit ID; never changes an existing entity. */ + public T newEntityWithId(Long id, UserContext context) { + if (context == null) throw new IllegalArgumentException("UserContext is required for entity creation"); + if (id == null || id <= 0) throw new IllegalArgumentException("Creation ID must be positive"); + T entity = request.internalNewEntity(); + if (!(entity instanceof BaseEntity base)) { + throw new IllegalArgumentException("Explicit-ID creation requires a generated BaseEntity"); + } + InternalIdGenerationService ids = context.capability(InternalIdGenerationService.class); + if (ids == null) { + throw new IllegalStateException("Explicit-ID creation requires an InternalIdGenerationService"); + } + ids.ensureFloor(base.typeName(), id); + base.__internalInitializeNewEntityId(id); + return context.initializeEntity(request.getTypeName(), entity); + } + public SmartList executeForList(UserContext context) { return context.executeForList(this); } diff --git a/teaql-core/src/main/java/io/teaql/core/InternalIdGenerationService.java b/teaql-core/src/main/java/io/teaql/core/InternalIdGenerationService.java index fe2468d2..a18332fc 100644 --- a/teaql-core/src/main/java/io/teaql/core/InternalIdGenerationService.java +++ b/teaql-core/src/main/java/io/teaql/core/InternalIdGenerationService.java @@ -47,7 +47,8 @@ default long nextId(String typeName) { /** * Advances the allocation state for {@code typeName} so future generated IDs are greater - * than {@code floor}. Generated schema bootstrap uses this after reserving model-defined IDs. + * than {@code floor}. Generated schema bootstrap and explicit-ID entity creation use this + * to reserve identities before subsequent automatic allocation. */ default void ensureFloor(String typeName, long floor) { throw new UnsupportedOperationException( diff --git a/teaql-core/src/main/java/io/teaql/core/value/BaseEntityExpression.java b/teaql-core/src/main/java/io/teaql/core/value/BaseEntityExpression.java index cbec4bd6..ec669990 100644 --- a/teaql-core/src/main/java/io/teaql/core/value/BaseEntityExpression.java +++ b/teaql-core/src/main/java/io/teaql/core/value/BaseEntityExpression.java @@ -24,11 +24,4 @@ default Expression save(UserContext userContext) { }); } - default Expression updateId(Long id) { - return apply( - entity -> { - entity.__internalSet("id", id); - return entity; - }); - } } diff --git a/teaql-core/src/test/java/io/teaql/core/BaseRequestTest.java b/teaql-core/src/test/java/io/teaql/core/BaseRequestTest.java index 429a79d3..b37dfced 100644 --- a/teaql-core/src/test/java/io/teaql/core/BaseRequestTest.java +++ b/teaql-core/src/test/java/io/teaql/core/BaseRequestTest.java @@ -325,6 +325,46 @@ public void testExecutableRequestCreatesEntityThroughTrustedContext() { assertEquals(EntityStatus.NEW, entity.get$status()); } + @Test + public void testExplicitIdCreationInitializesBeforeTrustedContextAndDoesNotExposeSystemUpdaters() throws Exception { + java.util.concurrent.atomic.AtomicInteger initialized = new java.util.concurrent.atomic.AtomicInteger(); + java.util.concurrent.atomic.AtomicLong floor = new java.util.concurrent.atomic.AtomicLong(); + InternalIdGenerationService ids = new InternalIdGenerationService() { + public Long generateId(UserContext current, Entity entity) { return floor.incrementAndGet(); } + public void ensureFloor(String type, long value) { floor.accumulateAndGet(value, Math::max); } + }; + UserContext context = (UserContext) Proxy.newProxyInstance( + UserContext.class.getClassLoader(), new Class[] { UserContext.class }, + (proxy, method, args) -> { + if (method.getName().equals("capability") && args[0] == InternalIdGenerationService.class) return ids; + if (method.getName().equals("initializeEntity")) { + BaseEntity value = (BaseEntity) args[1]; + assertEquals(Long.valueOf(1234L), value.getId()); + assertEquals(EntityStatus.NEW, value.get$status()); + initialized.incrementAndGet(); + return value; + } + throw new UnsupportedOperationException(method.getName()); + }); + var executable = new BaseRequest.TempRequest(BaseEntity.class, "BaseEntity", BaseEntity::new) + .internalComment("Create imported entity").purpose("Preserve external identity"); + BaseEntity entity = (BaseEntity) executable.newEntityWithId(1234L, context); + assertEquals(Long.valueOf(1234L), entity.getId()); + assertEquals(1, initialized.get()); + assertEquals(1234L, floor.get()); + assertEquals(Long.valueOf(1235L), ids.generateId(context, entity)); + org.junit.Assert.assertThrows(IllegalArgumentException.class, () -> executable.newEntityWithId(0L, context)); + org.junit.Assert.assertThrows(IllegalArgumentException.class, () -> executable.newEntityWithId(null, context)); + assertEquals(1, initialized.get()); + UserContext missingIds = (UserContext) Proxy.newProxyInstance( + UserContext.class.getClassLoader(), new Class[] { UserContext.class }, + (proxy, method, args) -> null); + org.junit.Assert.assertThrows(IllegalStateException.class, () -> executable.newEntityWithId(7L, missingIds)); + org.junit.Assert.assertThrows(NoSuchMethodException.class, () -> BaseEntity.class.getMethod("updateId", Long.class)); + org.junit.Assert.assertThrows(NoSuchMethodException.class, () -> BaseEntity.class.getMethod("updateVersion", Long.class)); + org.junit.Assert.assertThrows(NoSuchMethodException.class, () -> io.teaql.core.value.BaseEntityExpression.class.getMethod("updateId", Long.class)); + } + @Test public void testEnhanceChildren() { BaseRequest.TempRequest subReq = new BaseRequest.TempRequest(BaseEntity.class, "ChildEntity"); diff --git a/teaql-core/src/test/java/io/teaql/core/checker/CheckerTest.java b/teaql-core/src/test/java/io/teaql/core/checker/CheckerTest.java index 6487c596..cb99d13a 100644 --- a/teaql-core/src/test/java/io/teaql/core/checker/CheckerTest.java +++ b/teaql-core/src/test/java/io/teaql/core/checker/CheckerTest.java @@ -71,7 +71,7 @@ public void testMarkAsCheckedAndNeedCheck() { // Check another entity when list is not null but does not contain DummyEntity entity2 = new DummyEntity(); - entity2.updateId(2L); + entity2.__internalSet("id", 2L); assertTrue(checker.needCheck(context, entity2)); // Check null diff --git a/teaql-core/src/test/java/io/teaql/core/value/LoadedPropertyExpressionTest.java b/teaql-core/src/test/java/io/teaql/core/value/LoadedPropertyExpressionTest.java index 9e9c78f1..8e3c7717 100644 --- a/teaql-core/src/test/java/io/teaql/core/value/LoadedPropertyExpressionTest.java +++ b/teaql-core/src/test/java/io/teaql/core/value/LoadedPropertyExpressionTest.java @@ -15,19 +15,19 @@ static class Subject extends BaseEntity { @Test public void distinguishesValueLoadedNullAndNotLoaded() { - Subject value = new Subject(); value.updateId(1L); value.loadName("TeaQL"); + Subject value = new Subject(); value.__internalSet("id", 1L); value.loadName("TeaQL"); Expression root = new ValueExpression<>(value); Expression expression = new LoadedPropertyExpression<>(root, "name", Subject::getName); assertEquals("TeaQL", expression.eval()); assertEquals("TeaQL", expression.orIfNull("fallback")); - Subject loadedNull = new Subject(); loadedNull.updateId(2L); loadedNull.loadName(null); + Subject loadedNull = new Subject(); loadedNull.__internalSet("id", 2L); loadedNull.loadName(null); Expression nullExpression = new LoadedPropertyExpression<>( new ValueExpression<>(loadedNull), "name", Subject::getName); assertNull(nullExpression.eval()); assertEquals("fallback", nullExpression.orIfNull("fallback")); - Subject partial = new Subject(); partial.updateId(3L); + Subject partial = new Subject(); partial.__internalSet("id", 3L); Expression missing = new LoadedPropertyExpression<>( new ValueExpression<>(partial), "name", Subject::getName); TeaQLNotLoadedException error = assertThrows(TeaQLNotLoadedException.class, missing::eval); diff --git a/teaql-core/src/test/java/io/teaql/core/value/SmartListExpressionTest.java b/teaql-core/src/test/java/io/teaql/core/value/SmartListExpressionTest.java index 6843f483..571ce979 100644 --- a/teaql-core/src/test/java/io/teaql/core/value/SmartListExpressionTest.java +++ b/teaql-core/src/test/java/io/teaql/core/value/SmartListExpressionTest.java @@ -9,7 +9,7 @@ public class SmartListExpressionTest { static class DummyEntity extends BaseEntity { - public DummyEntity(Long id) { updateId(id); } + public DummyEntity(Long id) { __internalSet("id", id); } @Override public String typeName() { return "dummy"; } } diff --git a/teaql-jackson/src/test/java/io/teaql/jackson/BaseEntitySerializationTest.java b/teaql-jackson/src/test/java/io/teaql/jackson/BaseEntitySerializationTest.java index c9fccc24..9bac8a76 100644 --- a/teaql-jackson/src/test/java/io/teaql/jackson/BaseEntitySerializationTest.java +++ b/teaql-jackson/src/test/java/io/teaql/jackson/BaseEntitySerializationTest.java @@ -117,8 +117,8 @@ public static class CyclicEntity extends BaseEntity { @Test public void cyclicIndexedGraphsUseIdentityReferencesAndDoNotRetainAPathAcrossWrites() throws Exception { - var first = new CyclicEntity(); first.updateId(1L); first.updateVersion(1L); - var second = new CyclicEntity(); second.updateId(2L); second.updateVersion(1L); + var first = new CyclicEntity(); first.__internalSet("id", 1L); first.__internalSet("version", 1L); + var second = new CyclicEntity(); second.__internalSet("id", 2L); second.__internalSet("version", 1L); first.__internalSet("peer", second); second.__internalSet("peer", first); var mapper = new ObjectMapper().registerModule(TeaQLModule.INSTANCE); var json = mapper.readTree(mapper.writeValueAsString(first)); @@ -130,8 +130,8 @@ public void cyclicIndexedGraphsUseIdentityReferencesAndDoNotRetainAPathAcrossWri @Test public void sharedReferencesOutsideTheActivePathAreNotReducedToIdentityOnly() throws Exception { - var first = new CyclicEntity(); first.updateId(1L); first.updateVersion(1L); - var second = new CyclicEntity(); second.updateId(2L); second.updateVersion(1L); + var first = new CyclicEntity(); first.__internalSet("id", 1L); first.__internalSet("version", 1L); + var second = new CyclicEntity(); second.__internalSet("id", 2L); second.__internalSet("version", 1L); first.__internalSet("peer", second); second.putAdditional("_label", "shared child"); first.putAdditional("_again", second); @@ -209,8 +209,8 @@ public void setName(String name) { @Test public void serializesDynamicPropertiesWithTeaQLModule() throws Exception { BaseEntity entity = new BaseEntity(); - entity.updateId(1001L); - entity.updateVersion(7L); + entity.__internalSet("id", 1001L); + entity.__internalSet("version", 7L); entity.setComment("internal comment"); entity.setTraceChain(java.util.List.of(new io.teaql.core.TraceNode( io.teaql.core.TraceKind.AUDIT_REASON, entity.typeName(), 1001L, "internal trace"))); @@ -248,7 +248,7 @@ public void deserializesBaseEntityWithoutBeanMutation() throws Exception { @Test public void serializesBaseEntitySubclassesWithTeaQLSerializer() throws Exception { NamedEntity entity = new NamedEntity(); - entity.updateId(1001L); + entity.__internalSet("id", 1001L); entity.setName("should-not-use-bean-getter"); entity.putAdditional("#customer_asset_no", "A-10086"); 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 ba44096a..095507e6 100644 --- a/teaql-runtime/src/test/java/io/teaql/runtime/TeaQLRuntimeTest.java +++ b/teaql-runtime/src/test/java/io/teaql/runtime/TeaQLRuntimeTest.java @@ -183,7 +183,7 @@ public static class PageQueryExecutor implements QueryExecutor { SmartList rows = new SmartList<>(); for (int id = request.getSlice().getOffset() + 1; id <= request.getSlice().getOffset() + request.getSlice().getSize(); id++) { - DummyEntity entity = new DummyEntity(); entity.updateId((long) id); rows.add(entity); + DummyEntity entity = new DummyEntity(); entity.__internalSet("id", (long) id); rows.add(entity); } return new DefaultQueryResult(rows); } @@ -224,7 +224,7 @@ private static MutationPolicy allowingPolicy( private static DummyEntity changedEntity(long id, String comment) { DummyEntity entity = new DummyEntity(); - entity.updateId(id); + entity.__internalSet("id", id); entity.__internalSet("version", 1L); entity.set$status(EntityStatus.PERSISTED); entity.updateProperty("name", "private-value"); @@ -354,7 +354,7 @@ public void graphPolicyReviewsAllOperationsExactlyOnce() { .mutationPolicyRegistry(key -> java.util.Optional.of(policy)) .build(); ContainerEntity root = new ContainerEntity(); - root.updateId(910L); + root.__internalSet("id", 910L); root.set$status(EntityStatus.PERSISTED); DummyEntity child = changedEntity(911L, "child change"); root.updateProperty("rel1", child); @@ -903,26 +903,26 @@ public void testSaveGraphLedgerClassificationAndExecutionOrder() throws Exceptio root.setComment("root comment"); DummyEntity e1 = new DummyEntity(); // to delete - e1.updateId(101L); + e1.__internalSet("id", 101L); e1.set$status(EntityStatus.PERSISTED); e1.setEntityMutationLedger(root); e1.markForDeletion(); DummyEntity e2 = new DummyEntity(); // to update - e2.updateId(102L); + e2.__internalSet("id", 102L); e2.set$status(EntityStatus.PERSISTED); e2.setEntityMutationLedger(root); e2.updateProperty("name", "updated"); DummyEntity e3 = new DummyEntity(); // to insert - e3.updateId(103L); + e3.__internalSet("id", 103L); e3.set$status(EntityStatus.NEW); e3.setEntityMutationLedger(root); e3.updateProperty("name", "inserted"); root.markAsNew(new EntityKey(e3.typeName(), e3.getId())); DummyEntity e4 = new DummyEntity(); // to delete only - e4.updateId(104L); + e4.__internalSet("id", 104L); e4.set$status(EntityStatus.PERSISTED); e4.setEntityMutationLedger(root); e4.markForDeletion(); @@ -984,19 +984,19 @@ public void testSaveGraphMergesRelatedEntityMutationLedgers() { .build(); ContainerEntity rootEntity = new ContainerEntity(); - rootEntity.updateId(1L); + rootEntity.__internalSet("id", 1L); rootEntity.set$status(EntityStatus.PERSISTED); DummyEntity toOneChild = new DummyEntity(); - toOneChild.updateId(2L); + toOneChild.__internalSet("id", 2L); toOneChild.set$status(EntityStatus.PERSISTED); DummyEntity listChild1 = new DummyEntity(); - listChild1.updateId(3L); + listChild1.__internalSet("id", 3L); listChild1.set$status(EntityStatus.PERSISTED); DummyEntity listChild2 = new DummyEntity(); - listChild2.updateId(4L); + listChild2.__internalSet("id", 4L); listChild2.set$status(EntityStatus.PERSISTED); rootEntity.updateProperty("rel1", toOneChild); @@ -1030,13 +1030,13 @@ public void testSaveGraphDoesNotConsumeIndependentLedgerFromSameContext() { DefaultUserContext context = new DefaultUserContext(runtime); DummyEntity first = new DummyEntity(); - first.updateId(11L); + first.__internalSet("id", 11L); first.set$status(EntityStatus.PERSISTED); first.updateProperty("name", "save me"); first.setComment("save first graph only"); DummyEntity independent = new DummyEntity(); - independent.updateId(22L); + independent.__internalSet("id", 22L); independent.set$status(EntityStatus.PERSISTED); independent.updateProperty("name", "keep pending"); @@ -1069,13 +1069,13 @@ public MutationResult mutate(UserContext context, PersistenceMutation request) { DefaultUserContext context = new DefaultUserContext(runtime); DummyEntity failing = new DummyEntity(); - failing.updateId(31L); + failing.__internalSet("id", 31L); failing.set$status(EntityStatus.PERSISTED); failing.updateProperty("name", "must remain pending"); failing.setComment("fail this graph"); DummyEntity independent = new DummyEntity(); - independent.updateId(32L); + independent.__internalSet("id", 32L); independent.set$status(EntityStatus.PERSISTED); independent.updateProperty("name", "independent pending"); diff --git a/teaql-tfp-endpoint/src/test/java/io/teaql/tfp/TfpConformanceServer.java b/teaql-tfp-endpoint/src/test/java/io/teaql/tfp/TfpConformanceServer.java index ac326285..8a26d239 100644 --- a/teaql-tfp-endpoint/src/test/java/io/teaql/tfp/TfpConformanceServer.java +++ b/teaql-tfp-endpoint/src/test/java/io/teaql/tfp/TfpConformanceServer.java @@ -124,17 +124,17 @@ private static QueryExecutor queryExecutor() { return new QueryExecutor() { public QueryResult query(io.teaql.core.UserContext context, QueryRequest request) { CustomerOrder order = new CustomerOrder(); - order.updateId(7L); order.updateVersion(1L); order.setTenantId(1L); + order.__internalSet("id", 7L); order.__internalSet("version", 1L); order.setTenantId(1L); order.setStatus("NEW"); order.setOrderNumber("ORD-007"); order.setReviewed(Boolean.TRUE); SmartList rows = new SmartList<>(); rows.add(order); var searchRequest = ((io.teaql.runtime.DefaultQueryRequest) request).getSearchRequest(); if (!searchRequest.getFacetRequests().isEmpty()) { SmartList statuses = new SmartList<>(); - OrderStatus newest = new OrderStatus(); newest.updateId(1001L); + OrderStatus newest = new OrderStatus(); newest.__internalSet("id", 1001L); newest.setCode("NEW"); newest.setLabel("New"); newest.addDynamicProperty("orderCount", 1); statuses.add(newest); - OrderStatus paid = new OrderStatus(); paid.updateId(1002L); + OrderStatus paid = new OrderStatus(); paid.__internalSet("id", 1002L); paid.setCode("PAID"); paid.setLabel("Paid"); paid.addDynamicProperty("orderCount", 0); statuses.add(paid); rows.addFacet(searchRequest.getFacetRequests().get(0).getFacetName(), statuses);