Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/extend orm client #8

Merged
merged 4 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions RqliteDotnet.Test/RqliteClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<NamedQueryParameter, FooResultDto>("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()
Expand All @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion RqliteDotnet/DbFlag.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
namespace RqliteDotnet;

[Flags]
public enum DbFlag
{
Timings = 1,
Transaction
Transaction = 2
}
11 changes: 10 additions & 1 deletion RqliteDotnet/Dto/NamedQueryParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}\"");
}
}
2 changes: 1 addition & 1 deletion RqliteDotnet/Dto/QueryParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
{
public QueryParamType ParamType { get; set; }

public object Value { get; set; }

Check warning on line 7 in RqliteDotnet/Dto/QueryParameter.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Value' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 7 in RqliteDotnet/Dto/QueryParameter.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Value' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

public virtual string ToParamString()
{
return (ParamType == QueryParamType.Number ? Value.ToString() : $"\"{Value}\"")!;
return (ParamType == QueryParamType.Number ? FormattableString.Invariant($"{Value}") : $"\"{Value}\"")!;
}
}

Expand Down
5 changes: 4 additions & 1 deletion RqliteDotnet/HttpClientExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ public static async Task<T> SendTyped<T>(this HttpClient client, HttpRequestMess
var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();

response.EnsureSuccessStatusCode();

var result = JsonSerializer.Deserialize<T>(content, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
return result;

return result!;
}
}
48 changes: 48 additions & 0 deletions RqliteDotnet/IRqliteClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using RqliteDotnet.Dto;

namespace RqliteDotnet;

public interface IRqliteClient
{
/// <summary>
/// Ping Rqlite instance
/// </summary>
/// <returns>String containining Rqlite version</returns>
Task<string> Ping();

/// <summary>
/// Query DB and return result
/// </summary>
/// <param name="query"></param>
Task<QueryResults> Query(string query);

/// <summary>
/// Execute command and return result
/// </summary>
Task<ExecuteResults> Execute(string command);

/// <summary>
/// Execute one or several commands and return result
/// </summary>
/// <param name="commands">Commands to execute</param>
/// <param name="flags">Command flags, e.g. whether to use transaction</param>
/// <returns></returns>
Task<ExecuteResults> Execute(IEnumerable<string> commands, DbFlag? flags);

/// <summary>
/// Execute one or several commands and return result
/// </summary>
/// <param name="commands">Commands to execute</param>
/// <param name="flags">Command flags, e.g. whether to use transaction</param>
/// <returns></returns>
Task<ExecuteResults> ExecuteParams<T>(IEnumerable<(string, T[])> commands, DbFlag? flags) where T : QueryParameter;

/// <summary>
/// Query DB using parametrized statement
/// </summary>
/// <param name="query"></param>
/// <param name="qps"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
Task<QueryResults> QueryParams<T>(string query, params T[] qps) where T: QueryParameter;
}
82 changes: 60 additions & 22 deletions RqliteDotnet/RqliteClient.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -15,6 +21,11 @@
_httpClient = client ?? new HttpClient(){ BaseAddress = new Uri(uri) };
}

public RqliteClient(HttpClient client)
{
_httpClient = client ?? throw new ArgumentNullException(nameof(client));
}

/// <summary>
/// Ping Rqlite instance
/// </summary>
Expand All @@ -25,21 +36,21 @@

return x.Headers.GetValues("X-Rqlite-Version").FirstOrDefault()!;
}

/// <summary>
/// Query DB and return result
/// </summary>
/// <param name="query"></param>
public async Task<QueryResults> 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}");
var str = await r.Content.ReadAsStringAsync();

var result = JsonSerializer.Deserialize<QueryResults>(str, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
return result;

Check warning on line 53 in RqliteDotnet/RqliteClient.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.
}

/// <summary>
Expand All @@ -53,7 +64,7 @@
var result = await _httpClient.SendTyped<ExecuteResults>(request);
return result;
}

/// <summary>
/// Execute one or several commands and return result
/// </summary>
Expand All @@ -71,33 +82,55 @@
var result = await _httpClient.SendTyped<ExecuteResults>(request);
return result;
}


/// <summary>
/// Execute one or several commands and return result
/// </summary>
/// <param name="commands">Commands to execute</param>
/// <param name="flags">Command flags, e.g. whether to use transaction</param>
/// <returns></returns>
public async Task<ExecuteResults> ExecuteParams<T>(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<ExecuteResults>(request);
return result;
}

/// <summary>
/// Query DB using parametrized statement
/// </summary>
/// <param name="query"></param>
/// <param name="qps"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public async Task<QueryResults> QueryParams<T>(string query, params T[] qps) where T: QueryParameter
public async Task<QueryResults> QueryParams<T>(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<QueryResults>(request);

return result;
}

private static string BuildQuery<T>(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<QueryResults>(request);

return result;
sb.Append(typeof(T) == typeof(NamedQueryParameter) ? "}]" : "]");
return sb.ToString();
}

private string GetParameters(DbFlag? flags)
Expand All @@ -121,14 +154,19 @@

protected object GetValue(string valType, JsonElement el)
{
object? x = valType switch
if (el.ValueKind == JsonValueKind.Null)
{
return null;

Check warning on line 159 in RqliteDotnet/RqliteClient.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.
}
object x = valType switch

Check warning on line 161 in RqliteDotnet/RqliteClient.cs

View workflow job for this annotation

GitHub Actions / build

Converting null literal or possible null value to non-nullable type.
{
"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;

Check warning on line 170 in RqliteDotnet/RqliteClient.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.
}
}
63 changes: 56 additions & 7 deletions RqliteDotnet/RqliteOrmClient.cs
Original file line number Diff line number Diff line change
@@ -1,40 +1,89 @@
using System.Data;
using RqliteDotnet.Dto;

namespace RqliteDotnet;

public class RqliteOrmClient : RqliteClient
public interface IRqliteOrmClient : IRqliteClient
{
/// <summary>
/// Query Rqlite DB and return result as an instance of T
/// </summary>
/// <param name="query">Query to execute</param>
/// <typeparam name="T">Type of result object</typeparam>
/// <returns></returns>
Task<List<T>> Query<T>(string query) where T: new();

Task<List<U>> QueryParams<T, U>(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) {}

/// <summary>
/// Query Rqlite DB and return result as an instance of T
/// </summary>
/// <param name="query">Query to execute</param>
/// <typeparam name="T">Type of result object</typeparam>
/// <returns></returns>
public async Task<List<T>> Query<T>(string query) where T: new()
public async Task<List<T>> Query<T>(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<T>();

for (int i = 0; i < res.Values.Count; i++)
for (int i = 0; i < res.Values?.Count; i++)
{
var dto = new T();

foreach (var prop in typeof(T).GetProperties())
{
var index = res.Columns.FindIndex(c => c.ToLower() == prop.Name.ToLower());

Check warning on line 50 in RqliteDotnet/RqliteOrmClient.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
var val = GetValue(res.Types[index], res.Values[i][index]);

Check warning on line 51 in RqliteDotnet/RqliteOrmClient.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

prop.SetValue(dto, val);
}


list.Add(dto);
}

return list;
}

public async Task<List<U>> QueryParams<T, U>(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<U>();

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());

Check warning on line 81 in RqliteDotnet/RqliteOrmClient.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
var val = GetValue(res.Types[index], res.Values[i][index]);

Check warning on line 82 in RqliteDotnet/RqliteOrmClient.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

prop.SetValue(dto, val);
}

list.Add(dto);
}

Expand Down
Loading