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

LINQ improvements for string.ToLower()/ToUpper() and querying by arra… #2861

Merged
merged 1 commit into from
Dec 14, 2023
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
7 changes: 7 additions & 0 deletions src/LinqTests/Acceptance/where_clauses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ static where_clauses()

@where(x => x.StringDict.Count > 2);
@where(x => x.StringDict.Count() == 2);


@where(x => x.NumberArray != null && x.NumberArray.Length > 1 && x.NumberArray[1] == 3);
@where(x => x.StringArray != null && x.StringArray.Length > 2 && x.StringArray[2] == "Red");

@where(x => x.String.ToLower() == "red");
@where(x => x.String.ToUpper() == "RED");
}

[Theory]
Expand Down
9 changes: 9 additions & 0 deletions src/LinqTests/Internals/SimpleExpressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,13 @@ public void find_dictionary_item_member()
member.RawLocator.ShouldBe("d.data -> 'StringDict' ->> 'foo'");
member.TypedLocator.ShouldBe("d.data -> 'StringDict' ->> 'foo'");
}

[Fact]
public void find_array_indexer_field_of_string()
{
var expression = parse(x => x.StringArray[0]);
var member = expression.Member.ShouldBeOfType<StringMember>();
member.RawLocator.ShouldBe("CAST(d.data ->> 'StringArray' as jsonb) ->> 0");
member.TypedLocator.ShouldBe("CAST(d.data ->> 'StringArray' as jsonb) ->> 0");
}
}
45 changes: 41 additions & 4 deletions src/Marten/Linq/Members/DuplicatedArrayField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,19 @@

_wholeDataMember = new WholeDataMember(ElementType);

var innerPgType = PostgresqlProvider.Instance.GetDatabaseType(ElementType, EnumStorage.AsInteger);
var pgType = PostgresqlProvider.Instance.HasTypeMapping(ElementType) ? innerPgType + "[]" : "jsonb";
_innerPgType = PostgresqlProvider.Instance.GetDatabaseType(ElementType, EnumStorage.AsInteger);
var pgType = PostgresqlProvider.Instance.HasTypeMapping(ElementType) ? _innerPgType + "[]" : "jsonb";
Element = new SimpleElementMember(ElementType, pgType);

_count = new CollectionLengthMember(this);
_count = new ArrayLengthMember(this);

IsEmpty = new ArrayIsEmptyFilter(this);
NotEmpty = new ArrayIsNotEmptyFilter(this);
}

private readonly WholeDataMember _wholeDataMember;
private readonly CollectionLengthMember _count;
private readonly ArrayLengthMember _count;
private string _innerPgType;


public ISqlFragment IsEmpty { get; }
Expand Down Expand Up @@ -103,17 +104,36 @@
return new WhereFragment($"? = ANY({TypedLocator})", body.Arguments.Last().Value());
}

public IQueryableMember FindMember(MemberInfo member)

Check warning on line 107 in src/Marten/Linq/Members/DuplicatedArrayField.cs

View workflow job for this annotation

GitHub Actions / build

'DuplicatedArrayField.FindMember(MemberInfo)' hides inherited member 'DuplicatedField.FindMember(MemberInfo)'. Use the new keyword if hiding was intended.

Check warning on line 107 in src/Marten/Linq/Members/DuplicatedArrayField.cs

View workflow job for this annotation

GitHub Actions / build

'DuplicatedArrayField.FindMember(MemberInfo)' hides inherited member 'DuplicatedField.FindMember(MemberInfo)'. Use the new keyword if hiding was intended.

Check warning on line 107 in src/Marten/Linq/Members/DuplicatedArrayField.cs

View workflow job for this annotation

GitHub Actions / build

'DuplicatedArrayField.FindMember(MemberInfo)' hides inherited member 'DuplicatedField.FindMember(MemberInfo)'. Use the new keyword if hiding was intended.
{
if (member.Name == "Count" || member.Name == "Length")
{
return _count;
}

// PostgreSQL arrays are 1 based!!!!!!!
if (member is ArrayIndexMember indexMember)
{
if (ElementType == typeof(string))
{
return new StringMember(this, Casing.Default, indexMember)
{
RawLocator = $"{RawLocator}[{indexMember.Index + 1}]",
TypedLocator = $"{RawLocator}[{indexMember.Index + 1}]"
};
}

return new SimpleCastMember(this, Casing.Default, member, _innerPgType)
{
RawLocator = $"{RawLocator}[{indexMember.Index + 1}]",
TypedLocator = $"CAST({RawLocator}[{indexMember.Index + 1}] as {_innerPgType})"
};
}

return _wholeDataMember;
}

