Skip to content

Commit

Permalink
Support queryable array parameters and columns
Browse files Browse the repository at this point in the history
  • Loading branch information
roji committed Apr 20, 2023
1 parent 4781432 commit 83842cf
Show file tree
Hide file tree
Showing 29 changed files with 2,201 additions and 321 deletions.
1 change: 1 addition & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<PackageLicenseExpression>PostgreSQL</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/npgsql/efcore.pg</PackageProjectUrl>
<PackageIcon>postgresql.png</PackageIcon>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<EFCoreVersion>8.0.0-preview.3.23166.2</EFCoreVersion>
<MicrosoftExtensionsVersion>8.0.0-preview.3.23162.2</MicrosoftExtensionsVersion>
<EFCoreVersion>8.0.0-preview.4.23218.4</EFCoreVersion>
<MicrosoftExtensionsVersion>8.0.0-preview.4.23218.4</MicrosoftExtensionsVersion>
<NpgsqlVersion>8.0.0-preview.2</NpgsqlVersion>
</PropertyGroup>

Expand Down
3 changes: 1 addition & 2 deletions src/EFCore.PG.NTS/EFCore.PG.NTS.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
<PropertyGroup>
<AssemblyName>Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite</AssemblyName>
<RootNamespace>Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite</RootNamespace>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework Condition="'$(DeveloperBuild)' == 'True'">net8.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>

<Authors>Shay Rojansky</Authors>
<Description>NetTopologySuite PostGIS spatial support plugin for PostgreSQL/Npgsql Entity Framework Core provider.</Description>
Expand Down
3 changes: 1 addition & 2 deletions src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
<PropertyGroup>
<AssemblyName>Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime</AssemblyName>
<RootNamespace>Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime</RootNamespace>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework Condition="'$(DeveloperBuild)' == 'True'">net8.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>

<Authors>Shay Rojansky</Authors>
<Description>NodaTime support plugin for PostgreSQL/Npgsql Entity Framework Core provider.</Description>
Expand Down
3 changes: 1 addition & 2 deletions src/EFCore.PG/EFCore.PG.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
<PropertyGroup>
<AssemblyName>Npgsql.EntityFrameworkCore.PostgreSQL</AssemblyName>
<RootNamespace>Npgsql.EntityFrameworkCore.PostgreSQL</RootNamespace>
<TargetFrameworks Condition="'$(DeveloperBuild)' != 'True'">net6.0;net7.0;net8.0</TargetFrameworks>
<TargetFramework Condition="'$(DeveloperBuild)' == 'True'">net8.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>

