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

SQLite: allow opting out of RETURNING for virtual tables #29679

Closed
wants to merge 1 commit into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ public override ResultSetMapping AppendDeleteOperation(
int commandPosition,
out bool requiresTransaction)
// We normally do a simple DELETE, with an OUTPUT clause emitting "1" for concurrency checking.
// However, if there are triggers defined, OUTPUT (without INTO) is not supported, so we do UPDATE+SELECT.
// However, if there are triggers defined, OUTPUT (without INTO) is not supported, so we do DELETE+SELECT.
=> HasAnyTriggers(command)
? AppendDeleteAndSelectOperation(commandStringBuilder, command, commandPosition, out requiresTransaction)
: AppendDeleteReturningOperation(commandStringBuilder, command, commandPosition, out requiresTransaction);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,7 @@ public static IServiceCollection AddEntityFrameworkSqlite(this IServiceCollectio
.TryAdd<IQueryableMethodTranslatingExpressionVisitorFactory, SqliteQueryableMethodTranslatingExpressionVisitorFactory>()
.TryAdd<IRelationalSqlTranslatingExpressionVisitorFactory, SqliteSqlTranslatingExpressionVisitorFactory>()
.TryAdd<IQueryTranslationPostprocessorFactory, SqliteQueryTranslationPostprocessorFactory>()
.TryAdd<IUpdateSqlGenerator>(
sp =>
{
// Support for the RETURNING clause on INSERT/UPDATE/DELETE was added in Sqlite 3.35.
// Detect which version we're using, and fall back to the older INSERT/UPDATE+SELECT behavior on legacy versions.
var dependencies = sp.GetRequiredService<UpdateSqlGeneratorDependencies>();

return new Version(new SqliteConnection().ServerVersion) < new Version(3, 35)
? new SqliteLegacyUpdateSqlGenerator(dependencies)
: new SqliteUpdateSqlGenerator(dependencies);
})
.TryAdd<IUpdateSqlGenerator, SqliteUpdateSqlGenerator>()
.TryAdd<ISqlExpressionFactory, SqliteSqlExpressionFactory>()
.TryAddProviderSpecificServices(
b => b.TryAddScoped<ISqliteRelationalConnection, SqliteRelationalConnection>());
Expand Down

This file was deleted.

119 changes: 118 additions & 1 deletion src/EFCore.Sqlite.Core/Update/Internal/SqliteUpdateSqlGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore.Sqlite.Internal;

namespace Microsoft.EntityFrameworkCore.Sqlite.Update.Internal;
Expand All @@ -11,8 +13,10 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Update.Internal;
/// 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 SqliteUpdateSqlGenerator : UpdateSqlGenerator
public class SqliteUpdateSqlGenerator : UpdateAndSelectSqlGenerator
{
private readonly bool _isReturningClauseSupported;

/// <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 All @@ -22,6 +26,112 @@ public class SqliteUpdateSqlGenerator : UpdateSqlGenerator
public SqliteUpdateSqlGenerator(UpdateSqlGeneratorDependencies dependencies)
: base(dependencies)
{
// Support for the RETURNING clause on INSERT/UPDATE/DELETE was added in Sqlite 3.35.
// Detect which version we're using, and fall back to the older INSERT/UPDATE+SELECT behavior on legacy versions.
_isReturningClauseSupported = new Version(new SqliteConnection().ServerVersion) >= new Version(3, 35);
}

/// <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 ResultSetMapping AppendInsertOperation(
StringBuilder commandStringBuilder,
IReadOnlyModificationCommand command,
int commandPosition,
out bool requiresTransaction)
// We normally do a simple INSERT, with a RETURNING clause for generated columns or with "1" for concurrency checking.
// However, older SQLite versions and virtual tables don't support RETURNING, so we do INSERT+SELECT.
=> CanUseReturningClause(command)
? AppendInsertReturningOperation(commandStringBuilder, command, commandPosition, out requiresTransaction)
: AppendInsertAndSelectOperations(commandStringBuilder, command, commandPosition, out requiresTransaction);

/// <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 ResultSetMapping AppendUpdateOperation(
StringBuilder commandStringBuilder,
IReadOnlyModificationCommand command,
int commandPosition,
out bool requiresTransaction)
// We normally do a simple UPDATE, with a RETURNING clause for generated columns or with "1" for concurrency checking.
// However, older SQLite versions and virtual tables don't support RETURNING, so we do UPDATE+SELECT.
=> CanUseReturningClause(command)
? AppendUpdateReturningOperation(commandStringBuilder, command, commandPosition, out requiresTransaction)
: AppendUpdateAndSelectOperation(commandStringBuilder, command, commandPosition, out requiresTransaction);

