File: Storage\SQLite\v2\Interop\SqlStatement.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Text;
using Microsoft.CodeAnalysis.SQLite.Interop;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.SQLite.v2.Interop;
 
/// <summary>
/// Represents a prepared sqlite statement.  <see cref="SqlStatement"/>s can be 
/// <see cref="Step"/>ed (i.e. executed).  Executing a statement can result in 
/// either <see cref="Result.DONE"/> if the command completed and produced no
/// value, or <see cref="Result.ROW"/> if it evaluated out to a sql row that can
/// then be queried.
/// <para>
/// If a statement is parameterized then parameters can be provided by the 
/// BindXXX overloads.  Bind is 1-based (to match sqlite).</para>
/// <para>
/// When done executing a statement, the statement should be <see cref="Reset"/>.
/// The easiest way to ensure this is to just use a 'using' statement along with
/// a <see cref="ResettableSqlStatement"/>.  By resetting the statement, it can
/// then be used in the future with new bound parameters.</para>
/// <para>
/// Finalization/destruction of the underlying raw sqlite statement is handled
/// by <see cref="SqlConnection.Close_OnlyForUseBySQLiteConnectionPool"/>.</para>
/// </summary>
internal readonly struct SqlStatement(SqlConnection connection, SafeSqliteStatementHandle statement)
{
    internal void Close_OnlyForUseBySqlConnection()
        => statement.Dispose();
 
    public void ClearBindings()
        => connection.ThrowIfNotOk(NativeMethods.sqlite3_clear_bindings(statement));
 
    public void Reset()
        => connection.ThrowIfNotOk(NativeMethods.sqlite3_reset(statement));
 
    public Result Step(bool throwOnError = true)
    {
        var stepResult = NativeMethods.sqlite3_step(statement);
 
        // Anything other than DONE or ROW is an error when stepping.
        // throw if the caller wants that, or just return the value
        // otherwise.
        if (stepResult is not Result.DONE and not Result.ROW)
        {
            if (throwOnError)
            {
                connection.Throw(stepResult);
                throw ExceptionUtilities.Unreachable();
            }
        }
 
        return stepResult;
    }
 
    internal void BindStringParameter(int parameterIndex, string value)
    {
        const int OptimizedLengthThreshold = 2048;
 
        // Attempt to stackalloc UTF-8 converted small strings to avoid lots of allocs.
        // This code can be removed once we move to a build of sqlitepcl that contains:
        // https://github.com/ericsink/SQLitePCL.raw/pull/401
 
        // This is safe as sqlite will still copy our bytes over to its own internal memory (see
        // conversation here: https://github.com/dotnet/roslyn/pull/51111#pullrequestreview-588169715)
        // on the topic.  So it's fine that our own stackalloc'ed bytes will be gone when this function
        // returns and our caller continues to interact with this SqlStatement instance.
 
        // Only do this for short strings anyways.  If the string has more characters than this threshold, then it
        // will certainly have more bytes than this threshold.
        if (value.Length <= OptimizedLengthThreshold)
        {
            // Now see if the UTF-8 encoded versions is also within the threshold.  As almost all of our strings are
            // ascii, this will be true for nearly all of them.
            var utf8ByteCount = Encoding.UTF8.GetByteCount(value);
            if (utf8ByteCount <= OptimizedLengthThreshold)
            {
                Span<byte> bytes = stackalloc byte[utf8ByteCount];
#if NET
                Contract.ThrowIfFalse(Encoding.UTF8.GetBytes(value.AsSpan(), bytes) == utf8ByteCount);
#else
                unsafe
                {
                    fixed (char* charsPtr = value)
                    fixed (byte* bytesPtr = bytes)
                    {
                        Contract.ThrowIfFalse(Encoding.UTF8.GetBytes(charsPtr, value.Length, bytesPtr, utf8ByteCount) == utf8ByteCount);
                    }
                }
#endif
                connection.ThrowIfNotOk(NativeMethods.sqlite3_bind_text(statement, parameterIndex, bytes));
                return;
            }
        }
 
        connection.ThrowIfNotOk(NativeMethods.sqlite3_bind_text(statement, parameterIndex, value));
    }
 
    internal void BindInt64Parameter(int parameterIndex, long value)
        => connection.ThrowIfNotOk(NativeMethods.sqlite3_bind_int64(statement, parameterIndex, value));
 
    internal void BindBlobParameter(int parameterIndex, ReadOnlySpan<byte> bytes)
        => connection.ThrowIfNotOk(NativeMethods.sqlite3_bind_blob(statement, parameterIndex, bytes));
 
    internal int GetInt32At(int columnIndex)
        => NativeMethods.sqlite3_column_int(statement, columnIndex);
 
    internal long GetInt64At(int columnIndex)
        => NativeMethods.sqlite3_column_int64(statement, columnIndex);
 
    internal string GetStringAt(int columnIndex)
        => NativeMethods.sqlite3_column_text(statement, columnIndex);
}