Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions teaql-core/src/main/java/io/teaql/core/BaseEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,21 @@ 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);
this.id = 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);
Expand All @@ -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);
Expand Down
17 changes: 17 additions & 0 deletions teaql-core/src/main/java/io/teaql/core/ExecutableRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> executeForList(UserContext context) {
return context.executeForList(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,4 @@ default Expression<T, U> save(UserContext userContext) {
});
}

default Expression<T, U> updateId(Long id) {
return apply(
entity -> {
entity.__internalSet("id", id);
return entity;
});
}
}
40 changes: 40 additions & 0 deletions teaql-core/src/test/java/io/teaql/core/BaseRequestTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Subject, Subject> root = new ValueExpression<>(value);
Expression<Subject, String> 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<Subject, String> 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<Subject, String> missing = new LoadedPropertyExpression<>(
new ValueExpression<>(partial), "name", Subject::getName);
TeaQLNotLoadedException error = assertThrows(TeaQLNotLoadedException.class, missing::eval);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"; }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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);
Expand Down Expand Up @@ -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")));
Expand Down Expand Up @@ -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");

Expand Down
30 changes: 15 additions & 15 deletions teaql-runtime/src/test/java/io/teaql/runtime/TeaQLRuntimeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ public static class PageQueryExecutor implements QueryExecutor {
SmartList<DummyEntity> 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);
}
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -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");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CustomerOrder> rows = new SmartList<>(); rows.add(order);
var searchRequest = ((io.teaql.runtime.DefaultQueryRequest) request).getSearchRequest();
if (!searchRequest.getFacetRequests().isEmpty()) {
SmartList<OrderStatus> 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);
Expand Down
Loading