Skip to content

Commit c480086

Browse files
rustyconoverclaude
andcommitted
Serve canonical constraint records the extension will read
vgi.v3 replaced the descriptor constraint fields with provider-owned ConstraintInfo records, but Java left a worker only raw nested byte[] plumbing, negotiated the read profile only for the main catalog through a hand-written selector, and never checked the rules the extension enforces at ATTACH, so a mistake surfaced as a failed ATTACH rather than a worker error. - ConstraintRecord: typed record with a builder (NOT_NULL incl. nested paths, PRIMARY_KEY, UNIQUE, CHECK, FOREIGN_KEY), explicit enforcement and validation (no default attestation), encode()/decode(). CatalogTable gains Builder#constraints(List<ConstraintRecord>, rev) and withConstraintRecords. - Every catalog path (main, auxiliary, hosted CatalogInterface) advertises vgi.constraint.records.read.v1 + vgi.expression.duckdb.scalar.v1 when its tables carry records and selects what the client's offer allows (scalar only with records), merged into any explicit catalogCapabilities decision. Hosted catalogs get CatalogInterface#catalogReadProfiles (default: scan). - TableInfoSerializer refuses, naming table and column path: records that are not ENFORCED + ALL_ROWS, records owned by another table, duplicate ids, and Arrow nullability that disagrees with NOT_NULL/PRIMARY_KEY (every table, recursing into ordinary STRUCTs, skipping the row-id column). - Legacy descriptor constraint fields still fail, now naming the replacement. - The example worker drops its hand-written negotiation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2f4462a commit c480086

12 files changed

Lines changed: 1169 additions & 28 deletions

File tree

‎vgi-example-worker/src/main/java/farm/query/vgi/example/FixtureConstraints.java‎

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,6 @@ private static byte[] ref(String kind, String name, String owner) {
2020
return RecordCodec.serializeToBytes(new CatalogObjectRef(kind, List.of("data"), name, owner));
2121
}
2222

