Skip to content
Draft
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
189 changes: 189 additions & 0 deletions src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,195 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
/// </summary>
public class SqliteQuerySqlGenerator(QuerySqlGeneratorDependencies dependencies) : QuerySqlGenerator(dependencies)
{
private string? _updateTargetAlias;
private string? _updateTargetTableName;
private string? _updateTargetSchema;

/// <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>
protected override Expression VisitUpdate(UpdateExpression updateExpression)
{
var selectExpression = updateExpression.SelectExpression;

if (selectExpression.Tables.Count > 1
&& selectExpression is
{
Offset: null,
Limit: null,
Having: null,
Orderings: [],
GroupBy: [],
Projection: [],
}
&& (!ReferenceEquals(selectExpression.Tables[0], updateExpression.Table)
|| selectExpression.Tables[1] is InnerJoinExpression
|| selectExpression.Tables[1] is CrossJoinExpression))
{
// SQLite doesn't support referencing the UPDATE target table (by alias or name) in JOIN ON clauses
// within the FROM clause. To work around this, we:
// 1. Don't alias the UPDATE target table
// 2. Replace column references to the target alias with the table name (via VisitColumn/VisitTable overrides)
// 3. Lift predicates from JOINs that reference the target table into the WHERE clause,
// emitting those tables as comma-separated instead of JOINed
_updateTargetAlias = updateExpression.Table.Alias;
_updateTargetTableName = updateExpression.Table.Name;
_updateTargetSchema = updateExpression.Table.Schema;

try
{
Sql.Append("UPDATE ");
Visit(updateExpression.Table);

Sql.AppendLine();
Sql.Append("SET ");

for (var i = 0; i < updateExpression.ColumnValueSetters.Count; i++)
{
if (i == 1)
{
Sql.IncrementIndent();
}

if (i > 0)
{
Sql.AppendLine(",");
}

var (column, value) = updateExpression.ColumnValueSetters[i];

Sql.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(column.Name)).Append(" = ");
Visit(value);
}

if (updateExpression.ColumnValueSetters.Count > 1)
{
Sql.DecrementIndent();
}

var predicate = selectExpression.Predicate;
var firstTablePrinted = false;
Sql.AppendLine().Append("FROM ");
for (var i = 0; i < selectExpression.Tables.Count; i++)
{
var table = selectExpression.Tables[i];
var joinExpression = table as JoinExpressionBase;

if (updateExpression.Table.Alias == (joinExpression?.Table.Alias ?? table.Alias))
{
LiftPredicate(table);
continue;
}

if (firstTablePrinted)
{
// For joins whose ON predicate references the target table, lift the predicate to WHERE
// and emit just the table (comma-separated) instead of a JOIN
if (joinExpression is PredicateJoinExpressionBase predicateJoin
&& ReferencesTargetTable(predicateJoin.JoinPredicate))
{
Sql.Append(", ");
LiftPredicate(table);
Visit(joinExpression.Table);
}
else
{
Sql.AppendLine();
Visit(table);
}
}
else
{
firstTablePrinted = true;
LiftPredicate(table);
table = joinExpression?.Table ?? table;
Visit(table);
}
}

if (predicate != null)
{
Sql.AppendLine().Append("WHERE ");
Visit(predicate);
}

return updateExpression;

void LiftPredicate(TableExpressionBase joinTable)
{
if (joinTable is PredicateJoinExpressionBase predicateJoinExpression)
{
predicate = predicate == null
? predicateJoinExpression.JoinPredicate
: new SqlBinaryExpression(
ExpressionType.AndAlso,
predicateJoinExpression.JoinPredicate,
predicate,
typeof(bool),
predicate.TypeMapping);
}
}
}
finally
{
_updateTargetAlias = null;
_updateTargetTableName = null;
_updateTargetSchema = null;
}
}

return base.VisitUpdate(updateExpression);
}

