diff --git a/Tycho.UnitTests/TychoDbTests.cs b/Tycho.UnitTests/TychoDbTests.cs index d25304a..2c33c83 100644 --- a/Tycho.UnitTests/TychoDbTests.cs +++ b/Tycho.UnitTests/TychoDbTests.cs @@ -91,6 +91,35 @@ public async Task TychoDb_RegisterPatientAndInsertObject_ShouldBeSuccessful (IJs result.Should ().BeTrue (); } + [DataTestMethod] + [DynamicData(nameof(JsonSerializers))] + public async Task TychoDb_RegisterPatientAndInsertObjectAndQueryByIsDirty_ShouldBeSuccessful(IJsonSerializer jsonSerializer) + { + var db = + new TychoDb(Path.GetTempPath(), jsonSerializer, rebuildCache: true) + .AddTypeRegistration(x => x.PatientId) + .Connect(); + + var testObj = + new Patient + { + PatientId = 12345, + FirstName = "TEST", + LastName = "PATIENT", + IsDirty = true, + }; + + await db.WriteObjectAsync(testObj); + + var result = + await db.ReadObjectsAsync( + filter: new FilterBuilder() + .Filter(FilterType.Equals, x => x.IsDirty, true)); + + result.Should().NotBeNullOrEmpty(); + result.Should().HaveCount(1); + } + [DataTestMethod] [DynamicData (nameof (JsonSerializers))] public async Task TychoDb_InsertAndReadManyObjects_ShouldBeSuccessful (IJsonSerializer jsonSerializer) diff --git a/Tycho/Filter.cs b/Tycho/Filter.cs index fa0e22f..4e8f1be 100644 --- a/Tycho/Filter.cs +++ b/Tycho/Filter.cs @@ -34,26 +34,32 @@ public class Filter public bool IsPropertyPathNumeric { get; private set; } + public bool IsPropertyPathBool { get; private set; } + public string PropertyValuePath { get; set; } public bool IsPropertyValuePathNumeric { get; private set; } + public bool IsPropertyValuePathBool { get; private set; } + public object Value { get; private set; } - public Filter (FilterType filterType, string propertyPath, bool isPropertyPathNumeric, object value) + public Filter (FilterType filterType, string propertyPath, bool isPropertyPathNumeric, bool isPropertyPathBool, object value) { FilterType = filterType; PropertyPath = propertyPath; IsPropertyPathNumeric = isPropertyPathNumeric; + IsPropertyPathBool = isPropertyPathBool; Value = value; } - public Filter (FilterType filterType, string listPropertyPath, string propertyValuePath, bool isPropertyValuePathNumeric, object value) + public Filter (FilterType filterType, string listPropertyPath, string propertyValuePath, bool isPropertyValuePathNumeric, bool isPropertyValuePathBool, object value) { FilterType = filterType; PropertyPath = listPropertyPath; PropertyValuePath = propertyValuePath; - IsPropertyPathNumeric = IsPropertyPathNumeric; + IsPropertyValuePathNumeric = isPropertyValuePathNumeric; + IsPropertyValuePathBool = isPropertyValuePathBool; Value = value; } diff --git a/Tycho/FilterBuilder.cs b/Tycho/FilterBuilder.cs index 062d08e..754b439 100644 --- a/Tycho/FilterBuilder.cs +++ b/Tycho/FilterBuilder.cs @@ -20,8 +20,9 @@ public FilterBuilder Filter (FilterType filterType, Expression Filter (FilterType filterType, Expr var propertyPathString = QueryPropertyPath.BuildPath (propertyPath); var propertyValuePathString = QueryPropertyPath.BuildPath (propertyValuePath); var isPropertyValuePathNumeric = QueryPropertyPath.IsNumeric (propertyValuePath); + var isPropertyValuePathBool = QueryPropertyPath.IsBool(propertyValuePath); - _filters.Add (new Filter (filterType, propertyPathString, propertyValuePathString, isPropertyValuePathNumeric, value)); + _filters.Add (new Filter (filterType, propertyPathString, propertyValuePathString, isPropertyValuePathNumeric, isPropertyValuePathBool, value)); return this; } - public FilterBuilder Filter (FilterType filterType, string propertyPath, bool isPropertyPathNumeric, object value) + public FilterBuilder Filter (FilterType filterType, string propertyPath, bool isPropertyPathNumeric, bool isPropertyPathBool, object value) { - _filters.Add (new Filter (filterType, propertyPath, isPropertyPathNumeric, value)); + _filters.Add (new Filter (filterType, propertyPath, isPropertyPathNumeric, isPropertyPathBool, value)); return this; } @@ -108,12 +110,18 @@ public StringBuilder Build (StringBuilder commandBuilder) commandBuilder.AppendLine ($"EXISTS(SELECT 1 FROM JSON_TREE(Data, \'{filter.PropertyPath}\') AS JT, JSON_EACH(JT.Value, \'{filter.PropertyValuePath}\') AS VAL WHERE VAL.value like \'%{filter.Value}\')"); break; case FilterType.Equals: + if (filter.IsPropertyValuePathBool) + { + commandBuilder.AppendLine($"EXISTS(SELECT 1 FROM JSON_TREE(Data, \'{filter.PropertyPath}\') AS JT, JSON_EACH(JT.Value, \'{filter.PropertyValuePath}\') AS VAL WHERE VAL.value = {filter.Value})"); + break; + } + if(filter.IsPropertyValuePathNumeric) { commandBuilder.AppendLine ($"EXISTS(SELECT 1 FROM JSON_TREE(Data, \'{filter.PropertyPath}\') AS JT, JSON_EACH(JT.Value, \'{filter.PropertyValuePath}\') AS VAL WHERE CAST(VAL.value as NUMERIC) = \'{filter.Value}\')"); break; } - + commandBuilder.AppendLine ($"EXISTS(SELECT 1 FROM JSON_TREE(Data, \'{filter.PropertyPath}\') AS JT, JSON_EACH(JT.Value, \'{filter.PropertyValuePath}\') AS VAL WHERE VAL.value = \'{filter.Value}\')"); break; //TODO: This is an attack vector and should be parameterized @@ -133,6 +141,11 @@ public StringBuilder Build (StringBuilder commandBuilder) commandBuilder.AppendLine ($"EXISTS(SELECT 1 FROM JSON_TREE(Data, \'{filter.PropertyPath}\') AS JT, JSON_EACH(JT.Value, \'{filter.PropertyValuePath}\') AS VAL WHERE CAST(VAL.value as NUMERIC) <= {filter.Value})"); break; case FilterType.NotEquals: + if (filter.IsPropertyValuePathBool) + { + commandBuilder.AppendLine($"EXISTS(SELECT 1 FROM JSON_TREE(Data, \'{filter.PropertyPath}\') AS JT, JSON_EACH(JT.Value, \'{filter.PropertyValuePath}\') AS VAL WHERE VAL.value <> {filter.Value})"); + break; + } commandBuilder.AppendLine ($"EXISTS(SELECT 1 FROM JSON_TREE(Data, \'{filter.PropertyPath}\') AS JT, JSON_EACH(JT.Value, \'{filter.PropertyValuePath}\') AS VAL WHERE VAL.value <> \'{filter.Value}\')"); break; case FilterType.StartsWith: @@ -154,6 +167,12 @@ public StringBuilder Build (StringBuilder commandBuilder) commandBuilder.AppendLine ($"JSON_EXTRACT(Data, \'{filter.PropertyPath}\') like \'%{filter.Value}\'"); break; case FilterType.Equals: + if(filter.IsPropertyPathBool) + { + commandBuilder.AppendLine($"JSON_EXTRACT(Data, \'{filter.PropertyPath}\') = {filter.Value}"); + break; + } + if(filter.IsPropertyPathNumeric) { commandBuilder.AppendLine ($"CAST(JSON_EXTRACT(Data, \'{filter.PropertyPath}\') as NUMERIC) = \'{filter.Value}\'"); @@ -179,6 +198,12 @@ public StringBuilder Build (StringBuilder commandBuilder) commandBuilder.AppendLine ($"CAST(JSON_EXTRACT(Data, \'{filter.PropertyPath}\') as NUMERIC) <= {filter.Value}"); break; case FilterType.NotEquals: + if (filter.IsPropertyPathBool) + { + commandBuilder.AppendLine($"JSON_EXTRACT(Data, \'{filter.PropertyPath}\') <> {filter.Value}"); + break; + } + commandBuilder.AppendLine ($"JSON_EXTRACT(Data, \'{filter.PropertyPath}\') <> \'{filter.Value}\'"); break; case FilterType.StartsWith: diff --git a/Tycho/Queries.cs b/Tycho/Queries.cs index 9abf10f..05c4f7b 100644 --- a/Tycho/Queries.cs +++ b/Tycho/Queries.cs @@ -45,9 +45,9 @@ INSERT OR REPLACE INTO JsonValue(Key, FullTypeName, Data, Partition) SELECT Data FROM JsonValue Where - Key = $key - AND - FullTypeName = $fullTypeName"; +Key = $key +AND +FullTypeName = $fullTypeName"; public const string SelectPartitions = @" @@ -59,23 +59,25 @@ SELECT DISTINCT Partition SELECT Data FROM JsonValue Where - FullTypeName = $fullTypeName"; +FullTypeName = $fullTypeName"; public const string DeleteDataFromJsonValueWithKeyAndFullTypeName = @" DELETE FROM JsonValue Where - Key = $key - AND - FullTypeName = $fullTypeName"; +Key = $key +AND +FullTypeName = $fullTypeName +"; public const string DeleteDataFromJsonValueWithFullTypeName = @" DELETE FROM JsonValue Where - FullTypeName = $fullTypeName"; +FullTypeName = $fullTypeName +"; public const string AndPartitionHasValue = @" @@ -105,7 +107,8 @@ public static string CreateIndexForJsonValueAsNumeric(string fullIndexName, stri return @$" CREATE INDEX IF NOT EXISTS {fullIndexName} -ON JsonValue(FullTypeName, CAST(JSON_EXTRACT(Data, '{propertyPathString}') as NUMERIC));"; +ON JsonValue(FullTypeName, CAST(JSON_EXTRACT(Data, '{propertyPathString}') as NUMERIC)); +"; } public static string CreateIndexForJsonValue(string fullIndexName, string propertyPathString) @@ -113,7 +116,8 @@ public static string CreateIndexForJsonValue(string fullIndexName, string proper return @$" CREATE INDEX IF NOT EXISTS {fullIndexName} -ON JsonValue(FullTypeName, JSON_EXTRACT(Data, '{propertyPathString}'));"; +ON JsonValue(FullTypeName, JSON_EXTRACT(Data, '{propertyPathString}')); +"; } } diff --git a/Tycho/QueryPropertyPath.cs b/Tycho/QueryPropertyPath.cs index d3aac09..b4d1dc0 100644 --- a/Tycho/QueryPropertyPath.cs +++ b/Tycho/QueryPropertyPath.cs @@ -27,6 +27,15 @@ public static bool IsNumeric (Expression> return visitor.IsNumeric ?? false; } + public static bool IsBool(Expression> expression) + { + var visitor = new PropertyIsBoolVisitor(); + + visitor.Visit(expression.Body); + + return visitor.IsBool ?? false; + } + private class PropertyPathVisitor : ExpressionVisitor { internal readonly List PathBuilder = new List(); @@ -68,5 +77,28 @@ protected override Expression VisitMember (MemberExpression node) return base.VisitMember (node); } } + + private class PropertyIsBoolVisitor : ExpressionVisitor + { + internal bool? IsBool; + + protected override Expression VisitMember(MemberExpression node) + { + if (!(node.Member is PropertyInfo)) + { + throw new ArgumentException("The path can only contain properties", nameof(node)); + } + + var propertyType = ((PropertyInfo)node.Member).PropertyType; + + if (!IsBool.HasValue) + { + //TODO: Should we add comparisons for numerics of 0/1??? + IsBool = propertyType == typeof(bool); + } + + return base.VisitMember(node); + } + } } } diff --git a/Tycho/RegisteredTypeInformation.cs b/Tycho/RegisteredTypeInformation.cs index 4a5bb87..c5b589f 100644 --- a/Tycho/RegisteredTypeInformation.cs +++ b/Tycho/RegisteredTypeInformation.cs @@ -16,6 +16,8 @@ internal struct RegisteredTypeInformation public bool IsNumeric { get; set; } + public bool IsBool { get; set; } + public string TypeFullName { get; set; } public string TypeName { get; set; } @@ -38,6 +40,7 @@ public static RegisteredTypeInformation Create (Expression> IdProperty = GetExpressionMemberName (idProperty), IdPropertyPath = QueryPropertyPath.BuildPath(idProperty), IsNumeric = QueryPropertyPath.IsNumeric(idProperty), + IsBool = QueryPropertyPath.IsBool(idProperty), TypeFullName = type.FullName, TypeName = type.Name, TypeNamespace = type.Namespace,