public void ReplaceMember(MemberInfo member, IQueryableMember queryableMember)

Check warning on line 136 in src/Marten/Linq/Members/DuplicatedArrayField.cs

View workflow job for this annotation

GitHub Actions / build

'DuplicatedArrayField.ReplaceMember(MemberInfo, IQueryableMember)' hides inherited member 'DuplicatedField.ReplaceMember(MemberInfo, IQueryableMember)'. Use the new keyword if hiding was intended.

Check warning on line 136 in src/Marten/Linq/Members/DuplicatedArrayField.cs

View workflow job for this annotation

GitHub Actions / build

'DuplicatedArrayField.ReplaceMember(MemberInfo, IQueryableMember)' hides inherited member 'DuplicatedField.ReplaceMember(MemberInfo, IQueryableMember)'. Use the new keyword if hiding was intended.

Check warning on line 136 in src/Marten/Linq/Members/DuplicatedArrayField.cs

View workflow job for this annotation

GitHub Actions / build

'DuplicatedArrayField.ReplaceMember(MemberInfo, IQueryableMember)' hides inherited member 'DuplicatedField.ReplaceMember(MemberInfo, IQueryableMember)'. Use the new keyword if hiding was intended.
{
throw new NotSupportedException();
}
Expand Down Expand Up @@ -182,3 +202,20 @@
return _member.IsEmpty;
}
}

internal class ArrayLengthMember: QueryableMember, IComparableMember
{
public ArrayLengthMember(DuplicatedArrayField parent): base(parent, "Count", typeof(int))
{
RawLocator = TypedLocator = $"array_length({parent.RawLocator}, 1)";
Parent = parent;
}

public ICollectionMember Parent { get; }

public override ISqlFragment CreateComparison(string op, ConstantExpression constant)
{
var def = new CommandParameter(constant);
return new ComparisonFilter(this, def, op);
}
}
44 changes: 28 additions & 16 deletions src/Marten/Linq/Members/DuplicatedField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

namespace Marten.Linq.Members;

public class DuplicatedField: IQueryableMember, IComparableMember
public class DuplicatedField: IQueryableMember, IComparableMember, IHasChildrenMembers
{
private readonly Func<object, object> _parseObject = o => o;
private readonly bool useTimestampWithoutTimeZoneForDateTime;
Expand Down Expand Up @@ -161,11 +161,6 @@ public object GetValueForCompiledQueryParameter(Expression valueExpression)
return _parseObject(value);
}

public bool ShouldUseContainmentOperator()
{
return false;
}

