Skip to content

Commit 84e8c4a

Browse files
rustyconoverclaude
andcommitted
Bump version to 0.3.4 and add cast-compatible exchange conformance tests
Add exchange_cast_compatible method (48th) to the conformance suite. Enhance conformBatchToSchema to perform actual numeric value conversion (int32/int64/float32 → float64) instead of only cloning type metadata, and validate field names match before casting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f7882a6 commit 84e8c4a

7 files changed

Lines changed: 91 additions & 13 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ jobs:
120120
run: bun install && bun run postinstall
121121

122122
- name: Install Python dependencies
123-
run: pip install "vgi-rpc[http,cli]>=0.1.13" pytest
123+
run: pip install "vgi-rpc[http,cli]>=0.1.15" pytest
124124

125125
- name: Run conformance tests
126126
run: timeout 120 python -m pytest test_ts_conformance.py -x -v

examples/conformance-protocol.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
/**
5-
* Conformance protocol — 47-method reference RPC service exercising all framework
5+
* Conformance protocol — 48-method reference RPC service exercising all framework
66
* capabilities. Used by the Python CLI to verify wire-protocol compatibility.
77
*
88
* This module exports the Protocol instance so it can be reused by both the
@@ -745,6 +745,19 @@ protocol.exchange<{ callCount: number }>("exchange_zero_columns", {
745745
},
746746
});
747747

748+
protocol.exchange<{ factor: number }>("exchange_cast_compatible", {
749+
params: {},
750+
inputSchema: SCALE_INPUT,
751+
outputSchema: SCALE_OUTPUT,
752+
init: () => ({ factor: 1.0 }),
753+
exchange: (state, input: RecordBatch, out) => {
754+
const col = input.getChildAt(0)!;
755+
const values: number[] = [];
756+
for (let i = 0; i < input.numRows; i++) values.push(col.get(i) * state.factor);
757+
out.emit({ value: values });
758+
},
759+
});
760+
748761
protocol.exchange<never>("exchange_error_on_init", {
749762
params: {},
750763
inputSchema: SCALE_INPUT,

examples/conformance.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
/**
5-
* Conformance worker — 43-method reference RPC service exercising all framework
5+
* Conformance worker — 48-method reference RPC service exercising all framework
66
* capabilities. Used by the Python CLI to verify wire-protocol compatibility.
77
*
88
* Run: bun run examples/conformance.ts

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@query-farm/vgi-rpc",
3-
"version": "0.3.3",
3+
"version": "0.3.4",
44
"license": "Apache-2.0",
55
"homepage": "https://vgi-rpc-typescript.query.farm",
66
"repository": {

src/util/conform.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,31 @@
11
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { RecordBatch, Struct, makeData, type Schema } from "@query-farm/apache-arrow";
4+
import {
5+
type DataType,
6+
RecordBatch,
7+
Struct,
8+
Type,
9+
makeData,
10+
type Schema,
11+
vectorFromArray,
12+
} from "@query-farm/apache-arrow";
13+
14+
/** Return true when the source type's values can be losslessly read and
15+
* re-encoded into the target type (e.g., int32 → float64). */
16+
function needsValueCast(src: DataType, dst: DataType): boolean {
17+
if (src.typeId === dst.typeId) return false;
18+
// Same broad family (e.g. Float → Float64) — clone is sufficient.
19+
if (src.constructor === dst.constructor) return false;
20+
return true;
21+
}
22+
23+
/** Check if a type is a numeric type we can cast between.
24+
* Uses typeId instead of instanceof because IPC-deserialized types
25+
* may be generic (e.g., Int_ instead of Int64). */
26+
function isNumeric(t: DataType): boolean {
27+
return t.typeId === Type.Int || t.typeId === Type.Float;
28+
}
529

