Skip to content

Commit 23f265e

Browse files
committed
Add argument monotonicity protocol metadata
1 parent 9478b3b commit 23f265e

11 files changed

Lines changed: 184 additions & 7 deletions

File tree

‎README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ public final class DemoWorker {
9999

100100
The `compute()` signature drives everything: `@Vector` parameters are per-row input columns, `@Const` parameters are bind-time constants, `@Setting` parameters read session settings, and the last unannotated Arrow vector is the framework-allocated output.
101101

102+
Scalar functions may override `argumentMonotonicity()` and return a
103+
`List<ArgumentMonotonicity>`. The optional list follows `argumentSpecs()`
104+
declaration order and must contain exactly one entry per declaration. Fixed,
105+
defaulted, and constant arguments each occupy one slot; a vararg declaration
106+
occupies one slot regardless of call-time expansion. Named SQL invocation order
107+
does not reorder these claims. Returning `null` makes no claims.
108+
102109
**The worker JVM needs two flags** — Apache Arrow requires access to `java.nio` internals, and the shared-memory transport uses the FFM API:
103110

104111
```

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package farm.query.vgi.example.scalar;
44

55
import farm.query.vgi.function.FunctionMetadata;
6+
import farm.query.vgi.function.ArgumentMonotonicity;
67
import farm.query.vgi.function.NullHandling;
78
import farm.query.vgi.function.Stability;
89
import farm.query.vgi.scalar.ScalarFn;
@@ -19,6 +20,10 @@ public final class NullHandlingFunction extends ScalarFn {
1920
Stability.CONSISTENT, NullHandling.SPECIAL, false, false, false, false);
2021
}
2122

23+
@Override public java.util.List<ArgumentMonotonicity> argumentMonotonicity() {
24+
return java.util.List.of(ArgumentMonotonicity.STRICTLY_INCREASING);
25+
}
26+
2227
public void compute(@Vector BigIntVector value, BigIntVector result) {
2328
int rows = value.getValueCount();
2429
for (int i = 0; i < rows; i++) {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright 2026 Query Farm LLC - https://query.farm
2+
3+
package farm.query.vgi.function;
4+
5+
/** Monotonicity of a scalar function in one argument, with all others fixed. */
6+
public enum ArgumentMonotonicity {
7+
UNKNOWN,
8+
CONSTANT,
9+
NON_DECREASING,
10+
STRICTLY_INCREASING,
11+
NON_INCREASING,
12+
STRICTLY_DECREASING;
13+
14+
/** @return the canonical uppercase VGI wire value. */
15+
public String wireName() { return name(); }
16+
}

‎vgi/src/main/java/farm/query/vgi/function/FunctionDescriptor.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ default FunctionSpec spec() {
5555
*/
5656
default List<ArgSpec> argumentSpecs() { return spec().argumentSpecs(); }
5757

58+
/**
59+
* Scalar-only monotonicity claims in {@link #argumentSpecs()} declaration
60+
* order. {@code null} makes no claims; a vararg declaration occupies one
61+
* slot regardless of call-time expansion.
62+
*
63+
* @return one claim per argument declaration, or {@code null}.
64+
*/
65+
default List<ArgumentMonotonicity> argumentMonotonicity() { return null; }
66+
5867
/**
5968
* Authoritative typed parameter defaults. When present this is exactly one
6069
* row containing only defaulted parameters in signature order; a present

‎vgi/src/main/java/farm/query/vgi/internal/FunctionInfoSerializer.java‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static farm.query.vgi.internal.IpcStructBuilder.I64;
2121
import static farm.query.vgi.internal.IpcStructBuilder.UTF8;
2222
import static farm.query.vgi.internal.IpcStructBuilder.writeNullableInt64;
23+
import static farm.query.vgi.internal.IpcStructBuilder.writeNullableStringList;
2324
import static farm.query.vgi.internal.IpcStructBuilder.listOf;
2425
import static farm.query.vgi.internal.IpcStructBuilder.listOfPrim;
2526
import static farm.query.vgi.internal.IpcStructBuilder.mapUtf8Utf8;
@@ -92,6 +93,9 @@ static void enableBadEnumNullHandling() {
9293
nullable("parameter_default_values", BINARY),
9394
STABILITY.field(true),
9495
NULL_HANDLING.field(true),
96+
new Field("argument_monotonicity",
97+
new FieldType(true, new ArrowType.List(), null),
98+
List.of(nullable("item", UTF8))),
9599
nonNull("description", UTF8),
96100
listOf("examples", new Field("item",
97101
new FieldType(true, new ArrowType.Struct(), null),
@@ -157,6 +161,7 @@ static void enableBadEnumNullHandling() {
157161
nullable("secret_name", UTF8))))));
158162

159163
static byte[] serialize(FunctionInfo info) {
164+
validateArgumentMonotonicity(info);
160165
DictionaryProvider.MapDictionaryProvider provider = new DictionaryProvider.MapDictionaryProvider();
161166
for (EnumDict d : DICTS) d.register(provider);
162167

@@ -171,6 +176,7 @@ static byte[] serialize(FunctionInfo info) {
171176
writeNullableVarBinary(v.get("parameter_default_values"), info.parameter_default_values());
172177
STABILITY.write(v.get("stability"), info.stability());
173178
NULL_HANDLING.write(v.get("null_handling"), info.null_handling());
179+
writeNullableStringList(v.get("argument_monotonicity"), info.argument_monotonicity());
174180
writeVarChar(v.get("description"), info.description());
175181
writeStringList(v.get("examples"), List.of());
176182
writeStringList(v.get("categories"), info.categories());
@@ -204,6 +210,27 @@ static byte[] serialize(FunctionInfo info) {
204210
});
205211
}
206212

213+
private static void validateArgumentMonotonicity(FunctionInfo info) {
214+
List<String> values = info.argument_monotonicity();
215+
if (values == null) return;
216+
if (!"scalar".equals(info.function_type())) {
217+
throw new IllegalArgumentException("argument_monotonicity is only valid for scalar functions");
218+
}
219+
int expected = SchemaUtil.deserializeSchema(info.arguments()).getFields().size();
220+
if (values.size() != expected) {
221+
throw new IllegalArgumentException("argument_monotonicity has " + values.size()
222+
+ " entries, expected " + expected + " declaration slots");
223+
}
224+
var allowed = java.util.Set.of("UNKNOWN", "CONSTANT", "NON_DECREASING",
225+
"STRICTLY_INCREASING", "NON_INCREASING", "STRICTLY_DECREASING");
226+
for (int i = 0; i < values.size(); i++) {
227+
if (!allowed.contains(values.get(i))) {
228+
throw new IllegalArgumentException("argument_monotonicity[" + i
229+
+ "] has unknown value " + values.get(i));
230+
}
231+
}
232+
}
233+
207234
private static Field filterIdentities(String name) {
208235
return listOf(name, new Field("item",
209236
new FieldType(true, new ArrowType.Struct(), null),

‎vgi/src/main/java/farm/query/vgi/internal/IpcStructBuilder.java‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,17 @@ public static void writeStringList(FieldVector v, List<String> values) {
296296
w.setValueCount(1);
297297
}
298298

299+
/** Write a nullable list of non-null strings into row 0. */
300+
public static void writeNullableStringList(FieldVector v, List<String> values) {
301+
ListVector lv = (ListVector) v;
302+
if (values == null) {
303+
lv.setNull(0);
304+
lv.setValueCount(1);
305+
return;
306+
}
307+
writeStringList(v, values);
308+
}
309+
299310
/**
300311
* Write a list of ints into row 0 of a {@code ListVector}. Nulls in
301312
* {@code values} are skipped.

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

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2613,7 +2613,7 @@ private FunctionInfo toTableInOutFunctionInfo(TableInOutFunction fn, String sche
26132613
return new FunctionInfo(
26142614
base.comment(), base.tags(), base.name(), base.schema_path(), base.function_type(),
26152615
base.arguments(), base.output_schema(), base.parameter_default_values(),
2616-
base.stability(), base.null_handling(),
2616+
base.stability(), base.null_handling(), base.argument_monotonicity(),
26172617
base.description(), base.examples(), base.categories(), base.projection_pushdown(),
26182618
base.filter_pushdown(), base.sampling_pushdown(), base.late_materialization(),
26192619
base.filter_semantic_profiles(), base.additional_filter_functions(),
@@ -2640,7 +2640,7 @@ private FunctionInfo toAggregateFunctionInfo(AggregateFunction<?> fn, String sch
26402640
return new FunctionInfo(
26412641
base.comment(), base.tags(), base.name(), base.schema_path(), base.function_type(),
26422642
base.arguments(), base.output_schema(), base.parameter_default_values(),
2643-
base.stability(), base.null_handling(),
2643+
base.stability(), base.null_handling(), base.argument_monotonicity(),
26442644
base.description(), base.examples(), base.categories(), base.projection_pushdown(),
26452645
base.filter_pushdown(), base.sampling_pushdown(), base.late_materialization(),
26462646
base.filter_semantic_profiles(), base.additional_filter_functions(),
@@ -2660,13 +2660,13 @@ private FunctionInfo toBufferingFunctionInfo(
26602660
BindResponse r = fn.onBind(new TableInOutBindParams(fn.name(), Arguments.empty(), null, Map.of()));
26612661
FunctionInfo base = baseFunctionInfo(fn.name(), fn.metadata(), schemaName, "table_buffering",
26622662
ArgumentSpecSerializer.toIpcBytes(fn.argumentSpecs()), bindOutput(r),
2663-
encodeParameterDefaults(fn), /*hasFinalize=*/true, 1);
2663+
encodeParameterDefaults(fn), /*hasFinalize=*/true, 1, null);
26642664
// Re-stamp the buffering-specific ordering flags (baseFunctionInfo
26652665
// hardcodes them false; they're only meaningful for TableBuffering).
26662666
return new FunctionInfo(
26672667
base.comment(), base.tags(), base.name(), base.schema_path(), base.function_type(),
26682668
base.arguments(), base.output_schema(), base.parameter_default_values(),
2669-
base.stability(), base.null_handling(),
2669+
base.stability(), base.null_handling(), base.argument_monotonicity(),
26702670
base.description(), base.examples(), base.categories(), base.projection_pushdown(),
26712671
base.filter_pushdown(), base.sampling_pushdown(), base.late_materialization(),
26722672
base.filter_semantic_profiles(), base.additional_filter_functions(),
@@ -2714,16 +2714,38 @@ private static FunctionInfo baseFunctionInfo(farm.query.vgi.function.FunctionDes
27142714
// Only TableFunction has a meaningful maxWorkers; everything else
27152715
// is single-worker by definition.
27162716
int maxWorkers = fn instanceof TableFunction tf ? (int) tf.maxWorkers() : 1;
2717+
List<String> argumentMonotonicity = null;
2718+
if (fn.argumentMonotonicity() != null) {
2719+
if (!"scalar".equals(type)) {
2720+
throw new IllegalArgumentException(
2721+
"argumentMonotonicity is only valid for scalar functions");
2722+
}
2723+
if (fn.argumentMonotonicity().size() != fn.argumentSpecs().size()) {
2724+
throw new IllegalArgumentException("argumentMonotonicity has "
2725+
+ fn.argumentMonotonicity().size() + " entries, expected "
2726+
+ fn.argumentSpecs().size() + " declaration slots");
2727+
}
2728+
argumentMonotonicity = fn.argumentMonotonicity().stream()
2729+
.map(value -> {
2730+
if (value == null) {
2731+
throw new IllegalArgumentException(
2732+
"argumentMonotonicity must not contain null entries");
2733+
}
2734+
return value.wireName();
2735+
})
2736+
.toList();
2737+
}
27172738
return baseFunctionInfo(fn.name(), fn.metadata(), schemaName, type,
27182739
ArgumentSpecSerializer.toIpcBytes(fn.argumentSpecs()), outputSchema,
27192740
encodeParameterDefaults(fn), hasFinalize,
2720-
maxWorkers);
2741+
maxWorkers, argumentMonotonicity);
27212742
}
27222743

27232744
private static FunctionInfo baseFunctionInfo(String name, FunctionMetadata md, String schemaName,
27242745
String type, byte[] arguments, byte[] outputSchema,
27252746
byte[] parameterDefaultValues,
2726-
boolean hasFinalize, int maxWorkers) {
2747+
boolean hasFinalize, int maxWorkers,
2748+
List<String> argumentMonotonicity) {
27272749
return new FunctionInfo(
27282750
null,
27292751
md.tags() == null ? Map.of() : md.tags(),
@@ -2736,6 +2758,7 @@ private static FunctionInfo baseFunctionInfo(String name, FunctionMetadata md, S
27362758
stabilityWire(md.stability()),
27372759
(BAD_ENUM_MODE && "scalar".equals(type) && "double".equals(name))
27382760
? "WEIRD" : nullHandlingWire(md.nullHandling()),
2761+
argumentMonotonicity,
27392762
md.description(),
27402763
md.examples() == null ? List.of() : md.examples(),
27412764
md.categories() == null ? List.of() : md.categories(),

‎vgi/src/main/java/farm/query/vgi/protocol/FunctionInfo.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
* @param output_schema IPC-encoded output schema.
2727
* @param stability dictionary-encoded function stability, or {@code null}.
2828
* @param null_handling dictionary-encoded null-handling policy, or {@code null}.
29+
* @param argument_monotonicity scalar-only claims in argument declaration order, or {@code null}.
2930
* @param description human-readable description.
3031
* @param examples usage examples.
3132
* @param categories classification categories.
@@ -87,6 +88,7 @@ public record FunctionInfo(
8788
@Nullable byte[] parameter_default_values,
8889
@ArrowField(ArrowFieldType.DICT_INT16_UTF8) @Nullable String stability,
8990
@ArrowField(ArrowFieldType.DICT_INT16_UTF8) @Nullable String null_handling,
91+
@Nullable List<String> argument_monotonicity,
9092
String description,
9193
List<FunctionExample> examples,
9294
List<String> categories,

‎vgi/src/test/java/farm/query/vgi/generated/VgiProtocolSchemas.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// GENERATED by vgi.codegen.java_schemas. DO NOT EDIT BY HAND.
22
//
33
// Generator: python -m vgi.codegen.java_schemas v1
4-
// Content hash: f42952c0f44f
4+
// Content hash: 994c043d7a66
55
//
66
// To regenerate:
77
// uv run --project ~/Development/vgi-python python -m vgi.codegen.java_schemas \
@@ -227,6 +227,8 @@ private static Schema functionInfo() {
227227
new DictionaryEncoding(0L, false, new ArrowType.Int(16, true))),
228228
dict("null_handling", true, new ArrowType.Utf8(),
229229
new DictionaryEncoding(0L, false, new ArrowType.Int(16, true))),
230+
f("argument_monotonicity", true, new ArrowType.List(),
231+
f("item", true, new ArrowType.Utf8())),
230232
f("description", false, new ArrowType.Utf8()),
231233
f("examples", false, new ArrowType.List(),
232234
f("item", true, new ArrowType.Struct(),
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright 2026 Query Farm LLC - https://query.farm
2+
3+
package farm.query.vgi.internal;
4+
5+
import farm.query.vgi.function.ArgSpec;
6+
import farm.query.vgi.function.ArgumentMonotonicity;
7+
import farm.query.vgi.protocol.FunctionInfo;
8+
import farm.query.vgi.scalar.ScalarFn;
9+
import farm.query.vgi.types.Schemas;
10+
import farm.query.vgirpc.wire.Allocators;
11+
import org.apache.arrow.vector.BigIntVector;
12+
import org.apache.arrow.vector.complex.ListVector;
13+
import org.junit.jupiter.api.Test;
14+
15+
import java.util.List;
16+
17+
import static org.junit.jupiter.api.Assertions.assertEquals;
18+
import static org.junit.jupiter.api.Assertions.assertThrows;
19+
import static org.junit.jupiter.api.Assertions.assertTrue;
20+
21+
class FunctionInfoArgumentMonotonicityTest {
22+
23+
static class Identity extends ScalarFn {
24+
private final List<ArgumentMonotonicity> monotonicity;
25+
26+
Identity() {
27+
this(List.of(
28+
ArgumentMonotonicity.STRICTLY_INCREASING,
29+
ArgumentMonotonicity.CONSTANT));
30+
}
31+
32+
Identity(List<ArgumentMonotonicity> monotonicity) {
33+
this.monotonicity = monotonicity;
34+
}
35+
36+
@Override public String name() { return "identity"; }
37+
@Override public String description() { return "identity"; }
38+
@Override public List<ArgSpec> argumentSpecs() {
39+
return List.of(
40+
ArgSpec.positional("value", 0, Schemas.INT64),
41+
ArgSpec.positional("offset", 1, Schemas.INT64));
42+
}
43+
@Override public List<ArgumentMonotonicity> argumentMonotonicity() {
44+
return monotonicity;
45+
}
46+
public void compute(
47+
@farm.query.vgi.scalar.Vector BigIntVector value,
48+
@farm.query.vgi.scalar.Vector BigIntVector offset,
49+
BigIntVector result) {}
50+
}
51+
52+
@Test
53+
void serializesClaimsInDeclarationOrder() {
54+
FunctionInfo info = VgiServiceImpl.scalarFunctionInfo(new Identity(), "main");
55+
assertEquals(List.of("STRICTLY_INCREASING", "CONSTANT"), info.argument_monotonicity());
56+
try (var encoded = BatchUtil.readSingleBatch(
57+
FunctionInfoSerializer.serialize(info), Allocators.root())) {
58+
var values = (ListVector) encoded.getVector("argument_monotonicity");
59+
assertEquals("STRICTLY_INCREASING", values.getObject(0).get(0).toString());
60+
assertEquals("CONSTANT", values.getObject(0).get(1).toString());
61+
}
62+
}
63+
64+
@Test
65+
void rejectsWrongDeclarationCount() {
66+
var error = assertThrows(IllegalArgumentException.class,
67+
() -> VgiServiceImpl.scalarFunctionInfo(new Identity(List.of()), "main"));
68+
assertTrue(error.getMessage().contains("expected 2 declaration slots"));
69+
}
70+
}

0 commit comments

Comments
 (0)