Skip to content

Commit

Permalink
Added support for building a query, either from a lambda or programat…
Browse files Browse the repository at this point in the history
…ically instead of using strings.
  • Loading branch information
kenkendk committed May 29, 2019
1 parent 24b807d commit 5f2fe4b
Show file tree
Hide file tree
Showing 6 changed files with 1,352 additions and 31 deletions.
185 changes: 183 additions & 2 deletions Ceen.Database/DatabaseDialectSQLite.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -240,11 +241,12 @@ public virtual string CreateTableExistsCommand(Type type)
/// </summary>
/// <returns>The insert command.</returns>
/// <param name="type">The type to generate the command for.</param>
public virtual string CreateInsertCommand(Type type)
/// <param name="useInsertOrIgnore">Use &quote;INSERT OR IGNORE&quote; instead of the usual &quote;INSERT&quote; command </param>
public virtual string CreateInsertCommand(Type type, bool useInsertOrIgnore)
{
var mapping = GetTypeMap(type);
var statement =
$"INSERT INTO {QuoteName(mapping.Name)} ({string.Join(",", mapping.InsertColumns.Select(x => QuoteName(x.ColumnName)))}) VALUES ({string.Join(",", mapping.InsertColumns.Select(x => "?"))})";
$"INSERT{(useInsertOrIgnore ? " OR IGNORE" : "")} INTO {QuoteName(mapping.Name)} ({string.Join(",", mapping.InsertColumns.Select(x => QuoteName(x.ColumnName)))}) VALUES ({string.Join(",", mapping.InsertColumns.Select(x => "?"))})";

if (mapping.IsPrimaryKeyAutogenerated)
statement += "; SELECT last_insert_rowid();";
Expand Down Expand Up @@ -324,5 +326,184 @@ public virtual string Limit(int limit, int? offset)

return $"LIMIT {limit} OFFSET {offset.Value}";
}

/// <summary>
/// Parses a query element and returns the SQL and arguments
/// </summary>
/// <param name="type">The type to query</param>
/// <param name="element">The query element</param>
/// <returns>The parsed query and the arguments</returns>
public KeyValuePair<string, object[]> RenderClause(Type type, QueryElement element)
{
var lst = new List<object>();
var q = RenderClause(type, element, lst);
if (!string.IsNullOrWhiteSpace(q))
q = "WHERE " + q;
return new KeyValuePair<string, object[]>(q, lst.ToArray());
}

