diff --git a/nb-vectordata/src/main/java/io/nosqlbench/vectordata/binding/PredicateBinder.java b/nb-vectordata/src/main/java/io/nosqlbench/vectordata/binding/PredicateBinder.java index bf75a5840..2d32cc9e9 100644 --- a/nb-vectordata/src/main/java/io/nosqlbench/vectordata/binding/PredicateBinder.java +++ b/nb-vectordata/src/main/java/io/nosqlbench/vectordata/binding/PredicateBinder.java @@ -70,6 +70,11 @@ public final class PredicateBinder { /// The conditions, in order. Read once, to build the filter. public List 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 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 flat = Scan.flattenAnd(p.node()); + List 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()); } diff --git a/nb-vectordata/src/test/java/io/nosqlbench/vectordata/RecordBindingTest.java b/nb-vectordata/src/test/java/io/nosqlbench/vectordata/RecordBindingTest.java index 306b61ebf..398e32244 100644 --- a/nb-vectordata/src/test/java/io/nosqlbench/vectordata/RecordBindingTest.java +++ b/nb-vectordata/src/test/java/io/nosqlbench/vectordata/RecordBindingTest.java @@ -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 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