1. Your first scalar function
A scalar function transforms one input row into one output value, but C# receives and returns a whole Arrow column per call.
Create the project
Section titled “Create the project”dotnet new console --framework net10.0
dotnet add package QueryFarm.Vgi --version 0.3.0
Write the function
Section titled “Write the function”using Apache.Arrow;
using QueryFarm.Vgi.Attributes;
using QueryFarm.Vgi.Scalar;
namespace QueryFarm.Vgi.DocsExamples;
public sealed class UpperCaseFunction : ScalarFn
{
public override string Name => "upper_case";
public override string Description => "Convert strings to upper case";
private void Compute([Param(Doc = "Text to convert")] StringArray value, StringArray.Builder result)
{
for (var row = 0; row < value.Length; row++)
{
if (value.IsNull(row))
{
result.AppendNull();
}
else
{
result.Append(value.GetString(row).ToUpperInvariant());
}
}
}
}
ScalarFn inspects the single Compute method. [Param] marks a per-row input Arrow array; the
unannotated builder is the output. The Arrow types form the SQL signature, and the parameter name
becomes the named SQL argument.
Arrow carries nullability separately from values. Check IsNull(row) and append a null to the
output builder when the input is null.
Register and run it
Section titled “Register and run it”The complete documentation worker registers all examples, but a production worker follows the same fluent pattern:
using QueryFarm.Vgi;
using QueryFarm.Vgi.DocsExamples;
var worker = new Worker()
.CatalogName("demo")
.DefaultSchema("main")
.RegisterCatalog(new QueryFarm.Vgi.Protocol.CatalogInfo { Name = "demo" })
.RegisterSchema("main", "Functions from the C# documentation")
.RegisterSchema("catalog", "Catalog examples")
.RegisterScalar(new UpperCaseFunction())
.RegisterTableInOut(new EchoFunction())
.RegisterAggregate(new SumFunction())
.RegisterTableBuffering(new CollectFunction());
CatalogExample.Register(worker, new NumbersFunction());
await worker.RunFromArgsAsync(args);
Build a framework-dependent executable:
dotnet build --configuration Release
Then attach it from DuckDB or Haybarn. Use the executable produced beside the .dll, not
dotnet run, so the launcher owns one stable process command.
INSTALL vgi FROM community;
LOAD vgi;
ATTACH 'launch:/absolute/path/bin/Release/net10.0/your-worker' AS demo (TYPE vgi);
SELECT demo.upper_case('hello');
The result is HELLO.
In stdio mode stdout is the wire channel; in launcher mode its first line announces the Unix socket.
Write diagnostics to Console.Error or the VGI call context, never Console.Out.
Next, write a table function or compare all five function patterns.