Skip to content

Add @Supplied property type for batchable criteria-projected values #129

@jtnelson

Description

@jtnelson

Summary

Add a @Supplied property annotation (and a corresponding SuppliedProperty<T> return type) that decomposes a derived property into (Criteria, Function<Result, T>). The Runway pipeline collects all such properties across the records being loaded, batches their criteria into a single command-group submit alongside the load, and projects results back into each record's data map before framing.

@Supplied is a complement to @Computed, not a replacement. @Computed stays for methods that do composite work (multiple queries, branching logic, pure in-memory computation). @Supplied covers the common pattern of "this property is the projection of a single criteria query against the database."

The pattern this addresses

Models frequently expose properties that are conceptually "a derived value that requires one criteria query." Today they're written as @Computed:

@Computed
public Set<Sibling> siblings() {
    return computeOnce("siblings",
            () -> db.find(Sibling.class, /* criteria using this.id */));
}

These fire one wire call each, sequentially, during framing — even when the framer is processing many records that could all have their criteria batched into one round trip. @Computed runs late in the lifecycle (when data() is iterated or get(key) is called) and is opaque to Runway, so there is no way to anticipate or batch the queries.

The result is N sequential round trips for what is structurally one batched lookup.

Proposed API

@Supplied
public SuppliedProperty<Set<Sibling>> siblings() {
    return SuppliedProperty.of(
        Sibling.class,
        Criteria.where().key("parent").operator(Operator.LINKS_TO).value(id()),
        result -> result   // result is Set<Sibling>; projection is identity here
    );
}

Where SuppliedProperty<T> is:

public final class SuppliedProperty<T> {
    private final Class<? extends Record> resultClass;
    private final Criteria criteria;
    private final Function<?, T> projection;

    public static <T, R extends Record> SuppliedProperty<T> of(
            Class<R> resultClass,
            Criteria criteria,
            Function<Set<R>, T> projection) { ... }

    public static <T, R extends Record> SuppliedProperty<T> ofUnique(
            Class<R> resultClass,
            Criteria criteria,
            Function<R, T> projection) { ... }
}

The method body returns a description of how to compute the value, not the value itself. Runway resolves the description in a batched phase.

Execution model

  1. Phase 1 (existing): load. BatchReader.submit() runs navigate + cleanup BFS as today, returning record IDs and their data.
  2. Phase 2 (new): collect. Runway walks every loaded record's @Supplied methods, invokes them to obtain their SuppliedProperty descriptors, and groups all the criteria by result class into a single follow-on BatchReader.submit().
  3. Phase 3 (new): project. When phase 2's submit returns, each SuppliedProperty projects its slice of the result set into a concrete value and stashes it on the record (e.g., into the computeOnce cache or a parallel _supplied cache).
  4. Phase 4 (existing): frame. Framing reads the pre-resolved values. No wire calls during framing for @Supplied properties.

For a Conversation with R root Exchanges where each root exposes isMultivariate() and siblings() as @Supplied, the cost goes from 2R sequential findUnique calls to 1 batched submit.

Constraints and semantics

  • @Supplied criteria must not depend on values produced by other @Supplied methods on the same record. Phase 2 is one batch; chained dependencies would require sequencing. Allowed inputs: this.id(), declared fields, fields populated by phase 1 (navigate). Disallowed: another @Supplied value on the same record.
  • Result is cached per (record instance, method name), same convention as computeOnce.
  • Falls back gracefully on older Concourse servers. When supportsBulkCommands is false, Runway can execute @Supplied criteria sequentially using the same IncrementalReader path as legacy @Computed. Same correctness, no batching benefit.
  • Methods can mix. A model can have both @Supplied and @Computed methods; framing handles them as a unit (suppliers resolved first, computed evaluated later if iterated).

Where this fits in the framing pipeline

The collection phase fits naturally between load and frame. In Runway.java's selectOne / selectMany paths, after the load completes and before the result is returned to the caller, walk the loaded records' classes for @Supplied methods, build the batch, submit, project.

Record.computeOnce provides a usable storage mechanism; an alternative is a dedicated _supplied cache parallel to _audit.

Concrete consumer

cinchapi/relay-server has two @Computed methods on Exchange that fit this exactly:

  • isMultivariate()db.findUnique(Conversation, where root LINKS_TO this.id) and a findAny().isPresent() projection.
  • siblings() → same findUnique, different projection (the conversation's root minus this).

Both would become @Supplied and fold into the same batched submit as the Conversation load.

Across the broader application surface, any model method that's currently @Computed and consists of a single db.find or db.findUnique + projection is a candidate.

Out of scope

  • A @Supplied variant whose criteria can reference another @Supplied result. Could be added later via an explicit dependency declaration if the use case appears.
  • Field-level supplied annotations (only methods for now).
  • A unified replacement for @Computed. The two coexist.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions