diff --git a/RqliteDotnet.Test/RqliteClientTests.cs b/RqliteDotnet.Test/RqliteClientTests.cs index 16d5b08..5c56c14 100644 --- a/RqliteDotnet.Test/RqliteClientTests.cs +++ b/RqliteDotnet.Test/RqliteClientTests.cs @@ -34,6 +34,25 @@ public async Task QueryWithGenerics_Works() Assert.AreEqual(1, queryresults[0].Id); Assert.AreEqual("john", queryresults[0].Name); } + + [Test] + public async Task ParametrizedQueryWithGenerics_Works() + { + var client = HttpClientMock.GetParamQueryMock(); + + var rqClient = new RqliteOrmClient("http://localhost:6000", client); + var queryresults = await rqClient.QueryParams("select * from foo where Name = :name", + new NamedQueryParameter() + { + Name = "name", + ParamType = QueryParamType.String, + Value = "john" + }); + + Assert.AreEqual(1, queryresults.Count); + Assert.AreEqual(1, queryresults[0].Id); + Assert.AreEqual("john", queryresults[0].Name); + } [Test] public async Task BasicExecute_Works() @@ -47,6 +66,25 @@ public async Task BasicExecute_Works() Assert.AreEqual(1, result.Results[0].RowsAffected); Assert.AreEqual(2, result.Results[0].LastInsertId); } + + [Test] + public async Task ParametrizedExecute_Works() + { + var client = HttpClientMock.GetExecuteMock(); + + var rqClient = new RqliteClient("http://localhost:6000", client); + var result = await rqClient.ExecuteParams(new []{ + ("update foo set name = :newName where name = :oldName" + , new [] { + new NamedQueryParameter() {Name = "newName", Value = "doe", ParamType = QueryParamType.String} + , new NamedQueryParameter {Name = "oldName", Value = "john", ParamType = QueryParamType.String} + } + )}, DbFlag.Transaction); + + Assert.AreEqual(1, result.Results.Count); + Assert.AreEqual(1, result.Results[0].RowsAffected); + Assert.AreEqual(2, result.Results[0].LastInsertId); + } [Test] public async Task BasicQueryParam_Works() diff --git a/RqliteDotnet/DbFlag.cs b/RqliteDotnet/DbFlag.cs index f0b1541..67269e8 100644 --- a/RqliteDotnet/DbFlag.cs +++ b/RqliteDotnet/DbFlag.cs @@ -1,7 +1,8 @@ namespace RqliteDotnet; +[Flags] public enum DbFlag { Timings = 1, - Transaction + Transaction = 2 } \ No newline at end of file diff --git a/RqliteDotnet/Dto/NamedQueryParameter.cs b/RqliteDotnet/Dto/NamedQueryParameter.cs index f5755bb..942bc3b 100644 --- a/RqliteDotnet/Dto/NamedQueryParameter.cs +++ b/RqliteDotnet/Dto/NamedQueryParameter.cs @@ -6,6 +6,15 @@ public class NamedQueryParameter : QueryParameter public override string ToParamString() { - return $"\"{Name}\":" + (ParamType == QueryParamType.Number ? Value.ToString() : $"\"{Value}\""); + return $"\"{Name}\":" + PrintValue(); + } + + private string PrintValue() + { + if (Value == null) + { + return "null"; + } + return (ParamType == QueryParamType.Number ? FormattableString.Invariant($"{Value}") : $"\"{Value}\""); } } \ No newline at end of file diff --git a/RqliteDotnet/Dto/QueryParameter.cs b/RqliteDotnet/Dto/QueryParameter.cs index 609d485..57b354f 100644 --- a/RqliteDotnet/Dto/QueryParameter.cs +++ b/RqliteDotnet/Dto/QueryParameter.cs @@ -8,7 +8,7 @@ public class QueryParameter public virtual string ToParamString() { - return (ParamType == QueryParamType.Number ? Value.ToString() : $"\"{Value}\"")!; + return (ParamType == QueryParamType.Number ? FormattableString.Invariant($"{Value}") : $"\"{Value}\"")!; } } diff --git a/RqliteDotnet/HttpClientExtensions.cs b/RqliteDotnet/HttpClientExtensions.cs index 913c0db..a052b37 100644 --- a/RqliteDotnet/HttpClientExtensions.cs +++ b/RqliteDotnet/HttpClientExtensions.cs @@ -9,7 +9,10 @@ public static async Task SendTyped(this HttpClient client, HttpRequestMess var response = await client.SendAsync(request); var content = await response.Content.ReadAsStringAsync(); + response.EnsureSuccessStatusCode(); + var result = JsonSerializer.Deserialize(content, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); - return result; + + return result!; } } \ No newline at end of file diff --git a/RqliteDotnet/IRqliteClient.cs b/RqliteDotnet/IRqliteClient.cs new file mode 100644 index 0000000..6cf371d --- /dev/null +++ b/RqliteDotnet/IRqliteClient.cs @@ -0,0 +1,48 @@ +using RqliteDotnet.Dto; + +namespace RqliteDotnet; + +public interface IRqliteClient +{ + /// + /// Ping Rqlite instance + /// + /// String containining Rqlite version + Task Ping(); + + /// + /// Query DB and return result + /// + /// + Task Query(string query); + + /// + /// Execute command and return result + /// + Task Execute(string command); + + /// + /// Execute one or several commands and return result + /// + /// Commands to execute + /// Command flags, e.g. whether to use transaction + /// + Task Execute(IEnumerable commands, DbFlag? flags); + + /// + /// Execute one or several commands and return result + /// + /// Commands to execute + /// Command flags, e.g. whether to use transaction + /// + Task ExecuteParams(IEnumerable<(string, T[])> commands, DbFlag? flags) where T : QueryParameter; + + /// + /// Query DB using parametrized statement + /// + /// + /// + /// + /// + Task QueryParams(string query, params T[] qps) where T: QueryParameter; +} \ No newline at end of file diff --git a/RqliteDotnet/RqliteClient.cs b/RqliteDotnet/RqliteClient.cs index 1b0d4c3..e970dfc 100644 --- a/RqliteDotnet/RqliteClient.cs +++ b/RqliteDotnet/RqliteClient.cs @@ -1,12 +1,18 @@ -using System.Data; -using System.Net; +using RqliteDotnet.Dto; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Net.Http; using System.Text; using System.Text.Json; -using RqliteDotnet.Dto; +using System.Threading.Tasks; namespace RqliteDotnet; -public class RqliteClient + + +public class RqliteClient : IRqliteClient { private readonly HttpClient _httpClient; @@ -15,6 +21,11 @@ public RqliteClient(string uri, HttpClient? client = null) _httpClient = client ?? new HttpClient(){ BaseAddress = new Uri(uri) }; } + public RqliteClient(HttpClient client) + { + _httpClient = client ?? throw new ArgumentNullException(nameof(client)); + } + /// /// Ping Rqlite instance /// @@ -25,14 +36,14 @@ public async Task Ping() return x.Headers.GetValues("X-Rqlite-Version").FirstOrDefault()!; } - + /// /// Query DB and return result /// /// public async Task Query(string query) { - var data = "&q="+Uri.EscapeDataString(query); + var data = "&q=" + Uri.EscapeDataString(query); var baseUrl = "/db/query?timings"; var r = await _httpClient.GetAsync($"{baseUrl}&{data}"); @@ -53,7 +64,7 @@ public async Task Execute(string command) var result = await _httpClient.SendTyped(request); return result; } - + /// /// Execute one or several commands and return result /// @@ -71,7 +82,25 @@ public async Task Execute(IEnumerable commands, DbFlag? var result = await _httpClient.SendTyped(request); return result; } - + + /// + /// Execute one or several commands and return result + /// + /// Commands to execute + /// Command flags, e.g. whether to use transaction + /// + public async Task ExecuteParams(IEnumerable<(string, T[])> commands, DbFlag? flags) where T : QueryParameter + { + var parameters = GetParameters(flags); + var request = new HttpRequestMessage(HttpMethod.Post, $"/db/execute{parameters}"); + var compiled = commands.Select(c => $"{BuildQuery(c.Item1, c.Item2)}"); + var s = string.Join(",", compiled); + + request.Content = new StringContent($"[{s}]", Encoding.UTF8, "application/json"); + var result = await _httpClient.SendTyped(request); + return result; + } + /// /// Query DB using parametrized statement /// @@ -79,25 +108,29 @@ public async Task Execute(IEnumerable commands, DbFlag? /// /// /// - public async Task QueryParams(string query, params T[] qps) where T: QueryParameter + public async Task QueryParams(string query, params T[] qps) where T : QueryParameter { var request = new HttpRequestMessage(HttpMethod.Post, "/db/query?timings"); - var sb = new StringBuilder(typeof(T) == typeof(NamedQueryParameter) ? - $"[[\"{query}\",{{" : - $"[[\"{query}\","); + var q = BuildQuery(query, qps); + + request.Content = new StringContent($"[{q}]", Encoding.UTF8, "application/json"); + var result = await _httpClient.SendTyped(request); + + return result; + } + + private static string BuildQuery(string query, T[] qps) where T : QueryParameter + { + var sb = new StringBuilder(typeof(T) == typeof(NamedQueryParameter) ? $"[\"{query}\",{{" : $"[\"{query}\","); foreach (var qp in qps) { - sb.Append(qp.ToParamString()+","); + sb.Append(qp.ToParamString() + ","); } sb.Length -= 1; - sb.Append(typeof(T) == typeof(NamedQueryParameter) ? "}]]" : "]]"); - - request.Content = new StringContent(sb.ToString(), Encoding.UTF8, "application/json"); - var result = await _httpClient.SendTyped(request); - - return result; + sb.Append(typeof(T) == typeof(NamedQueryParameter) ? "}]" : "]"); + return sb.ToString(); } private string GetParameters(DbFlag? flags) @@ -121,12 +154,17 @@ private string GetParameters(DbFlag? flags) protected object GetValue(string valType, JsonElement el) { - object? x = valType switch + if (el.ValueKind == JsonValueKind.Null) + { + return null; + } + object x = valType switch { "text" => el.GetString(), - "integer" or "numeric" => el.GetInt32(), + "integer" or "numeric" or "int" => el.GetInt32(), "real" => el.GetDouble(), - _ => throw new ArgumentException("Unsupported type") + "bigint" => el.GetInt64(), + _ => throw new ArgumentException($"Unsupported type {valType}") }; return x; diff --git a/RqliteDotnet/RqliteOrmClient.cs b/RqliteDotnet/RqliteOrmClient.cs index 3b06781..b36204f 100644 --- a/RqliteDotnet/RqliteOrmClient.cs +++ b/RqliteDotnet/RqliteOrmClient.cs @@ -1,29 +1,47 @@ using System.Data; +using RqliteDotnet.Dto; namespace RqliteDotnet; -public class RqliteOrmClient : RqliteClient +public interface IRqliteOrmClient : IRqliteClient { + /// + /// Query Rqlite DB and return result as an instance of T + /// + /// Query to execute + /// Type of result object + /// + Task> Query(string query) where T: new(); + + Task> QueryParams(string query, params T[] qps) + where T: QueryParameter + where U : new(); +} + +public class RqliteOrmClient : RqliteClient, IRqliteOrmClient +{ + public RqliteOrmClient(HttpClient client) : base(client) { } + public RqliteOrmClient(string uri, HttpClient? client = null) : base(uri, client) {} - + /// /// Query Rqlite DB and return result as an instance of T /// /// Query to execute /// Type of result object /// - public async Task> Query(string query) where T: new() + public async Task> Query(string query) where T : new() { var response = await Query(query); if (response.Results!.Count > 1) throw new DataException("Query returned more than 1 result. At the moment only 1 result supported"); var res = response.Results[0]; - + if (!string.IsNullOrEmpty(res.Error)) throw new InvalidOperationException(res.Error); var list = new List(); - for (int i = 0; i < res.Values.Count; i++) + for (int i = 0; i < res.Values?.Count; i++) { var dto = new T(); @@ -31,10 +49,41 @@ public RqliteOrmClient(string uri, HttpClient? client = null) : base(uri, client { var index = res.Columns.FindIndex(c => c.ToLower() == prop.Name.ToLower()); var val = GetValue(res.Types[index], res.Values[i][index]); - + prop.SetValue(dto, val); } - + + list.Add(dto); + } + + return list; + } + + public async Task> QueryParams(string query, params T[] qps) + where T : QueryParameter + where U : new() + { + var response = await QueryParams(query, qps); + if (response.Results!.Count > 1) + throw new DataException("Query returned more than 1 result. At the moment only 1 result supported"); + var res = response.Results[0]; + + if (!string.IsNullOrEmpty(res.Error)) + throw new InvalidOperationException(res.Error); + var list = new List(); + + for (int i = 0; i < res.Values?.Count; i++) + { + var dto = new U(); + + foreach (var prop in typeof(U).GetProperties()) + { + var index = res.Columns.FindIndex(c => c.ToLower() == prop.Name.ToLower()); + var val = GetValue(res.Types[index], res.Values[i][index]); + + prop.SetValue(dto, val); + } + list.Add(dto); }