diff --git a/.gitignore b/.gitignore index 496ee2c..7375804 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.DS_Store \ No newline at end of file +.DS_Store +.vs diff --git a/sandbox/ConsoleApp1/Program.cs b/sandbox/ConsoleApp1/Program.cs index 50957ca..a15d83f 100644 --- a/sandbox/ConsoleApp1/Program.cs +++ b/sandbox/ConsoleApp1/Program.cs @@ -18,11 +18,17 @@ INSERT INTO user (id, name, age) (3, 'Charlie', 25); """); +connection.ExecuteNonQuery(stackalloc char[1024], $""" + INSERT INTO user (id, name, age) + VALUES ({4}, {"Darwin"u8.ToArray():text}, {80}); + """); + using var reader = connection.ExecuteReader(""" SELECT name FROM user """); + while (reader.Read()) { Console.WriteLine($"{reader.GetString(0)}!"); diff --git a/src/CsSqlite.Unity/Assets/CsSqlite.Unity/Runtime/CsSqlite.dll b/src/CsSqlite.Unity/Assets/CsSqlite.Unity/Runtime/CsSqlite.dll index b0aa0b5..3379819 100644 Binary files a/src/CsSqlite.Unity/Assets/CsSqlite.Unity/Runtime/CsSqlite.dll and b/src/CsSqlite.Unity/Assets/CsSqlite.Unity/Runtime/CsSqlite.dll differ diff --git a/src/CsSqlite/ExecuteInterporatedStringHandler.cs b/src/CsSqlite/ExecuteInterporatedStringHandler.cs new file mode 100644 index 0000000..e650b0c --- /dev/null +++ b/src/CsSqlite/ExecuteInterporatedStringHandler.cs @@ -0,0 +1,135 @@ +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace CsSqlite; + +[InterpolatedStringHandler] +public ref struct ExecuteInterporatedStringHandler +{ + char[]? chars; + Span commandText; + public readonly ReadOnlySpan CommandText => commandText[..textWritten]; + int textWritten; + SqliteParam[] parameters; + int parameterWritten; + public readonly ReadOnlySpan Parameters => parameters[..parameterWritten]; + + public ExecuteInterporatedStringHandler(int literalLength, int formattedCount) + { + if (literalLength > 0) + { + this.commandText = this.chars = ArrayPool.Shared.Rent(literalLength + formattedCount * 3); + } + + this.parameters = formattedCount > 0 ? ArrayPool.Shared.Rent(formattedCount) : []; + } + + public ExecuteInterporatedStringHandler(int literalLength, int formattedCount, Span commandText) + { + this.commandText = literalLength + formattedCount * 3 > commandText.Length ? this.chars = ArrayPool.Shared.Rent(literalLength + formattedCount * 3) : commandText; + this.parameters = formattedCount > 0 ? ArrayPool.Shared.Rent(formattedCount) : []; + } + + public void AppendLiteral(string s) + { + s.AsSpan().CopyTo(commandText[textWritten..]); + textWritten += s.Length; + } + + public void AppendFormatted(string? s) + { + parameters[parameterWritten++] = new(SqlitePramKind.String, new(s?.AsMemory() ?? "".AsMemory())); + WriteParameterPlaceholder(); + } + + public void AppendFormatted(ReadOnlyMemory s) + { + parameters[parameterWritten++] = new(SqlitePramKind.String, new(s)); + WriteParameterPlaceholder(); + } + + public void AppendFormatted(ReadOnlyMemory s) => AppendFormatted(s, default); + + public void AppendFormatted(ReadOnlyMemory s, ReadOnlySpan format) + { + parameters[parameterWritten++] = + format.Length == 0 || format.SequenceEqual("text") ? new(SqlitePramKind.Utf8String, new(s)) : + format.SequenceEqual("blob") ? new(SqlitePramKind.Blob, new(s)) : + throw new ArgumentException($"The {nameof(format)} must be text or blob.", nameof(format)); + WriteParameterPlaceholder(); + } + + public void AppendFormatted(long value) + { + parameters[parameterWritten++] = new(SqlitePramKind.Integer, new(value)); + WriteParameterPlaceholder(); + } + + public void AppendFormatted(double value) + { + parameters[parameterWritten++] = new(SqlitePramKind.Double, new(value)); + WriteParameterPlaceholder(); + } + + void WriteParameterPlaceholder() + { + commandText[textWritten++] = ' '; + commandText[textWritten++] = '?'; + commandText[textWritten++] = ' '; + } + + public void Dispose() + { + if (chars != null) + { + ArrayPool.Shared.Return(chars); + chars = null; + } + + if (parameters != null) + { + // Clear because SqliteParam can contain managed objects + ArrayPool.Shared.Return(parameters, true); + parameters = null!; + } + } +} + +public readonly struct SqliteParam(SqlitePramKind kind, SqlitePramPayload payload) +{ + public readonly SqlitePramKind Kind = kind; + public readonly SqlitePramPayload Payload = payload; + +} + +public enum SqlitePramKind : byte +{ + Integer, + Double, + String, + Utf8String, + Blob, +} + +[StructLayout(LayoutKind.Explicit)] +public readonly struct SqlitePramPayload +{ + [FieldOffset(0)] readonly MemoryLike memoryLikeLong; + public readonly long Long => memoryLikeLong.Long; + [FieldOffset(0)] readonly MemoryLike memoryLikeDouble; + public readonly double Double => memoryLikeDouble.Long; + [FieldOffset(0)] public readonly ReadOnlyMemory String; + [FieldOffset(0)] public readonly ReadOnlyMemory BlobOrUtf8String; + + public SqlitePramPayload(long l) { this.memoryLikeLong = new(l); } + public SqlitePramPayload(double d) { this.memoryLikeDouble = new(d); } + public SqlitePramPayload(ReadOnlyMemory t) { this.String = t; } + public SqlitePramPayload(ReadOnlyMemory b) { this.BlobOrUtf8String = b; } + + private readonly struct MemoryLike(T l) + { + public readonly T Long = l; + private readonly object? dummy = null; + } +} diff --git a/src/CsSqlite/InterpolatedStringHandlerArgumentAttribute.cs b/src/CsSqlite/InterpolatedStringHandlerArgumentAttribute.cs new file mode 100644 index 0000000..9a02a39 --- /dev/null +++ b/src/CsSqlite/InterpolatedStringHandlerArgumentAttribute.cs @@ -0,0 +1,11 @@ +namespace CsSqlite; + +#if NETSTANDARD2_1 +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class InterpolatedStringHandlerArgumentAttribute : Attribute +{ + public InterpolatedStringHandlerArgumentAttribute(string argument) => Arguments = [argument]; + public InterpolatedStringHandlerArgumentAttribute(params string[] arguments) => Arguments = arguments; + public string[] Arguments { get; } +} +#endif diff --git a/src/CsSqlite/InterpolatedStringHandlerAttribute.cs b/src/CsSqlite/InterpolatedStringHandlerAttribute.cs new file mode 100644 index 0000000..abf151a --- /dev/null +++ b/src/CsSqlite/InterpolatedStringHandlerAttribute.cs @@ -0,0 +1,6 @@ +namespace CsSqlite; + +#if NETSTANDARD2_1 +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)] +internal sealed class InterpolatedStringHandlerAttribute : Attribute { } +#endif diff --git a/src/CsSqlite/SpliteParameters.cs b/src/CsSqlite/SpliteParameters.cs index 3eb4cd4..f113b19 100644 --- a/src/CsSqlite/SpliteParameters.cs +++ b/src/CsSqlite/SpliteParameters.cs @@ -43,6 +43,12 @@ public void Add(int index, long value) BindParameter(index, value); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(int index, double value) + { + BindParameter(index, value); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(int index, ReadOnlySpan text) { @@ -76,6 +82,11 @@ public void AddLiteral(int index, BindText(index, utf8Text, true); } + public void AddBytes(int index, ReadOnlySpan blob) + { + BindBlob(index, blob); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(ReadOnlySpan name, int value) { diff --git a/src/CsSqlite/SqliteConnection.cs b/src/CsSqlite/SqliteConnection.cs index 1e370b7..8a65588 100644 --- a/src/CsSqlite/SqliteConnection.cs +++ b/src/CsSqlite/SqliteConnection.cs @@ -23,7 +23,8 @@ enum State : byte public void Open() { ThrowIfDisposed(); - if (state == State.Open) return; + if (state == State.Open) + return; var buffer = ArrayPool.Shared.Rent(path.Length * 3); try @@ -111,6 +112,41 @@ public int ExecuteNonQuery(ReadOnlySpan commandText) return command.ExecuteNonQuery(); } + public int ExecuteNonQuery(ref ExecuteInterporatedStringHandler commandHandler) + { + using var command = CreateCommand(commandHandler.CommandText); + var handlerParameters = commandHandler.Parameters; + var commandParameters = command.Parameters; + for (int i = 0; i < handlerParameters.Length; i++) + { + var p = handlerParameters[i]; + switch (p.Kind) + { + case SqlitePramKind.Integer: + commandParameters.Add(i + 1, p.Payload.Long); + break; + case SqlitePramKind.Double: + commandParameters.Add(i + 1, p.Payload.Double); + break; + case SqlitePramKind.String: + commandParameters.Add(i + 1, p.Payload.String.Span); + break; + case SqlitePramKind.Utf8String: + commandParameters.Add(i + 1, p.Payload.BlobOrUtf8String.Span); + break; + case SqlitePramKind.Blob: + commandParameters.AddBytes(i + 1, p.Payload.BlobOrUtf8String.Span); + break; + } + } + return command.ExecuteNonQuery(); + } + + public int ExecuteNonQuery(Span commandText, [InterpolatedStringHandlerArgument("commandText")] ref ExecuteInterporatedStringHandler commandHandler) + { + return ExecuteNonQuery(ref commandHandler); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public SqliteReader ExecuteReader(ReadOnlySpan utf8CommandText) { @@ -127,6 +163,43 @@ public SqliteReader ExecuteReader(ReadOnlySpan commandText) return new(this, Prepare(commandText), true); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SqliteReader ExecuteReader(ref ExecuteInterporatedStringHandler commandHandler) + { + using var command = CreateCommand(commandHandler.CommandText); + var handlerParameters = commandHandler.Parameters; + var commandParameters = command.Parameters; + for (int i = 0; i < handlerParameters.Length; i++) + { + var p = handlerParameters[i]; + switch (p.Kind) + { + case SqlitePramKind.String: + commandParameters.Add(i + 1, p.Payload.String.Span); + break; + case SqlitePramKind.Utf8String: + commandParameters.Add(i + 1, p.Payload.BlobOrUtf8String.Span); + break; + case SqlitePramKind.Integer: + commandParameters.Add(i + 1, p.Payload.Long); + break; + case SqlitePramKind.Double: + commandParameters.Add(i + 1, p.Payload.Double); + break; + case SqlitePramKind.Blob: + commandParameters.AddBytes(i + 1, p.Payload.BlobOrUtf8String.Span); + break; + } + } + return command.ExecuteReader(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SqliteReader ExecuteReader(Span commandText, [InterpolatedStringHandlerArgument("commandText")] ref ExecuteInterporatedStringHandler commandHandler) + { + return ExecuteReader(ref commandHandler); + } + internal void ThrowIfDisposed() { if (IsDisposed)