string IQueryableMember.SelectorForDuplication(string pgType)
{
throw new NotSupportedException();
Expand All @@ -190,11 +185,6 @@ public ISqlFragment CreateComparison(string op, ConstantExpression constant)
public string JSONBLocator { get; set; }
public string LocatorForIncludedDocumentId => TypedLocator;

public string LocatorFor(string rootTableAlias)
{
return $"{rootTableAlias}.{_columnName}";
}

public string TypedLocator { get; set; }

void ISqlFragment.Apply(CommandBuilder builder)
Expand All @@ -209,11 +199,6 @@ bool ISqlFragment.Contains(string sqlText)

public Type MemberType => InnerMember.MemberType;

public string ToOrderExpression(Expression expression)
{
return TypedLocator;
}

public string UpdateSqlFragment()
{
return $"{ColumnName} = {InnerMember.SelectorForDuplication(PgType)}";
Expand All @@ -234,4 +219,31 @@ public virtual TableColumn ToColumn()
}


public IQueryableMember FindMember(MemberInfo member)
{
// Only really using this for string ToLower() and ToUpper()
if (MemberType == typeof(string))
{
return member.Name switch
{
nameof(string.ToLower) => new StringMember(this, Casing.Default, member)
{
RawLocator = $"lower({RawLocator})", TypedLocator = $"lower({RawLocator})"
},
nameof(string.ToUpper) => new StringMember(this, Casing.Default, member)
{
RawLocator = $"upper({RawLocator})", TypedLocator = $"upper({RawLocator})"
},
_ => throw new BadLinqExpressionException($"Marten does not (yet) support the method {member.Name} in duplicated field querying")
};
}

throw new BadLinqExpressionException(
$"Marten does not (yet) support the method {member.Name} in duplicated field querying");
}

public void ReplaceMember(MemberInfo member, IQueryableMember queryableMember)
{
// Nothing
}
}
1 change: 0 additions & 1 deletion src/Marten/Linq/Members/QueryableMember.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using JasperFx.Core;
using JasperFx.Core.Reflection;
using Marten.Exceptions;
using Marten.Linq.Members.ValueCollections;
using Marten.Linq.Parsing;
using Marten.Linq.Parsing.Operators;
using Marten.Linq.SqlGeneration.Filters;
Expand Down
16 changes: 15 additions & 1 deletion src/Marten/Linq/Members/SimpleCastMember.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
using System.Reflection;
using Marten.Linq.Members.ValueCollections;
using Weasel.Core;
using Weasel.Postgresql;

namespace Marten.Linq.Members;

