Conversation
- Defines the `CustomArrayExpression` type. - Replaces `ArrayAnyExpression`. - Provides hooks for both `ANY` and `ALL` operations. Related: - https://www.postgresql.org/docs/current/static/functions-comparisons.html
roji
left a comment
There was a problem hiding this comment.
@austindrenski thanks for this great work, it's really nice to see the provider translating more complex expression types. This will definitely be a nice addition!
I know this is still WIP, but I've submitted a first review. Drop me a note when you want me to look again.
| // Prefer the default EF Core translation if one exists | ||
| var result = base.VisitSubQuery(expression); | ||
| if (result != null) | ||
| if (base.VisitSubQuery(expression) is Expression result) |
There was a problem hiding this comment.
Why not just chain these with null coalescing:
=> base.VisitSubQuery(expression) ?? VisitLikeAny(expression) ?? VisitEqualsAny(expression)| /// <remarks> | ||
| /// See https://www.postgresql.org/docs/current/static/functions-comparisons.html | ||
| /// </remarks> | ||
| public class CustomArrayExpression : Expression, IEquatable<CustomArrayExpression> |
There was a problem hiding this comment.
Let's rename this to ArrayAnyAllExpression to make the naming more specific. After all there may be other array-related expressions out there.
| namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal | ||
| { | ||
| /// <summary> | ||
| /// Represents a PostgreSQL ANY expression (e.g. 1 = ANY ('{0,1,2}'), 'cat' LIKE ANY ('{a%,b%,c%}')). |
| /// <summary> | ||
| /// The collection of values or patterns to test for the <see cref="Operand"/>. | ||
| /// </summary> | ||
| public virtual Expression Collection { get; } |
| /// <summary> | ||
| /// True if the operator is modified by ANY; otherwise, false to modify with ALL. | ||
| /// </summary> | ||
| readonly bool _any; |
There was a problem hiding this comment.
A two-value enum would be slightly cleaner
| => obj is CustomArrayExpression likeAnyExpression && Equals(likeAnyExpression); | ||
|
|
||
| /// <inheritdoc /> | ||
| public bool Equals(CustomArrayExpression other) |
| /// <summary> | ||
| /// The type of the operator expression (ANY or ALL). | ||
| /// </summary> | ||
| public virtual string OperatorType => _any ? "ANY" : "ALL"; |
There was a problem hiding this comment.
Badly named, the operator is the comparison operator, rather than the ANY/ALL. I'd omit this entirely, it's the responsibility of NpgsqlQuerySqlGenerator to actually render this expression to SQL, so it should know how to translate your bool (or 2-value enum as I suggested) to SQL.
| /// <summary> | ||
| /// Produces expressions like: 1 = ANY ('{0,1,2}') or 'cat' LIKE ANY ('{a%,b%,c%}'). | ||
| /// </summary> | ||
| public Expression VisitArrayOperator(CustomArrayExpression arrayExpression) |
| !(whereClause.Predicate is MethodCallExpression methodCallExpression)) | ||
| return null; | ||
|
|
||
| if (!(Visit(methodCallExpression.Object ?? methodCallExpression.Arguments[1]) is Expression instance)) |
There was a problem hiding this comment.
Doesn't this Visit() call actually generate SQL? Shouldn't it happen only after you've made sure you can translate the subquery (i.e. checking the specific methods below)?
… enum - Recognizes `AnyResultOperator` vs `AllResultOperator`
roji
left a comment
There was a problem hiding this comment.
I forgot to write the most important comment in my last review...
Unfortunately StartsWith and EndsWith are a bit more complicated than you'd think, because of escaping issues... the problem in your current implementation is that % and _ will be interpreted as wildcards. See NpgsqlStringStartsWithTranslator to see how regular StartsWith is translated. If it's a constant pattern we escape client-side and all is well, but if it isn't, then we have to resort to some trickery: we first do an imprecise LIKE which leverages indices but treats % and _ as wildcards, and then do LEFT() to extract the substring and do a precise comparison.
Unfortunately you can't duplicate all this complexity with LIKE ANY - it seems like we're a bit stuck on that. You can still implement the logic for constant/literal arrays of strings (e.g. .Where(x => new[] { "a", "b", "c" }.Any(y => x.Animal.StartsWith(y))), where you can escape client-side, and fall back to client-evaluation for other cases.
I'm not 100% sure this is worth the effort - tell me what you think.
| /// <summary> | ||
| /// The primary key. | ||
| /// </summary> | ||
| [Key] |
There was a problem hiding this comment.
nit: please remove, Id is a key by convention.
| /// <param name="sql">The SQL statement or fragment to search for in the logs.</param> | ||
| public void AssertContainsSql(string sql) | ||
| { | ||
| Assert.Contains(sql, Fixture.TestSqlLoggerFactory.Sql); |
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public void Dispose() |
| /// <returns> | ||
| /// A <see cref="LikeAnyContext"/> for testing. | ||
| /// </returns> | ||
| public LikeAnyContext CreateContext() |
| /// <summary> | ||
| /// Represents an ALL array comparison. | ||
| /// </summary> | ||
| ALL = 1 << 0 |
There was a problem hiding this comment.
I don't think there's any reason to give values to this enum's members, as they're not meant to be persisted or transferred anywhere (and I'm curious as to the << 0 - you're really used to flag enums? :)).
There was a problem hiding this comment.
lol... bad habit of compulsively incorporating flag enums... but definitely irrelevant here.
I've dropped the assignments. I think I was considering some uninitialized condition when I drafted it, but simplified to avoid it.
| /// A 'LIKE ANY' or 'ILIKE ANY' expression or null. | ||
| /// </returns> | ||
| [CanBeNull] | ||
| protected virtual Expression VisitLikeAny([NotNull] SubQueryExpression expression) |
| !(whereClause.Predicate is MethodCallExpression call)) | ||
| return null; | ||
|
|
||
| Expression source = queryModel.MainFromClause.FromExpression; |
I'll need to spend some more time looking into this... For now, I'm okay punting on 5b1f138 addresses your latest review, adds some fixes/tests, and disables support for If possible, I would like to get a minimal level of Does this sound feasible to you? If so, do you see any blocking issues with the current implementation? |
- Details need to be worked out for `StartsWith(...)` and `EndsWith(...)`. - But this should not block standard `LIKE`/`ILIKE` `ANY`/`ALL` translation. - Added tests + fixes for `ALL` comparisons.
8105216 to
5b1f138
Compare
Yes, that sounds feasible. I'll review again and try to merge in the next couple of days. Note that 2.1.0 is supposed to come out this week, at which point the priority will be to release everything on the Npgsql side. |
- Note: Includes type handler file renames from "Npgsq*" to "Npgsql*"
- This could be split into another patch.
- First goal: feature parity with mapped `T[]` operations.
- 8 of 10 unit tests passing:
- Passes
- Roundtrip
- SequenceEqual
- Length/Count
- Contains/Any
- Issues:
- Blocked by npgsql#430
- There should be a better system for these types of mappings.
- Along the lines of `IMethodCallTranslator`.
- First goal: feature parity with mapped `T[]` operations.
- 10 of 10 unit tests passing:
- Passes
- Roundtrip
- SequenceEqual
- Length/Count
- Contains/Any
- Indexers
- Issues:
- Blocked by npgsql#430
- There should be a better system for these types of mappings.
- Along the lines of `IMethodCallTranslator`.
roji
left a comment
There was a problem hiding this comment.
Almost there, I'll be watching closely for your fixes so as to merge them as soon as they come.
| // ReSharper disable AssignNullToNotNullAttribute | ||
| switch (call.Method.Name) | ||
| { | ||
| // TODO: Can StartsWith and EndsWith be implemented correctly? |
There was a problem hiding this comment.
nit: indent TODO comment and remove the commented-out cases (until implemented).
| var source = queryModel.MainFromClause.FromExpression; | ||
|
|
||
| // ReSharper disable AssignNullToNotNullAttribute | ||
| switch (call.Method.Name) |
There was a problem hiding this comment.
I think that a string comparison for the method name isn't the right approach here - it will capture any method named Like. Better to have a static readonly MethodInfo field for Like() and ILike(), looking it up via the GetRuntimeMethod() extension, and then comparing for actual equality (see the NpgsqlRegexIsMatchTranslator for an example).
We use string comparison in cases where we've also already made sure that the type containing the method is the correct one.
There was a problem hiding this comment.
Completely agree. Got bit by this yesterday in the list operations PR, but didn't think to check back here...
| } | ||
|
|
||
| [CanBeNull] | ||
| static MethodCallExpression ComputeAny([NotNull] QueryModel queryModel) |
There was a problem hiding this comment.
nit: any reason to separate this out to a utility method? It's only called once...
| #region StartsWithTests | ||
|
|
||
| [Fact(Skip = "StartsWith not currently supported")] | ||
| public void Array_Any_StartsWith() |
There was a problem hiding this comment.
Please remove the skipped tests for now (i.e. keep them in your local branch where you'll be working on {Starts,Ends]With support.
Also removes future features that were commented/skipped.
|
@roji Just pushed some changes to address your review. |
- First goal: feature parity with mapped `T[]` operations.
- 10 of 10 unit tests passing:
- Passes
- Roundtrip
- SequenceEqual
- Length/Count
- Contains/Any
- Indexers
- Issues:
- Blocked by npgsql#430
- There should be a better system for these types of mappings.
- Along the lines of `IMethodCallTranslator`.
|
Thanks, merged for 2.1. |
- First goal: feature parity with mapped `T[]` operations.
- 10 of 10 unit tests passing:
- Passes
- Roundtrip
- SequenceEqual
- Length/Count
- Contains/Any
- Indexers
- Issues:
- Blocked by npgsql#430
- There should be a better system for these types of mappings.
- Along the lines of `IMethodCallTranslator`.
- First goal: feature parity with mapped `T[]` operations.
- 10 of 10 unit tests passing:
- Passes
- Roundtrip
- SequenceEqual
- Length/Count
- Contains/Any
- Indexers
- Issues:
- Blocked by npgsql#430
- There should be a better system for these types of mappings.
- Along the lines of `IMethodCallTranslator`.
- First goal: feature parity with mapped `T[]` operations.
- 10 of 10 unit tests passing:
- Passes
- Roundtrip
- SequenceEqual
- Length/Count
- Contains/Any
- Indexers
- Issues:
- Blocked by npgsql#430
- There should be a better system for these types of mappings.
- Along the lines of `IMethodCallTranslator`.
- First goal: feature parity with mapped `T[]` operations.
- 10 of 10 unit tests passing:
- Passes
- Roundtrip
- SequenceEqual
- Length/Count
- Contains/Any
- Indexers
- Issues:
- Blocked by npgsql#430
- There should be a better system for these types of mappings.
- Along the lines of `IMethodCallTranslator`.
- First goal: feature parity with mapped `T[]` operations.
- 10 of 10 unit tests passing:
- Passes
- Roundtrip
- SequenceEqual
- Length/Count
- Contains/Any
- Indexers
- Issues:
- Blocked by npgsql#430
- There should be a better system for these types of mappings.
- Along the lines of `IMethodCallTranslator`.
- First goal: feature parity with mapped `T[]` operations.
- 10 of 10 unit tests passing:
- Passes
- Roundtrip
- SequenceEqual
- Length/Count
- Contains/Any
- Indexers
- Issues:
- Blocked by npgsql#430
- There should be a better system for these types of mappings.
- Along the lines of `IMethodCallTranslator`.
- First goal: feature parity with mapped `T[]` operations.
- 10 of 10 unit tests passing:
- Passes
- Roundtrip
- SequenceEqual
- Length/Count
- Contains/Any
- Indexers
- Issues:
- Blocked by npgsql#430
- There should be a better system for these types of mappings.
- Along the lines of `IMethodCallTranslator`.
- First goal: feature parity with mapped `T[]` operations.
- 10 of 10 unit tests passing:
- Passes
- Roundtrip
- SequenceEqual
- Length/Count
- Contains/Any
- Indexers
- Issues:
- Blocked by npgsql#430
- There should be a better system for these types of mappings.
- Along the lines of `IMethodCallTranslator`.
- First goal: feature parity with mapped `T[]` operations.
- 10 of 10 unit tests passing:
- Passes
- Roundtrip
- SequenceEqual
- Length/Count
- Contains/Any
- Indexers
- Issues:
- Blocked by npgsql#430
- There should be a better system for these types of mappings.
- Along the lines of `IMethodCallTranslator`.
- First goal: feature parity with mapped `T[]` operations.
- 10 of 10 unit tests passing:
- Passes
- Roundtrip
- SequenceEqual
- Length/Count
- Contains/Any
- Indexers
- Issues:
- Blocked by npgsql#430
- There should be a better system for these types of mappings.
- Along the lines of `IMethodCallTranslator`.
Translate LIKE ANY expressions
Work in progress. See #422 for more information.
Status
This PR currently has very brittle support for translation.
Examples
1.
Any+StartsWith2.
Any+EndsWith3.
Any+Like4.
Any+ILike5.
All+StartsWith6.
All+EndsWith7.
All+Like8.
All+ILike