630
/**
731
* Rebuild a batch's data to match the given schema's field types.
@@ -12,13 +36,54 @@ import { RecordBatch, Struct, makeData, type Schema } from "@query-farm/apache-a
1236
* match the writer's schema. Cloning each child Data with the schema's field
1337
* type fixes the type metadata while preserving the underlying buffers.
1438
*
15-
* This is also used to cast compatible input types (e.g., decimal→double,
16-
* int32→int64) when the input batch schema doesn't exactly match the method's
17-
* declared input schema.
39+
* This is also used to cast compatible input types (e.g., int32→float64,
40+
* float32→float64) when the input batch schema doesn't exactly match the
41+
* method's declared input schema. When the underlying buffer layout differs
42+
* (e.g., 4-byte int32 vs 8-byte float64), we read the values and build a
43+
* new vector with the target type.
1844
*/
1945
export function conformBatchToSchema(batch: RecordBatch, schema: Schema): RecordBatch {
2046
if (batch.numRows === 0) return batch;
21-
const children = schema.fields.map((f, i) => batch.data.children[i].clone(f.type));
47+
48+
// Validate field count and names match before attempting any cast.
49+
if (batch.schema.fields.length !== schema.fields.length) {
50+
throw new TypeError(
51+
`Field count mismatch: expected ${schema.fields.length}, got ${batch.schema.fields.length}`,
52+
);
53+
}
54+
for (let i = 0; i < schema.fields.length; i++) {
55+
if (batch.schema.fields[i].name !== schema.fields[i].name) {
56+
throw new TypeError(
57+
`Field name mismatch at index ${i}: expected '${schema.fields[i].name}', got '${batch.schema.fields[i].name}'`,
58+
);
59+
}
60+
}
61+
62+
const children = schema.fields.map((f, i) => {
63+
const srcChild = batch.data.children[i];
64+
const srcType = srcChild.type;
65+
const dstType = f.type;
66+
67+
if (!needsValueCast(srcType, dstType)) {
68+
return srcChild.clone(dstType);
69+
}
70+
71+
// Numeric → numeric: read values and rebuild with target type.
72+
if (isNumeric(srcType) && isNumeric(dstType)) {
73+
// Read source values via the batch's column vector.
74+
const col = batch.getChildAt(i)!;
75+
const values: number[] = [];
76+
for (let r = 0; r < batch.numRows; r++) {
77+
const v = col.get(r);
78+
values.push(typeof v === "bigint" ? Number(v) : (v as number));
79+
}
80+
return vectorFromArray(values, dstType).data[0];
81+
}
82+
83+
// Fallback: clone type metadata (works for same-layout types).
84+
return srcChild.clone(dstType);
85+
});
86+
2287
const structType = new Struct(schema.fields);
2388
const data = makeData({
2489
type: structType,

test/client.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,9 +1099,9 @@ function defineConformanceTests<TCtx>(
10991099
// -----------------------------------------------------------------
11001100

11011101
describe("TestDescribeConformance", () => {
1102-
it("verify 47 methods", async () => {
1102+
it("verify 48 methods", async () => {
11031103
const desc = await describeFactory(ctx);
1104-
expect(desc.methods.length).toBe(47);
1104+
expect(desc.methods.length).toBe(48);
11051105
expect(["Conformance", "ConformanceService"]).toContain(desc.protocolName);
11061106
});
11071107

test/http/conformance.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,12 @@ async function writeExchangeInput(values: number[]): Promise<string> {
156156
// ==========================================================================
157157

158158
describe("HTTP conformance: describe", () => {
159-
it("lists all 47 methods via HTTP", async () => {
159+
it("lists all 48 methods via HTTP", async () => {
160160
const { stdout, exitCode, stderr } = await run(cliHttp("describe"));
161161
if (exitCode !== 0) throw new Error(`exit ${exitCode}: ${stdout}\n${stderr}`);
162162
const data = JSON.parse(stdout);
163163
expect(data.protocol_name).toBe("Conformance");
164-
expect(Object.keys(data.methods).length).toBe(47);
164+
expect(Object.keys(data.methods).length).toBe(48);
165165
});
166166
});
167167

0 commit comments

Comments
 (0)