23-
static farm.query.vgi.Worker.CatalogCapabilitySelection negotiate(CatalogAttachRequest request) {
24-
if (!"example".equals(request.name())) return new farm.query.vgi.Worker.CatalogCapabilitySelection(null, null);
25-
var read = List.of("vgi.constraint.records.read.v1", "vgi.expression.duckdb.scalar.v1");
26-
var advertised = new CatalogCapabilities(read, List.of(), List.of(), List.of());
27-
var client = request.client_capabilities() == null ? null
28-
: RecordCodec.deserializeFromBytes(request.client_capabilities(), ClientCapabilities.class);
29-
var offer = client == null || client.catalog() == null
30-
? new CatalogCapabilities(List.of(), List.of(), List.of(), List.of())
31-
: RecordCodec.deserializeFromBytes(client.catalog(), CatalogCapabilities.class);
32-
var selected = new CatalogCapabilities(read.stream().filter(offer.read()::contains)
33-
.filter(profile -> profile.equals(read.getFirst()) || offer.read().contains(read.getFirst())).sorted().toList(),
34-
List.of(), List.of(), List.of());
35-
return new farm.query.vgi.Worker.CatalogCapabilitySelection(RecordCodec.serializeToBytes(advertised),
36-
RecordCodec.serializeToBytes(selected));
37-
}
38-
3923
static List<byte[]> snapshot(String table, org.apache.arrow.vector.types.pojo.Field[] fields) {
4024
if (table.startsWith("versioned_constraints_v")) {
4125
int version = Integer.parseInt(table.substring(table.length() - 1));

‎vgi-example-worker/src/main/java/farm/query/vgi/example/Main.java‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,6 @@ private static boolean isTruthy(String v) {
366366
*/
367367
public static Worker buildWorker(String catalogName, String implVer, String dataSpec) {
368368
Worker w = Worker.builder()
369-
.catalogCapabilities(FixtureConstraints::negotiate)
370369
.catalogName(catalogName)
371370
.implementationVersion(implVer)
372371
.dataVersionSpec(dataSpec)

‎vgi-example-worker/src/test/java/farm/query/vgi/example/FixtureConstraintsTest.java‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,28 @@
1111
import farm.query.vgi.protocol.*;
1212

1313
class FixtureConstraintsTest {
14-
@Test void readsOnlySelectOfferedProfilesAndTheirPrerequisite() {
14+
@Test void exampleCatalogNegotiatesConstraintReadsWithoutProviderCode() {
1515
String read = "vgi.constraint.records.read.v1";
1616
String scalar = "vgi.expression.duckdb.scalar.v1";
17+
var worker = Main.buildWorker("example", null, null);
18+
var svc = new farm.query.vgi.internal.VgiServiceImpl(worker, worker.scalars(), worker.tables(),
19+
worker.tableInOuts(), worker.aggregates());
1720
for (var offered : List.of(List.<String>of(), List.of(scalar), List.of(read), List.of(read, scalar))) {
1821
var offer = new CatalogCapabilities(offered, List.of(), List.of("vgi.constraint.enforce.v1"), List.of());
1922
var client = new ClientCapabilities("fixture-test", List.of(), List.of(), true, List.of(), RecordCodec.serializeToBytes(offer));
20-
var request = new CatalogAttachRequest("example", null, null, null, RecordCodec.serializeToBytes(client));
21-
var selected = RecordCodec.deserializeFromBytes(FixtureConstraints.negotiate(request).selected(), CatalogCapabilities.class);
23+
var result = svc.catalog_attach(new CatalogAttachRequest("example", null, null, null, RecordCodec.serializeToBytes(client)), null);
24+
var advertised = RecordCodec.deserializeFromBytes(result.catalog_capabilities(), CatalogCapabilities.class);
25+
var selected = RecordCodec.deserializeFromBytes(result.selected_capabilities(), CatalogCapabilities.class);
26+
assertEquals(List.of(read, scalar), advertised.read());
2227
assertEquals(offered.contains(read) ? offered : List.of(), selected.read());
2328
assertTrue(selected.write().isEmpty());
2429
assertTrue(selected.ddl().isEmpty());
2530
assertTrue(selected.transaction().isEmpty());
31+
// Every served table satisfies the extension's ATTACH-time constraint rules.
32+
for (String schema : List.of("main", "data")) {
33+
assertDoesNotThrow(() -> svc.catalog_schema_contents_tables(result.attach_opaque_data(), List.of(schema), null, null));
34+
}
2635
}
27-
assertNull(FixtureConstraints.negotiate(CatalogAttachRequest.of("other", null, null, null)).selected());
2836
}
2937

3038
@Test void invalidImmutableDataCannotBecomeTrustedFacts() {

‎vgi/src/main/java/farm/query/vgi/CatalogInterface.java‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,31 @@ record AttachInfo(@Nullable String defaultSchema, @Nullable String comment, Map<
9393
*/
9494
AttachInfo attach(byte[] attachId, CatalogAttachRequest request);
9595

96+
/**
97+
* The read profiles this catalog implements for an attach, advertised on the attach result
98+
* and selected against the client's offer by the framework. Tables carrying canonical
99+
* constraint records ({@link farm.query.vgi.catalog.ConstraintRecord}) are readable only
100+
* under {@link farm.query.vgi.catalog.ConstraintRecord#CONSTRAINT_RECORDS_READ}.
101+
*
102+
* <p>Default: both constraint read profiles when any table of {@link #schemas} carries
103+
* records, found by listing every schema's tables right after {@link #attach}. Override
104+
* when that listing is expensive, or when tables gain records after attach.
105+
*
106+
* @param attachId the attach id
107+
* @return the read profiles, or empty
108+
*/
109+
default List<String> catalogReadProfiles(byte[] attachId) {
110+
for (SchemaInfo s : schemas(attachId)) {
111+
for (TableInfo t : tables(attachId, s.path())) {
112+
if (!t.constraints().isEmpty()) {
113+
return List.of(farm.query.vgi.catalog.ConstraintRecord.CONSTRAINT_RECORDS_READ,
114+
farm.query.vgi.catalog.ConstraintRecord.SCALAR_EXPRESSION_READ);
115+
}
116+
}
117+
}
118+
return List.of();
119+
}
120+
96121
/**
97122
* Release an attach. Default: nothing to release.
98123
*

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,17 +240,40 @@ public Map<String, List<farm.query.vgi.catalog.ScanBranch>> multiBranchTables()
240240

241241
private Worker() {}
242242

243-
/** Provider-owned capability advertisement and exact selection for the main catalog. */
243+
/**
244+
* Provider-owned capability advertisement and exact selection for the main catalog.
245+
*
246+
* @param advertised {@code CatalogCapabilities} IPC bytes the catalog implements, or null
247+
* @param selected {@code CatalogCapabilities} IPC bytes selected for this attach, or null
248+
*/
244249
public record CatalogCapabilitySelection(byte[] advertised, byte[] selected) {}
245250
private java.util.function.Function<farm.query.vgi.protocol.CatalogAttachRequest, CatalogCapabilitySelection>
246251
catalogCapabilitySelector;
247252

253+
/**
254+
* Sets the main catalog's own capability advertisement and selection, decided per attach.
255+
*
256+
* <p>Not needed to serve constraint records: every catalog (main, auxiliary and
257+
* {@link #registerCatalog hosted}) whose tables carry
258+
* {@link farm.query.vgi.catalog.ConstraintRecord}s advertises the constraint read profiles
259+
* and selects those the client offers automatically, merged into this decision.
260+
*
261+
* @param selector the per-attach decision
262+
* @return this builder
263+
*/
248264
public Worker catalogCapabilities(java.util.function.Function<farm.query.vgi.protocol.CatalogAttachRequest,
249265
CatalogCapabilitySelection> selector) {
250266
catalogCapabilitySelector = java.util.Objects.requireNonNull(selector);
251267
return this;
252268
}
253269

270+
/**
271+
* The main catalog's own capability decision for an attach (before the framework merges
272+
* in the constraint read profiles).
273+
*
274+
* @param request the attach request
275+
* @return the decision; both sides null when no selector is set
276+
*/
254277
public CatalogCapabilitySelection selectCatalogCapabilities(farm.query.vgi.protocol.CatalogAttachRequest request) {
255278
return catalogCapabilitySelector == null ? new CatalogCapabilitySelection(null, null)
256279
: catalogCapabilitySelector.apply(request);

‎vgi/src/main/java/farm/query/vgi/catalog/CatalogTable.java‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,26 @@ public Builder requiredFilters(List<List<List<String>>> groups) {
415415
return this;
416416
}
417417

418-
/** Sets explicit canonical records and their provider-owned revision. */
418+
/**
419+
* Sets the table's canonical constraint records and their provider-owned revision.
420+
* Each record must be owned by this table, and Arrow nullability must agree with the
421+
* {@code NOT_NULL}/{@code PRIMARY_KEY} records; both are checked when the table is served.
422+
*
423+
* @param records the records, built with {@link ConstraintRecord#builder}
424+
* @param revision provider-owned definition revision (non-empty), or {@code null}
425+
* @return this builder
426+
*/
427+
public Builder constraints(List<ConstraintRecord> records, byte[] revision) {
428+
return canonicalConstraints(ConstraintRecord.encodeAll(records), revision);
429+
}
430+
431+
/**
432+
* Sets explicit canonical records, already encoded, and their provider-owned revision.
433+
*
434+
* @param records {@code ConstraintInfo} IPC records ({@link ConstraintRecord#encode()})
435+
* @param revision provider-owned definition revision, or {@code null}
436+
* @return this builder
437+
*/
419438
public Builder canonicalConstraints(List<byte[]> records, byte[] revision) {
420439
this.constraints = List.copyOf(records);
421440
this.definitionRevision = revision;
@@ -490,7 +509,24 @@ public CatalogTable withConstraints(List<List<Integer>> pk,
490509
requiredFilters, constraints, definitionRevision);
491510
}
492511

493-
/** Returns a copy with explicit provider-owned canonical records and revision. */
512+
/**
513+
* Returns a copy with the given canonical constraint records and revision.
514+
*
515+
* @param records the records, built with {@link ConstraintRecord#builder}
516+
* @param revision provider-owned definition revision (non-empty), or {@code null}
517+
* @return a new table carrying the records
518+
*/
519+
public CatalogTable withConstraintRecords(List<ConstraintRecord> records, byte[] revision) {
520+
return withCanonicalConstraints(ConstraintRecord.encodeAll(records), revision);
521+
}
522+
523+
/**
524+
* Returns a copy with explicit provider-owned canonical records and revision.
525+
*
526+
* @param records {@code ConstraintInfo} IPC records ({@link ConstraintRecord#encode()})
527+
* @param revision provider-owned definition revision, or {@code null}
528+
* @return a new table carrying the records
529+
*/
494530
public CatalogTable withCanonicalConstraints(List<byte[]> records, byte[] revision) {
495531
return new CatalogTable(schema, name, columns, comment, tags,
496532
scanFunctionName, scanFunctionPositional, scanFunctionNamed,

0 commit comments

Comments
 (0)