internal class SimpleCastMember: QueryableMember, IComparableMember
{
private HasValueMember _hasValue;

internal static SimpleCastMember ForArrayIndex(ValueCollectionMember parent, ArrayIndexMember member)
{
var pgType = PostgresqlProvider.Instance.GetDatabaseType(parent.ElementType, EnumStorage.AsInteger);

// CAST(d.data -> 'NumberArray' ->> 1 as integer)
return new SimpleCastMember(parent, Casing.Default, member, pgType)
{
RawLocator = $"{parent.SimpleLocator} ->> {member.Index}",
TypedLocator = $"CAST({parent.SimpleLocator} ->> {member.Index} as {pgType})"
};
}

public SimpleCastMember(IQueryableMember parent, Casing casing, MemberInfo member, string pgType): base(parent,
casing, member)
Expand Down
27 changes: 27 additions & 0 deletions src/Marten/Linq/Members/StringMember.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Reflection;
using Marten.Linq.Members.ValueCollections;
using Marten.Linq.Parsing.Operators;

namespace Marten.Linq.Members;
Expand All @@ -7,12 +9,37 @@ internal class StringMember: QueryableMember, IComparableMember
{
private readonly string _lowerLocator;

internal static StringMember ForArrayIndex(ValueCollectionMember parent, ArrayIndexMember member)
{
return new StringMember(parent, Casing.Default, member)
{
RawLocator = $"{parent.RawLocator} ->> {member.Index}",
TypedLocator = $"{parent.RawLocator} ->> {member.Index}"
};
}

public StringMember(IQueryableMember parent, Casing casing, MemberInfo member): base(parent, casing, member)
{
TypedLocator = RawLocator;
_lowerLocator = $"lower({RawLocator})";
}

public override IQueryableMember FindMember(MemberInfo member)
{
return member.Name switch
{
nameof(string.ToLower) => new StringMember(this, Casing.Default, member)
{
RawLocator = $"lower({RawLocator})", TypedLocator = $"lower({RawLocator})"
},
nameof(string.ToUpper) => new StringMember(this, Casing.Default, member)
{
RawLocator = $"upper({RawLocator})", TypedLocator = $"upper({RawLocator})"
},
_ => base.FindMember(member)
};
}

public override string BuildOrderingExpression(Ordering ordering, CasingRule casingRule)
{
var expression = casingRule == CasingRule.CaseSensitive ? RawLocator : _lowerLocator;
Expand Down
34 changes: 34 additions & 0 deletions src/Marten/Linq/Members/ValueCollections/ArrayIndexMember.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using System.Reflection;

namespace Marten.Linq.Members.ValueCollections;

internal class ArrayIndexMember : MemberInfo
{
public int Index { get; }

public ArrayIndexMember(int index)
{
Index = index;
}

public override object[] GetCustomAttributes(bool inherit)
{
return Array.Empty<object>();
}

public override object[] GetCustomAttributes(Type attributeType, bool inherit)
{
return Array.Empty<object>();
}

public override bool IsDefined(Type attributeType, bool inherit)
{
return false;
}

public override Type DeclaringType => typeof(Array);
public override MemberTypes MemberType => MemberTypes.Custom;
public override string Name => "ArrayIndex";
public override Type ReflectedType => typeof(int);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public ValueCollectionMember(StoreOptions storeOptions, IQueryableMember parent,
var innerPgType = PostgresqlProvider.Instance.GetDatabaseType(ElementType, EnumStorage.AsInteger);
var pgType = PostgresqlProvider.Instance.HasTypeMapping(ElementType) ? innerPgType + "[]" : "jsonb";

SimpleLocator = $"{parent.RawLocator} -> '{MemberName}'";

RawLocator = $"CAST({rawLocator} as jsonb)";
TypedLocator = $"CAST({rawLocator} as {pgType})";

Expand All @@ -59,6 +61,11 @@ public ValueCollectionMember(StoreOptions storeOptions, IQueryableMember parent,
NotEmpty = new CollectionIsNotEmpty(this);
}

/// <summary>
/// Only used to craft children locators later
/// </summary>
public string SimpleLocator { get; }

public ISqlFragment IsEmpty { get; }
public ISqlFragment NotEmpty { get; }

Expand Down Expand Up @@ -162,14 +169,19 @@ public override string SelectorForDuplication(string pgType)
return $"CAST(ARRAY(SELECT jsonb_array_elements_text({RawLocator.Replace("d.", "")})) as {pgType})";
}


public override IQueryableMember FindMember(MemberInfo member)
{
if (member.Name == "Count" || member.Name == "Length")
{
return _count;
}

// TODO -- this could be memoized a bit
if (member is ArrayIndexMember indexMember)
return ElementType == typeof(string)
? StringMember.ForArrayIndex(this, indexMember)
: SimpleCastMember.ForArrayIndex(this, indexMember);

return _wholeDataMember;
}

Expand Down
29 changes: 17 additions & 12 deletions src/Marten/Linq/Parsing/SimpleExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,7 @@ public SimpleExpression(IQueryableMemberCollection queryableMembers, Expression
HasConstant = true;
break;

// case PartialEvaluationExceptionExpression p:
// {
// var inner = p.Exception;
//
// throw new BadLinqExpressionException(
// $"Error in value expression inside of the query for '{p.EvaluatedExpression}'. See the inner exception:",
// inner);
// }
// case QuerySourceReferenceExpression:
// Member = new WholeDataMember(queryableMembers.ElementType);
// return;

case ParameterExpression:
if (queryableMembers is IValueCollectionMember collection)
{
Expand All @@ -66,7 +56,7 @@ public SimpleExpression(IQueryableMemberCollection queryableMembers, Expression
}
catch (Exception e)
{
throw new BadLinqExpressionException($"Whoa pardner, Marten could not parse '{expression}' with the SimpleExpression construct");
throw new BadLinqExpressionException($"Whoa pardner, Marten could not parse '{expression}' with the SimpleExpression construct", e);
}
break;
}
Expand All @@ -91,6 +81,11 @@ public SimpleExpression(IQueryableMemberCollection queryableMembers, Expression
}
}

public override Expression Visit(Expression node)
{
return base.Visit(node);
}

// Pretend for right now that there's only one of all of these
// obviously won't be true forever
public ConstantExpression Constant { get; set; }
Expand Down Expand Up @@ -150,6 +145,8 @@ public ISqlFragment CompareTo(SimpleExpression right, string op)
}




protected override Expression VisitBinary(BinaryExpression node)
{
switch (node.NodeType)
Expand All @@ -159,6 +156,14 @@ protected override Expression VisitBinary(BinaryExpression node)
return null;

case ExpressionType.ArrayIndex:
var index = (int)node.Right.ReduceToConstant().Value;
Members.Insert(0, new ArrayIndexMember(index));

if (node.Left is MemberExpression m)
{
return VisitMember(m);
}

return base.VisitBinary(node);

case ExpressionType.Equal:
Expand Down
Loading