diff --git a/CHANGELOG.md b/CHANGELOG.md index 81d336f..4e216b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Represents the **NuGet** versions. +## v2.3.10 +- *Fixed:* `DbTableSchema`, `DbColumnSchema` and `DataParserArgs` now correctly support the full range of by-convention properties (e.g. `RowVersion`, `TenantId` and `IsDeleted`). Both the `SqlServerSchemaConfig` and `MySqlSchemaConfig` updated to default names as appropriate. Additional properties added to `Db*Schema` to support additional code-generation scenarios, etc. + ## v2.3.9 - *Fixed:* The YAML-based `MigrationCommand.Data` logic previously set the `CreatedBy`, `CreatedDate`, `UpdatedBy` and `UpdatedDate` (or specified equivalent) regardless of whether the data was being inserted or merged (insert/update). This has been corrected such that the appropriate values are only set for the specific type of operation being performed; i.e. `Created*`-only or `Updated*`-only. - *Fixed:* Enabled additional command-line arguments to be passed for `MigrationCommand.CodeGen` to enable inherited usage flexibility. Removed `MigrationArgsBase.ScriptName` and `MigrationArgsBase.ScriptArguments` and included within existing `MigrationArgsBase.Parameters` as singular dictionary of key/value pairs (simplification). diff --git a/Common.targets b/Common.targets index 18425e9..4534171 100644 --- a/Common.targets +++ b/Common.targets @@ -1,6 +1,6 @@ - 2.3.9 + 2.3.10 preview Avanade Avanade diff --git a/src/DbEx.MySql/MySqlSchemaConfig.cs b/src/DbEx.MySql/MySqlSchemaConfig.cs index dc5b3c7..a659ba0 100644 --- a/src/DbEx.MySql/MySqlSchemaConfig.cs +++ b/src/DbEx.MySql/MySqlSchemaConfig.cs @@ -45,6 +45,18 @@ public MySqlSchemaConfig(string databaseName) : base(databaseName, false) { } /// Value is 'updated_by'. public override string UpdatedByColumnName => "updated_by"; + /// + /// Value is 'tenant_id'. + public override string TenantIdColumnName => "tenant_id"; + + /// + /// Value is 'row_version'. + public override string RowVersionColumnName => "row_version"; + + /// + /// Value is 'is_deleted'. + public override string IsDeletedColumnName => "is_deleted"; + /// /// Value is 'code'. public override string RefDataCodeColumnName => "code"; @@ -68,10 +80,16 @@ public override void PrepareDataParserArgs(DataParserArgs dataParserArgs) dataParserArgs.RefDataColumnDefaults.TryAdd("sort_order", i => i); } + dataParserArgs.IdColumnNameSuffix ??= IdColumnNameSuffix; dataParserArgs.CreatedByColumnName ??= CreatedByColumnName; dataParserArgs.CreatedDateColumnName ??= CreatedDateColumnName; dataParserArgs.UpdatedByColumnName ??= UpdatedByColumnName; dataParserArgs.UpdatedDateColumnName ??= UpdatedDateColumnName; + dataParserArgs.TenantIdColumnName ??= TenantIdColumnName; + dataParserArgs.RowVersionColumnName ??= RowVersionColumnName; + dataParserArgs.IsDeletedColumnName ??= IsDeletedColumnName; + dataParserArgs.RefDataCodeColumnName ??= RefDataCodeColumnName; + dataParserArgs.RefDataTextColumnName ??= RefDataTextColumnName; } /// diff --git a/src/DbEx.SqlServer/SqlServerSchemaConfig.cs b/src/DbEx.SqlServer/SqlServerSchemaConfig.cs index 8198cce..4e8731b 100644 --- a/src/DbEx.SqlServer/SqlServerSchemaConfig.cs +++ b/src/DbEx.SqlServer/SqlServerSchemaConfig.cs @@ -45,6 +45,18 @@ public SqlServerSchemaConfig(string databaseName) : base(databaseName) { } /// Value is 'UpdatedBy'. public override string UpdatedByColumnName => "UpdatedBy"; + /// + /// Value is 'TenantId'. + public override string TenantIdColumnName => "TenantId"; + + /// + /// Value is 'RowVersion'. + public override string RowVersionColumnName => "RowVersion"; + + /// + /// Value is 'IsDeleted'. + public override string IsDeletedColumnName => "IsDeleted"; + /// /// Value is 'Code'. public override string RefDataCodeColumnName => "Code"; @@ -68,10 +80,16 @@ public override void PrepareDataParserArgs(DataParserArgs dataParserArgs) dataParserArgs.RefDataColumnDefaults.TryAdd("SortOrder", i => i); } + dataParserArgs.IdColumnNameSuffix ??= IdColumnNameSuffix; dataParserArgs.CreatedByColumnName ??= CreatedByColumnName; dataParserArgs.CreatedDateColumnName ??= CreatedDateColumnName; dataParserArgs.UpdatedByColumnName ??= UpdatedByColumnName; dataParserArgs.UpdatedDateColumnName ??= UpdatedDateColumnName; + dataParserArgs.TenantIdColumnName ??= TenantIdColumnName; + dataParserArgs.RowVersionColumnName ??= RowVersionColumnName; + dataParserArgs.IsDeletedColumnName ??= IsDeletedColumnName; + dataParserArgs.RefDataCodeColumnName ??= RefDataCodeColumnName; + dataParserArgs.RefDataTextColumnName ??= RefDataTextColumnName; } /// diff --git a/src/DbEx/DatabaseExtensions.cs b/src/DbEx/DatabaseExtensions.cs index 446ad76..82706f7 100644 --- a/src/DbEx/DatabaseExtensions.cs +++ b/src/DbEx/DatabaseExtensions.cs @@ -8,8 +8,11 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; +using System.Text; using System.Threading; using System.Threading.Tasks; +using OnRamp.Utility; namespace DbEx { @@ -31,9 +34,11 @@ public static async Task> SelectSchemaAsync(this IDatabase d var tables = new List(); DbTableSchema? table = null; - var idColumnNameSuffix = dataParserArgs?.IdColumnNameSuffix ?? databaseSchemaConfig.IdColumnNameSuffix; - var refDataCodeColumn = dataParserArgs?.RefDataCodeColumnName ?? databaseSchemaConfig.RefDataCodeColumnName; - var refDataTextColumn = dataParserArgs?.RefDataTextColumnName ?? databaseSchemaConfig.RefDataTextColumnName; + dataParserArgs ??= new DataParserArgs(); + databaseSchemaConfig.PrepareDataParserArgs(dataParserArgs); + var idColumnNameSuffix = dataParserArgs?.IdColumnNameSuffix!; + var refDataCodeColumn = dataParserArgs?.RefDataCodeColumnName!; + var refDataTextColumn = dataParserArgs?.RefDataTextColumnName!; var refDataPredicate = new Func(t => t.Columns.Any(c => c.Name == refDataCodeColumn && !c.IsPrimaryKey && c.DotNetType == "string") && t.Columns.Any(c => c.Name == refDataTextColumn && !c.IsPrimaryKey && c.DotNetType == "string")); // Get all the tables and their columns. @@ -56,8 +61,11 @@ await database.SqlStatement(await sr.ReadToEndAsync().ConfigureAwait(false)).Sel tables.Add(table = dt); var dc = databaseSchemaConfig.CreateColumnFromInformationSchema(table, dr); - dc.IsCreatedAudit = dc.Name == (dataParserArgs?.CreatedByColumnName ?? databaseSchemaConfig.CreatedByColumnName) || dc.Name == (dataParserArgs?.CreatedDateColumnName ?? databaseSchemaConfig.CreatedDateColumnName); - dc.IsUpdatedAudit = dc.Name == (dataParserArgs?.UpdatedByColumnName ?? databaseSchemaConfig.UpdatedByColumnName) || dc.Name == (dataParserArgs?.UpdatedDateColumnName ?? databaseSchemaConfig.UpdatedDateColumnName); + dc.IsCreatedAudit = dc.Name == dataParserArgs?.CreatedByColumnName || dc.Name == dataParserArgs?.CreatedDateColumnName; + dc.IsUpdatedAudit = dc.Name == dataParserArgs?.UpdatedByColumnName || dc.Name == dataParserArgs?.UpdatedDateColumnName; + dc.IsTenantId = dc.Name == dataParserArgs?.TenantIdColumnName; + dc.IsRowVersion = dc.Name == dataParserArgs?.RowVersionColumnName; + dc.IsIsDeleted = dc.Name == dataParserArgs?.IsDeletedColumnName; table.Columns.Add(dc); return 0; @@ -153,6 +161,31 @@ from c in t.Columns } } + // Attempt to infer if a reference data column where not explicitly specified. + var sb = new StringBuilder(); + + foreach (var t in tables) + { + foreach (var c in t.Columns.Where(x => !x.IsPrimaryKey)) + { + if (c.IsForeignRefData) + { + c.IsRefData = true; + continue; + } + + sb.Clear(); + c.Name.Split(new char[] { '_', '-' }, StringSplitOptions.RemoveEmptyEntries).ForEach(part => sb.Append(StringConverter.ToPascalCase(part))); + var words = Regex.Split(sb.ToString(), DbTableSchema.WordSplitPattern).Where(x => !string.IsNullOrEmpty(x)); + if (words.Count() > 1 && new string[] { "Id", "Code" }.Contains(words.Last(), StringComparer.InvariantCultureIgnoreCase)) + { + var name = string.Join(string.Empty, words.Take(words.Count() - 1)); + if (tables.Any(x => x.Name == name && x.Schema == t.Schema && x.IsRefData)) + c.IsRefData = true; + } + } + } + return tables; } } diff --git a/src/DbEx/DatabaseSchemaConfig.cs b/src/DbEx/DatabaseSchemaConfig.cs index 24fe499..aeecddf 100644 --- a/src/DbEx/DatabaseSchemaConfig.cs +++ b/src/DbEx/DatabaseSchemaConfig.cs @@ -44,27 +44,33 @@ protected DatabaseSchemaConfig(string databaseName, bool supportsSchema = true) /// /// Gets the name of the column (where it exists). /// - /// Defaults to 'CreatedDate'. public abstract string CreatedDateColumnName { get; } /// /// Gets the name of the column (where it exists). /// - /// Defaults to 'CreatedBy'. public abstract string CreatedByColumnName { get; } /// /// Gets the name of the column (where it exists). /// - /// Defaults to 'UpdatedDate'. public abstract string UpdatedDateColumnName { get; } /// /// Gets the name of the column (where it exists). /// - /// Defaults to 'UpdatedBy'. public abstract string UpdatedByColumnName { get; } + /// + /// Gets the name of the column (where it exists). + /// + public abstract string TenantIdColumnName { get; } + + /// + /// Gets the name of the row-version ( equivalent) column (where it exists). + /// + public abstract string RowVersionColumnName { get; } + /// /// Gets the default column. /// @@ -75,6 +81,11 @@ protected DatabaseSchemaConfig(string databaseName, bool supportsSchema = true) /// public abstract string RefDataTextColumnName { get; } + /// + /// Gets the default column. + /// + public abstract string IsDeletedColumnName { get; } + /// /// Gets the default reference data predicate to determine . /// diff --git a/src/DbEx/DbSchema/DbColumnSchema.cs b/src/DbEx/DbSchema/DbColumnSchema.cs index 9af86b7..4314be9 100644 --- a/src/DbEx/DbSchema/DbColumnSchema.cs +++ b/src/DbEx/DbSchema/DbColumnSchema.cs @@ -13,6 +13,7 @@ public class DbColumnSchema { private string? _dotNetType; private string? _dotNetName; + private string? _dotNetCleanedName; private string? _sqlType; /// @@ -53,6 +54,11 @@ public DbColumnSchema(DbTableSchema dbTable, string name, string type) /// public ulong? Length { get; set; } + /// + /// Indicates whether the column has a length greater than zero. + /// + public bool HasLength => Length != null && Length > 0; + /// /// Gets or sets the precision. /// @@ -119,7 +125,12 @@ public DbColumnSchema(DbTableSchema dbTable, string name, string type) public string? ForeignColumn { get; set; } /// - /// Indicates whether the foreign key is references a reference data table/entity. + /// Indicates whether the column or the name (after removing 'Id' or 'Code') matches a reference data table/entity in the same schema (where applicable). + /// + public bool IsRefData { get; set; } + + /// + /// Indicates whether the foreign key is referencing a reference data table/entity. /// public bool IsForeignRefData { get; set; } @@ -138,6 +149,26 @@ public DbColumnSchema(DbTableSchema dbTable, string name, string type) /// public bool IsUpdatedAudit { get; set; } + /// + /// Indicates whether the column is a row-version column; i.e. name is RowVersion. + /// + public bool IsRowVersion { get; set; } + + /// + /// Indicates whether the column is a tenant identifier column; i.e. name is TenantId. + /// + public bool IsTenantId { get; set; } + + /// + /// Indicates whether the column is an is-deleted column; i.e. name is IsDeleted. + /// + public bool IsIsDeleted { get; set; } + + /// + /// Indicates whether the column may contain JSON content by convention ( is a `string` and the ends with `Json`) . + /// + public bool IsJsonContent => DotNetType == "string" && Name.EndsWith("Json", StringComparison.OrdinalIgnoreCase); + /// /// Gets the corresponding .NET name. /// @@ -149,19 +180,14 @@ public DbColumnSchema(DbTableSchema dbTable, string name, string type) public string DotNetName => _dotNetName ??= DbTableSchema.CreateDotNetName(Name); /// - /// Gets the fully defined SQL type. + /// Gets the corresponding .NET name cleaned; by removing any known suffixes where or /// - public string SqlType => _sqlType ??= DbTable?.Config.ToFormattedSqlType(this) ?? throw new InvalidOperationException($"The {nameof(DbTable)} must be set before the {nameof(SqlType)} property can be accessed."); + public string DotNetCleanedName => _dotNetCleanedName ??= DbTableSchema.CreateDotNetName(Name, IsRefData || IsJsonContent); /// - /// Prepares the schema by updating the calculated properties: , and . + /// Gets the fully defined SQL type. /// - public void Prepare() - { - _dotNetType = DbTable.Config.ToDotNetTypeName(this); - _dotNetName = DbTableSchema.CreateDotNetName(Name); - _sqlType = DbTable.Config.ToFormattedSqlType(this); - } + public string SqlType => _sqlType ??= DbTable?.Config.ToFormattedSqlType(this) ?? throw new InvalidOperationException($"The {nameof(DbTable)} must be set before the {nameof(SqlType)} property can be accessed."); /// /// Clones the creating a new instance. @@ -195,11 +221,16 @@ public void CopyFrom(DbColumnSchema column) ForeignSchema = column.ForeignSchema; ForeignColumn = column.ForeignColumn; IsForeignRefData = column.IsForeignRefData; + IsRefData = column.IsRefData; ForeignRefDataCodeColumn = column.ForeignRefDataCodeColumn; IsCreatedAudit = column.IsCreatedAudit; IsUpdatedAudit = column.IsUpdatedAudit; + IsRowVersion = column.IsRowVersion; + IsTenantId = column.IsTenantId; + IsIsDeleted = column.IsIsDeleted; _dotNetType = column._dotNetType; _dotNetName = column._dotNetName; + _dotNetCleanedName = column._dotNetCleanedName; _sqlType = column._sqlType; } } diff --git a/src/DbEx/DbSchema/DbTableSchema.cs b/src/DbEx/DbSchema/DbTableSchema.cs index aaba8ca..9c61db5 100644 --- a/src/DbEx/DbSchema/DbTableSchema.cs +++ b/src/DbEx/DbSchema/DbTableSchema.cs @@ -45,15 +45,25 @@ public static string CreateAlias(string name) /// Create a .NET friendly name. /// /// The name. + /// Indicates whether to remove the known suffix. /// The .NET friendly name. - public static string CreateDotNetName(string name) + public static string CreateDotNetName(string name, bool removeKnownSuffix = false) { if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); var sb = new StringBuilder(); name.Split(new char[] { '_', '-' }, StringSplitOptions.RemoveEmptyEntries).ForEach(part => sb.Append(StringConverter.ToPascalCase(part))); - return sb.ToString(); + var dotNet = sb.ToString(); + + if (removeKnownSuffix) + { + var words = Regex.Split(dotNet, WordSplitPattern).Where(x => !string.IsNullOrEmpty(x)); + if (words.Count() > 1 && new string[] { "Id", "Code", "Json" }.Contains(words.Last(), StringComparer.InvariantCultureIgnoreCase)) + dotNet = string.Join(string.Empty, words.Take(words.Count() - 1)); + } + + return dotNet; } /// @@ -86,6 +96,23 @@ public DbTableSchema(DatabaseSchemaConfig config, string schema, string name) Alias = CreateAlias(Name); } + /// + /// Initializes a new instance of the class referencing an existing instance. + /// + /// The existing . + public DbTableSchema(DbTableSchema table) + { + Config = table.Config; + Schema = table.Schema; + Name = table.Name; + QualifiedName = table.QualifiedName; + Alias = table.Alias; + IsAView = table.IsAView; + IsRefData = table.IsRefData; + Columns.AddRange(table.Columns); + RefDataCodeColumn = table.RefDataCodeColumn; + } + /// /// Gets the . /// @@ -141,6 +168,31 @@ public DbTableSchema(DatabaseSchemaConfig config, string schema, string name) /// public List PrimaryKeyColumns => Columns?.Where(x => x.IsPrimaryKey).ToList() ?? new List(); + /// + /// Gets the standard list (i.e. not primary key, not created audit, not updated audit, not tenant-id, not row-version, not is-deleted). + /// + public List StandardColumns => Columns?.Where(x => !x.IsPrimaryKey && !x.IsCreatedAudit && !x.IsUpdatedAudit && !x.IsTenantId && !x.IsRowVersion && !x.IsIsDeleted).ToList() ?? new List(); + + /// + /// Gets the tenant idenfifier (if any). + /// + public DbColumnSchema? TenantIdColumn => Columns?.FirstOrDefault(x => x.IsTenantId); + + /// + /// Gets the row version (if any). + /// + public DbColumnSchema? RowVersionColumn => Columns?.FirstOrDefault(x => x.IsRowVersion); + + /// + /// Gets the is-deleted (if any). + /// + public DbColumnSchema? IsDeletedColumn => Columns?.FirstOrDefault(x => x.IsIsDeleted); + + /// + /// Indicates whether the table has any audit columns. + /// + public bool HasAuditColumns => Columns?.Any(x => x.IsCreatedAudit || x.IsUpdatedAudit) ?? false; + /// /// Gets or sets the . /// diff --git a/src/DbEx/Migration/Data/DataParserArgs.cs b/src/DbEx/Migration/Data/DataParserArgs.cs index 3fe5669..9fdb524 100644 --- a/src/DbEx/Migration/Data/DataParserArgs.cs +++ b/src/DbEx/Migration/Data/DataParserArgs.cs @@ -70,6 +70,24 @@ public DataParserArgs() { } /// Defaults to where not specified (i.e. null). public string? UpdatedByColumnName { get; set; } + /// + /// Gets or sets the name of the column (where it exists). + /// + /// Defaults to where not specified (i.e. null). + public string? TenantIdColumnName { get; set; } + + /// + /// Gets or sets the name of the row-version ( equivalent) column (where it exists). + /// + /// Defaults to where not specified (i.e. null). + public string? RowVersionColumnName { get; set; } + + /// + /// Gets or sets the name of the column (where it exists). + /// + /// Defaults to where not specified (i.e. null). + public string? IsDeletedColumnName { get; set; } + /// /// Gets or sets the name of the column. /// @@ -169,10 +187,13 @@ public void CopyFrom(DataParserArgs args) UserName = args.UserName; DateTimeNow = args.DateTimeNow; + IdColumnNameSuffix = args.IdColumnNameSuffix; CreatedDateColumnName = args.CreatedDateColumnName; CreatedByColumnName = args.CreatedByColumnName; UpdatedDateColumnName = args.UpdatedDateColumnName; UpdatedByColumnName = args.UpdatedByColumnName; + RowVersionColumnName = args.RowVersionColumnName; + TenantIdColumnName = args.TenantIdColumnName; RefDataCodeColumnName = args.RefDataCodeColumnName; RefDataTextColumnName = args.RefDataTextColumnName; IdentifierGenerator = args.IdentifierGenerator; diff --git a/tests/DbEx.Test/DatabaseSchemaTest.cs b/tests/DbEx.Test/DatabaseSchemaTest.cs index 6af7510..9013112 100644 --- a/tests/DbEx.Test/DatabaseSchemaTest.cs +++ b/tests/DbEx.Test/DatabaseSchemaTest.cs @@ -171,6 +171,9 @@ public async Task SqlServerSelectSchema() Assert.AreEqual("ContactTypeId", col.ForeignColumn); Assert.AreEqual("Code", col.ForeignRefDataCodeColumn); Assert.AreEqual("((1))", col.DefaultValue); + Assert.AreEqual("ContactTypeId", col.DotNetName); + Assert.AreEqual("ContactType", col.DotNetCleanedName); + Assert.IsTrue(col.IsRefData); col = tab.Columns[5]; Assert.AreEqual("GenderId", col.Name); @@ -192,9 +195,11 @@ public async Task SqlServerSelectSchema() Assert.AreEqual("Gender", col.ForeignTable); Assert.AreEqual("GenderId", col.ForeignColumn); Assert.IsNull(col.DefaultValue); + Assert.IsFalse(col.IsTenantId); col = tab.Columns[6]; Assert.AreEqual("TenantId", col.Name); + Assert.IsTrue(col.IsTenantId); col = tab.Columns[7]; Assert.AreEqual("Notes", col.Name); @@ -397,6 +402,7 @@ public async Task MySqlSelectSchema() Assert.IsNull(col.ForeignColumn); Assert.IsNull(col.DefaultValue); Assert.AreEqual("ContactTypeId", col.DotNetName); + Assert.AreEqual("ContactTypeId", col.DotNetCleanedName); col = tab.Columns[1]; Assert.AreEqual("code", col.Name);