<Authors>Shay Rojansky;Austin Drenski;Yoh Deadfall;</Authors>
<Description>PostgreSQL/Npgsql provider for Entity Framework Core.</Description>
Expand Down
4 changes: 2 additions & 2 deletions src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Data.Common;
using Npgsql.EntityFrameworkCore.PostgreSQL.Diagnostics.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal;
Expand Down Expand Up @@ -114,6 +113,7 @@ public static IServiceCollection AddEntityFrameworkNpgsql(this IServiceCollectio
.TryAdd<IEvaluatableExpressionFilter, NpgsqlEvaluatableExpressionFilter>()
.TryAdd<IQuerySqlGeneratorFactory, NpgsqlQuerySqlGeneratorFactory>()
.TryAdd<IRelationalSqlTranslatingExpressionVisitorFactory, NpgsqlSqlTranslatingExpressionVisitorFactory>()
.TryAdd<IQueryTranslationPreprocessorFactory, NpgsqlQueryTranslationPreprocessorFactory>()
.TryAdd<IQueryTranslationPostprocessorFactory, NpgsqlQueryTranslationPostprocessorFactory>()
.TryAdd<IRelationalParameterBasedSqlProcessorFactory, NpgsqlParameterBasedSqlProcessorFactory>()
.TryAdd<ISqlExpressionFactory, NpgsqlSqlExpressionFactory>()
Expand All @@ -130,4 +130,4 @@ public static IServiceCollection AddEntityFrameworkNpgsql(this IServiceCollectio

return serviceCollection;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;

/// <summary>
/// A SQL expression that represents a slicing into a PostgreSQL array (e.g. array[2:3]).
/// </summary>
/// <remarks>
/// <see href="https://www.postgresql.org/docs/current/arrays.html#ARRAYS-ACCESSING" />.
/// </remarks>
public class PostgresArraySliceExpression : SqlExpression, IEquatable<PostgresArraySliceExpression>
{
/// <summary>
/// The array being sliced.
/// </summary>
public virtual SqlExpression Array { get; }

/// <summary>
/// The lower bound of the slice.
/// </summary>
public virtual SqlExpression? LowerBound { get; }

/// <summary>
/// The upper bound of the slice.
/// </summary>
public virtual SqlExpression? UpperBound { get; }

/// <summary>
/// Creates a new instance of the <see cref="PostgresArraySliceExpression" /> class.
/// </summary>
/// <param name="array">The array tp slice into.</param>
/// <param name="lowerBound">The lower bound of the slice.</param>
/// <param name="upperBound">The upper bound of the slice.</param>
public PostgresArraySliceExpression(
SqlExpression array,
SqlExpression? lowerBound,
SqlExpression? upperBound)
: base(array.Type, array.TypeMapping)
{
Check.NotNull(array, nameof(array));

if (lowerBound is null && upperBound is null)
{
throw new ArgumentException("At least one of lowerBound or upperBound must be provided");
}

Array = array;
LowerBound = lowerBound;
UpperBound = upperBound;
}

/// <summary>
/// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
/// return this expression.
/// </summary>
/// <param name="array">The <see cref="Array" /> property of the result.</param>
/// <param name="lowerBound">The lower bound of the slice.</param>
/// <param name="upperBound">The upper bound of the slice.</param>
/// <returns>This expression if no children changed, or an expression with the updated children.</returns>
public virtual PostgresArraySliceExpression Update(SqlExpression array, SqlExpression? lowerBound, SqlExpression? upperBound)
=> array == Array && lowerBound == LowerBound && upperBound == UpperBound
? this
: new PostgresArraySliceExpression(array, lowerBound, upperBound);

/// <inheritdoc />
protected override Expression VisitChildren(ExpressionVisitor visitor)
=> Update(
(SqlExpression)visitor.Visit(Array),
(SqlExpression?)visitor.Visit(LowerBound),
(SqlExpression?)visitor.Visit(UpperBound));

/// <inheritdoc />
public virtual bool Equals(PostgresArraySliceExpression? other)
=> ReferenceEquals(this, other)
|| other is not null
&& base.Equals(other)
&& Array.Equals(other.Array)
&& (LowerBound is null ? other.LowerBound is null : LowerBound.Equals(other.LowerBound))
&& (UpperBound is null ? other.UpperBound is null : UpperBound.Equals(other.UpperBound));

/// <inheritdoc />
public override bool Equals(object? obj) => obj is PostgresArraySliceExpression e && Equals(e);

/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Array, LowerBound, UpperBound);

/// <inheritdoc />
protected override void Print(ExpressionPrinter expressionPrinter)
{
expressionPrinter.Visit(Array);
expressionPrinter.Append("[");
expressionPrinter.Visit(LowerBound);
expressionPrinter.Append(":");
expressionPrinter.Visit(UpperBound);
expressionPrinter.Append("]");
}

/// <inheritdoc />
public override string ToString() => $"{Array}[{LowerBound}:{UpperBound}]";
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ protected override void Print(ExpressionPrinter expressionPrinter)
PostgresExpressionType.LTreeMatches
when Right.TypeMapping?.StoreType == "lquery" ||
Right.TypeMapping is NpgsqlArrayTypeMapping arrayMapping &&
arrayMapping.ElementMapping.StoreType == "lquery"
arrayMapping.ElementTypeMapping.StoreType == "lquery"
=> "~",
PostgresExpressionType.LTreeMatches
when Right.TypeMapping?.StoreType == "ltxtquery"
Expand Down
124 changes: 124 additions & 0 deletions src/EFCore.PG/Query/Expressions/Internal/PostgresUnnestExpression.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;

/// <summary>
/// An expression that represents a PostgreSQL <c>unnest</c> function call in a SQL tree.
/// </summary>
/// <remarks>
/// <para>
/// This expression is just a <see cref="TableValuedFunctionExpression" />, adding the ability to provide an explicit column name
/// for its output (<c>SELECT * FROM unnest(array) AS f(foo)</c>). This is necessary since when the column name isn't explicitly
/// specified, it is automatically identical to the table alias (<c>f</c> above); since the table alias may get uniquified by
/// EF, this would break queries.
/// </para>
/// <para>
/// See <see href="https://www.postgresql.org/docs/current/functions-array.html#ARRAY-FUNCTIONS-TABLE">unnest</see> for more
/// information and examples.
/// </para>
/// <para>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </para>
/// </remarks>
public class PostgresUnnestExpression : TableValuedFunctionExpression
{
/// <summary>
/// The array to be un-nested into a table.
/// </summary>
/// <remarks>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </remarks>
public virtual SqlExpression Array
=> Arguments[0];

/// <summary>
/// The name of the column to be projected out from the <c>unnest</c> call.
/// </summary>
/// <remarks>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </remarks>
public virtual string ColumnName { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public PostgresUnnestExpression(SqlExpression array, string columnName)
: this("u", array, columnName)
{
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public PostgresUnnestExpression(string alias, SqlExpression array, string columnName)
: base(alias, "unnest", schema: null, builtIn: true, new[] { array })
{
ColumnName = columnName;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override TableValuedFunctionExpression Update(IReadOnlyList<SqlExpression> arguments)
=> arguments is [var singleArgument]
? Update(singleArgument)
: throw new ArgumentException();

/// <summary>
/// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
/// return this expression.
/// </summary>
/// <param name="array">The <see cref="Array" /> property of the result.</param>
/// <returns>This expression if no children changed, or an expression with the updated children.</returns>
public virtual PostgresUnnestExpression Update(SqlExpression array)
=> array != Array
? new PostgresUnnestExpression(Alias, array, ColumnName)
: this;

/// <inheritdoc />
protected override void Print(ExpressionPrinter expressionPrinter)
{
expressionPrinter.Append(Name);
expressionPrinter.Append("(");
expressionPrinter.VisitCollection(Arguments);
expressionPrinter.Append(")");

PrintAnnotations(expressionPrinter);
expressionPrinter
.Append(" AS ")
.Append(Alias)
.Append("(")
.Append(ColumnName)
.Append(")");
}

/// <inheritdoc />
public override bool Equals(object? obj)
=> obj != null
&& (ReferenceEquals(this, obj)
|| obj is PostgresUnnestExpression unnestExpression
&& Equals(unnestExpression));

private bool Equals(PostgresUnnestExpression unnestExpression)
=> base.Equals(unnestExpression) && ColumnName == unnestExpression.ColumnName;

/// <inheritdoc />
public override int GetHashCode()
=> base.GetHashCode();
}
37 changes: 37 additions & 0 deletions src/EFCore.PG/Query/Internal/NpgsqlQueryRootProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public class NpgsqlQueryRootProcessor : RelationalQueryRootProcessor
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public NpgsqlQueryRootProcessor(
QueryTranslationPreprocessorDependencies dependencies,
RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
QueryCompilationContext queryCompilationContext)
: base(dependencies, relationalDependencies, queryCompilationContext)
{
}

/// <summary>
/// Converts a <see cref="ParameterExpression" /> to a <see cref="ParameterQueryRootExpression" />, to be later translated to
/// PostgreSQL <c>unnest</c> over an array parameter.
/// </summary>
/// <remarks>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </remarks>
protected override bool ShouldConvertToQueryRoot(ParameterExpression parameterExpression)
=> true;
}
Loading

0 comments on commit 83842cf

Please sign in to comment.