mirror of
https://github.com/nosqlbench/nosqlbench.git
synced 2026-10-12 09:24:52 -05:00
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:
1 parent
d461f5831b
commit
166d297bde
4 files changed
+229
No files matched your search
+36
@@ -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);
|
||||
}
|
||||
}
|
||||
+95
@@ -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;
|
||||
}
|
||||
}
|
||||
+76
@@ -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); }
|
||||
}
|
||||
+22
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user