private bool ReferencesTargetTable(Expression expression)
=> expression switch
{
ColumnExpression column => column.TableAlias == _updateTargetAlias,
SqlBinaryExpression binary => ReferencesTargetTable(binary.Left) || ReferencesTargetTable(binary.Right),
SqlUnaryExpression unary => ReferencesTargetTable(unary.Operand),
_ => false
};

/// <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>
protected override Expression VisitTable(TableExpression tableExpression)
{
if (_updateTargetAlias is not null && tableExpression.Alias == _updateTargetAlias)
{
Sql.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tableExpression.Name, tableExpression.Schema));
return tableExpression;
}

return base.VisitTable(tableExpression);
}

/// <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>
protected override Expression VisitColumn(ColumnExpression columnExpression)
{
if (_updateTargetAlias is not null && columnExpression.TableAlias == _updateTargetAlias)
{
Sql
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(_updateTargetTableName!, _updateTargetSchema))
.Append(".")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(columnExpression.Name));
return columnExpression;
}

return base.VisitColumn(columnExpression);
}

/// <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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Data.Sqlite;

namespace Microsoft.EntityFrameworkCore.BulkUpdates.Inheritance;

#nullable disable
Expand Down Expand Up @@ -102,20 +100,36 @@ public override async Task Update_base_type(bool async)
"""
@p='Animal' (Size = 6)

UPDATE "Animals" AS "a0"
UPDATE "Animals"
SET "Name" = @p
FROM (
SELECT "a"."Id"
FROM "Animals" AS "a"
WHERE "a"."CountryId" = 1 AND "a"."Name" = 'Great spotted kiwi'
) AS "s"
WHERE "a0"."Id" = "s"."Id"
WHERE "Animals"."Id" = "s"."Id"
""");
}

// #31402
public override Task Update_base_type_with_OfType(bool async)
=> Assert.ThrowsAsync<SqliteException>(() => base.Update_base_property_on_derived_type(async));
public override async Task Update_base_type_with_OfType(bool async)
{
await base.Update_base_type_with_OfType(async);

AssertExecuteUpdateSql(
"""
@p='NewBird' (Size = 7)

UPDATE "Animals"
SET "Name" = @p
FROM "Birds" AS "b", "Kiwi" AS "k0", (
SELECT "a"."Id"
FROM "Animals" AS "a"
LEFT JOIN "Kiwi" AS "k" ON "a"."Id" = "k"."Id"
WHERE "a"."CountryId" = 1 AND "k"."Id" IS NOT NULL
) AS "s"
WHERE "Animals"."Id" = "s"."Id" AND "Animals"."Id" = "k0"."Id" AND "Animals"."Id" = "b"."Id"
""");
}

public override async Task Update_where_hierarchy_subquery(bool async)
{
Expand All @@ -124,9 +138,20 @@ public override async Task Update_where_hierarchy_subquery(bool async)
AssertExecuteUpdateSql();
}

// #31402
public override Task Update_base_property_on_derived_type(bool async)
=> Assert.ThrowsAsync<SqliteException>(() => base.Update_base_property_on_derived_type(async));
public override async Task Update_base_property_on_derived_type(bool async)
{
await base.Update_base_property_on_derived_type(async);

AssertExecuteUpdateSql(
"""
@p='SomeOtherKiwi' (Size = 13)

UPDATE "Animals"
SET "Name" = @p
FROM "Birds" AS "b", "Kiwi" AS "k"
WHERE "Animals"."Id" = "k"."Id" AND "Animals"."Id" = "b"."Id" AND "Animals"."CountryId" = 1
""");
}

public override async Task Update_derived_property_on_derived_type(bool async)
{
Expand All @@ -136,11 +161,11 @@ public override async Task Update_derived_property_on_derived_type(bool async)
"""
@p='0'

UPDATE "Kiwi" AS "k"
UPDATE "Kiwi"
SET "FoundOn" = @p
FROM "Animals" AS "a"
INNER JOIN "Birds" AS "b" ON "a"."Id" = "b"."Id"
WHERE "a"."Id" = "k"."Id" AND "a"."CountryId" = 1
WHERE "a"."Id" = "Kiwi"."Id" AND "a"."CountryId" = 1
""");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Data.Sqlite;

