Skip to content

Allow using foreign keys in Dataverse reverse engineering #34689

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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 @@ -155,6 +155,14 @@ public class SqlServerLoggingDefinitions : RelationalLoggingDefinitions
/// </summary>
public EventDefinitionBase? LogReflexiveConstraintIgnored;

/// <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 EventDefinitionBase? LogDataverseForeignKeyInvalid;

/// <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
9 changes: 9 additions & 0 deletions src/EFCore.SqlServer/Diagnostics/SqlServerEventId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ private enum Id
ColumnWithoutTypeWarning,
ForeignKeyReferencesUnknownPrincipalTableWarning,
MissingViewDefinitionRightsWarning,
DataverseForeignKeyInvalidWarning,
}

private static readonly string ValidationPrefix = DbLoggerCategory.Model.Validation.Name + ".";
Expand Down Expand Up @@ -286,6 +287,14 @@ private static EventId MakeScaffoldingId(Id id)
/// </remarks>
public static readonly EventId ReflexiveConstraintIgnored = MakeScaffoldingId(Id.ReflexiveConstraintIgnored);

/// <summary>
/// An invalid foreign key constraint was skipped.
/// </summary>
/// <remarks>
/// This event is in the <see cref="DbLoggerCategory.Scaffolding"/> category.
/// </remarks>
public static readonly EventId DataverseForeignKeyInvalidWarning = MakeScaffoldingId(Id.DataverseForeignKeyInvalidWarning);

/// <summary>
/// A duplicate foreign key constraint was skipped.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,27 @@ public static void ReflexiveConstraintIgnored(
// No DiagnosticsSource events because these are purely design-time messages
}

/// <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 static void DataverseForeignKeyInvalidWarning(
this IDiagnosticsLogger<DbLoggerCategory.Scaffolding> diagnostics,
string foreignKeyName,
string tableName)
{
var definition = SqlServerResources.LogDataverseForeignKeyInvalidWarning(diagnostics);

if (diagnostics.ShouldLog(definition))
{
definition.Log(diagnostics, foreignKeyName, tableName);
}

// No DiagnosticsSource events because these are purely design-time messages
}

/// <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
25 changes: 25 additions & 0 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,10 @@
<value>Skipping foreign key '{foreignKeyName}' on table '{tableName}' since all of its columns reference themselves.</value>
<comment>Debug SqlServerEventId.ReflexiveConstraintIgnored string string</comment>
</data>
<data name="LogDataverseForeignKeyInvalidWarning" xml:space="preserve">
<value>Skipping foreign key '{foreignKeyName}' on table '{tableName}' since it is not supported by the Dataverse TDS Endpoint</value>
<comment>Debug SqlServerEventId.DataverseForeignKeyInvalidWarning string string</comment>
</data>
<data name="LogSavepointsDisabledBecauseOfMARS" xml:space="preserve">
<value>Savepoints are disabled because Multiple Active Result Sets (MARS) is enabled. If 'SaveChanges' fails, then the transaction cannot be automatically rolled back to a known clean state. Instead, the transaction should be rolled back by the application before retrying 'SaveChanges'. See https://go.microsoft.com/fwlink/?linkid=2149338 for more information and examples. To identify the code which triggers this warning, call 'ConfigureWarnings(w =&gt; w.Throw(SqlServerEventId.SavepointsDisabledBecauseOfMARS))'.</value>
<comment>Warning SqlServerEventId.SavepointsDisabledBecauseOfMARS</comment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -682,10 +682,7 @@ FROM [sys].[views] AS [v]
// This is done separately due to MARS property may be turned off
GetColumns(connection, tables, tableFilterSql, viewFilter, typeAliases, databaseCollation);

if (SupportsIndexes())
{
GetIndexes(connection, tables, tableFilterSql);
}
GetIndexes(connection, tables, tableFilterSql);

GetForeignKeys(connection, tables, tableFilterSql);

