diff --git a/CHANGELOG.md b/CHANGELOG.md index 40ae3b1..321b27f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Represents the **NuGet** versions. +## v2.1.0 +- *Enhancement:* Added `DataParserArgs.ColumnDefaults` so that _any_ table column(s) can be defaulted where required (where not directly specified). +- *Enhancement:* Improved help text to include the schema command and arguments. + ## v2.0.0 - *Enhancement:* Added MySQL database migrations. - *Note:* Given the extent of this and previous change a major version change is warranted (published version `v1.1.1` should be considered as deprecated as a result). diff --git a/Common.targets b/Common.targets index a72f21a..29ad714 100644 --- a/Common.targets +++ b/Common.targets @@ -1,6 +1,6 @@ - 2.0.0 + 2.1.0 preview Avanade Avanade diff --git a/src/DbEx.MySql/Console/MySqlMigrationConsole.cs b/src/DbEx.MySql/Console/MySqlMigrationConsole.cs index 2818942..121d4f5 100644 --- a/src/DbEx.MySql/Console/MySqlMigrationConsole.cs +++ b/src/DbEx.MySql/Console/MySqlMigrationConsole.cs @@ -3,6 +3,7 @@ using DbEx.Console; using DbEx.Migration; using DbEx.MySql.Migration; +using Microsoft.Extensions.Logging; using System; using System.Reflection; @@ -40,5 +41,20 @@ public sealed class MySqlMigrationConsole : MigrationConsoleBase protected override DatabaseMigrationBase CreateMigrator() => new MySqlMigration(Args); + + /// + public override string AppTitle => base.AppTitle + " [MySQL]"; + + /// + protected override void OnWriteHelp() + { + base.OnWriteHelp(); + Logger?.LogInformation("{help}", "Script command and argument(s):"); + Logger?.LogInformation("{help}", " script [default] Creates a default (empty) SQL script."); + Logger?.LogInformation("{help}", " script alter Creates a SQL script to perform an ALTER TABLE."); + Logger?.LogInformation("{help}", " script create
Creates a SQL script to perform a CREATE TABLE."); + Logger?.LogInformation("{help}", " script refdata
Creates a SQL script to perform a CREATE TABLE as reference data."); + Logger?.LogInformation("{help}", string.Empty); + } } } \ No newline at end of file diff --git a/src/DbEx.SqlServer/Console/SqlServerMigrationConsole.cs b/src/DbEx.SqlServer/Console/SqlServerMigrationConsole.cs index 1f52379..6a93f63 100644 --- a/src/DbEx.SqlServer/Console/SqlServerMigrationConsole.cs +++ b/src/DbEx.SqlServer/Console/SqlServerMigrationConsole.cs @@ -3,6 +3,7 @@ using DbEx.Console; using DbEx.Migration; using DbEx.SqlServer.Migration; +using Microsoft.Extensions.Logging; using System; using System.Reflection; @@ -40,5 +41,23 @@ public sealed class SqlServerMigrationConsole : MigrationConsoleBase protected override DatabaseMigrationBase CreateMigrator() => new SqlServerMigration(Args); + + /// + public override string AppTitle => base.AppTitle + " [SQL Server]"; + + /// + protected override void OnWriteHelp() + { + base.OnWriteHelp(); + Logger?.LogInformation("{help}", "Script command and argument(s):"); + Logger?.LogInformation("{help}", " script [default] Creates a default (empty) SQL script."); + Logger?.LogInformation("{help}", " script alter
Creates a SQL script to perform an ALTER TABLE."); + Logger?.LogInformation("{help}", " script cdc
Creates a SQL script to turn on CDC for the specified table."); + Logger?.LogInformation("{help}", " script cdcdb Creates a SQL script to turn on CDC for the database."); + Logger?.LogInformation("{help}", " script create
Creates a SQL script to perform a CREATE TABLE."); + Logger?.LogInformation("{help}", " script refdata
Creates a SQL script to perform a CREATE TABLE as reference data."); + Logger?.LogInformation("{help}", " script schema Creates a SQL script to perform a CREATE SCHEMA."); + Logger?.LogInformation("{help}", string.Empty); + } } } \ No newline at end of file diff --git a/src/DbEx/Console/MigrationConsoleBase.cs b/src/DbEx/Console/MigrationConsoleBase.cs index c2ebf86..d737399 100644 --- a/src/DbEx/Console/MigrationConsoleBase.cs +++ b/src/DbEx/Console/MigrationConsoleBase.cs @@ -30,6 +30,7 @@ public abstract class MigrationConsoleBase private const string EntryAssemblyOnlyOptionName = "EO"; private CommandArgument? _commandArg; private CommandArgument? _additionalArgs; + private CommandOption? _helpOption; /// /// Initializes a new instance of the class. @@ -112,9 +113,9 @@ public async Task RunAsync(string[] args, CancellationToken cancellationTok // Set up the app. using var app = new CommandLineApplication(PhysicalConsole.Singleton) { Name = AppName, Description = AppTitle }; - app.HelpOption(); + _helpOption = app.HelpOption(); - _commandArg = app.Argument("command", "Database migration command.").IsRequired(); + _commandArg = app.Argument("command", "Database migration command (see https://github.com/Avanade/dbex#commands-functions).").IsRequired(); ConsoleOptions.Add(nameof(OnRamp.CodeGeneratorDbArgsBase.ConnectionString), app.Option("-cs|--connection-string", "Database connection string.", CommandOptionType.SingleValue)); ConsoleOptions.Add(nameof(OnRamp.CodeGeneratorDbArgsBase.ConnectionStringEnvironmentVariableName), app.Option("-cv|--connection-varname", "Database connection string environment variable name.", CommandOptionType.SingleValue)); ConsoleOptions.Add(nameof(MigrationArgs.SchemaOrder), app.Option("-so|--schema-order", "Database schema name (multiple can be specified in priority order).", CommandOptionType.MultipleValue)); @@ -199,7 +200,12 @@ public async Task RunAsync(string[] args, CancellationToken cancellationTok // Execute the command-line app. try { - return await app.ExecuteAsync(args, cancellationToken).ConfigureAwait(false); + var result = await app.ExecuteAsync(args, cancellationToken).ConfigureAwait(false); + + if (result == 0 && _helpOption.HasValue()) + OnWriteHelp(); + + return result; } catch (CommandParsingException cpex) { @@ -402,5 +408,10 @@ protected virtual void OnWriteFooter(double totalMilliseconds) Logger?.LogInformation("{Content}", $"{AppName} Complete. [{totalMilliseconds}ms]"); Logger?.LogInformation("{Content}", string.Empty); } + + /// + /// Invoked to write additional help information to the . + /// + protected virtual void OnWriteHelp() { } } } \ No newline at end of file diff --git a/src/DbEx/Migration/Data/DataParserArgs.cs b/src/DbEx/Migration/Data/DataParserArgs.cs index 849cd3c..01ebc45 100644 --- a/src/DbEx/Migration/Data/DataParserArgs.cs +++ b/src/DbEx/Migration/Data/DataParserArgs.cs @@ -89,11 +89,55 @@ public class DataParserArgs /// The list should contain the column name and function that returns the default value (the input to the function is the corresponding row count as specified). public Dictionary> RefDataColumnDefaults { get; } = new Dictionary>(); + /// + /// Adds a reference data column default to the . + /// + /// The column name. + /// The function that provides the default value. + /// The to support fluent-style method-chaining. + public DataParserArgs RefDataColumnDefault(string column, Func @default) + { + RefDataColumnDefaults.Add(column, @default); + return this; + } + + /// + /// Gets or sets the column defaults collection. + /// + /// The list should contain the column name and function that returns the default value (the input to the function is the corresponding row count as specified). + public DataParserColumnDefaultCollection ColumnDefaults { get; } = new DataParserColumnDefaultCollection(); + + /// + /// Adds a to the . + /// + /// The schema name; a '*' denotes any schema. + /// The table name; a '*' denotes any table. + /// The name of the column to be updated. + /// The function that provides the default value. + /// The to support fluent-style method-chaining. + public DataParserArgs ColumnDefault(string schema, string table, string column, Func @default) + { + ColumnDefaults.Add(new DataParserColumnDefault(schema, table, column, @default)); + return this; + } + /// /// Gets the runtime parameters. /// public Dictionary Parameters { get; } = new Dictionary(); + /// + /// Adds a parameter to the . + /// + /// The parameter key. + /// The parameter value. + /// The to support fluent-style method-chaining. + public DataParserArgs Parameter(string key, object? value) + { + Parameters.Add(key, value); + return this; + } + /// /// Gets or sets the updater. /// @@ -122,6 +166,8 @@ public void CopyFrom(DataParserArgs args) DbSchemaUpdaterAsync = args.DbSchemaUpdaterAsync; RefDataColumnDefaults.Clear(); args.RefDataColumnDefaults.ForEach(x => RefDataColumnDefaults.Add(x.Key, x.Value)); + ColumnDefaults.Clear(); + args.ColumnDefaults.ForEach(ColumnDefaults.Add); Parameters.Clear(); args.Parameters.ForEach(x => Parameters.Add(x.Key, x.Value)); } diff --git a/src/DbEx/Migration/Data/DataParserColumnDefault.cs b/src/DbEx/Migration/Data/DataParserColumnDefault.cs new file mode 100644 index 0000000..0c4e2f4 --- /dev/null +++ b/src/DbEx/Migration/Data/DataParserColumnDefault.cs @@ -0,0 +1,47 @@ +// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/DbEx + +using System; + +namespace DbEx.Migration.Data +{ + /// + /// Provides the configuration. + /// + public class DataParserColumnDefault + { + /// + /// Initializes a new instance of the class. + /// + /// The schema name; a '*' denotes any schema. + /// The table name; a '*' denotes any table. + /// The name of the column to be updated. + /// The function that provides the default value. + public DataParserColumnDefault(string schema, string table, string column, Func @default) + { + Schema = schema ?? throw new ArgumentNullException(nameof(schema)); + Table = table ?? throw new ArgumentNullException(nameof(table)); + Column = column ?? throw new ArgumentNullException(nameof(column)); + Default = @default ?? throw new ArgumentNullException(nameof(@default)); + } + + /// + /// Gets the schema name; a '*' denotes any schema. + /// + public string Schema { get; } + + /// + /// Gets the table name; a '*' denotes any table. + /// + public string Table { get; } + + /// + /// Gets the column name. + /// + public string Column { get; } + + /// + /// Gets the function that provides the default value. + /// + public Func Default { get; } + } +} \ No newline at end of file diff --git a/src/DbEx/Migration/Data/DataParserColumnDefaultCollection.cs b/src/DbEx/Migration/Data/DataParserColumnDefaultCollection.cs new file mode 100644 index 0000000..8f37977 --- /dev/null +++ b/src/DbEx/Migration/Data/DataParserColumnDefaultCollection.cs @@ -0,0 +1,75 @@ +// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/DbEx + +using DbEx.DbSchema; +using System; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; + +namespace DbEx.Migration.Data +{ + /// + /// Provides a keyed collection. + /// + public class DataParserColumnDefaultCollection : KeyedCollection<(string, string, string), DataParserColumnDefault> + { + /// + protected override (string, string, string) GetKeyForItem(DataParserColumnDefault item) => (item.Schema, item.Table, item.Column); + + /// + /// Attempts to get the for the specified , and names. + /// + /// The schema name. + /// The table name. + /// The column name. + /// The corresponding item where found; otherwise, null. + /// true where found; otherwise, false. + /// Attempts to match as follows: + /// + /// Schema, table and column names match item exactly; + /// Schema and column names match item exactly, and the underlying default table name is configured with '*'; + /// Column names match item exactly, and the underlying default schema and table names are both configured with '*'; + /// Item is not found. + /// + /// + public bool TryGetValue(string schema, string table, string column, [NotNullWhen(true)] out DataParserColumnDefault? item) + { + if (schema == null) + throw new ArgumentNullException(nameof(schema)); + + if (table == null) + throw new ArgumentNullException(nameof(table)); + + if (column == null) + throw new ArgumentNullException(nameof(column)); + + if (TryGetValue((schema, table, column), out item)) + return true; + + if (TryGetValue((schema, "*", column), out item)) + return true; + + if (TryGetValue(("*", "*", column), out item)) + return true; + + item = null; + return false; + } + + /// + /// Get all the configured column defaults for the specified . + /// + /// The . + /// The configured defaults. + public DataParserColumnDefaultCollection GetDefaultsForTable(DbTableSchema table) + { + var dc = new DataParserColumnDefaultCollection(); + foreach (var c in (table ?? throw new ArgumentNullException(nameof(table))).Columns) + { + if (TryGetValue(table.Schema, table.Name, c.Name, out var item)) + dc.Add(item); + } + + return dc; + } + } +} \ No newline at end of file diff --git a/src/DbEx/Migration/Data/DataTable.cs b/src/DbEx/Migration/Data/DataTable.cs index 87cc77f..5bec5b0 100644 --- a/src/DbEx/Migration/Data/DataTable.cs +++ b/src/DbEx/Migration/Data/DataTable.cs @@ -167,14 +167,19 @@ private void AddColumn(string name) /// internal async Task PrepareAsync(CancellationToken cancellationToken) { + var cds = Args.ColumnDefaults.GetDefaultsForTable(DbTable); + for (int i = 0; i < Rows.Count; i++) { var row = Rows[i]; + + // Apply the configured auditing defaults. await AddColumnWhereNotSpecifiedAsync(row, Args.CreatedDateColumnName ?? Parser.DatabaseSchemaConfig.CreatedDateColumnName, () => Task.FromResult(Args.DateTimeNow)).ConfigureAwait(false); await AddColumnWhereNotSpecifiedAsync(row, Args.CreatedByColumnName ?? Parser.DatabaseSchemaConfig.CreatedByColumnName, () => Task.FromResult(Args.UserName)).ConfigureAwait(false); await AddColumnWhereNotSpecifiedAsync(row, Args.UpdatedDateColumnName ?? Parser.DatabaseSchemaConfig.UpdatedDateColumnName, () => Task.FromResult(Args.DateTimeNow)).ConfigureAwait(false); await AddColumnWhereNotSpecifiedAsync(row, Args.UpdatedByColumnName ?? Parser.DatabaseSchemaConfig.UpdatedByColumnName, () => Task.FromResult(Args.UserName)).ConfigureAwait(false); + // Apply an reference data defaults. if (IsRefData && Args.RefDataColumnDefaults != null) { foreach (var rdd in Args.RefDataColumnDefaults) @@ -183,6 +188,7 @@ internal async Task PrepareAsync(CancellationToken cancellationToken) } } + // Generate the identifier where specified to do so. if (UseIdentifierGenerator) { var pkc = DbTable.PrimaryKeyColumns[0]; @@ -209,6 +215,12 @@ internal async Task PrepareAsync(CancellationToken cancellationToken) } } } + + // Apply any configured column defaults. + foreach (var cd in cds) + { + await AddColumnWhereNotSpecifiedAsync(row, cd.Column, () => Task.FromResult(cd.Default(i + 1))).ConfigureAwait(false); + } } } diff --git a/tests/DbEx.Test.Console/Migrations/004-create-test-contact-table.sql b/tests/DbEx.Test.Console/Migrations/004-create-test-contact-table.sql index 9edb72d..f91fb2a 100644 --- a/tests/DbEx.Test.Console/Migrations/004-create-test-contact-table.sql +++ b/tests/DbEx.Test.Console/Migrations/004-create-test-contact-table.sql @@ -4,6 +4,7 @@ [Phone] VARCHAR (15) NULL, [DateOfBirth] DATE NULL, [ContactTypeId] INT NOT NULL DEFAULT 1, - [GenderId] INT NULL + [GenderId] INT NULL, + [TenantId] NVARCHAR(50), CONSTRAINT [FK_Test_Contact_ContactType] FOREIGN KEY ([ContactTypeId]) REFERENCES [Test].[ContactType] ([ContactTypeId]) ) \ No newline at end of file diff --git a/tests/DbEx.Test.Console/Program.cs b/tests/DbEx.Test.Console/Program.cs index 43e0223..04c3c87 100644 --- a/tests/DbEx.Test.Console/Program.cs +++ b/tests/DbEx.Test.Console/Program.cs @@ -9,10 +9,11 @@ internal static Task Main(string[] args) => SqlServerMigrationConsole .Create("Data Source=.;Initial Catalog=DbEx.Console;Integrated Security=True;TrustServerCertificate=true") .Configure(c => { - c.Args.DataParserArgs.Parameters.Add("DefaultName", "Bazza"); - c.Args.DataParserArgs.RefDataColumnDefaults.Add("SortOrder", i => i); c.Args.AddAssembly(typeof(DbEx.Test.OutboxConsole.Program).Assembly); c.Args.AddSchemaOrder("Test", "Outbox"); + c.Args.DataParserArgs.Parameter("DefaultName", "Bazza") + .RefDataColumnDefault("SortOrder", i => i) + .ColumnDefault("*", "*", "TenantId", _ => "test-tenant"); }) .RunAsync(args); } diff --git a/tests/DbEx.Test.Console/Properties/launchSettings.json b/tests/DbEx.Test.Console/Properties/launchSettings.json index 9a02bc5..b4a43ba 100644 --- a/tests/DbEx.Test.Console/Properties/launchSettings.json +++ b/tests/DbEx.Test.Console/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "DbEx.Test.Console": { "commandName": "Project", - "commandLineArgs": "reset" + "commandLineArgs": "--help" } } } \ No newline at end of file diff --git a/tests/DbEx.Test/DatabaseSchemaTest.cs b/tests/DbEx.Test/DatabaseSchemaTest.cs index 63cfecb..7d690f3 100644 --- a/tests/DbEx.Test/DatabaseSchemaTest.cs +++ b/tests/DbEx.Test/DatabaseSchemaTest.cs @@ -101,7 +101,7 @@ public async Task SqlServerSelectSchema() Assert.AreEqual("[Test].[Contact]", tab.QualifiedName); Assert.IsFalse(tab.IsAView); Assert.IsFalse(tab.IsRefData); - Assert.AreEqual(6, tab.Columns.Count); + Assert.AreEqual(7, tab.Columns.Count); Assert.AreEqual(1, tab.PrimaryKeyColumns.Count); col = tab.Columns[0]; @@ -189,6 +189,9 @@ public async Task SqlServerSelectSchema() Assert.AreEqual("GenderId", col.ForeignColumn); Assert.IsNull(col.DefaultValue); + col = tab.Columns[6]; + Assert.AreEqual("TenantId", col.Name); + // [Test].[MultiPk] tab = tables.Where(x => x.Name == "MultiPk").SingleOrDefault(); Assert.IsNotNull(tab); diff --git a/tests/DbEx.Test/SqlServerMigrationTest.cs b/tests/DbEx.Test/SqlServerMigrationTest.cs index a3336fa..50c6169 100644 --- a/tests/DbEx.Test/SqlServerMigrationTest.cs +++ b/tests/DbEx.Test/SqlServerMigrationTest.cs @@ -84,7 +84,8 @@ public async Task A130_MigrateAll_Console() Phone = dr.GetValue("Phone"), DateOfBirth = dr.GetValue("DateOfBirth"), ContactTypeId = dr.GetValue("ContactTypeId"), - GenderId = dr.GetValue("GenderId") + GenderId = dr.GetValue("GenderId"), + TenantId = dr.GetValue("TenantId") }).ConfigureAwait(false)).ToList(); Assert.AreEqual(3, res.Count); @@ -96,6 +97,7 @@ public async Task A130_MigrateAll_Console() Assert.AreEqual(new DateTime(2001, 10, 22), row.DateOfBirth); Assert.AreEqual(1, row.ContactTypeId); Assert.AreEqual(2, row.GenderId); + Assert.AreEqual("test-tenant", row.TenantId); row = res[1]; Assert.AreEqual(2, row.ContactId); @@ -104,6 +106,7 @@ public async Task A130_MigrateAll_Console() Assert.AreEqual(null, row.DateOfBirth); Assert.AreEqual(2, row.ContactTypeId); Assert.IsNull(row.GenderId); + Assert.AreEqual("test-tenant", row.TenantId); row = res[2]; Assert.AreEqual(3, row.ContactId); @@ -112,6 +115,7 @@ public async Task A130_MigrateAll_Console() Assert.AreEqual(new DateTime(2001, 10, 22), row.DateOfBirth); Assert.AreEqual(1, row.ContactTypeId); Assert.AreEqual(2, row.GenderId); + Assert.IsNull(row.TenantId); // Must be set within SQL script itself; the column default does not extend to SQL scripts themselves. // Check that the person data was updated as expected - converted and auto-assigned id, plus createdby and createddate columns, and finally runtime variable. var res2 = (await db.SqlStatement("SELECT * FROM [Test].[Person]").SelectQueryAsync(dr => new @@ -143,7 +147,8 @@ public async Task A130_MigrateAll_Console() Phone = dr.GetValue("Phone"), DateOfBirth = dr.GetValue("DateOfBirth"), ContactTypeId = dr.GetValue("ContactTypeId"), - GenderId = dr.GetValue("GenderId") + GenderId = dr.GetValue("GenderId"), + TenantId = dr.GetValue("TenantId") }).ConfigureAwait(false)).ToList(); Assert.AreEqual(1, res.Count); @@ -165,6 +170,7 @@ public async Task A130_MigrateAll_Console() m.Args.DataParserArgs.Parameters.Add("DefaultName", "Bazza"); m.Args.DataParserArgs.RefDataColumnDefaults.Add("SortOrder", i => i); + m.Args.DataParserArgs.ColumnDefaults.Add(new DataParserColumnDefault("*", "*", "TenantId", _ => "test-tenant")); var r = await m.MigrateAsync().ConfigureAwait(false);