namespace Microsoft.EntityFrameworkCore.BulkUpdates.Inheritance;

#nullable disable
Expand Down Expand Up @@ -87,20 +85,36 @@ public override async Task Update_base_type(bool async)
"""
@p='Animal' (Size = 6)

UPDATE "Animals" AS "a0"
UPDATE "Animals"
SET "Name" = @p
FROM (
SELECT "a"."Id"
FROM "Animals" AS "a"
WHERE "a"."Name" = 'Great spotted kiwi'
) AS "s"
WHERE "a0"."Id" = "s"."Id"
WHERE "Animals"."Id" = "s"."Id"
""");
}

// #31402
public override Task Update_base_type_with_OfType(bool async)
=> Assert.ThrowsAsync<SqliteException>(() => base.Update_base_property_on_derived_type(async));
public override async Task Update_base_type_with_OfType(bool async)
{
await base.Update_base_type_with_OfType(async);

AssertExecuteUpdateSql(
"""
@p='NewBird' (Size = 7)

UPDATE "Animals"
SET "Name" = @p
FROM "Birds" AS "b", "Kiwi" AS "k0", (
SELECT "a"."Id"
FROM "Animals" AS "a"
LEFT JOIN "Kiwi" AS "k" ON "a"."Id" = "k"."Id"
WHERE "k"."Id" IS NOT NULL
) AS "s"
WHERE "Animals"."Id" = "s"."Id" AND "Animals"."Id" = "k0"."Id" AND "Animals"."Id" = "b"."Id"
""");
}

public override async Task Update_where_hierarchy_subquery(bool async)
{
Expand All @@ -109,9 +123,20 @@ public override async Task Update_where_hierarchy_subquery(bool async)
AssertExecuteUpdateSql();
}

// #31402
public override Task Update_base_property_on_derived_type(bool async)
=> Assert.ThrowsAsync<SqliteException>(() => base.Update_base_property_on_derived_type(async));
public override async Task Update_base_property_on_derived_type(bool async)
{
await base.Update_base_property_on_derived_type(async);

AssertExecuteUpdateSql(
"""
@p='SomeOtherKiwi' (Size = 13)

UPDATE "Animals"
SET "Name" = @p
FROM "Birds" AS "b", "Kiwi" AS "k"
WHERE "Animals"."Id" = "k"."Id" AND "Animals"."Id" = "b"."Id"
""");
}

public override async Task Update_derived_property_on_derived_type(bool async)
{
Expand All @@ -121,11 +146,11 @@ public override async Task Update_derived_property_on_derived_type(bool async)
"""
@p='0'

UPDATE "Kiwi" AS "k"
UPDATE "Kiwi"
SET "FoundOn" = @p
FROM "Animals" AS "a"
INNER JOIN "Birds" AS "b" ON "a"."Id" = "b"."Id"
WHERE "a"."Id" = "k"."Id"
WHERE "a"."Id" = "Kiwi"."Id"
""");
}

Expand Down Expand Up @@ -186,10 +211,10 @@ public override async Task Update_with_interface_in_property_expression(bool asy
"""
@p='0'

UPDATE "Coke" AS "c"
UPDATE "Coke"
SET "SugarGrams" = @p
FROM "Drinks" AS "d"
WHERE "d"."Id" = "c"."Id"
WHERE "d"."Id" = "Coke"."Id"
""");
}

Expand All @@ -201,10 +226,10 @@ public override async Task Update_with_interface_in_EF_Property_in_property_expr
"""
@p='0'

UPDATE "Coke" AS "c"
UPDATE "Coke"
SET "SugarGrams" = @p
FROM "Drinks" AS "d"
WHERE "d"."Id" = "c"."Id"
WHERE "d"."Id" = "Coke"."Id"
""");
}

Expand Down
Loading
Loading