Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
roji committed Jan 16, 2023
1 parent 16216eb commit 1d76c26
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 29 deletions.
68 changes: 66 additions & 2 deletions src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public static class SqlServerEntityTypeExtensions
{
private const string DefaultHistoryTableNameSuffix = "History";

#region Memory-optimized table

/// <summary>
/// Returns a value indicating whether the entity type is mapped to a memory-optimized table.
/// </summary>
Expand Down Expand Up @@ -58,6 +60,10 @@ public static void SetIsMemoryOptimized(this IMutableEntityType entityType, bool
public static ConfigurationSource? GetIsMemoryOptimizedConfigurationSource(this IConventionEntityType entityType)
=> entityType.FindAnnotation(SqlServerAnnotationNames.MemoryOptimized)?.GetConfigurationSource();

#endregion Memory-optimized table

#region Temporal table

/// <summary>
/// Returns a value indicating whether the entity type is mapped to a temporal table.
/// </summary>
Expand Down Expand Up @@ -272,13 +278,17 @@ public static void SetHistoryTableSchema(this IMutableEntityType entityType, str
public static ConfigurationSource? GetHistoryTableSchemaConfigurationSource(this IConventionEntityType entityType)
=> entityType.FindAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema)?.GetConfigurationSource();

#endregion Temporal table

#region SQL OUTPUT clause

/// <summary>
/// Returns a value indicating whether to use the SQL OUTPUT clause when saving changes to the table. The OUTPUT clause is
/// incompatible with certain SQL Server features, such as tables with triggers.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <returns><see langword="true" /> if the SQL OUTPUT clause is used to save changes to the table.</returns>
public static bool GetIsSqlOutputClauseUsed(this IReadOnlyEntityType entityType)
public static bool IsSqlOutputClauseUsed(this IReadOnlyEntityType entityType)
{
if (entityType.FindAnnotation(SqlServerAnnotationNames.UseSqlOutputClause) is { Value: bool useSqlOutputClause } )
{
Expand All @@ -288,7 +298,7 @@ public static bool GetIsSqlOutputClauseUsed(this IReadOnlyEntityType entityType)
if (entityType.GetMappingStrategy() == RelationalAnnotationNames.TphMappingStrategy
&& entityType.BaseType is not null)
{
return entityType.GetRootType().GetIsSqlOutputClauseUsed();
return entityType.GetRootType().IsSqlOutputClauseUsed();
}

return true;
Expand Down Expand Up @@ -327,4 +337,58 @@ public static void UseSqlOutputClause(this IMutableEntityType entityType, bool?
/// <returns>The configuration source for the memory-optimized setting.</returns>
public static ConfigurationSource? GetUseSqlOutputClauseConfigurationSource(this IConventionEntityType entityType)
=> entityType.FindAnnotation(SqlServerAnnotationNames.UseSqlOutputClause)?.GetConfigurationSource();

/// <summary>
/// Returns a value indicating whether to use the SQL OUTPUT clause when saving changes to the specified table.
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="storeObject">The identifier of the table-like store object.</param>
/// <returns>A value indicating whether the SQL OUTPUT clause is used to save changes to the associated table.</returns>
public static bool IsSqlOutputClauseUsed(this IReadOnlyEntityType entityType, in StoreObjectIdentifier storeObject)
{
var overrides = entityType.FindMappingFragment(storeObject);
if (overrides != null)
{
return overrides.IsSqlOutputClauseUsed() ?? entityType.IsSqlOutputClauseUsed();
}

if (StoreObjectIdentifier.Create(entityType, storeObject.StoreObjectType) == storeObject)
{
return entityType.IsSqlOutputClauseUsed();
}

throw new InvalidOperationException(
RelationalStrings.TableNotMappedEntityType(entityType.DisplayName(), storeObject.DisplayName()));
}

/// <summary>
/// Sets a value indicating whether the associated table is ignored by Migrations.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="excluded">A value indicating whether the associated table is ignored by Migrations.</param>
/// <param name="storeObject">The identifier of the table-like store object.</param>
public static void UseSqlOutputClause(
this IMutableEntityType entityType,
bool? useSqlOutputClause,
in StoreObjectIdentifier storeObject)
=> entityType.GetOrCreateMappingFragment(storeObject).UseSqlOutputClause(excluded);

/// <summary>
/// Sets a value indicating whether the associated table is ignored by Migrations.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="excluded">A value indicating whether the associated table is ignored by Migrations.</param>
/// <param name="storeObject">The identifier of the table-like store object.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>The configured value.</returns>
public static bool? UseSqlOutputClause(
this IConventionEntityType entityType,
bool? useSqlOutputClause,
in StoreObjectIdentifier storeObject,
bool fromDataAnnotation = false)
=> entityType.GetOrCreateMappingFragment(storeObject, fromDataAnnotation).SetIsTableExcludedFromMigrations(
excluded, fromDataAnnotation);

#endregion SQL OUTPUT clause
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;

/// <summary>
/// SQL Server specific extension methods for <see cref="IReadOnlyEntityTypeMappingFragment" />.
/// </summary>
public static class SqlServerEntityTypeMappingFragmentExtensions
{
/// <summary>
/// Returns a value indicating whether to use the SQL OUTPUT clause when saving changes to the associated table.
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
/// </summary>
/// <param name="fragment">The entity type mapping fragment.</param>
/// <returns>The configured value.</returns>
public static bool? IsSqlOutputClauseUsed(this IReadOnlyEntityTypeMappingFragment fragment)
=> fragment.FindAnnotation(SqlServerAnnotationNames.UseSqlOutputClause) is not { Value: false };

/// <summary>
/// Sets whether to use the SQL OUTPUT clause when saving changes to the associated table.
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
/// </summary>
/// <param name="fragment">The entity type mapping fragment.</param>
/// <param name="useSqlOutputClause">The value to set.</param>
public static void UseSqlOutputClause(this IMutableEntityTypeMappingFragment fragment, bool? useSqlOutputClause)
=> fragment.SetAnnotation(SqlServerAnnotationNames.UseSqlOutputClause, useSqlOutputClause);

/// <summary>
/// Sets whether to use the SQL OUTPUT clause when saving changes to the associated table.
/// The OUTPUT clause is incompatible with certain SQL Server features, such as tables with triggers.
/// </summary>
/// <param name="fragment">The entity type mapping fragment.</param>
/// <param name="useSqlOutputClause">The value to set.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>A value indicating whether the SQL OUTPUT clause is used to save changes to the associated table.</returns>
public static bool? UseSqlOutputClause(
this IConventionEntityTypeMappingFragment fragment,
bool? useSqlOutputClause,
bool fromDataAnnotation = false)
=> (bool?)fragment.SetAnnotation(SqlServerAnnotationNames.UseSqlOutputClause, useSqlOutputClause, fromDataAnnotation)?.Value;

/// <summary>
/// Gets the configuration source for the setting whether to use the SQL OUTPUT clause when saving changes to the associated table.
/// </summary>
/// <param name="fragment">The entity type mapping fragment.</param>
/// <returns>The configuration source for the temporal history table schema setting.</returns>
public static ConfigurationSource? GetUseSqlOutputClauseConfigurationSource(this IConventionEntityTypeMappingFragment fragment)
=> fragment.FindAnnotation(SqlServerAnnotationNames.UseSqlOutputClause)?.GetConfigurationSource();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.

// ReSharper disable once CheckNamespace

namespace Microsoft.EntityFrameworkCore;

/// <summary>
Expand Down Expand Up @@ -311,11 +310,12 @@ public static TableBuilder<TEntity> UseSqlOutputClause<TEntity>(
this TableBuilder<TEntity> tableBuilder,
bool useSqlOutputClause = true)
where TEntity : class
{
tableBuilder.Metadata.UseSqlOutputClause(useSqlOutputClause);

return tableBuilder;
}
=> ((TableBuilder)tableBuilder).UseSqlOutputClause(tableBuilder, useSqlOutputClause);
// {
// tableBuilder.Metadata.UseSqlOutputClause(useSqlOutputClause);
//
// return tableBuilder;
// }

/// <summary>
/// Configures whether to use the SQL OUTPUT clause when saving changes to the table. The OUTPUT clause is incompatible with
Expand Down
20 changes: 20 additions & 0 deletions src/EFCore.SqlServer/Extensions/SqlServerTableExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;

/// <summary>
/// SQL Server specific extension methods for <see cref="ITable" />.
/// </summary>
public static class SqlServerTableExtensions
{
/// <summary>
/// Returns a value indicating whether to use the SQL OUTPUT clause when saving changes to the table. The OUTPUT clause is
/// incompatible with certain SQL Server features, such as tables with triggers.
/// </summary>
/// <param name="table">The table.</param>
/// <returns><see langword="true" /> if the SQL OUTPUT clause is used to save changes to the table.</returns>
public static bool IsSqlOutputClauseUsed(this ITable table)
=> table.EntityTypeMappings.First().EntityType.IsSqlOutputClauseUsed(StoreObjectIdentifier.Table(table.Name, table.Schema));
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,14 @@ public void ProcessEntityTypeBaseTypeChanged(
{
// Update the old TPH root, removing or adding the setting if triggers are found anywhere in the hierarchy
if (oldBaseType?.GetRootType() is { } oldRootEntityType
&& !oldRootEntityType.GetIsSqlOutputClauseUsed()
&& !oldRootEntityType.IsSqlOutputClauseUsed()
&& !HasAnyTriggers(oldRootEntityType))
{
oldRootEntityType.UseSqlOutputClause(null);
}

if (newBaseType?.GetRootType() is { } newRootEntityType
&& newRootEntityType.GetIsSqlOutputClauseUsed()
&& newRootEntityType.IsSqlOutputClauseUsed()
&& HasAnyTriggers(entityType))
{
newRootEntityType.UseSqlOutputClause(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -994,9 +994,6 @@ protected override void AppendRowsAffectedWhereCondition(StringBuilder commandSt
.Append("@@ROWCOUNT = ")
.Append(expectedRowsAffected.ToString(CultureInfo.InvariantCulture));

// Data seeding doesn't provide any entries, so we we don't know if the target table is compatible or not; assume it isn't to generate
// SQL that works everywhere.
private static bool CanUseOutputClause(IReadOnlyModificationCommand command)
=> command.Entries.Count > 0
&& command.Entries[0].EntityType.GetIsSqlOutputClauseUsed();
=> command.Table?.IsSqlOutputClauseUsed() == true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public void Output_clause_is_enabled_by_default()
modelBuilder.Entity<Order>();
var model = modelBuilder.Model;

Assert.True(model.FindEntityType(typeof(Order))!.GetIsSqlOutputClauseUsed());
Assert.True(model.FindEntityType(typeof(Order))!.IsSqlOutputClauseUsed());
}

[ConditionalFact]
Expand All @@ -27,14 +27,14 @@ public void Trigger_disables_output_clause()
var entityType = model.FindEntityType(typeof(Order))!;

entityTypeBuilder.ToTable(t => t.HasTrigger("Trigger1"));
Assert.False(entityType.GetIsSqlOutputClauseUsed());
Assert.False(entityType.IsSqlOutputClauseUsed());
entityTypeBuilder.ToTable(t => t.HasTrigger("Trigger2"));
Assert.False(entityType.GetIsSqlOutputClauseUsed());
Assert.False(entityType.IsSqlOutputClauseUsed());

entityTypeBuilder.Metadata.RemoveTrigger("Trigger1");
Assert.False(entityType.GetIsSqlOutputClauseUsed());
Assert.False(entityType.IsSqlOutputClauseUsed());
entityTypeBuilder.Metadata.RemoveTrigger("Trigger2");
Assert.True(entityType.GetIsSqlOutputClauseUsed());
Assert.True(entityType.IsSqlOutputClauseUsed());
}

[ConditionalFact]
Expand All @@ -49,24 +49,24 @@ public void Trigger_disables_output_clause_across_TPH_hierarchy()
var rootEntityType = model.FindEntityType(typeof(Order))!;
var derivedEntityType = model.FindEntityType(typeof(SpecialOrder))!;

Assert.True(rootEntityType.GetIsSqlOutputClauseUsed());
Assert.True(derivedEntityType.GetIsSqlOutputClauseUsed());
Assert.True(rootEntityType.IsSqlOutputClauseUsed());
Assert.True(derivedEntityType.IsSqlOutputClauseUsed());

derivedEntityTypeBuilder.ToTable(t => t.HasTrigger("DerivedTrigger"));
Assert.False(derivedEntityType.GetIsSqlOutputClauseUsed());
Assert.False(rootEntityType.GetIsSqlOutputClauseUsed());
Assert.False(derivedEntityType.IsSqlOutputClauseUsed());
Assert.False(rootEntityType.IsSqlOutputClauseUsed());

rootEntityTypeBuilder.ToTable(t => t.HasTrigger("RootTrigger"));
Assert.False(derivedEntityType.GetIsSqlOutputClauseUsed());
Assert.False(rootEntityType.GetIsSqlOutputClauseUsed());
Assert.False(derivedEntityType.IsSqlOutputClauseUsed());
Assert.False(rootEntityType.IsSqlOutputClauseUsed());

derivedEntityTypeBuilder.Metadata.RemoveTrigger("DerivedTrigger");
Assert.False(derivedEntityType.GetIsSqlOutputClauseUsed());
Assert.False(rootEntityType.GetIsSqlOutputClauseUsed());
Assert.False(derivedEntityType.IsSqlOutputClauseUsed());
Assert.False(rootEntityType.IsSqlOutputClauseUsed());

rootEntityTypeBuilder.Metadata.RemoveTrigger("RootTrigger");
Assert.True(derivedEntityType.GetIsSqlOutputClauseUsed());
Assert.True(rootEntityType.GetIsSqlOutputClauseUsed());
Assert.True(derivedEntityType.IsSqlOutputClauseUsed());
Assert.True(rootEntityType.IsSqlOutputClauseUsed());
}

private class Order
Expand Down

0 comments on commit 1d76c26

Please sign in to comment.