feat(virtdata): render records and derive CQL columns from a layout

RecordText renders each record of a facet in a named vernacular — the
rendering contract beside the binding one, for a statement whose shape
changes per cycle. CqlColumns derives CQL column definitions from a
record layout, typed from the bind types so the columns accept what the
binders produce, and the recordLayout, recordFields, and recordColumns
expression functions expose the layout to a workload at load time.
This commit is contained in:
Jonathan Shook committed 2026-09-05 00:02:51 +00:00
1 parent d461f5831b
commit 166d297bde
4 files changed
+229

No files matched your search

@@ -33,6 +33,10 @@ import io.nosqlbench.vectordata.TestDataView;
import io.nosqlbench.vectordata.VectorReader;
import io.nosqlbench.vectordata.VvecReader;
import io.nosqlbench.vectordata.WholeFacetFallback;
import io.nosqlbench.vectordata.anode.MNode;
import io.nosqlbench.vectordata.binding.Layout;
import io.nosqlbench.vectordata.records.RecordFacet;
import io.nosqlbench.virtdata.lib.vectors.vectordata.CqlColumns;
import io.nosqlbench.virtdata.lib.vectors.vectordata.PrefetchMeter;
import io.nosqlbench.virtdata.lib.vectors.vectordata.WindowedReader;
import org.apache.logging.log4j.LogManager;
@@ -242,4 +246,36 @@ public class VectorDataExprs implements ExprFunctionProvider {
"[" + start.longValue() + ".." + end.longValue() + ")");
}
@ExprExample(args = {"\"airports:demo\"", "\"metadata_content\""}, expectNotNull = true)
@ExprFunctionSpec(
name = "recordLayout",
synopsis = "recordLayout(\"dataset:profile\", \"facet_name\")",
description = "Return the field layout of a facet of opaque records — names in wire order, with the bind type of each — learned from its first record."
)
public Layout recordLayout(String datasetNameAndProfile, String facetName) {
return Layout.discover(dataset(datasetNameAndProfile).openFacetRecords(facetName));
}
@ExprExample(args = {"\"airports:demo\"", "\"metadata_content\""}, matches = ".+")
@ExprFunctionSpec(
name = "recordFields",
synopsis = "recordFields(\"dataset:profile\", \"facet_name\")",
description = "Return the field names of a facet of opaque records as a comma-separated list, in wire order — the column list an INSERT names."
)
public String recordFields(String datasetNameAndProfile, String facetName) {
return String.join(", ", recordLayout(datasetNameAndProfile, facetName).names());
}
@ExprExample(args = {"\"airports:demo\"", "\"metadata_content\""}, matches = ".+")
@ExprFunctionSpec(
name = "recordColumns",
synopsis = "recordColumns(\"dataset:profile\", \"facet_name\")",
description = "Return CQL column definitions for a facet of opaque records — 'name type, name type, ...' in wire order, typed from the bind types so the columns accept what RecordField and RecordFields bind. Container element types come from the first record."
)
public String recordColumns(String datasetNameAndProfile, String facetName) {
RecordFacet facet = dataset(datasetNameAndProfile).openFacetRecords(facetName);
Layout layout = Layout.discover(facet);
MNode sample = facet.count() == 0 ? null : MNode.fromBytes(facet.recordBytes(0));
return CqlColumns.columns(layout, sample);
}
}
@@ -0,0 +1,95 @@
package io.nosqlbench.virtdata.lib.vectors.vectordata;
/*
* Copyright (c) nosqlbench
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import io.nosqlbench.vectordata.anode.MNode;
import io.nosqlbench.vectordata.anode.MValue;
import io.nosqlbench.vectordata.binding.BindType;
import io.nosqlbench.vectordata.binding.Layout;
import java.util.ArrayList;
import java.util.List;
/// CQL column definitions derived from a record layout — the schema a
/// table needs to hold what [RecordFields] binds, typed from the bind
/// types rather than the schema vernacular so that what the columns
/// accept is what the projections produce: a `Half` is a `float`, a
/// `Millis` a `timestamp`, a `DateTime` the text it is on the wire. A
/// container's element type comes from a sample record when the layout
/// alone cannot say, and is `text` when no sample can either.
public final class CqlColumns {
private CqlColumns() { }
/// `name type, name type, …` for every field of the layout, in
/// wire order, element types read from `sample` where a tag alone
/// leaves them open. `sample` may be `null`.
public static String columns(Layout layout, MNode sample) {
List<String> out = new ArrayList<>();
for (int i = 0; i < layout.names().size(); i++) {
String name = layout.names().get(i);
MValue value = sample == null ? null : sample.get(name);
out.add(name + " " + cqlType(layout.types().get(i), value));
}
return String.join(", ", out);
}
/// The CQL type a bind type maps to, with `sample` resolving a
/// container's element types when it holds any.
public static String cqlType(BindType type, MValue sample) {
return switch (type.kind()) {
case TEXT, TIMESTAMP_TEXT, NULL -> "text";
case ASCII -> "ascii";
case BOOL -> "boolean";
case BLOB, ULID -> "blob";
case INT16 -> "smallint";
case INT32 -> "int";
case INT64 -> "bigint";
case FLOAT16, FLOAT32 -> "float";
case FLOAT64 -> "double";
case DECIMAL -> "decimal";
case VARINT -> "varint";
case DATE -> "date";
case TIME -> "time";
case TIMESTAMP_MILLIS, TIMESTAMP_NANOS -> "timestamp";
case TIME_UUID -> "timeuuid";
case UUID -> "uuid";
case LIST -> "list<" + element(type.element(), firstItem(sample)) + ">";
case SET -> "set<" + element(type.element(), firstItem(sample)) + ">";
case MAP -> {
MValue key = null, value = null;
if (sample instanceof MValue.MapValue m && !m.node().fields().isEmpty()) { key = new MValue.Text(""); value = m.node().fields().values().iterator().next(); }
if (sample instanceof MValue.TypedMap t && !t.entries().isEmpty()) { key = t.entries().get(0).key(); value = t.entries().get(0).value(); }
yield "map<" + element(type.key(), key) + ", " + element(type.value(), value) + ">";
}
};
}
private static String element(BindType declared, MValue sample) {
if (declared != null) return cqlType(declared, sample);
if (sample != null) return cqlType(BindType.ofTag(sample.tag()), sample);
return "text";
}
private static MValue firstItem(MValue sample) {
if (sample instanceof MValue.ListValue l && !l.items().isEmpty()) return l.items().get(0);
if (sample instanceof MValue.SetValue s && !s.items().isEmpty()) return s.items().get(0);
if (sample instanceof MValue.ArrayValue a && !a.items().isEmpty()) return a.items().get(0);
return null;
}
}
@@ -0,0 +1,76 @@
package io.nosqlbench.virtdata.lib.vectors.vectordata;
/*
* Copyright (c) nosqlbench
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import io.nosqlbench.vectordata.Catalog;
import io.nosqlbench.vectordata.CatalogSources;
import io.nosqlbench.vectordata.FacetNames;
import io.nosqlbench.vectordata.VectorDataSettings;
import io.nosqlbench.vectordata.anode.Vernacular;
import io.nosqlbench.vectordata.records.Codecs;
import io.nosqlbench.vectordata.records.RecordCodec;
import io.nosqlbench.vectordata.records.Records;
import io.nosqlbench.virtdata.api.annotations.Categories;
import io.nosqlbench.virtdata.api.annotations.Category;
import io.nosqlbench.virtdata.api.annotations.Example;
import io.nosqlbench.virtdata.api.annotations.ThreadSafeMapper;
import java.util.Arrays;
import java.util.function.LongFunction;
/*
* Renders each record of a facet of opaque records as text in a
* vernacular — 'cql', 'sql', 'json', 'jsonl', 'yaml', 'cddl',
* 'readout', 'display', and their '-schema' and '-value' forms. A
* metadata record renders as a values tuple and a predicate as a WHERE
* expression with its comparands inlined. Rendering is the other
* contract from binding: it is the right form for a statement whose
* shape changes per cycle, such as a predicate facet whose filters
* differ from query to query, and the wrong one for a prepared
* statement, which binds through RecordFields instead. The cycle value
* is the record ordinal. */
@ThreadSafeMapper
@Categories(Category.vectors)
public class RecordText implements LongFunction<String> {
private final Records<String> records;
@Example({"RecordText('exampledataset:exampleprofile','metadata_predicates','cql')",
"Render each predicate as a CQL WHERE expression, ready to inline into a statement"})
@Example({"RecordText('exampledataset:exampleprofile','metadata_content','json')",
"Render each metadata record as a JSON object"})
public RecordText(String datasetAndProfile, String facetName, String vernacular) {
this(datasetAndProfile, facetName, vernacular, VectorDataSettings.defaults());
}
/** Construct with explicit vectordata settings, including an isolated cache location. */
public RecordText(String datasetAndProfile, String facetName, String vernacular, VectorDataSettings settings) {
RecordCodec<String> codec = Codecs.byName(vernacular);
if (codec == null)
throw new RuntimeException("Unknown vernacular '" + vernacular + "': expected one of " + Arrays.toString(Vernacular.values()));
records = Catalog.of(CatalogSources.defaults(), settings).openProfile(datasetAndProfile)
.openFacetRecords(FacetNames.canonical(facetName)).decode(codec);
}
/// The number of records the facet holds.
public long count() { return records.count(); }
@Override
public String apply(long ordinal) { return records.get(ordinal); }
}
@@ -193,6 +193,28 @@ public class RecordBindingFunctionsTest {
assertTrue(e.available().contains("id"), "naming what the facet has");
}
@Test
void recordTextRendersAVernacularPerRecord() {
RecordText cql = new RecordText("records:default", "metadata_predicates", "cql", settings);
assertEquals("created >= " + (EPOCH_MILLIS + 3) + " AND tag = 't0'", cql.apply(3), "a predicate as a bare WHERE clause, comparands inlined");
assertEquals(20, cql.count());
RecordText json = new RecordText("records:default", "metadata_content", "jsonl", settings);
assertTrue(json.apply(1).startsWith("{\"id\":1,\"tag\":\"t1\""), json.apply(1));
RuntimeException unknown = assertThrows(RuntimeException.class, () -> new RecordText("records:default", "metadata_content", "klingon", settings));
assertTrue(unknown.getMessage().contains("cql") && unknown.getMessage().contains("json"), unknown.getMessage());
}
@Test
void cqlColumnsFollowTheLayoutAndTheSample() {
byte[] record = row(1);
String columns = CqlColumns.columns(io.nosqlbench.vectordata.binding.Layout.discover(record), MNode.fromBytes(record));
assertEquals("id bigint, tag text, score double, created timestamp, owner uuid, flags list<bigint>, h float, n timestamp, count int", columns);
assertEquals("list<text>", CqlColumns.cqlType(BindType.list(null), null), "an element type nothing determines is text");
assertEquals("map<text, text>", CqlColumns.cqlType(BindType.map(null, null), null));
assertEquals("smallint", CqlColumns.cqlType(BindType.INT16, null));
assertEquals("text", CqlColumns.cqlType(BindType.TIMESTAMP_TEXT, null), "text on the wire, text in the column");
}
// ── fixture writers ────────────────────────────────────────────
static void fvec(Path path, float[][] vectors) throws IOException {