/// <summary>
/// Renders an SQL where clause from a query element
/// </summary>
/// <param name="element">The element to use</param>
/// <returns>The SQL where clause</returns>
private string RenderClause(Type type, object element, List<object> args)
{
if (element == null || element is Empty)
return string.Empty;

if (element is And andElement)
return string.Join(
" AND ",
andElement
.Items
.Select(x => RenderClause(type, x, args))
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => $"({x})")
);
else if (element is Or orElement)
return string.Join(
" OR ",
orElement
.Items
.Select(x => RenderClause(type, x, args))
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => $"({x})")
);
else if (element is Property property)
return GetTypeMap(type).QuotedColumnName(property.PropertyName);
else if (element is Not not)
return $"NOT ({RenderClause(type, not.Expression, args)})";
else if (element is Compare compare)
{
if (
string.Equals(compare.Operator, "IN", StringComparison.OrdinalIgnoreCase)
||
string.Equals(compare.Operator, "NOT IN", StringComparison.OrdinalIgnoreCase)
)
{
var items = compare.RightHandSide as IEnumerable;
if (items == null)
return RenderClause(type, QueryUtil.Equal(compare.LeftHandSide, null), args);

var op =
string.Equals(compare.Operator, "IN", StringComparison.OrdinalIgnoreCase)
? "="
: "!=";

// Special handling of null in lists
if (items.Cast<object>().Any(x => x != null))
return RenderClause(
type,
QueryUtil.Or(
QueryUtil.In(compare.LeftHandSide, items.Cast<object>().Where(x => x != null)),
QueryUtil.Compare(compare.LeftHandSide, op, null)
),
args
);

// No nulls, just return plain "IN" or "NOT IN"
args.Add(items);
return $"{RenderClause(type, compare.LeftHandSide, args)} {compare.Operator} ?";
}

// Extract the arguments, if they are arguments
var lhs = compare.LeftHandSide is Value lhsVal ? lhsVal.Item : compare.LeftHandSide;
var rhs = compare.RightHandSide is Value rhsVal ? rhsVal.Item : compare.RightHandSide;

// Special handling for enums, as they are string serialized in the database
if (IsQueryItemEnum(type, lhs) || IsQueryItemEnum(type, rhs))
{
if (!new string[] {"=", "LIKE", "!=", "NOT LIKE"}.Any(x => string.Equals(x, compare.Operator, StringComparison.InvariantCultureIgnoreCase)))
throw new ArgumentException("Can only compare enums with equal or not equal as they are stored as strings in the database");

// Force enum arguments to strings
if (lhs != null && !(lhs is QueryElement))
lhs = lhs.ToString();
if (rhs != null && !(rhs is QueryElement))
rhs = rhs.ToString();
}

// Special handling of null values to be more C# like
var anyNulls = lhs == null || rhs == null;

// Rewire gteq and lteq to handle nulls like C#
if (anyNulls && string.Equals(compare.Operator, "<="))
return RenderClause(type,
QueryUtil.Or(
QueryUtil.Compare(lhs, "<", rhs),
QueryUtil.Compare(lhs, "=", rhs)
)
, args);

if (anyNulls && string.Equals(compare.Operator, ">="))
return RenderClause(type,
QueryUtil.Or(
QueryUtil.Compare(lhs, ">", rhs),
QueryUtil.Compare(lhs, "=", rhs)
)
, args);

// Rewire compare operator to also match nulls
if (anyNulls && (string.Equals(compare.Operator, "=") || string.Equals(compare.Operator, "LIKE", StringComparison.OrdinalIgnoreCase)))
{
if (lhs == null)
return $"{RenderClause(type, rhs, args)} IS NULL";
else
return $"{RenderClause(type, lhs, args)} IS NULL";
}

if (anyNulls && (string.Equals(compare.Operator, "!=") || string.Equals(compare.Operator, "NOT LIKE", StringComparison.OrdinalIgnoreCase)))
{
if (lhs == null)
return $"{RenderClause(type, rhs, args)} IS NOT NULL";
else
return $"{RenderClause(type, lhs, args)} IS NOT NULL";
}

return $"{RenderClause(type, lhs, args)} {compare.Operator} {RenderClause(type, rhs, args)}";
}
else if (element is Value ve)
{
args.Add(ve.Item);
return "?";
}
else if (element is Arithmetic arithmetic)
{
return $"{RenderClause(type, arithmetic.LeftHandSide, args)} {arithmetic.Operator} {RenderClause(type, arithmetic.RightHandSide, args)}";
}
else if (element is QueryElement)
{
throw new Exception($"Unexpected query element: {element.GetType()}");
}
else
{
args.Add(element);
return "?";
}
}

/// <summary>
/// Checks if a query item is an enum
/// </summary>
/// <param name="type">The type used for properties</param>
/// <param name="item">The item to check</param>
/// <returns>A value indicating if the item is an enum type</returns>
private bool IsQueryItemEnum(Type type, object item)
{
if (item is Value v)
item = v.Item;

if (item == null)
return false;

if (item is Property p)
return GetTypeMap(type).AllColumnsByMemberName[p.PropertyName].MemberType.IsEnum;

if (item is QueryElement)
return false;

return item.GetType().IsEnum;
}
}
}
Loading

0 comments on commit 5f2fe4b

Please sign in to comment.