Skip to content
125 changes: 124 additions & 1 deletion src/Libraries/Nop.Data/Extensions/FluentMigratorExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Data;
using System.Linq.Expressions;
using System.Reflection;
using FluentMigrator.Builders.Alter;
using FluentMigrator.Builders.Alter.Table;
using FluentMigrator.Builders.Create;
using FluentMigrator.Builders.Create.Table;
using FluentMigrator.Builders.Delete;
using FluentMigrator.Builders.Schema;
using FluentMigrator.Builders.Schema.Table;
using FluentMigrator.Infrastructure.Extensions;
using FluentMigrator.Model;
using FluentMigrator.Runner;
Expand Down Expand Up @@ -63,7 +68,7 @@ private static void DefineByOwnType(string columnName, Type propType, CreateTabl
/// <param name="builder">The builder to add the database engine(s) to</param>
/// <returns>The migration runner builder</returns>
public static IMigrationRunnerBuilder AddNopDbEngines(this IMigrationRunnerBuilder builder)
{
{
if (!DataSettingsManager.IsDatabaseInstalled())
return builder.AddSqlServer().AddMySql5().AddPostgres92();

Expand Down Expand Up @@ -145,6 +150,124 @@ public static void TableFor<TEntity>(this ICreateExpressionRoot expressionRoot)
builder.RetrieveTableExpressions(type);
}

/// <summary>
/// Targets a specific column of the entity’s mapped table for an ALTER COLUMN operation,
/// resolving both table and column names using <see cref="NameCompatibilityManager"/>.
/// </summary>
/// <typeparam name="TEntity">The entity type mapped to the database table.</typeparam>
/// <param name="expressionRoot">The root expression for an ALTER TABLE operation.</param>
/// <param name="selector">An expression selecting the entity property to alter.</param>
/// <returns>
/// A fluent syntax interface for specifying the new column type
/// and additional ALTER COLUMN options.
/// </returns>
public static IAlterTableColumnAsTypeSyntax AlterColumnFor<TEntity>(this IAlterExpressionRoot expressionRoot, Expression<Func<TEntity, object>> selector) where TEntity : BaseEntity
{
var tableName = NameCompatibilityManager.GetTableName(typeof(TEntity));
var propertyMemberExpression = selector.Body as MemberExpression
?? (selector.Body as UnaryExpression)?.Operand as MemberExpression
?? throw new ArgumentException("Selector must be a property expression.", nameof(selector));
var columnName = NameCompatibilityManager.GetColumnName(typeof(TEntity), propertyMemberExpression.Member.Name);
return expressionRoot.Table(tableName).AlterColumn(columnName);
}

/// <summary>
/// Deletes a column from the table mapped to the specified entity,
/// resolving the table name using <see cref="NameCompatibilityManager"/>.
/// </summary>
/// <typeparam name="TEntity">The entity type mapped to the database table.</typeparam>
/// <param name="expressionRoot">The root delete expression.</param>
/// <param name="columnName">The name of the column to delete.</param>
public static void Column<TEntity>(this IDeleteExpressionRoot expressionRoot, string columnName) where TEntity : BaseEntity
{
var tableName = NameCompatibilityManager.GetTableName(typeof(TEntity));
expressionRoot.Column(columnName).FromTable(tableName);
}

/// <summary>
/// Determines whether the database table mapped to the specified entity exists,
/// resolving the table name using <see cref="NameCompatibilityManager"/>.
/// </summary>
/// <typeparam name="TEntity">The entity type mapped to the database table.</typeparam>
/// <param name="expressionRoot">The root schema expression.</param>
/// <returns><c>true</c> if the table exists; otherwise, <c>false</c>.</returns>
public static bool TableExist<TEntity>(this ISchemaExpressionRoot expressionRoot) where TEntity : BaseEntity
{
var tableName = NameCompatibilityManager.GetTableName(typeof(TEntity));
return expressionRoot.Table(tableName).Exists();
}

/// <summary>
/// Checks whether a mapped column exists in the database table for the specified entity.
/// Resolves both the table name and column name using <see cref="NameCompatibilityManager"/>.
/// </summary>
/// <typeparam name="TEntity">The entity type mapped to the database table.</typeparam>
/// <param name="expressionRoot">The root schema expression.</param>
/// <param name="selector">An expression selecting the entity property to check.</param>
/// <returns><c>true</c> if the column exists; otherwise, <c>false</c>.</returns>
public static bool ColumnExist<TEntity>(
this ISchemaExpressionRoot expressionRoot, Expression<Func<TEntity, object>> selector) where TEntity : BaseEntity
{
var tableName = NameCompatibilityManager.GetTableName(typeof(TEntity));
var propertyMemberExpression = selector.Body as MemberExpression
?? (selector.Body as UnaryExpression)?.Operand as MemberExpression
?? throw new ArgumentException("Selector must be a property expression.", nameof(selector));
var columnName = NameCompatibilityManager.GetColumnName(typeof(TEntity), propertyMemberExpression.Member.Name);
return expressionRoot.Table(tableName).Column(columnName).Exists();
}

/// <summary>
/// Checks whether a mapped column exists in the database table for the specified entity.
/// Resolves both the table name and column name using <see cref="NameCompatibilityManager"/>.
/// </summary>
/// <typeparam name="TEntity">The entity type mapped to the database table.</typeparam>
/// <param name="expressionRoot">The root schema expression.</param>
/// <param name="columnName">The column name</param>
/// <returns><c>true</c> if the column exists; otherwise, <c>false</c>.</returns>
public static bool ColumnExist<TEntity>(
this ISchemaExpressionRoot expressionRoot, string columnName) where TEntity : BaseEntity
{
var tableName = NameCompatibilityManager.GetTableName(typeof(TEntity));
return expressionRoot.Table(tableName).Column(columnName).Exists();
}

/// <summary>
/// Targets the entity's mapped table for schema-related operations.
/// </summary>
/// <param name="expressionRoot">The root expression for schema inspection</param>
/// <typeparam name="TEntity">The entity type mapped to the database table</typeparam>
/// <returns>
/// A fluent syntax interface for performing schema operations
/// such as checking table or column existence.
/// </returns>
public static ISchemaTableSyntax TableFor<TEntity>(this ISchemaExpressionRoot expressionRoot) where TEntity : BaseEntity
{
var tableName = NameCompatibilityManager.GetTableName(typeof(TEntity));
return expressionRoot.Table(tableName);
}

/// <summary>
/// Adds a new column to the entity's mapped table for ALTER TABLE operations,
/// resolving the column name via <see cref="NameCompatibilityManager"/>.
/// </summary>
/// <typeparam name="TEntity">The entity type mapped to the database table</typeparam>
/// <param name="tableSchema">The alter table expression</param>
/// <param name="selector">An expression selecting the entity property</param>
/// <returns>
/// A fluent syntax interface allowing further ALTER TABLE operations
/// on the specified column.
/// </returns>
public static IAlterTableColumnAsTypeSyntax AddColumnFor<TEntity>(
this IAlterExpressionRoot expressionRoot, Expression<Func<TEntity, object>> selector) where TEntity : BaseEntity
{
var tableName = NameCompatibilityManager.GetTableName(typeof(TEntity));
var propertyMemberExpression = selector.Body as MemberExpression
?? (selector.Body as UnaryExpression)?.Operand as MemberExpression
?? throw new ArgumentException("Selector must be a property expression.", nameof(selector));
var columnName = NameCompatibilityManager.GetColumnName(typeof(TEntity), propertyMemberExpression.Member.Name);
return expressionRoot.Table(tableName).AddColumn(columnName);
}

/// <summary>
/// Retrieves expressions for building an entity table
/// </summary>
Expand Down
79 changes: 38 additions & 41 deletions src/Libraries/Nop.Data/Migrations/UpgradeTo490/SchemaMigration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,89 +15,86 @@ public class SchemaMigration : ForwardOnlyMigration
public override void Up()
{
//#7387
var productTableName = nameof(Product);

var ageVerificationColumnName = nameof(Product.AgeVerification);
if (!Schema.Table(productTableName).Column(ageVerificationColumnName).Exists())
if (!Schema.ColumnExist<Product>(t => t.AgeVerification))
{
Alter.Table(productTableName)
.AddColumn(ageVerificationColumnName)
Alter.AddColumnFor<Product>(t => t.AgeVerification)
.AsBoolean()
.NotNullable()
.WithDefaultValue(false);
}

var minimumAgeToPurchaseColumnName = nameof(Product.MinimumAgeToPurchase);
if (!Schema.Table(productTableName).Column(minimumAgeToPurchaseColumnName).Exists())
if (!Schema.ColumnExist<Product>(t => t.MinimumAgeToPurchase))
{
Alter.Table(productTableName)
.AddColumn(minimumAgeToPurchaseColumnName)
Alter.AddColumnFor<Product>(t => t.MinimumAgeToPurchase)
.AsInt32()
.NotNullable()
.WithDefaultValue(0);
}

//#7294
var topicTableName = nameof(Topic);
var topicAvailableEndDateColumnName = nameof(Topic.AvailableEndDateTimeUtc);
var topicAvailableStartDateColumnName = nameof(Topic.AvailableStartDateTimeUtc);

if (!Schema.Table(topicTableName).Column(topicAvailableEndDateColumnName).Exists())
if (!Schema.ColumnExist<Topic>(t => t.AvailableEndDateTimeUtc))
{
Alter.Table(topicTableName)
.AddColumn(topicAvailableEndDateColumnName)
Alter.AddColumnFor<Topic>(t => t.AvailableEndDateTimeUtc)
.AsDateTime()
.Nullable();
}

if (!Schema.Table(topicTableName).Column(topicAvailableStartDateColumnName).Exists())
if (!Schema.ColumnExist<Topic>(t => t.AvailableStartDateTimeUtc))
{
Alter.Table(topicTableName)
.AddColumn(topicAvailableStartDateColumnName)
Alter.AddColumnFor<Topic>(t => t.AvailableStartDateTimeUtc)
.AsDateTime()
.Nullable();
}

//#873
var productTagTableName = nameof(ProductTag);

if (!Schema.Table(productTagTableName).Column(nameof(ProductTag.MetaDescription)).Exists())
Alter.Table(productTagTableName).AddColumn(nameof(ProductTag.MetaDescription)).AsString().Nullable();
if (!Schema.ColumnExist<ProductTag>(t => t.MetaDescription))
{
Alter.AddColumnFor<ProductTag>(t => t.MetaDescription)
.AsString()
.Nullable();
}

if (!Schema.Table(productTagTableName).Column(nameof(ProductTag.MetaKeywords)).Exists())
Alter.Table(productTagTableName).AddColumn(nameof(ProductTag.MetaKeywords)).AsString(400).Nullable();
if (!Schema.ColumnExist<ProductTag>(t => t.MetaKeywords))
{
Alter.AddColumnFor<ProductTag>(t => t.MetaKeywords)
.AsString(400)
.Nullable();
}

if (!Schema.Table(productTagTableName).Column(nameof(ProductTag.MetaTitle)).Exists())
Alter.Table(productTagTableName).AddColumn(nameof(ProductTag.MetaTitle)).AsString(400).Nullable();
if (!Schema.ColumnExist<ProductTag>(t => t.MetaTitle))
{
Alter.AddColumnFor<ProductTag>(t => t.MetaTitle)
.AsString(400)
.Nullable();
}

//#7390
if (!Schema.Table(nameof(Menu)).Exists())
if (!Schema.TableExist<Menu>())
Create.TableFor<Menu>();

if (!Schema.Table(nameof(MenuItem)).Exists())
if (!Schema.TableExist<Menu>())
Create.TableFor<MenuItem>();

var footerColumn1ColumnName = "IncludeInFooterColumn1";
if (Schema.Table(topicTableName).Column(footerColumn1ColumnName).Exists())
Delete.Column(footerColumn1ColumnName).FromTable(topicTableName);
if (Schema.ColumnExist<Topic>(footerColumn1ColumnName))
Delete.Column<Topic>(footerColumn1ColumnName);

var footerColumn2ColumnName = "IncludeInFooterColumn2";
if (Schema.Table(topicTableName).Column(footerColumn2ColumnName).Exists())
Delete.Column(footerColumn2ColumnName).FromTable(topicTableName);
if (Schema.ColumnExist<Topic>(footerColumn2ColumnName))
Delete.Column<Topic>(footerColumn2ColumnName);

var footerColumn3ColumnName = "IncludeInFooterColumn3";
if (Schema.Table(topicTableName).Column(footerColumn3ColumnName).Exists())
Delete.Column(footerColumn3ColumnName).FromTable(topicTableName);
if (Schema.ColumnExist<Topic>(footerColumn3ColumnName))
Delete.Column<Topic>(footerColumn3ColumnName);

var includeTopicInTopMenuColumnName = "IncludeInTopMenu";
if (Schema.Table(topicTableName).Column(includeTopicInTopMenuColumnName).Exists())
Delete.Column(includeTopicInTopMenuColumnName).FromTable(topicTableName);
if (Schema.ColumnExist<Topic>(includeTopicInTopMenuColumnName))
Delete.Column(includeTopicInTopMenuColumnName);

var categoryTableName = nameof(Category);
var includeCategoryInTopMenuColumnName = "IncludeInTopMenu";
if (Schema.Table(categoryTableName).Column(includeCategoryInTopMenuColumnName).Exists())
Delete.Column(includeCategoryInTopMenuColumnName).FromTable(categoryTableName);


if (Schema.ColumnExist<Category>(includeCategoryInTopMenuColumnName))
Delete.Column<Category>(includeCategoryInTopMenuColumnName);
}
}