diff --git a/teaql-core/src/main/java/io/teaql/core/UserContext.java b/teaql-core/src/main/java/io/teaql/core/UserContext.java index b5751454..6a485b0e 100644 --- a/teaql-core/src/main/java/io/teaql/core/UserContext.java +++ b/teaql-core/src/main/java/io/teaql/core/UserContext.java @@ -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 diff --git a/teaql-core/src/main/java/io/teaql/core/businessid/BusinessClock.java b/teaql-core/src/main/java/io/teaql/core/businessid/BusinessClock.java index d8bfaea4..68a0f3d4 100644 --- a/teaql-core/src/main/java/io/teaql/core/businessid/BusinessClock.java +++ b/teaql-core/src/main/java/io/teaql/core/businessid/BusinessClock.java @@ -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(); + } } diff --git a/teaql-runtime/src/main/java/io/teaql/runtime/DefaultUserContext.java b/teaql-runtime/src/main/java/io/teaql/runtime/DefaultUserContext.java index 3f8f68b0..73e906f4 100644 --- a/teaql-runtime/src/main/java/io/teaql/runtime/DefaultUserContext.java +++ b/teaql-runtime/src/main/java/io/teaql/runtime/DefaultUserContext.java @@ -158,7 +158,7 @@ public T capability(Class 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(); @@ -341,10 +341,10 @@ private static String sqlOperation(io.teaql.core.ExecutionMetadata metadata) { @SuppressWarnings("unchecked") @Override public final 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); diff --git a/teaql-runtime/src/main/java/io/teaql/runtime/TeaQLRuntime.java b/teaql-runtime/src/main/java/io/teaql/runtime/TeaQLRuntime.java index 91b43bd9..249d2bc3 100644 --- a/teaql-runtime/src/main/java/io/teaql/runtime/TeaQLRuntime.java +++ b/teaql-runtime/src/main/java/io/teaql/runtime/TeaQLRuntime.java @@ -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; @@ -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> checkers = new java.util.concurrent.ConcurrentHashMap<>(); private final List generatedBootstraps = new java.util.concurrent.CopyOnWriteArrayList<>(); @@ -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() { @@ -101,6 +105,10 @@ BusinessIdService getBusinessIdService() { return businessIdService; } + BusinessClock getBusinessClock() { + return businessClock; + } + BusinessIdSchemaContributor getBusinessIdSchemaContributor() { return businessIdSchemaContributor; } @@ -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); @@ -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; @@ -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. diff --git a/teaql-runtime/src/main/java/io/teaql/runtime/businessid/SystemBusinessClock.java b/teaql-runtime/src/main/java/io/teaql/runtime/businessid/SystemBusinessClock.java index 55c1c7e9..f236cfdb 100644 --- a/teaql-runtime/src/main/java/io/teaql/runtime/businessid/SystemBusinessClock.java +++ b/teaql-runtime/src/main/java/io/teaql/runtime/businessid/SystemBusinessClock.java @@ -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. */ @@ -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(); } } diff --git a/teaql-runtime/src/test/java/io/teaql/runtime/BusinessClockTest.java b/teaql-runtime/src/test/java/io/teaql/runtime/BusinessClockTest.java new file mode 100644 index 00000000..f90cdfe3 --- /dev/null +++ b/teaql-runtime/src/test/java/io/teaql/runtime/BusinessClockTest.java @@ -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.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()); + } +}