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
2 changes: 2 additions & 0 deletions examples/conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
This retained SQLite workspace is generated from `model.xml`. It verifies
explicit `ensureSchema`, Checker rejection before persistence, Create, typed Q
and `SmartList`, E loaded/null/not-loaded semantics, Update/version, and Delete.
It also proves that optimistic versions remain isolated when different entity
types use the same numeric ID and their mutation ledgers are merged.

```bash
make run
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import io.teaql.core.meta.EntityMetaFactory;
import io.teaql.core.meta.SimpleEntityMetaFactory;
import io.teaql.core.DataServiceExecutor;
import io.teaql.core.EntityKey;
import io.teaql.core.EntityMutationLedger;
import io.teaql.core.DataServiceRegistry;
import io.teaql.core.sql.portable.IdSpaceIdGenerator;
import io.teaql.core.SchemaExecutor;
Expand Down Expand Up @@ -80,6 +82,7 @@ public TeaQLRuntime teaQLRuntime(
public CommandLineRunner teaQLConsoleStartup(
TeaQLRuntime runtime, DataServiceExecutor dataServiceExecutor) {
return args -> {
verifySameIdVersionIsolation();
UserContext context = new CustomUserContext(runtime);
if (!(dataServiceExecutor instanceof SchemaExecutor schema)) {
throw new IllegalStateException("default data service has no schema capability");
Expand Down Expand Up @@ -181,10 +184,26 @@ public CommandLineRunner teaQLConsoleStartup(
.executeForList(context);
require(remaining.isEmpty(), "Deleted row remains visible to ordinary Q API");
System.out.println("PASS Delete (default Q excludes deleted rows)");
System.out.println("PASS Java minimum runtime conformance: 7/7");
System.out.println("PASS Java minimum runtime conformance: 8/8");
};
}

private static void verifySameIdVersionIsolation() {
EntityKey order = new EntityKey("Order", 1L);
EntityKey execution = new EntityKey("InferenceExecution", 1L);
EntityMutationLedger target = new EntityMutationLedger();
EntityMutationLedger source = new EntityMutationLedger();
target.setOriginalVersion(order, 3L);
source.setOriginalVersion(execution, 9L);
source.set(execution, "execution_status", "COMPLETED");
target.mergeFrom(source);
require(Long.valueOf(3L).equals(target.getOriginalVersion(order)),
"Order#1 version was overwritten");
require(Long.valueOf(9L).equals(target.getOriginalVersion(execution)),
"InferenceExecution#1 version was resolved through Order#1");
System.out.println("PASS Mutation ledger identity (same ID, different entity types keep versions 3/9)");
}

private static void require(boolean condition, String message) {
if (!condition) {
throw new IllegalStateException(message);
Expand Down
23 changes: 23 additions & 0 deletions teaql-core/src/main/java/io/teaql/core/checker/CheckResult.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.teaql.core.checker;

import java.time.LocalDateTime;
import java.util.Locale;

public class CheckResult {
private RuleId ruleId;
Expand All @@ -12,6 +13,7 @@ public class CheckResult {
private Object systemValue;

private String naturalLanguageStatement;
private String sourceInstancePath;

public static CheckResult required(ObjectLocation location) {
CheckResult checkResult = new CheckResult();
Expand Down Expand Up @@ -144,6 +146,27 @@ public void setNaturalLanguageStatement(String pNaturalLanguageStatement) {
naturalLanguageStatement = pNaturalLanguageStatement;
}

/** Exact RFC 6901 pointer submitted through an accepted input alias. */
public String getSourceInstancePath() {
return sourceInstancePath;
}

public void setSourceInstancePath(String pSourceInstancePath) {
sourceInstancePath = pSourceInstancePath;
}

public WireCheckResult toWire(JsonFieldNamingProfile profile) {
return new WireCheckResult(
ruleId == null ? null : ruleId.name().toLowerCase(Locale.ROOT),
rootType,
location == null ? null : location.segments(),
location == null ? null : location.instancePath(profile),
sourceInstancePath,
inputValue,
systemValue,
naturalLanguageStatement);
}

public enum RuleId {
MIN,
MAX,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.teaql.core.checker;

/** Model-selected naming policy for JSON fields and RFC 6901 instance paths. */
public enum JsonFieldNamingProfile {
CAMEL_CASE("camelCase"),
SNAKE_CASE("snake_case"),
PASCAL_CASE("PascalCase");

private final String modelValue;

JsonFieldNamingProfile(String pModelValue) {
modelValue = pModelValue;
}

public String modelValue() {
return modelValue;
}

public String render(String canonicalName) {
if (this == SNAKE_CASE) {
return canonicalName;
}
String[] parts = canonicalName.split("_", -1);
StringBuilder result = new StringBuilder();
for (int i = 0; i < parts.length; i++) {
if (parts[i].isEmpty()) continue;
if (i == 0 && this == CAMEL_CASE) {
result.append(parts[i]);
} else {
result.append(Character.toUpperCase(parts[i].charAt(0)))
.append(parts[i].substring(1));
}
}
return result.toString();
}

public static JsonFieldNamingProfile fromModelValue(String value) {
if (value == null || value.isBlank() || "camelCase".equals(value)) {
return CAMEL_CASE;
}
for (JsonFieldNamingProfile profile : values()) {
if (profile.modelValue.equals(value)) return profile;
}
throw new IllegalArgumentException("Unsupported json_field_naming: " + value);
}
}
25 changes: 23 additions & 2 deletions teaql-core/src/main/java/io/teaql/core/checker/ObjectLocation.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,39 @@ public String nativePath() {

/** RFC 6901 JSON pointer using TeaQL's default lower-camel wire policy. */
public String instancePath() {
return render(true, true);
return instancePath(JsonFieldNamingProfile.CAMEL_CASE);
}

/** RFC 6901 JSON pointer rendered with the model-selected wire profile. */
public String instancePath(JsonFieldNamingProfile profile) {
return render(profile, true);
}

public List<WireLocationSegment> segments() {
List<WireLocationSegment> result = new ArrayList<>();
for (ObjectLocation current = this; current != null; current = current.getParent()) {
if (current instanceof HashLocation hash) {
result.add(0, WireLocationSegment.property(hash.getMember()));
} else if (current instanceof ArrayLocation array) {
result.add(0, WireLocationSegment.index(array.getIndex()));
}
}
return List.copyOf(result);
}

private String render(boolean lowerCamel, boolean pointer) {
return render(lowerCamel ? JsonFieldNamingProfile.CAMEL_CASE : null, pointer);
}

private String render(JsonFieldNamingProfile profile, boolean pointer) {
List<ObjectLocation> locations = new ArrayList<>();
for (ObjectLocation current = this; current != null; current = current.getParent()) {
locations.add(0, current);
}
StringBuilder result = new StringBuilder();
for (ObjectLocation location : locations) {
if (location instanceof HashLocation hash) {
String member = lowerCamel ? lowerCamel(hash.getMember()) : hash.getMember();
String member = profile == null ? hash.getMember() : profile.render(hash.getMember());
if (pointer) {
result.append('/').append(escapePointer(member));
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.teaql.core.checker;

import java.util.List;

/** Stable external projection of a Checker violation. */
public record WireCheckResult(
String ruleId,
String entityType,
List<WireLocationSegment> location,
String instancePath,
String sourceInstancePath,
Object inputValue,
Object systemValue,
String message) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.teaql.core.checker;

/** Canonical KSML property or array-index segment used by wire violations. */
public record WireLocationSegment(String kind, String name, Integer index) {
public static WireLocationSegment property(String name) {
return new WireLocationSegment("property", name, null);
}

public static WireLocationSegment index(int index) {
return new WireLocationSegment("index", null, index);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.teaql.core.checker;

import java.util.List;
import org.junit.Test;
import static org.junit.Assert.*;

Expand All @@ -14,9 +15,25 @@ public void rendersCanonicalNativeAndExternalPaths() {
assertEquals("order_items[2].user_url", location.modelPath());
assertEquals("orderItems[2].userUrl", location.nativePath());
assertEquals("/orderItems/2/userUrl", location.instancePath());
assertEquals("/order_items/2/user_url", location.instancePath(JsonFieldNamingProfile.SNAKE_CASE));
assertEquals("/OrderItems/2/UserUrl", location.instancePath(JsonFieldNamingProfile.PASCAL_CASE));
assertEquals("order_items[2].user_url", location.toString());
}

@Test
public void projectsCheckerResultWithProfileAndSubmittedAlias() {
CheckResult result = CheckResult.required(ObjectLocation.hashRoot("user_url"));
result.setRootType("customer_account");
result.setSourceInstancePath("/user_url");

WireCheckResult wire = result.toWire(JsonFieldNamingProfile.CAMEL_CASE);
assertEquals("required", wire.ruleId());
assertEquals("customer_account", wire.entityType());
assertEquals(List.of(WireLocationSegment.property("user_url")), wire.location());
assertEquals("/userUrl", wire.instancePath());
assertEquals("/user_url", wire.sourceInstancePath());
}

@Test
public void escapesJsonPointerMembers() {
assertEquals("/a~0~1b", ObjectLocation.hashRoot("a~/b").instancePath());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.teaql.tfp;

import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Map;

/** Canonical payload plus the exact accepted JSON pointer used for each KSML field. */
public record NormalizedWireObject(ObjectNode values, Map<String, String> sourceInstancePaths) {}
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,13 @@ private Map<String, Object> handleMutationActive(UserContext context, TrustedFed
if (writable == null || !entityPayload.isObject()) {
throw new TfpEndpointException("TFP_INVALID_REQUEST", "Invalid mutation payload");
}
ObjectNode normalizedPayload = (ObjectNode) entityPayload;
WireEntityMetadata metadata = trusted.wireMetadata(entityName);
if (metadata != null) {
normalizedPayload = WireFieldAdapter.normalize(normalizedPayload, metadata).values();
}
ObjectNode mappedPayload = objectMapper.createObjectNode();
entityPayload.fields().forEachRemaining(entry -> {
normalizedPayload.fields().forEachRemaining(entry -> {
String mapped = writable.get(entry.getKey());
if (mapped == null) throw new TfpEndpointException("TFP_FORBIDDEN_FIELD",
"Mutation field is not allowed: " + entry.getKey());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public final class TrustedFederalContext {
private final Map<String, Map<String, String>> readableFields;
private final Map<String, Map<String, String>> writableFields;
private final Map<String, Set<String>> allowedActions;
private final Map<String, WireEntityMetadata> wireMetadata;
private final int maxPageSize;

public TrustedFederalContext(String tenantField, Object tenantId,
Expand All @@ -21,6 +22,17 @@ public TrustedFederalContext(String tenantField, Object tenantId,
Map<String, Map<String, String>> readableFields,
Map<String, Map<String, String>> writableFields,
Map<String, Set<String>> allowedActions, int maxPageSize) {
this(tenantField, tenantId, authenticatedUser, approvedPurpose, allowedEntities,
readableFields, writableFields, allowedActions, maxPageSize, Map.of());
}

public TrustedFederalContext(String tenantField, Object tenantId,
String authenticatedUser, String approvedPurpose,
Set<String> allowedEntities,
Map<String, Map<String, String>> readableFields,
Map<String, Map<String, String>> writableFields,
Map<String, Set<String>> allowedActions, int maxPageSize,
Map<String, WireEntityMetadata> wireMetadata) {
this.tenantField = tenantField;
this.tenantId = tenantId;
this.authenticatedUser = authenticatedUser;
Expand All @@ -30,15 +42,26 @@ public TrustedFederalContext(String tenantField, Object tenantId,
this.writableFields = Map.copyOf(writableFields);
this.allowedActions = Map.copyOf(allowedActions);
this.maxPageSize = maxPageSize;
this.wireMetadata = Map.copyOf(wireMetadata);
}

public String tenantField() { return tenantField; }
public Object tenantId() { return tenantId; }
public String authenticatedUser() { return authenticatedUser; }
public String approvedPurpose() { return approvedPurpose; }
public Set<String> allowedEntities() { return allowedEntities; }
public Map<String, String> readableFields(String entity) { return readableFields.get(entity); }
public Map<String, String> writableFields(String entity) { return writableFields.get(entity); }
public Map<String, String> readableFields(String entity) {
return effectiveFields(entity, readableFields.get(entity));
}
public Map<String, String> writableFields(String entity) {
return effectiveFields(entity, writableFields.get(entity));
}
public Set<String> allowedActions(String entity) { return allowedActions.get(entity); }
public int maxPageSize() { return maxPageSize; }
public WireEntityMetadata wireMetadata(String entity) { return wireMetadata.get(entity); }

private Map<String, String> effectiveFields(String entity, Map<String, String> fields) {
WireEntityMetadata metadata = wireMetadata.get(entity);
return fields == null || metadata == null ? fields : metadata.acceptedPolicyMap(fields);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.teaql.tfp;

import java.util.LinkedHashMap;
import java.util.Map;

/** Generated, serializer-independent mapping between wire names and canonical KSML fields. */
public final class WireEntityMetadata {
private final String entity;
private final Map<String, String> acceptedToCanonical;
private final Map<String, String> canonicalToWire;

public WireEntityMetadata(String entity, Map<String, String> canonicalToWire,
Map<String, String> aliases) {
this.entity = entity;
this.canonicalToWire = Map.copyOf(canonicalToWire);
Map<String, String> accepted = new LinkedHashMap<>();
canonicalToWire.forEach((canonical, wire) -> {
register(accepted, canonical, canonical);
register(accepted, wire, canonical);
});
aliases.forEach((alias, canonical) -> {
if (!canonicalToWire.containsKey(canonical)) {
throw new IllegalArgumentException("Unknown canonical field for alias: " + canonical);
}
register(accepted, alias, canonical);
});
this.acceptedToCanonical = Map.copyOf(accepted);
}

/** Converts dependency-free metadata emitted by GeneratedRuntimeModule. */
public static Map<String, WireEntityMetadata> fromGenerated(
Map<String, Map<String, String>> mappings,
Map<String, Map<String, String>> aliases) {
Map<String, WireEntityMetadata> result = new LinkedHashMap<>();
mappings.forEach((entity, fields) -> result.put(entity,
new WireEntityMetadata(entity, fields, aliases.getOrDefault(entity, Map.of()))));
return Map.copyOf(result);
}

private static void register(Map<String, String> accepted, String name, String canonical) {
String previous = accepted.putIfAbsent(name, canonical);
if (previous != null && !previous.equals(canonical)) {
throw new IllegalArgumentException("Wire field alias is ambiguous: " + name);
}
}

public String entity() { return entity; }

public String canonicalField(String submitted) {
return acceptedToCanonical.get(submitted);
}

public String wireField(String canonical) {
return canonicalToWire.get(canonical);
}

Map<String, String> acceptedPolicyMap(Map<String, String> canonicalPolicy) {
Map<String, String> result = new LinkedHashMap<>();
acceptedToCanonical.forEach((accepted, canonical) -> {
String internal = canonicalPolicy.get(canonical);
if (internal != null) result.put(accepted, internal);
});
return Map.copyOf(result);
}
}
Loading
Loading