Skip to content

Commit 574cadd

Browse files
rustyconoverclaude
andcommitted
feat(catalog): worker-settable per-schema tags
The vgi-lint metadata linter expects schema-level tags (vgi.description_llm / vgi.description_md, surfaced via duckdb_schemas().tags), but the worker SDK had no way to set them: catalog_schemas hard-coded SchemaInfo.tags to Map.of(). (The catalog source_url half of the Rust fix was already wired in Java: Worker.sourceUrl() flows through CatalogInfoSerializer, which emits the source_url field rather than null.) - Worker: add schemaTags(schema, Map) builder + schemaTags() accessor, paralleling schemaComment/schemaComments. - VgiServiceImpl: add tags to the internal SchemaDesc, populate it in workerSchemas() from Worker.schemaTags(), and surface it via SchemaInfo.tags in catalog_schemas / catalog_schema_get (was Map.of()). - Add SchemaInfoTagsTest covering tag flow-through, merge, and the empty-by-default case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 64d4f4f commit 574cadd

3 files changed

Lines changed: 107 additions & 5 deletions

File tree

vgi/src/main/java/farm/query/vgi/Worker.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public final class Worker {
4040
private final Map<String, String> catalogTags = new LinkedHashMap<>();
4141
private String defaultSchema = "main";
4242
private final Map<String, String> schemaComments = new LinkedHashMap<>();
43+
private final Map<String, Map<String, String>> schemaTags = new LinkedHashMap<>();
4344
private String implementationVersion;
4445
private String dataVersionSpec;
4546
private final List<CatalogDataVersionRelease> releases = new ArrayList<>();
@@ -316,6 +317,29 @@ public Worker schemaComment(String schema, String comment) {
316317
*/
317318
public Map<String, String> schemaComments() { return schemaComments; }
318319

320+
/**
321+
* Attach key/value metadata tags to a schema, surfaced via
322+
* {@code catalog_schemas} / {@code catalog_schema_get} and reported through
323+
* DuckDB's {@code duckdb_schemas().tags}. Typical keys are
324+
* {@code vgi.description_llm} and {@code vgi.description_md}. Merged into any
325+
* tags previously set for the schema (later calls overwrite duplicate keys).
326+
*
327+
* @param schema the schema name
328+
* @param tags schema tag key/value pairs to merge in
329+
* @return this builder
330+
*/
331+
public Worker schemaTags(String schema, Map<String, String> tags) {
332+
schemaTags.computeIfAbsent(schema, k -> new LinkedHashMap<>()).putAll(tags);
333+
return this;
334+
}
335+
336+
/**
337+
* Tags registered via {@link #schemaTags(String, Map)}.
338+
*
339+
* @return the per-schema tags keyed by schema name
340+
*/
341+
public Map<String, Map<String, String>> schemaTags() { return schemaTags; }
342+
319343
/**
320344
* An auxiliary catalog served by the same worker process next to the main
321345
* catalog, MetaWorker-style: it appears as its own row in

vgi/src/main/java/farm/query/vgi/internal/VgiServiceImpl.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -989,7 +989,7 @@ public ItemsResponse catalog_schemas(byte[] attach_opaque_data, byte[] transacti
989989
List<byte[]> items = new ArrayList<>();
990990
for (SchemaDesc s : workerSchemas()) {
991991
items.add(RecordCodec.serializeToBytes(
992-
new SchemaInfo(s.comment, Map.of(), attach_opaque_data, s.name, schemaCounts(s))));
992+
new SchemaInfo(s.comment, s.tags, attach_opaque_data, s.name, schemaCounts(s))));
993993
}
994994
return new ItemsResponse(items);
995995
}
@@ -1014,14 +1014,14 @@ public ItemsResponse catalog_schema_get(byte[] attach_opaque_data, String name,
10141014
for (SchemaDesc s : workerSchemas()) {
10151015
if (s.name.equals(name)) {
10161016
return new ItemsResponse(List.of(RecordCodec.serializeToBytes(
1017-
new SchemaInfo(s.comment, Map.of(), attach_opaque_data, name, schemaCounts(s)))));
1017+
new SchemaInfo(s.comment, s.tags, attach_opaque_data, name, schemaCounts(s)))));
10181018
}
10191019
}
10201020
return ItemsResponse.empty();
10211021
}
10221022

10231023
/** Schema descriptors registered with the worker (default + auxiliary). */
1024-
private record SchemaDesc(String name, String comment) {}
1024+
private record SchemaDesc(String name, String comment, Map<String, String> tags) {}
10251025

10261026
/**
10271027
* The auxiliary catalog an attach belongs to, or {@code null} for the main
@@ -1107,9 +1107,11 @@ private Map<String, Long> extraSchemaCounts(Worker.ExtraCatalog extra) {
11071107
private List<SchemaDesc> workerSchemas() {
11081108
List<SchemaDesc> result = new ArrayList<>();
11091109
Map<String, String> comments = worker.schemaComments();
1110+
Map<String, Map<String, String>> tags = worker.schemaTags();
11101111
String defaultSchema = worker.defaultSchema();
11111112
result.add(new SchemaDesc(defaultSchema,
1112-
comments.getOrDefault(defaultSchema, "Default schema")));
1113+
comments.getOrDefault(defaultSchema, "Default schema"),
1114+
tags.getOrDefault(defaultSchema, Map.of())));
11131115
// The minimal `versioned` fixture exposes only its default schema
11141116
// (vgi-python parity); the example worker's auxiliary `data` schema is
11151117
// not part of it. attach/versioning_http.test asserts exactly one schema.
@@ -1124,7 +1126,10 @@ private List<SchemaDesc> workerSchemas() {
11241126
for (var m : worker.macros()) {
11251127
if (!defaultSchema.equals(m.schema())) extras.add(m.schema());
11261128
}
1127-
for (String s : extras) result.add(new SchemaDesc(s, comments.getOrDefault(s, "")));
1129+
for (String s : extras) {
1130+
result.add(new SchemaDesc(s, comments.getOrDefault(s, ""),
1131+
tags.getOrDefault(s, Map.of())));
1132+
}
11281133
return result;
11291134
}
11301135

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2026 Query Farm LLC - https://query.farm
2+
3+
package farm.query.vgi.internal;
4+
5+
import farm.query.vgi.Worker;
6+
import farm.query.vgi.protocol.ItemsResponse;
7+
import farm.query.vgi.protocol.SchemaInfo;
8+
import farm.query.vgirpc.marshal.RecordCodec;
9+
import org.junit.jupiter.api.Test;
10+
11+
import java.util.List;
12+
import java.util.Map;
13+
14+
import static org.junit.jupiter.api.Assertions.assertEquals;
15+
import static org.junit.jupiter.api.Assertions.assertTrue;
16+
17+
/**
18+
* Verifies that per-schema tags declared on the {@link Worker} via
19+
* {@link Worker#schemaTags(String, Map)} flow through the
20+
* {@code catalog_schemas} enumeration onto the wire {@link SchemaInfo#tags()}
21+
* (surfaced by DuckDB as {@code duckdb_schemas().tags}). The default — no tags
22+
* configured — yields an empty tag map.
23+
*/
24+
class SchemaInfoTagsTest {
25+
26+
private static VgiServiceImpl service(Worker w) {
27+
// sealOpaqueData=false ⇒ stdio/AF_UNIX contract: the attach token is the
28+
// plaintext id, so we can drive catalog_schemas directly with any bytes.
29+
return new VgiServiceImpl(w, List.of(), List.of(), List.of(), List.of(), false);
30+
}
31+
32+
private static SchemaInfo onlySchema(ItemsResponse resp) {
33+
List<byte[]> items = resp.items();
34+
assertEquals(1, items.size(), "expected exactly one schema");
35+
return RecordCodec.deserializeFromBytes(items.get(0), SchemaInfo.class);
36+
}
37+
38+
@Test
39+
void schemaTagsSurfaceOnSchemaInfo() {
40+
Worker w = Worker.builder()
41+
.schemaTags("main", Map.of(
42+
"vgi.description_llm", "Main schema for testing.",
43+
"vgi.description_md", "# Main\nMain schema."));
44+
45+
SchemaInfo info = onlySchema(service(w).catalog_schemas("attach".getBytes(), null));
46+
47+
assertEquals("main", info.name());
48+
assertEquals("Main schema for testing.", info.tags().get("vgi.description_llm"));
49+
assertEquals("# Main\nMain schema.", info.tags().get("vgi.description_md"));
50+
assertEquals(2, info.tags().size());
51+
}
52+
53+
@Test
54+
void schemaTagsMergeAcrossCalls() {
55+
Worker w = Worker.builder()
56+
.schemaTags("main", Map.of("a", "1"))
57+
.schemaTags("main", Map.of("b", "2"));
58+
59+
SchemaInfo info = onlySchema(service(w).catalog_schemas("attach".getBytes(), null));
60+
61+
assertEquals("1", info.tags().get("a"));
62+
assertEquals("2", info.tags().get("b"));
63+
}
64+
65+
@Test
66+
void noTagsConfiguredYieldsEmptyMap() {
67+
SchemaInfo info = onlySchema(
68+
service(Worker.builder()).catalog_schemas("attach".getBytes(), null));
69+
70+
assertEquals("main", info.name());
71+
assertTrue(info.tags().isEmpty(), "tags should default to empty");
72+
}
73+
}

0 commit comments

Comments
 (0)