tursodatabase/turso · error · InvalidOperationException

Missing parameter values for {parameters}.

Error message

Missing parameter values for {parameters}.

What it means

Thrown when a prepared statement still has unbound SQL parameters after SqliteCommand bound everything in its Parameters collection. SQLite requires every placeholder (@name, $name, :name, or positional ?) in a statement to receive a value before execution, and Turso.Data.Sqlite enforces this at bind time (SqliteCommand.BindParameters). Note that extra parameters in the collection that match no placeholder are silently skipped, so a typo'd parameter name surfaces here as a 'missing' parameter, not as 'parameter not found'. The message names the unbound parameter, or its 1-based ordinal when the placeholder is positional (unnamed).

Source

Thrown at bindings/dotnet/src/Turso.Data.Sqlite/SqliteCommand.cs:671

            if (string.IsNullOrEmpty(parameter.ParameterName))
                throw new InvalidOperationException(Properties.Resources.RequiresSet(nameof(parameter.ParameterName)));
            if (!parameter.HasValue)
                throw new InvalidOperationException(Properties.Resources.RequiresSet(nameof(parameter.Value)));

            var parameterIndex = FindParameterIndex(statement, parameter.ParameterName, parameterCount);
            if (parameterIndex == 0)
                continue;

            TursoBindings.BindParameter(statement, parameterIndex, parameter.ToTursoValue());
            boundParameters[parameterIndex] = true;
        }

        for (var i = 1; i <= parameterCount; i++)
        {
            if (!boundParameters[i])
            {
                var parameterName = TursoBindings.GetParameterName(statement, i);
                throw new InvalidOperationException(
                    parameterName is null
                        ? Properties.Resources.MissingParameters(i)
                        : Properties.Resources.MissingParameters(parameterName));
            }
        }
    }

    private static bool IsEmptyCommand(string commandText)
    {
        foreach (var line in commandText.Split('\n'))
        {
            var trimmedLine = line.Trim();
            if (trimmedLine.Length != 0 && !trimmedLine.StartsWith("--", StringComparison.Ordinal))
                return false;
        }

        return true;
    }

View on GitHub (pinned to 6c72522679)

Solutions

  1. Add a SqliteParameter for every placeholder in the SQL text, with a name matching exactly (prefix characters @/:/$ on the SqliteParameter are tolerated, the base name must match).
  2. If the SQL was built dynamically, log the final CommandText at the error site and diff its placeholders against cmd.Parameters to find the mismatch.
  3. For positional '?' placeholders, ensure the number of parameters added is at least the number of placeholders in the statement.
  4. Null out stale parameters (cmd.Parameters.Clear()) before reusing a cached SqliteCommand so leftovers do not mask the real set.

Example fix

// before
cmd.CommandText = "SELECT * FROM users WHERE id = @id AND name = @name";
cmd.Parameters.AddWithValue("id", 42);
cmd.ExecuteReader(); // throws: Missing parameter values for @name

// after
cmd.CommandText = "SELECT * FROM users WHERE id = @id AND name = @name";
cmd.Parameters.AddWithValue("id", 42);
cmd.Parameters.AddWithValue("name", "alice");
cmd.ExecuteReader();
Defensive patterns

Strategy: validation

Validate before calling

static readonly Regex ParamRegex = new(@"[@:$]\w+", RegexOptions.Compiled);

static void EnsureParametersBound(SqliteCommand cmd)
{
    var declared = ParamRegex.Matches(cmd.CommandText)
        .Select(m => m.Value.TrimStart('@', ':', '$'))
        .Where(n => n.Length > 0)
        .Distinct(StringComparer.Ordinal)
        .ToHashSet(StringComparer.Ordinal);
    var supplied = cmd.Parameters.Cast<SqliteParameter>()
        .Select(p => p.ParameterName.TrimStart('@', ':', '$'))
        .ToHashSet(StringComparer.Ordinal);
    var missing = declared.Where(n => !supplied.Contains(n)).ToList();
    if (declared.Count == 0 && cmd.Parameters.Count > 0)
        return; // positional '?' SQL: ensure counts match instead
    if (missing.Count > 0)
        throw new InvalidOperationException($"Unbound SQL parameters: {string.Join(", ", missing)}");
}

// usage: EnsureParametersBound(cmd) before cmd.ExecuteReader()

Prevention

When it happens

Trigger: Executing 'SELECT * FROM t WHERE id = @id' without cmd.Parameters.Add(new SqliteParameter("id", value)); misspelling the parameter name relative to the SQL text (e.g. SQL uses @userId but code adds "uid"); dynamically building a WHERE clause with new placeholders while reusing a stale Parameters collection; using positional '?' placeholders where the statement declares more placeholders than parameters supplied; forgetting that each statement in a multi-statement batch contributes its own placeholders.

Common situations: Dynamic query builders (filter appended but parameter forgotten), copy-paste SQL with renamed placeholders, migrating code from providers that tolerate missing parameters by binding NULL, and ORM/query-helper bugs that cache a command but reset only part of its Parameters.

Related errors


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