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
13 changes: 10 additions & 3 deletions examples/business-id-runtime/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Business ID focused runtime example

This example verifies the new Java runtime Business ID boundary without running
the full multi-module regression suite. It covers the default eight-digit
daily sequence, controlled Context date, root scoping, Mutation Ledger retry
This example verifies the Java runtime Business ID boundary without running
the full multi-module regression suite. It covers the default six-character
volume-obscuring Base36 profile, controlled Context date, Domain Root scoping,
the byte-identical Java/Rust golden vectors, Mutation Ledger retry
stability, explicit SQLite schema setup, cross-instance allocation, restart,
strong `OrderNumber` parsing, and immutable established aggregates.

Expand All @@ -14,6 +15,12 @@ the context's `BusinessClock`; tests can replace that clock. The JDBC allocator
uses root/aggregate/namespace/date scoping, optimistic allocation, bounded
retry, and a range error when the configured digits are exhausted.

The default `daily-permuted-v1` profile additionally requires a
`BusinessIdKeyProvider` capability. The example installs a deterministic
32-byte test key; production applications must obtain versioned key material
from their own secret provider. The profile is volume-obscuring, not an
authentication token or substitute for authorization.

For an existing counter table, `context.ensureSchema()` verifies the four
required columns, a single-column `scope_key` primary or unique key, a text
`scope_key` with capacity at least 512, and non-null 64-bit integral counter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class BusinessIdRuntimeExampleTest {

public record OrderNumber(String value) {
public OrderNumber {
if (value == null || !value.matches("CO-\\d{8}-\\d{8}")) {
if (value == null || !value.matches("CO-\\d{8}-[0-9A-Z]{6}")) {
throw new IllegalArgumentException("invalid OrderNumber: " + value);
}
}
Expand Down Expand Up @@ -82,7 +82,7 @@ public void inMemoryProfileIsScopedTypedAndRetryStable() {
Order first = new Order(101);
BusinessIdValue initial = context.businessIds().ensure(
context, ORDER_NUMBER, "tenant-a", "commerce_order", first);
Assert.assertEquals("CO-20260920-00000001", initial.value());
Assert.assertEquals("CO-20260920-KSHQ82", initial.value());
Assert.assertEquals(new OrderNumber(initial.value()), first.orderNumber());
Assert.assertEquals(
first.orderNumber(),
Expand All @@ -96,20 +96,20 @@ public void inMemoryProfileIsScopedTypedAndRetryStable() {

Order second = new Order(102);
Assert.assertEquals(
"CO-20260920-00000002",
"CO-20260920-B8S7XN",
context.businessIds().ensure(
context, ORDER_NUMBER, "tenant-a", "commerce_order", second).value());

Order otherTenant = new Order(103);
Assert.assertEquals(
"CO-20260920-00000001",
"CO-20260920-Y5Z052",
context.businessIds().ensure(
context, ORDER_NUMBER, "tenant-b", "commerce_order", otherTenant).value());

UserContext nextDay = context(LocalDate.of(2026, 9, 21));
nextDay.putAttribute(BusinessIdService.class.getName(), service);
Assert.assertEquals(
"CO-20260921-00000001",
"CO-20260921-XAUWT0",
nextDay.businessIds().ensure(
nextDay, ORDER_NUMBER, "tenant-a", "commerce_order", new Order(104)).value());

Expand All @@ -124,18 +124,67 @@ public void inMemoryProfileIsScopedTypedAndRetryStable() {
"commerce_order",
establishedWithoutNumber));
Assert.assertEquals(BusinessIdErrorCode.BUSINESS_ID_IMMUTABLE, immutable.getCode());

Order impossibleDate = new Order(109);
impossibleDate.orderNumber = new OrderNumber("CO-20260231-ABC123");
BusinessIdException invalidDate = Assert.assertThrows(
BusinessIdException.class,
() -> context.businessIds().ensure(
context,
ORDER_NUMBER,
"tenant-a",
"commerce_order",
impossibleDate));
Assert.assertEquals(
BusinessIdErrorCode.BUSINESS_ID_FORMAT_INVALID, invalidDate.getCode());
}

@Test
public void defaultProfileFailsClosedWithoutKeyProvider() {
UserContext context = context(LocalDate.of(2026, 9, 20), null, false);
DefaultBusinessIdService service =
new DefaultBusinessIdService(new InMemoryBusinessIdAllocator());
context.putAttribute(BusinessIdService.class.getName(), service);

BusinessIdException failure = Assert.assertThrows(
BusinessIdException.class,
() -> context.businessIds().ensure(
context,
ORDER_NUMBER,
"domain-root-a",
"commerce_order",
new Order(106)));
Assert.assertEquals(
BusinessIdErrorCode.BUSINESS_ID_KEY_NOT_FOUND, failure.getCode());
}

@Test
public void explicitLegacyProfileRetainsOneBasedDecimalSequence() {
BusinessIdDefinition legacy = BusinessIdDefinition.legacyDailySequence(
"legacy_order_number", "CO", "legacy_order_number");
DailySequenceBusinessIdProfile profile = new DailySequenceBusinessIdProfile();
BusinessIdPlan plan = profile.plan(new BusinessIdGenerationRequest(
legacy,
"domain-root-a",
"commerce_order",
LocalDate.of(2026, 9, 20)));
InMemoryBusinessIdAllocator allocator = new InMemoryBusinessIdAllocator();
Assert.assertEquals(
"CO-20260920-00000001",
profile.format(plan, allocator.allocate(plan)).value());
Assert.assertEquals(
"CO-20260920-00000002",
profile.format(plan, allocator.allocate(plan)).value());
Assert.assertEquals(
"CO-20260920-00000001",
profile.validate(legacy, "CO-20260920-00000001").value());
}

@Test
public void sqliteAllocatorIsExplicitCrossInstanceAndRestartSafe() throws Exception {
Path databasePath = Files.createTempFile("teaql-business-id", ".db");
try {
BusinessIdPlan plan = new DailySequenceBusinessIdProfile().plan(
new BusinessIdGenerationRequest(
ORDER_NUMBER,
"tenant-a",
"commerce_order",
LocalDate.of(2026, 9, 20)));
BusinessIdPlan plan = plan(LocalDate.of(2026, 9, 20));

try (Connection schemaConnection = open(databasePath)) {
JdbcBusinessIdAllocator allocator =
Expand Down Expand Up @@ -179,7 +228,18 @@ public void sqliteAllocatorIsExplicitCrossInstanceAndRestartSafe() throws Except
try (Connection restarted = open(databasePath)) {
JdbcBusinessIdAllocator allocator =
new JdbcBusinessIdAllocator(database(restarted));
Assert.assertEquals(51, allocator.allocate(plan).sequence());
Assert.assertEquals(50, allocator.allocate(plan).sequence());

BusinessIdDefinition legacy = BusinessIdDefinition.legacyDailySequence(
"legacy_order_number", "CO", "legacy_order_number");
BusinessIdPlan legacyPlan = new DailySequenceBusinessIdProfile().plan(
new BusinessIdGenerationRequest(
legacy,
"domain-root-a",
"commerce_order",
LocalDate.of(2026, 9, 20)));
Assert.assertEquals(1, allocator.allocate(legacyPlan).sequence());
Assert.assertEquals(2, allocator.allocate(legacyPlan).sequence());
}
} finally {
Files.deleteIfExists(databasePath);
Expand All @@ -192,10 +252,7 @@ public void sqliteAllocatorReportsMissingSchemaWithoutRetryExhaustion() throws E
try (Connection connection = open(databasePath)) {
JdbcBusinessIdAllocator allocator =
new JdbcBusinessIdAllocator(database(connection));
BusinessIdPlan plan = new DailySequenceBusinessIdProfile().plan(
new BusinessIdGenerationRequest(
ORDER_NUMBER, "tenant-a", "commerce_order",
LocalDate.of(2026, 9, 20)));
BusinessIdPlan plan = plan(LocalDate.of(2026, 9, 20));

IllegalStateException failure = Assert.assertThrows(
IllegalStateException.class, () -> allocator.allocate(plan));
Expand All @@ -215,9 +272,7 @@ public void sqliteAllocatorRejectsMalformedSequenceRowWithoutRetryExhaustion()
JdbcBusinessIdAllocator allocator =
new JdbcBusinessIdAllocator(database(connection));
LocalDate businessDate = LocalDate.of(2026, 9, 20);
BusinessIdPlan plan = new DailySequenceBusinessIdProfile().plan(
new BusinessIdGenerationRequest(
ORDER_NUMBER, "tenant-a", "commerce_order", businessDate));
BusinessIdPlan plan = plan(businessDate);
UserContext context = context(businessDate, allocator);
context.putAttribute(SchemaExecutor.class.getName(), noOpSchemaExecutor());
context.ensureSchema();
Expand Down Expand Up @@ -319,11 +374,9 @@ public void sqliteAllocatorAcceptsNonNullableUniqueScopeKey() throws Exception {
context.putAttribute(SchemaExecutor.class.getName(), noOpSchemaExecutor());
context.ensureSchema();
context.ensureSchema();
BusinessIdPlan plan = new DailySequenceBusinessIdProfile().plan(
new BusinessIdGenerationRequest(ORDER_NUMBER, "tenant-a",
"commerce_order", LocalDate.of(2026, 9, 20)));
BusinessIdPlan plan = plan(LocalDate.of(2026, 9, 20));
Assert.assertEquals(0L, allocator.allocate(plan).sequence());
Assert.assertEquals(1L, allocator.allocate(plan).sequence());
Assert.assertEquals(2L, allocator.allocate(plan).sequence());
} finally {
Files.deleteIfExists(databasePath);
}
Expand Down Expand Up @@ -355,9 +408,7 @@ public void sqliteAllocatorRejectsExhaustedAndOverflowedSequencesWithoutMutation
Path databasePath = Files.createTempFile("teaql-business-id-range", ".db");
try {
LocalDate businessDate = LocalDate.of(2026, 9, 20);
BusinessIdPlan plan = new DailySequenceBusinessIdProfile().plan(
new BusinessIdGenerationRequest(
ORDER_NUMBER, "tenant-a", "commerce_order", businessDate));
BusinessIdPlan plan = plan(businessDate);
try (Connection connection = open(databasePath)) {
JdbcBusinessIdAllocator allocator =
new JdbcBusinessIdAllocator(database(connection));
Expand Down Expand Up @@ -411,6 +462,13 @@ private static UserContext context(LocalDate date) {

static UserContext context(
LocalDate date, JdbcBusinessIdAllocator businessIdInfrastructure) {
return context(date, businessIdInfrastructure, true);
}

static UserContext context(
LocalDate date,
JdbcBusinessIdAllocator businessIdInfrastructure,
boolean registerKeyProvider) {
TeaQLRuntime.Builder builder = TeaQLRuntime.builder()
.metadata(new EntityMetaFactory() {
@Override
Expand All @@ -427,6 +485,9 @@ public List<EntityDescriptor> allEntityDescriptors() {
}
})
.executionLogging(false);
if (registerKeyProvider) {
builder.businessIdKeyProvider(testKeyProvider());
}
if (businessIdInfrastructure != null) {
builder.businessIdInfrastructure(businessIdInfrastructure);
}
Expand All @@ -439,6 +500,26 @@ public List<EntityDescriptor> allEntityDescriptors() {
return context;
}

static BusinessIdPlan plan(LocalDate businessDate) {
return plan(businessDate, "tenant-a");
}

static BusinessIdPlan plan(LocalDate businessDate, String domainRootKey) {
UserContext context = context(businessDate);
return new PermutedDailyBusinessIdProfile(context, testKeyProvider()).plan(
new BusinessIdGenerationRequest(
ORDER_NUMBER, domainRootKey, "commerce_order", businessDate));
}

static StaticBusinessIdKeyProvider testKeyProvider() {
return new StaticBusinessIdKeyProvider(new BusinessIdEncodingKey(1, new byte[] {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f
}));
}

private static Connection open(Path path) throws SQLException {
Connection connection = DriverManager.getConnection("jdbc:sqlite:" + path);
try (Statement statement = connection.createStatement()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
import io.teaql.businessid.jdbc.JdbcBusinessIdAllocator;
import io.teaql.core.SchemaExecutor;
import io.teaql.core.UserContext;
import io.teaql.core.businessid.BusinessIdDefinition;
import io.teaql.core.businessid.BusinessIdGenerationRequest;
import io.teaql.core.businessid.BusinessIdPlan;
import io.teaql.runtime.businessid.DailySequenceBusinessIdProfile;
import io.teaql.core.sql.dialect.OracleDialect;
import java.sql.Connection;
import java.sql.DriverManager;
Expand Down Expand Up @@ -137,12 +134,8 @@ private void verify(String dialect) throws Exception {
url.contains("teaql_live_"));
}

BusinessIdDefinition definition = BusinessIdDefinition.dailySequence(
"order_number", "CO", "commerce_order");
BusinessIdPlan plan = new DailySequenceBusinessIdProfile().plan(
new BusinessIdGenerationRequest(
definition, "tenant-" + UUID.randomUUID(),
"commerce_order", BUSINESS_DATE));
BusinessIdPlan plan = BusinessIdRuntimeExampleTest.plan(
BUSINESS_DATE, "tenant-" + UUID.randomUUID());

try (Connection schemaConnection = DriverManager.getConnection(url, user, password)) {
JdbcBusinessIdAllocator allocator = allocator(schemaConnection, dialect);
Expand Down Expand Up @@ -178,7 +171,7 @@ private void verify(String dialect) throws Exception {
}
int total = 2 * ALLOCATIONS_PER_INSTANCE;
Assert.assertEquals(total, allocated.size());
for (long sequence = 1; sequence <= total; sequence++) {
for (long sequence = 0; sequence < total; sequence++) {
Assert.assertTrue("Missing sequence " + sequence, allocated.contains(sequence));
}
} finally {
Expand All @@ -187,7 +180,7 @@ private void verify(String dialect) throws Exception {

try (Connection restarted = DriverManager.getConnection(url, user, password)) {
JdbcBusinessIdAllocator allocator = allocator(restarted, dialect);
Assert.assertEquals(2L * ALLOCATIONS_PER_INSTANCE + 1,
Assert.assertEquals(2L * ALLOCATIONS_PER_INSTANCE,
allocator.allocate(plan).sequence());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,8 @@ public BusinessIdAllocation allocate(BusinessIdPlan plan) {
inserted = database.executeUpdate(
"INSERT INTO " + table
+ " (scope_key, current_value, version, updated_at)"
+ " VALUES (?, 1, 1, ?)",
new Object[]{scopeKey, updatedAt});
+ " VALUES (?, ?, 1, ?)",
new Object[]{scopeKey, plan.initialSequence(), updatedAt});
} catch (RuntimeException insertionFailure) {
lastConflict = insertionFailure;
// A competing instance may have inserted this key. Do not retry an
Expand All @@ -258,7 +258,8 @@ public BusinessIdAllocation allocate(BusinessIdPlan plan) {
}
if (inserted != null) {
if (inserted == 1) {
return new BusinessIdAllocation(plan.scope(), 1);
return new BusinessIdAllocation(
plan.scope(), plan.initialSequence());
}
throw new IllegalStateException(
"Expected one inserted Business ID row for " + scopeKey
Expand All @@ -268,7 +269,7 @@ public BusinessIdAllocation allocate(BusinessIdPlan plan) {
Map<String, Object> row = rows.get(0);
long current = number(row, "current_value");
long version = number(row, "version");
if (current < 1 || version < 1) {
if (current < 0 || version < 1) {
throw new IllegalStateException(
"Invalid Business ID sequence row for " + scopeKey);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ public record BusinessIdDefinition(
String namespace,
int policyVersion) {

public static final String DEFAULT_PROFILE = "daily-sequence";
public static final String DEFAULT_PROFILE = "daily-permuted-v1";
public static final String LEGACY_DAILY_SEQUENCE_PROFILE = "daily-sequence";
public static final String DEFAULT_DATE_FORMAT = "yyyyMMdd";
public static final int DEFAULT_DIGITS = 8;
public static final int DEFAULT_DIGITS = 6;

public BusinessIdDefinition {
fieldName = requireText(fieldName, "fieldName");
Expand All @@ -23,6 +24,9 @@ public record BusinessIdDefinition(
reset = requireText(reset, "reset");
separator = requireText(separator, "separator");
namespace = requireText(namespace, "namespace");
if (DEFAULT_PROFILE.equals(profile) && digits != DEFAULT_DIGITS) {
throw invalid("daily-permuted-v1 requires exactly 6 digits");
}
if (digits < 1 || digits > 18) {
throw invalid("digits must be between 1 and 18");
}
Expand All @@ -46,13 +50,30 @@ public static BusinessIdDefinition dailySequence(
}

public long maximumSequence() {
if (DEFAULT_PROFILE.equals(profile)) {
return 2_176_782_335L;
}
long maximum = 1;
for (int i = 0; i < digits; i++) {
maximum = Math.multiplyExact(maximum, 10);
}
return maximum - 1;
}

public static BusinessIdDefinition legacyDailySequence(
String fieldName, String prefix, String namespace) {
return new BusinessIdDefinition(
fieldName,
LEGACY_DAILY_SEQUENCE_PROFILE,
prefix,
DEFAULT_DATE_FORMAT,
"daily",
8,
"-",
namespace,
1);
}

private static String requireText(String value, String field) {
if (value == null || value.isBlank()) {
throw invalid(field + " must not be blank");
Expand Down
Loading
Loading