Skip to content
Query.Farm
Talk with Us

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.

dotnet new console --framework net10.0
dotnet add package QueryFarm.Vgi --version 0.3.0
UpperCaseFunction.cs
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.

Nulls are explicit

Arrow carries nullability separately from values. Check IsNull(row) and append a null to the output builder when the input is null.

The complete documentation worker registers all examples, but a production worker follows the same fluent pattern:

Program.cs
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.

Keep stdout reserved

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.