tursodatabase/turso · error · InvalidOperationException

Function {function} was called with a NULL value at ordinal

Error message

Function {function} was called with a NULL value at ordinal {ordinal}.

What it means

A user-defined function registered with CreateFunction using a typed delegate (e.g. Func<int, int, int>) received SQL NULL at an argument position whose .NET type is a non-nullable value type (int, long, double, etc., not int? or object). ConvertArgument cannot map NULL/DBNull onto a non-nullable value type, so it throws InvalidOperationException naming the function and the 0-based ordinal. Nullable value types (int?) and reference types (string, byte[], object) accept NULL and return null/default instead.

Source

Thrown at bindings/dotnet/src/Turso.Data.Sqlite/SqliteConnection.Functions.cs:81

        => function(ConvertArgument<T1>(name, args[0], 0), ConvertArgument<T2>(name, args[1], 1));

    private static object? InvokeTypedFunction<TState, TResult>(TState state, Func<TState, TResult> function, object?[] args)
        => function(state);

    private static object? InvokeTypedFunction<TState, T1, TResult>(string name, TState state, Func<TState, T1, TResult> function, object?[] args)
        => function(state, ConvertArgument<T1>(name, args[0], 0));

    private static object? InvokeTypedFunction<TState, T1, T2, TResult>(string name, TState state, Func<TState, T1, T2, TResult> function, object?[] args)
        => function(state, ConvertArgument<T1>(name, args[0], 0), ConvertArgument<T2>(name, args[1], 1));

    private static T ConvertArgument<T>(string functionName, object? value, int ordinal)
    {
        if (value is null or DBNull)
        {
            if (!typeof(T).IsValueType || Nullable.GetUnderlyingType(typeof(T)) is not null)
                return default!;

            throw new InvalidOperationException(Properties.Resources.UDFCalledWithNull(functionName, ordinal));
        }

        var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
        if (targetType == typeof(object))
            return (T)value;
        if (targetType == typeof(byte[]) && value is byte[] bytes)
            return (T)(object)bytes;
        if (targetType == typeof(string))
            return (T)(object)Convert.ToString(value, CultureInfo.InvariantCulture)!;
        if (targetType == typeof(bool))
            return (T)(object)Convert.ToBoolean(value, CultureInfo.InvariantCulture);

        return (T)Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
    }

    private static TursoExtensionValue InvokeScalarFunction(IntPtr context, int argc, IntPtr argv, IntPtr contextDestructor, IntPtr valueDestructor)
    {
        try

View on GitHub (pinned to 6c72522679)

Solutions

  1. Change the UDF's argument type to its nullable form (Func<int?, ...>) or use object?/byte[]? and handle null inside the function.
  2. Filter NULLs at the call site: 'SELECT myfn(col) FROM t WHERE col IS NOT NULL' or wrap the argument in COALESCE(col, 0).
  3. Fix the schema: add a NOT NULL constraint or backfill the column if NULLs are not intended.

Example fix

// before
conn.CreateFunction<long, long>("double", v => v * 2);
cmd.CommandText = "SELECT double(x) FROM t"; // x is NULL -> throws

// after
conn.CreateFunction<long?, long?>("double", v => v is null ? null : v * 2);
cmd.CommandText = "SELECT double(x) FROM t";
Defensive patterns

Strategy: type-guard

Validate before calling

static void RegisterSafeFunction(SqliteConnection conn, string name, Func<object?, long?> f)
{
    conn.CreateFunction<object?, long?>(name, f); // object? accepts NULL instead of throwing
}

// or filter/replace NULLs in SQL before they reach the UDF:
// SELECT myfn(COALESCE(nullable_col, 0)) FROM t;
// SELECT myfn(col) FROM t WHERE col IS NOT NULL;

Type guard

static bool IsSqlNull(object? value) => value is null or DBNull;

// inside an object?-typed UDF:
// if (IsSqlNull(args[0])) return null;

Try / catch

null

Prevention

When it happens

Trigger: 'SELECT myfn(NULL)' where myfn was registered as Func<long, long>; passing a column that contains NULLs (e.g. 'SELECT myfn(nullable_col) FROM t') to a function whose argument type is a plain value type; JOIN/WHERE shapes that yield NULL for a missing side; COALESCE omitted because the developer assumed the column was NOT NULL.

Common situations: Schema drift where a column becomes nullable (migration adds rows with NULL), testing only with clean seed data, porting UDFs from providers or databases where NULL arguments were silently coerced to 0.

Related errors


AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-08-20). Data as JSON: /api/errors/32c8efabefbc04fa. Report an issue: GitHub.