feat(vectordata): bind an already-decoded predicate through its binder

A caller that decodes a record itself, to read its fingerprint and pick
one binder per shape, binds the node directly with the same shape check
rather than decoding twice; the binder also names the shape it was
compiled against.
This commit is contained in:
Jonathan Shook committed 2026-09-05 00:15:52 +00:00
1 parent 7a7928b4e9
commit 19bee2506d
2 files changed
+33 -2

No files matched your search

@@ -70,6 +70,11 @@ public final class PredicateBinder {
/// The conditions, in order. Read once, to build the filter.
public List<Condition> conditions() { return conditions; }
/// The shape this binder was compiled against, in the display
/// grammar with every comparand replaced by its type default — the
/// identity a caller keys one binder per shape by.
public String shape() { return shape.display(); }
/// Receives one condition's comparands from a bound predicate.
@FunctionalInterface
public interface ConditionSink { void accept(Condition condition, List<Comparand> comparands); }
@@ -91,9 +96,17 @@ public final class PredicateBinder {
// template cannot make an MNode into a predicate.
if (!(node instanceof ANode.P p))
throw BindException.record("this record is not a predicate — its dialect byte says MNode, and a form does not override what a record is");
if (!p.node().isCongruent(shape))
bindEach(p.node(), sink);
}
/// Binds one already-decoded predicate, for a caller that decoded
/// the record itself — to read its fingerprint and choose the
/// binder, say — so the node is not decoded twice. The shape check
/// is the same.
public void bindEach(PNode predicate, ConditionSink sink) {
if (!predicate.isCongruent(shape))
throw BindException.record("this predicate's shape differs from the one this binder was compiled against; its values would bind to the wrong conditions");
List<Scan.FlatCondition> flat = Scan.flattenAnd(p.node());
List<Scan.FlatCondition> flat = Scan.flattenAnd(predicate);
if (flat == null) throw BindException.record("predicate is not a flat conjunction");
for (int i = 0; i < conditions.size() && i < flat.size(); i++) sink.accept(conditions.get(i), flat.get(i).comparands());
}
@@ -374,6 +374,24 @@ class RecordBindingTest {
assertTrue(e.getMessage().contains("flat conjunction"), e.getMessage());
}
/// A caller that decoded the record itself — to read its fingerprint
/// and pick one binder per shape — binds the node directly, with the
/// same shape check, and the binder names the shape it was compiled
/// against.
@Test void aDecodedPredicateBindsWithoutASecondDecode() throws IOException {
Layout layout = typedLayout("decoded");
PNode template = and(pred("created", OpType.GE, new Comparand.Int(0)), pred("tag", OpType.EQ, new Comparand.Text("")));
PredicateBinder binder = PredicateBinder.compile(template, layout);
assertEquals("(created >= 0 AND tag = '')", binder.shape());
PNode actual = and(pred("created", OpType.GE, new Comparand.Int(7)), pred("tag", OpType.EQ, new Comparand.Text("red")));
assertEquals(binder.shape(), actual.fingerprint().display(), "the same shape keys the same binder");
List<Comparand> bound = new ArrayList<>();
binder.bindEach(actual, (cond, cs) -> bound.add(cs.get(0)));
assertEquals(List.of(new Comparand.Int(7), new Comparand.Text("red")), bound);
PNode other = pred("id", OpType.EQ, new Comparand.Int(1));
assertTrue(assertThrows(BindException.class, () -> binder.bindEach(other, (c, cs) -> { })).getMessage().contains("shape differs"));
}
// ── error paths and the small surface ──────────────────────────
/// A facet with no records has no layout to learn, and says so