Expand Down Expand Up @@ -1028,7 +1025,7 @@ private void GetIndexes(DbConnection connection, IReadOnlyList<DatabaseTable> ta
[i].[has_filter],
[i].[filter_definition],
[i].[fill_factor],
COL_NAME([ic].[object_id], [ic].[column_id]) AS [column_name],
[c].[name] AS [column_name],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder why COL_NAME was used previously??

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was my main concern, I haven't found a situation yet where this produces different results but I assume it must be there for some reason. It was introduced in 746ff59 but I can't see any specific reasoning behind it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! It was there because earlier there was no join with sys.columns - and it was just left there

https://github.com/dotnet/efcore/blob/32434d2d86a75e091e331940384115e447ff33de/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs - so nothing to be concerned about

[ic].[is_descending_key],
[ic].[is_included_column]
FROM [sys].[indexes] AS [i]
Expand Down Expand Up @@ -1089,7 +1086,7 @@ FROM [sys].[indexes] i

if (primaryKeyGroups.Length == 1)
{
if (TryGetPrimaryKey(primaryKeyGroups[0], out var primaryKey))
if (TryGetPrimaryKey(primaryKeyGroups[0], out var primaryKey) && IsValidPrimaryKey(primaryKey))
{
_logger.PrimaryKeyFound(primaryKey.Name!, DisplayName(tableSchema, tableName));
table.PrimaryKey = primaryKey;
Expand Down Expand Up @@ -1137,6 +1134,16 @@ FROM [sys].[indexes] i
}
}

bool IsValidPrimaryKey(DatabasePrimaryKey primaryKey)
{
if (_engineEdition != EngineEdition.DynamicsTdsEndpoint)
{
return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use brackets, not one line return statements

}

return primaryKey.Columns.Count == 1 && primaryKey.Columns[0].StoreType == "uniqueidentifier";
}

bool TryGetPrimaryKey(
IGrouping<(string Name, string? TypeDesc, byte FillFactor), DbDataRecord> primaryKeyGroup,
[NotNullWhen(true)] out DatabasePrimaryKey? primaryKey)
Expand Down Expand Up @@ -1377,6 +1384,14 @@ FROM [sys].[foreign_keys] AS [f]
foreignKey.PrincipalColumns.Add(principalColumn);
}

if (!invalid && _engineEdition == EngineEdition.DynamicsTdsEndpoint && !IsValidDataverseForeignKey(foreignKey))
{
invalid = true;
_logger.DataverseForeignKeyInvalidWarning(
fkName!,
DisplayName(table.Schema, table.Name));
}

if (!invalid)
{
if (foreignKey.Columns.SequenceEqual(foreignKey.PrincipalColumns))
Expand Down Expand Up @@ -1406,6 +1421,36 @@ FROM [sys].[foreign_keys] AS [f]
}
}
}

bool IsValidDataverseForeignKey(DatabaseForeignKey foreignKey)
{
if (foreignKey.Columns.Count != 1)
{
return false;
}

if (foreignKey.Columns[0].StoreType != "uniqueidentifier")
{
return false;
}

if (foreignKey.PrincipalTable.PrimaryKey == null)
{
return false;
}

if (foreignKey.PrincipalTable.PrimaryKey.Columns.Count != 1)
{
return false;
}

if (foreignKey.PrincipalTable.PrimaryKey.Columns[0].Name != foreignKey.PrincipalColumns[0].Name)
{
return false;
}

return true;
}
}

private void GetTriggers(DbConnection connection, IReadOnlyList<DatabaseTable> tables, string tableFilter)
Expand Down Expand Up @@ -1456,9 +1501,6 @@ private bool SupportsMemoryOptimizedTable()
private bool SupportsSequences()
=> _compatibilityLevel >= 110 && IsFullFeaturedEngineEdition();

private bool SupportsIndexes()
=> _engineEdition != EngineEdition.DynamicsTdsEndpoint;

private bool SupportsViews()
=> _engineEdition != EngineEdition.DynamicsTdsEndpoint;

Expand Down