diff --git a/Demo/Program.cs b/Demo/Program.cs index 0887a3d..737fac5 100644 --- a/Demo/Program.cs +++ b/Demo/Program.cs @@ -1,6 +1,8 @@ using Libsql.Client; -var dbClient = DatabaseClient.Create(); +var dbClient = DatabaseClient.Create(opts => { + opts.Url = ":memory:"; +}); var rs = await dbClient.Execute("CREATE TABLE IF NOT EXISTS `users` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `height` REAL, `data` BLOB)"); @@ -12,16 +14,20 @@ Console.WriteLine(string.Join("\n", rs2.Rows.Select(row => string.Join(", ", row.Select(x => x.ToString()))))); Console.WriteLine(string.Join("\n", rs2.Rows.Select(row => string.Join(", ", row.Select(x => x.ToString()))))); -var user = ToUser(rs2.Rows.First().ToArray()); +var user = ToUser(rs2.Rows.First()); Console.WriteLine(user); -User ToUser(Value[] row) +var users = rs2.Rows.Select(ToUser); + +User ToUser(IEnumerable row) { + var rowArray = row.ToArray(); + if ( - row[0] is Integer { Value: var id } && - row[1] is Text { Value: var name } && - row[2] is Real { Value: var height } && - row[3] is Blob { Value: var data }) + rowArray[0] is Integer { Value: var id } && + rowArray[1] is Text { Value: var name } && + rowArray[2] is Real { Value: var height } && + rowArray[3] is Blob { Value: var data }) { return new User(id, name, height, data); } diff --git a/Libsql.Client/DatabaseClient.cs b/Libsql.Client/DatabaseClient.cs index 42de48f..228cde9 100644 --- a/Libsql.Client/DatabaseClient.cs +++ b/Libsql.Client/DatabaseClient.cs @@ -2,31 +2,26 @@ namespace Libsql.Client { + /// + /// Provides a static method to create an instance of . + /// public static class DatabaseClient { + /// + /// Creates a new instance of the interface. + /// + /// An optional action to configure the . + /// A new instance of the interface. + /// A client constitutes a connection to the database. + /// Thrown when is null. + /// Thrown when the database fails to open and/or connect. public static IDatabaseClient Create(Action configure = default) { var options = DatabaseClientOptions.Default; configure?.Invoke(options); if (options.Url is null) throw new ArgumentNullException(nameof(options.Url)); - return new DatabaseWrapper(options.Url); - - // if (IsInMemory(options.Url)) - // { - // return new DatabaseWrapper(":memory:"); - // } - // - // var uri = new Uri(options.Url); - // return uri.Scheme switch - // { - // "http" or "https" => throw new ArgumentException($"{uri.Scheme}:// is not yet supported"), - // "ws" or "wss" => throw new ArgumentException($"{uri.Scheme}:// is not yet supported"), - // "file" => throw new ArgumentException(options.Url), - // _ => throw new ArgumentException("Invalid scheme") - // }; + return new DatabaseWrapper(options); } - - private static bool IsInMemory(string url) => url is "" || url is ":memory:"; } } \ No newline at end of file diff --git a/Libsql.Client/DatabaseClientOptions.cs b/Libsql.Client/DatabaseClientOptions.cs index 5a19a53..6e67eb7 100644 --- a/Libsql.Client/DatabaseClientOptions.cs +++ b/Libsql.Client/DatabaseClientOptions.cs @@ -1,5 +1,8 @@ namespace Libsql.Client { + /// + /// Represents the options for configuring . + /// public class DatabaseClientOptions { private DatabaseClientOptions(string url, string authToken = null, bool useHttps = false) @@ -10,16 +13,21 @@ private DatabaseClientOptions(string url, string authToken = null, bool useHttps } internal static DatabaseClientOptions Default => new DatabaseClientOptions(""); + + /// + /// Gets or sets the URL of the database server. + /// + /// Default: "". "" or ":memory:" will create an in-memory database. public string Url { get; set; } + + /// + /// Gets or sets the authentication token used to connect to the database. + /// public string AuthToken { get; set; } + + /// + /// Gets or sets a value indicating whether to use HTTPS protocol for database connections. + /// public bool UseHttps { get; set; } - - public void Deconstruct(out string url, out string token, out bool useHttps) - { - url = Url; - token = AuthToken; - useHttps = UseHttps; - } } } - diff --git a/Libsql.Client/DatabaseWrapper.cs b/Libsql.Client/DatabaseWrapper.cs index 7d51366..b1acae0 100644 --- a/Libsql.Client/DatabaseWrapper.cs +++ b/Libsql.Client/DatabaseWrapper.cs @@ -11,15 +11,33 @@ internal class DatabaseWrapper : IDatabaseClient, IDisposable private libsql_database_t _db; private libsql_connection_t _connection; - public unsafe DatabaseWrapper(string url) + public unsafe DatabaseWrapper(DatabaseClientOptions options) { - Debug.Assert(url != null, "url is null"); + Debug.Assert(options.Url != null, "url is null"); + + if (!(options.Url == "" || options.Url == ":memory:")) + { + try + { + var uri = new Uri(options.Url); + switch (uri.Scheme) + { + case "http": + case "https": + case "ws": + case "wss": + throw new LibsqlException($"{uri.Scheme}:// is not yet supported"); + } + } + catch (UriFormatException) { } + } + + // C# empty strings have null pointers, so we need to give the url some meat. + var url = options.Url is "" ? "\0" : options.Url; + var error = new Error(); int exitCode; - // C# empty strings have null pointers, so we need to give the urln it some meat. - if (url is "") url = "\0"; - fixed (libsql_database_t* dbPtr = &_db) { fixed (byte* urlPtr = Encoding.UTF8.GetBytes(url)) @@ -46,31 +64,34 @@ private unsafe void Connect() error.ThrowIfNonZero(exitCode, "Failed to connect to database"); } - public unsafe Task Execute(string sql) + public async Task Execute(string sql) { - return Task.Run(() => + return await Task.Run(() => { - var error = new Error(); - var rows = new libsql_rows_t(); - int exitCode; - - fixed (byte* sqlPtr = Encoding.UTF8.GetBytes(sql)) + unsafe { - exitCode = Bindings.libsql_execute(_connection, sqlPtr, &rows, &error.Ptr); - } + var error = new Error(); + var rows = new libsql_rows_t(); + int exitCode; + + fixed (byte* sqlPtr = Encoding.UTF8.GetBytes(sql)) + { + exitCode = Bindings.libsql_execute(_connection, sqlPtr, &rows, &error.Ptr); + } - error.ThrowIfNonZero(exitCode, "Failed to execute query"); + error.ThrowIfNonZero(exitCode, "Failed to execute query"); - return new ResultSet( - 0, - 0, - rows.GetColumnNames(), - new Rows(rows) - ); + return new ResultSet( + 0, + 0, + rows.GetColumnNames(), + new Rows(rows) + ); + } }); } - public Task Execute(string sql, params object[] args) + public Task Execute(string sql, params object[] args) { throw new NotImplementedException(); } diff --git a/Libsql.Client/Error.cs b/Libsql.Client/Error.cs index 18cf07c..4be3c1d 100644 --- a/Libsql.Client/Error.cs +++ b/Libsql.Client/Error.cs @@ -13,7 +13,7 @@ public unsafe void ThrowIfNonZero(int exitCode, string message) var text = Marshal.PtrToStringAuto((IntPtr)Ptr); - throw new Exception($"{message}: {text}"); + throw new LibsqlException($"{message}: {text}"); } } } \ No newline at end of file diff --git a/Libsql.Client/IDatabaseClient.cs b/Libsql.Client/IDatabaseClient.cs index 9dead75..922842c 100644 --- a/Libsql.Client/IDatabaseClient.cs +++ b/Libsql.Client/IDatabaseClient.cs @@ -2,9 +2,26 @@ namespace Libsql.Client { + /// + /// Interface for a Libsql database client. + /// public interface IDatabaseClient { - Task Execute(string sql); - Task Execute(string sql, params object[] args); + /// + /// Executes the given SQL query and returns the result set. + /// + /// The SQL query to execute. + /// The result set returned by the query. + /// Thrown when the query fails to execute. + Task Execute(string sql); + + /// + /// Executes the given SQL query with the specified parameters and returns the result set. + /// + /// The SQL query to execute. + /// The parameters to use in the query. + /// The result set returned by the query. + /// Thrown when the query fails to execute. + Task Execute(string sql, params object[] args); } } \ No newline at end of file diff --git a/Libsql.Client/IResultSet.cs b/Libsql.Client/IResultSet.cs new file mode 100644 index 0000000..9a1dd74 --- /dev/null +++ b/Libsql.Client/IResultSet.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace Libsql.Client +{ + /// + /// Represents the result set of a SQL query. + /// + public interface IResultSet + { + /// + /// The ID of the last row inserted by the connection. + /// + /// The ID of the last row inserted. + long LastInsertRowId { get; } + + /// + /// The number of rows affected by the last query executed by the connection. + /// + /// The number of rows affected. + ulong RowsAffected { get; } + + /// + /// The names of the columns in the result set. + /// + /// An enumerable of column names. + IEnumerable Columns { get; } + + /// + /// The rows in the result set. + /// + /// An enumerable of enumerable rows. Rows are enumerated by column. + IEnumerable> Rows { get; } + } +} diff --git a/Libsql.Client/Libsql.Client.csproj b/Libsql.Client/Libsql.Client.csproj index 6e5e612..03f11e8 100644 --- a/Libsql.Client/Libsql.Client.csproj +++ b/Libsql.Client/Libsql.Client.csproj @@ -4,7 +4,7 @@ Libsql.Client Tom van Dinther A client library for Libsql - 0.2.0 + 0.2.1 Copyright (c) Tom van Dinther 2023 https://github.com/tvandinther/libsql-client-dotnet https://raw.githubusercontent.com/tvandinther/libsql-client-dotnet/master/LICENSE @@ -50,13 +50,6 @@ - - - all - runtime; build; native; contentfiles; analyzers - - - <_Parameter1>$(AssemblyName).Tests diff --git a/Libsql.Client/LibsqlException.cs b/Libsql.Client/LibsqlException.cs new file mode 100644 index 0000000..8b0080b --- /dev/null +++ b/Libsql.Client/LibsqlException.cs @@ -0,0 +1,24 @@ +using System; + +namespace Libsql.Client +{ + /// + /// Represents an exception that is thrown when an error occurs in the Libsql.Client library. + /// + public class LibsqlException : ApplicationException + { + internal LibsqlException() + { + } + + internal LibsqlException(string message) + : base(message) + { + } + + internal LibsqlException(string message, Exception inner) + : base(message, inner) + { + } + } +} diff --git a/Libsql.Client/ResultSet.cs b/Libsql.Client/ResultSet.cs index b768578..d5d59ab 100644 --- a/Libsql.Client/ResultSet.cs +++ b/Libsql.Client/ResultSet.cs @@ -1,13 +1,15 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace Libsql.Client { - public class ResultSet : IEquatable + internal class ResultSet : IResultSet { public long LastInsertRowId { get; } + public ulong RowsAffected { get; } + public IEnumerable Columns { get; } + public IEnumerable> Rows { get; } public ResultSet(long lastInsertRowId, ulong rowsAffected, IEnumerable columns, IEnumerable> rows) @@ -17,33 +19,5 @@ public ResultSet(long lastInsertRowId, ulong rowsAffected, IEnumerable c Columns = columns; Rows = rows; } - - public bool Equals(ResultSet other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return LastInsertRowId == other.LastInsertRowId && RowsAffected == other.RowsAffected && - Equals(Columns, other.Columns) && Equals(Rows, other.Rows); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((ResultSet)obj); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = LastInsertRowId.GetHashCode(); - hashCode = (hashCode * 397) ^ RowsAffected.GetHashCode(); - hashCode = (hashCode * 397) ^ (Columns != null ? Columns.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ (Rows != null ? Rows.GetHashCode() : 0); - return hashCode; - } - } - } + } } \ No newline at end of file diff --git a/Libsql.Client/Values/Blob.cs b/Libsql.Client/Values/Blob.cs index af58449..6e1aeed 100644 --- a/Libsql.Client/Values/Blob.cs +++ b/Libsql.Client/Values/Blob.cs @@ -3,8 +3,14 @@ namespace Libsql.Client { + /// + /// Represents a BLOB value in a database row. + /// public class Blob : Value, IEquatable { + /// + /// The binary data. + /// public byte[] Value { get; } internal Blob(byte[] value) diff --git a/Libsql.Client/Values/Integer.cs b/Libsql.Client/Values/Integer.cs index 7e6a704..3e68dd7 100644 --- a/Libsql.Client/Values/Integer.cs +++ b/Libsql.Client/Values/Integer.cs @@ -2,8 +2,14 @@ namespace Libsql.Client { + /// + /// Represents an INTEGER value in a database row. + /// public class Integer : Value, IEquatable { + /// + /// The integer value. + /// public int Value { get; } internal Integer(int value) diff --git a/Libsql.Client/Values/Null.cs b/Libsql.Client/Values/Null.cs index d59eb6d..34b3e91 100644 --- a/Libsql.Client/Values/Null.cs +++ b/Libsql.Client/Values/Null.cs @@ -2,6 +2,9 @@ namespace Libsql.Client { + /// + /// Represents a NULL value in a database row. + /// public class Null : Value, IEquatable { internal Null() { } diff --git a/Libsql.Client/Values/Real.cs b/Libsql.Client/Values/Real.cs index eff80a8..7fa6b01 100644 --- a/Libsql.Client/Values/Real.cs +++ b/Libsql.Client/Values/Real.cs @@ -3,8 +3,14 @@ namespace Libsql.Client { + /// + /// Represents a REAL value in a database row. + /// public class Real : Value, IEquatable { + /// + /// The real value. + /// public double Value { get; } internal Real(double value) diff --git a/Libsql.Client/Values/Text.cs b/Libsql.Client/Values/Text.cs index 4fc44f0..866c53d 100644 --- a/Libsql.Client/Values/Text.cs +++ b/Libsql.Client/Values/Text.cs @@ -2,8 +2,14 @@ namespace Libsql.Client { + /// + /// Represents a TEXT value in a database row. + /// public class Text : Value, IEquatable { + /// + /// The text value. + /// public string Value { get; } internal Text(string value) diff --git a/Libsql.Client/Values/Value.cs b/Libsql.Client/Values/Value.cs index 348346e..ba1cbec 100644 --- a/Libsql.Client/Values/Value.cs +++ b/Libsql.Client/Values/Value.cs @@ -2,6 +2,10 @@ namespace Libsql.Client { + /// + /// Represents a value in a database row. + /// + /// Type check for subclasses of to determine the type of the value. public abstract class Value : IEquatable { public abstract override int GetHashCode(); diff --git a/README.md b/README.md index a3a75b1..77b2e7e 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,66 @@ A .NET client library for libsql. - Batched statements. - Transactions. ---- +## Usage -### Progress +For an example, see the Demo project in the repository. + +### Creating a Database + +```csharp +// Create an in-memory database. +var dbClient = DatabaseClient.Create(opts => { + opts.Url = ":memory:"; +}); +``` + +### Executing SQL Statements + +```csharp +await dbClient.Execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, height REAL)"); +``` + +### Querying the Database + +```csharp +User ToUser(Value[] row) +{ + var rowArray = row.ToArray(); + + if ( + rowArray[0] is Integer { Value: var id } && + rowArray[1] is Text { Value: var name } && + rowArray[2] is Real { Value: var height } + { + return new User(id, name, height); + } + + throw new ArgumentException(); +} + +var result = await dbClient.Execute("SELECT * FROM users"); + +var users = result.Rows.Select(ToUser); +``` + +### Closing the Database + +```csharp +dbClient.Dispose(); +``` + +or with a `using` statement: + +```csharp +using (var dbClient = DatabaseClient.Create(opts => { + opts.Url = ":memory:"; +})) +{ + // ... +} +``` + +## Progress - A database can be created: - [x] In memory. - [x] From file. diff --git a/libsql-client-dotnet.sln.DotSettings.user b/libsql-client-dotnet.sln.DotSettings.user index 60c4583..cf8f6c8 100644 --- a/libsql-client-dotnet.sln.DotSettings.user +++ b/libsql-client-dotnet.sln.DotSettings.user @@ -1,21 +1,21 @@  - <SessionState ContinuousTestingMode="0" Name="SelectIntType" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <TestAncestor> - <TestId>xUnit::99E8754E-A898-48C2-B3A2-098EA08A4627::net7.0::Libsql.Client.Test.SelectTests.SelectIntType</TestId> - </TestAncestor> + <SessionState ContinuousTestingMode="0" Name="SelectIntType" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>xUnit::99E8754E-A898-48C2-B3A2-098EA08A4627::net7.0::Libsql.Client.Test.SelectTests.SelectIntType</TestId> + </TestAncestor> </SessionState> - <SessionState ContinuousTestingMode="0" IsActive="True" Name="ValueType_Null_AreNotEqual" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <TestAncestor> - <TestId>xUnit::99E8754E-A898-48C2-B3A2-098EA08A4627::net7.0::Libsql.Client.Tests.ValueEquality.BlobTests.ValueType_Null_AreNotEqual</TestId> - </TestAncestor> + <SessionState ContinuousTestingMode="0" IsActive="True" Name="ValueType_Null_AreNotEqual" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>xUnit::99E8754E-A898-48C2-B3A2-098EA08A4627::net7.0::Libsql.Client.Tests.ValueEquality.BlobTests.ValueType_Null_AreNotEqual</TestId> + </TestAncestor> </SessionState> - <SessionState ContinuousTestingMode="0" Name="ValueType_Null_AreNotEqual #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <TestAncestor> - <TestId>xUnit::99E8754E-A898-48C2-B3A2-098EA08A4627::net7.0::Libsql.Client.Tests.ValueEquality.BlobTests.ValueType_Null_AreNotEqual</TestId> - </TestAncestor> + <SessionState ContinuousTestingMode="0" Name="ValueType_Null_AreNotEqual #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>xUnit::99E8754E-A898-48C2-B3A2-098EA08A4627::net7.0::Libsql.Client.Tests.ValueEquality.BlobTests.ValueType_Null_AreNotEqual</TestId> + </TestAncestor> </SessionState> - <SessionState ContinuousTestingMode="0" Name="SelectTextType" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <TestAncestor> - <TestId>xUnit::99E8754E-A898-48C2-B3A2-098EA08A4627::net7.0::Libsql.Client.Test.SelectTests.SelectTextType</TestId> - </TestAncestor> + <SessionState ContinuousTestingMode="0" Name="SelectTextType" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>xUnit::99E8754E-A898-48C2-B3A2-098EA08A4627::net7.0::Libsql.Client.Test.SelectTests.SelectTextType</TestId> + </TestAncestor> </SessionState> \ No newline at end of file