/// <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 ResultSetMapping AppendDeleteOperation(
StringBuilder commandStringBuilder,
IReadOnlyModificationCommand command,
int commandPosition,
out bool requiresTransaction)
// We normally do a simple DELETE, with a RETURNING clause with "1" for concurrency checking.
// However, older SQLite versions and virtual tables don't support RETURNING, so we do DELETE+SELECT.
=> CanUseReturningClause(command)
? AppendDeleteReturningOperation(commandStringBuilder, command, commandPosition, out requiresTransaction)
: AppendDeleteAndSelectOperation(commandStringBuilder, command, commandPosition, out requiresTransaction);

/// <summary>
/// Appends a <c>WHERE</c> condition for the identity (i.e. key value) of the given column.
/// </summary>
/// <param name="commandStringBuilder">The builder to which the SQL should be appended.</param>
/// <param name="columnModification">The column for which the condition is being generated.</param>
protected override void AppendIdentityWhereCondition(StringBuilder commandStringBuilder, IColumnModification columnModification)
{
Check.NotNull(commandStringBuilder, nameof(commandStringBuilder));
Check.NotNull(columnModification, nameof(columnModification));

SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, "rowid");
commandStringBuilder.Append(" = ")
.Append("last_insert_rowid()");
}

/// <summary>
/// Appends a SQL command for selecting the number of rows affected.
/// </summary>
/// <param name="commandStringBuilder">The builder to which the SQL should be appended.</param>
/// <param name="name">The name of the table.</param>
/// <param name="schema">The table schema, or <see langword="null" /> to use the default schema.</param>
/// <param name="commandPosition">The ordinal of the command for which rows affected it being returned.</param>
/// <returns>The <see cref="ResultSetMapping" /> for this command.</returns>
protected override ResultSetMapping AppendSelectAffectedCountCommand(
StringBuilder commandStringBuilder,
string name,
string? schema,
int commandPosition)
{
Check.NotNull(commandStringBuilder, nameof(commandStringBuilder));
Check.NotEmpty(name, nameof(name));

commandStringBuilder
.Append("SELECT changes()")
.AppendLine(SqlGenerationHelper.StatementTerminator)
.AppendLine();

return ResultSetMapping.LastInResultSet | ResultSetMapping.ResultSetWithRowsAffectedOnly;
}

/// <summary>
/// Appends a <c>WHERE</c> condition checking rows affected.
/// </summary>
/// <param name="commandStringBuilder">The builder to which the SQL should be appended.</param>
/// <param name="expectedRowsAffected">The expected number of rows affected.</param>
protected override void AppendRowsAffectedWhereCondition(StringBuilder commandStringBuilder, int expectedRowsAffected)
{
Check.NotNull(commandStringBuilder, nameof(commandStringBuilder));

commandStringBuilder.Append("changes() = ").Append(expectedRowsAffected);
}

/// <summary>
Expand All @@ -32,4 +142,11 @@ public SqliteUpdateSqlGenerator(UpdateSqlGeneratorDependencies dependencies)
/// </summary>
public override string GenerateNextSequenceValueOperation(string name, string? schema)
=> throw new NotSupportedException(SqliteStrings.SequencesNotSupported);

// Data seeding doesn't provide any entries, so we we don't know if the target table is compatible or not; assume it does to
// generate SQL that works everywhere.
private bool CanUseReturningClause(IReadOnlyModificationCommand command)
=> _isReturningClauseSupported
&& command.Entries.Count > 0
&& /* check for virtual table */ true;
}