Skip to content
Merged
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
12 changes: 12 additions & 0 deletions teaql-core/src/main/java/io/teaql/core/UserContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,18 @@ default ResolvedRoundTripReference resolveReference(
this, trustedReferencePrincipal(), scope, reference, expectedEntityType);
}

/**
* Returns the domain-visible time for this operation. Operational clocks
* used for timeouts, durations and logging are intentionally separate.
*/
default java.time.LocalDateTime businessTime() {
BusinessClock clock = capability(BusinessClock.class);
if (clock == null) {
throw new TeaQLRuntimeException("BusinessClock capability is not registered");
}
return clock.businessDateTime(this);
}

/**
* Returns the Dynamic Fields facade for reading/writing dynamic field values.
* The facade is resolved via {@link #capability(Class)} and must be registered
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@

import io.teaql.core.UserContext;
import java.time.LocalDate;
import java.time.LocalDateTime;

/** Supplies domain-visible time through the current operation context. */
public interface BusinessClock {
LocalDate businessDate(UserContext context);

/**
* Returns the complete business date-time. Existing date-only clock
* implementations remain deterministic and resolve to start of day.
*/
default LocalDateTime businessDateTime(UserContext context) {
return businessDate(context).atStartOfDay();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public <T> T capability(Class<T> capabilityType) {
value = runtime.getIdGenerationService();
}
if (value == null && capabilityType == BusinessClock.class) {
value = SystemBusinessClock.INSTANCE;
value = runtime.getBusinessClock();
}
if (value == null && capabilityType == BusinessIdProfileFactory.class) {
value = new DefaultBusinessIdProfileFactory();
Expand Down Expand Up @@ -341,10 +341,10 @@ private static String sqlOperation(io.teaql.core.ExecutionMetadata metadata) {
@SuppressWarnings("unchecked")
@Override
public final <T> T evaluate(String expression, Object... args) {
// Built-in: "now" returns the current local date-time.
// Built-in: "now" comes from the context-owned business clock.
if ("now".equalsIgnoreCase(expression)) {
Object captured = getAttribute(io.teaql.core.checker.Checker.TEAQL_FIX_TIME);
return (T) (captured != null ? captured : java.time.LocalDateTime.now());
return (T) (captured != null ? captured : businessTime());
}
// Delegate to subclass or extension for application-defined expressions.
return evaluateExpression(expression, args);
Expand Down
17 changes: 16 additions & 1 deletion teaql-runtime/src/main/java/io/teaql/runtime/TeaQLRuntime.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
import io.teaql.core.businessid.BusinessIdKeyProvider;
import io.teaql.core.businessid.BusinessIdSchemaContributor;
import io.teaql.core.businessid.BusinessIdService;
import io.teaql.core.businessid.BusinessClock;
import io.teaql.runtime.businessid.DefaultBusinessIdService;
import io.teaql.core.reference.RoundTripReferenceCodec;
import io.teaql.core.reference.RoundTripReferenceProvider;
import io.teaql.runtime.businessid.SystemBusinessClock;
import java.util.*;
import java.util.stream.Stream;

Expand All @@ -31,6 +33,7 @@ public class TeaQLRuntime {
private final BusinessIdSchemaContributor businessIdSchemaContributor;
private final BusinessIdKeyProvider businessIdKeyProvider;
private final RoundTripReferenceCodec roundTripReferenceCodec;
private final BusinessClock businessClock;
private final Map<String, Checker<?>> checkers = new java.util.concurrent.ConcurrentHashMap<>();
private final List<GeneratedSchemaBootstrap> generatedBootstraps =
new java.util.concurrent.CopyOnWriteArrayList<>();
Expand All @@ -51,6 +54,7 @@ private TeaQLRuntime(Builder builder) {
this.roundTripReferenceCodec = builder.roundTripReferenceProvider == null
? null
: new RoundTripReferenceCodec(builder.roundTripReferenceProvider);
this.businessClock = builder.businessClock;
}

public static Builder builder() {
Expand Down Expand Up @@ -101,6 +105,10 @@ BusinessIdService getBusinessIdService() {
return businessIdService;
}

BusinessClock getBusinessClock() {
return businessClock;
}

BusinessIdSchemaContributor getBusinessIdSchemaContributor() {
return businessIdSchemaContributor;
}
Expand Down Expand Up @@ -524,7 +532,7 @@ private void checkAndFix(UserContext context, Entity entity) {
context.beginFixEvidence();
boolean ownsFixTime = context.getAttribute(Checker.TEAQL_FIX_TIME) == null;
if (ownsFixTime) {
context.putAttribute(Checker.TEAQL_FIX_TIME, java.time.LocalDateTime.now());
context.putAttribute(Checker.TEAQL_FIX_TIME, context.businessTime());
}
try {
checker.checkAndFix(context, (BaseEntity) entity);
Expand Down Expand Up @@ -1007,6 +1015,7 @@ public static class Builder {
private BusinessIdSchemaContributor businessIdSchemaContributor;
private BusinessIdKeyProvider businessIdKeyProvider;
private RoundTripReferenceProvider roundTripReferenceProvider;
private BusinessClock businessClock = SystemBusinessClock.INSTANCE;

public Builder metadata(EntityMetaFactory metadata) {
this.metadata = metadata;
Expand Down Expand Up @@ -1045,6 +1054,12 @@ public Builder businessIdService(BusinessIdService businessIdService) {
return this;
}

/** Installs the time source visible to domain behavior in each context. */
public Builder businessClock(BusinessClock businessClock) {
this.businessClock = java.util.Objects.requireNonNull(businessClock, "businessClock");
return this;
}

/**
* Installs one provider-backed allocator for generated Fix execution and
* the explicit {@code context.ensureSchema()} lifecycle.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.teaql.core.UserContext;
import io.teaql.core.businessid.BusinessClock;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;

/** Host time is isolated behind a context capability and can be replaced in tests. */
Expand All @@ -14,8 +15,16 @@ private SystemBusinessClock() {

@Override
public LocalDate businessDate(UserContext context) {
return businessDateTime(context).toLocalDate();
}

@Override
public LocalDateTime businessDateTime(UserContext context) {
return LocalDateTime.now(resolveZone(context));
}

private ZoneId resolveZone(UserContext context) {
Object zone = context.getAttribute("teaql.business.zone");
ZoneId zoneId = zone instanceof ZoneId ? (ZoneId) zone : ZoneId.systemDefault();
return LocalDate.now(zoneId);
return zone instanceof ZoneId ? (ZoneId) zone : ZoneId.systemDefault();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.teaql.runtime;

import static org.junit.Assert.assertEquals;

import io.teaql.core.UserContext;
import io.teaql.core.businessid.BusinessClock;
import io.teaql.core.meta.SimpleEntityMetaFactory;
import java.time.LocalDate;
import java.time.LocalDateTime;
import org.junit.Test;

public class BusinessClockTest {

@Test
public void contextUsesTheRuntimeClockForDateTimeAndNowExpression() {
LocalDateTime expected = LocalDateTime.of(2032, 2, 29, 10, 15, 30);
BusinessClock fixedClock = new BusinessClock() {
@Override
public LocalDate businessDate(UserContext context) {
return expected.toLocalDate();
}

@Override
public LocalDateTime businessDateTime(UserContext context) {
return expected;
}
};
TeaQLRuntime runtime = TeaQLRuntime.builder()
.metadata(new SimpleEntityMetaFactory())
.businessClock(fixedClock)
.build();

DefaultUserContext context = new DefaultUserContext(runtime);

assertEquals(expected.toLocalDate(), context.businessDate());
assertEquals(expected, context.businessTime());
assertEquals(expected, context.<LocalDateTime>evaluate("now"));
}

@Test
public void existingDateOnlyClockRemainsSourceCompatibleAndDeterministic() {
LocalDate expected = LocalDate.of(2033, 1, 2);
TeaQLRuntime runtime = TeaQLRuntime.builder()
.metadata(new SimpleEntityMetaFactory())
.businessClock(ignored -> expected)
.build();

DefaultUserContext context = new DefaultUserContext(runtime);

assertEquals(expected.atStartOfDay(), context.businessTime());
}
}
Loading