diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/EditSession.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/EditSession.cs index 482c615e..c9e652a1 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/EditData/EditSession.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/EditSession.cs @@ -167,31 +167,10 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData throw new InvalidOperationException(SR.EditDataFailedAddRow); } - // Set the default values of the row if we know them - string[] defaultValues = new string[objectMetadata.Columns.Length]; - for(int i = 0; i < objectMetadata.Columns.Length; i++) - { - EditColumnMetadata col = objectMetadata.Columns[i]; - - // If the column is calculated, return the calculated placeholder as the display value - if (col.IsCalculated.HasTrue()) - { - defaultValues[i] = SR.EditDataComputedColumnPlaceholder; - } - else - { - if (col.DefaultValue != null) - { - newRow.SetCell(i, col.DefaultValue); - } - defaultValues[i] = col.DefaultValue; - } - } - EditCreateRowResult output = new EditCreateRowResult { - NewRowId = newRowId, - DefaultValues = defaultValues + NewRowId = newRow.RowId, + DefaultValues = newRow.DefaultValues }; return output; } diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowCreate.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowCreate.cs index 70ae9f76..35d8e561 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowCreate.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowCreate.cs @@ -9,6 +9,7 @@ using System.Data; using System.Data.Common; using System.Data.SqlClient; using System.Linq; +using System.Text; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.EditData.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution; @@ -23,9 +24,11 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement /// public sealed class RowCreate : RowEditBase { - private const string InsertStart = "INSERT INTO {0}({1})"; - private const string InsertCompleteScript = "{0} VALUES ({1})"; - private const string InsertCompleteOutput = "{0} OUTPUT {1} VALUES ({2})"; + private const string InsertScriptStart = "INSERT INTO {0}"; + private const string InsertScriptColumns = "({0})"; + private const string InsertScriptOut = " OUTPUT {0}"; + private const string InsertScriptDefault = " DEFAULT VALUES"; + private const string InsertScriptValues = " VALUES ({0})"; internal readonly CellUpdate[] newCells; @@ -39,6 +42,11 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement : base(rowId, associatedResultSet, associatedMetadata) { newCells = new CellUpdate[associatedResultSet.Columns.Length]; + + // Process the default cell values. If the column is calculated, then the value is a placeholder + DefaultValues = associatedMetadata.Columns.Select((col, index) => col.IsCalculated.HasTrue() + ? SR.EditDataComputedColumnPlaceholder + : col.DefaultValue).ToArray(); } /// @@ -47,6 +55,12 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement /// protected override int SortId => 1; + /// + /// Default values for the row, will be applied as cell updates if there isn't a user- + /// provided cell update during commit + /// + public string[] DefaultValues { get; } + #region Public Methods /// @@ -74,50 +88,13 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement { Validate.IsNotNull(nameof(connection), connection); - // Process all the columns. Add the column to the output columns, add updateable - // columns to the input parameters - List outColumns = new List(); - List inColumns = new List(); - DbCommand command = connection.CreateCommand(); - for (int i = 0; i < AssociatedResultSet.Columns.Length; i++) - { - DbColumnWrapper column = AssociatedResultSet.Columns[i]; - CellUpdate cell = newCells[i]; - - // Add the column to the output - outColumns.Add($"inserted.{SqlScriptFormatter.FormatIdentifier(column.ColumnName)}"); - - // Skip columns that cannot be updated - if (!column.IsUpdatable) - { - continue; - } - - // If we're missing a cell, then we cannot continue - if (cell == null) - { - throw new InvalidOperationException(SR.EditDataCreateScriptMissingValue); - } - - // Create a parameter for the value and add it to the command - // Add the parameterization to the list and add it to the command - string paramName = $"@Value{RowId}{i}"; - inColumns.Add(paramName); - SqlParameter param = new SqlParameter(paramName, cell.Column.SqlDbType) - { - Value = cell.Value - }; - command.Parameters.Add(param); - } - string joinedInColumns = string.Join(", ", inColumns); - string joinedOutColumns = string.Join(", ", outColumns); + // Build the script and generate a command + ScriptBuildResult result = BuildInsertScript(forCommand: true); - // Get the start clause - string start = GetTableClause(); - - // Put the whole #! together - command.CommandText = string.Format(InsertCompleteOutput, start, joinedOutColumns, joinedInColumns); + DbCommand command = connection.CreateCommand(); + command.CommandText = result.ScriptText; command.CommandType = CommandType.Text; + command.Parameters.AddRange(result.ScriptParameters); return command; } @@ -129,15 +106,9 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement /// EditRow of pending update public override EditRow GetEditRow(DbCellValue[] cachedRow) { - // Iterate over the new cells. If they are null, generate a blank value - EditCell[] editCells = newCells.Select(cell => - { - DbCellValue dbCell = cell == null - ? new DbCellValue {DisplayValue = string.Empty, IsNull = false, RawObject = null} - : cell.AsDbCellValue; - return new EditCell(dbCell, true); - }) - .ToArray(); + // Get edit cells for each + EditCell[] editCells = newCells.Select(GetEditCell).ToArray(); + return new EditRow { Id = RowId, @@ -152,35 +123,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement /// INSERT INTO statement public override string GetScript() { - // Process all the cells, and generate the values - List values = new List(); - for (int i = 0; i < AssociatedResultSet.Columns.Length; i++) - { - DbColumnWrapper column = AssociatedResultSet.Columns[i]; - CellUpdate cell = newCells[i]; - - // Skip columns that cannot be updated - if (!column.IsUpdatable) - { - continue; - } - - // If we're missing a cell, then we cannot continue - if (cell == null) - { - throw new InvalidOperationException(SR.EditDataCreateScriptMissingValue); - } - - // Format the value and add it to the list - values.Add(SqlScriptFormatter.FormatValue(cell.Value, column)); - } - string joinedValues = string.Join(", ", values); - - // Get the start clause - string start = GetTableClause(); - - // Put the whole #! together - return string.Format(InsertCompleteScript, start, joinedValues); + return BuildInsertScript(forCommand: false).ScriptText; } /// @@ -195,9 +138,11 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement // Remove the cell update from list of set cells newCells[columnId] = null; - return new EditRevertCellResult {IsRowDirty = true, Cell = null}; - // @TODO: Return default value when we have support checked in - // @TODO: RETURN THE DEFAULT VALUE + return new EditRevertCellResult + { + IsRowDirty = true, + Cell = GetEditCell(null, columnId) + }; } /// @@ -227,16 +172,140 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement #endregion - private string GetTableClause() - { - // Get all the columns that will be provided - var inColumns = from c in AssociatedResultSet.Columns - where c.IsUpdatable - select SqlScriptFormatter.FormatIdentifier(c.ColumnName); + /// + /// Generates an INSERT script that will insert this row + /// + /// + /// If true the script will be generated with an OUTPUT clause for returning all + /// values in the inserted row (including computed values). The script will also generate + /// parameters for inserting the values. + /// If false the script will not have an OUTPUT clause and will have the values + /// directly inserted into the script (with proper escaping, of course). + /// + /// A script build result object with the script text and any parameters + /// + /// Thrown if there are columns that are not readonly, do not have default values, and were + /// not assigned values. + /// + private ScriptBuildResult BuildInsertScript(bool forCommand) + { + // Process all the columns in this table + List inValues = new List(); + List inColumns = new List(); + List outColumns = new List(); + List sqlParameters = new List(); + for (int i = 0; i < AssociatedObjectMetadata.Columns.Length; i++) + { + DbColumnWrapper column = AssociatedResultSet.Columns[i]; + CellUpdate cell = newCells[i]; + + // Add an out column if we're doing this for a command + if (forCommand) + { + outColumns.Add($"inserted.{SqlScriptFormatter.FormatIdentifier(column.ColumnName)}"); + } + + // Skip columns that cannot be updated + if (!column.IsUpdatable) + { + continue; + } + + // Make sure a value was provided for the cell + if (cell == null) + { + // If there isn't a default, then fail + if (DefaultValues[i] == null) + { + throw new InvalidOperationException(SR.EditDataCreateScriptMissingValue); + } + + // There is a default value, so trust the db will apply it + continue; + } - // Package it into a single INSERT statement starter - string inColumnsJoined = string.Join(", ", inColumns); - return string.Format(InsertStart, AssociatedObjectMetadata.EscapedMultipartName, inColumnsJoined); + // Add the input values + if (forCommand) + { + // Since this script is for command use, add parameter for the input value to the list + string paramName = $"@Value{RowId}_{i}"; + inValues.Add(paramName); + + SqlParameter param = new SqlParameter(paramName, cell.Column.SqlDbType) {Value = cell.Value}; + sqlParameters.Add(param); + } + else + { + // This script isn't for command use, add the value, formatted for insertion + inValues.Add(SqlScriptFormatter.FormatValue(cell.Value, column)); + } + + // Add the column to the in columns + inColumns.Add(SqlScriptFormatter.FormatIdentifier(column.ColumnName)); + } + + // Begin the script (ie, INSERT INTO blah) + StringBuilder queryBuilder = new StringBuilder(); + queryBuilder.AppendFormat(InsertScriptStart, AssociatedObjectMetadata.EscapedMultipartName); + + // Add the input columns (if there are any) + if (inColumns.Count > 0) + { + string joinedInColumns = string.Join(", ", inColumns); + queryBuilder.AppendFormat(InsertScriptColumns, joinedInColumns); + } + + // Add the output columns (this will be empty if we are not building for command) + if (outColumns.Count > 0) + { + string joinedOutColumns = string.Join(", ", outColumns); + queryBuilder.AppendFormat(InsertScriptOut, joinedOutColumns); + } + + // Add the input values (if there any) or use the default values + if (inValues.Count > 0) + { + string joinedInValues = string.Join(", ", inValues); + queryBuilder.AppendFormat(InsertScriptValues, joinedInValues); + } + else + { + queryBuilder.AppendFormat(InsertScriptDefault); + } + + return new ScriptBuildResult + { + ScriptText = queryBuilder.ToString(), + ScriptParameters = sqlParameters.ToArray() + }; + } + + private EditCell GetEditCell(CellUpdate cell, int index) + { + DbCellValue dbCell; + if (cell == null) + { + // Cell hasn't been provided by user yet, attempt to use the default value + dbCell = new DbCellValue + { + DisplayValue = DefaultValues[index] ?? string.Empty, + IsNull = false, // TODO: This doesn't properly consider null defaults + RawObject = null, + RowId = RowId + }; + } + else + { + // Cell has been provided by user, so use that + dbCell = cell.AsDbCellValue; + } + return new EditCell(dbCell, isDirty: true); + } + + private class ScriptBuildResult + { + public string ScriptText { get; set; } + public SqlParameter[] ScriptParameters { get; set; } } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowUpdate.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowUpdate.cs index db377e2c..491fe5e0 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowUpdate.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowUpdate.cs @@ -82,7 +82,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement foreach (var updateElement in cellUpdates) { string formattedColumnName = SqlScriptFormatter.FormatIdentifier(updateElement.Value.Column.ColumnName); - string paramName = $"@Value{RowId}{updateElement.Key}"; + string paramName = $"@Value{RowId}_{updateElement.Key}"; setComponents.Add($"{formattedColumnName} = {paramName}"); SqlParameter parameter = new SqlParameter(paramName, updateElement.Value.Column.SqlDbType) { diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/Common.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/Common.cs index 36a73b1b..1f55976d 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/Common.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/Common.cs @@ -8,6 +8,7 @@ using System.Data.Common; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Castle.Components.DictionaryAdapter; using Microsoft.SqlTools.ServiceLayer.EditData; using Microsoft.SqlTools.ServiceLayer.EditData.Contracts; using Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement; @@ -22,6 +23,8 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData public class Common { public const string OwnerUri = "testFile"; + public const string DefaultValue = "defaultValue"; + public const string TableName = "tbl"; public static EditInitializeParams BasicInitializeParameters { @@ -62,17 +65,14 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData return session; } - public static EditTableMetadata GetStandardMetadata(DbColumn[] columns, bool isMemoryOptimized = false) + public static EditTableMetadata GetStandardMetadata(DbColumn[] columns, bool isMemoryOptimized = false, int defaultColumns = 0) { - // Create column metadata providers - var columnMetas = columns.Select((c, i) => + // Create column metadata providers + var columnMetas = columns.Select((c, i) => new EditColumnMetadata { - var ecm = new EditColumnMetadata - { - EscapedName = c.ColumnName, - Ordinal = i - }; - return ecm; + EscapedName = c.ColumnName, + Ordinal = i, + DefaultValue = i < defaultColumns ? DefaultValue : null }).ToArray(); // Create column wrappers @@ -82,7 +82,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData EditTableMetadata editTableMetadata = new EditTableMetadata { Columns = columnMetas, - EscapedMultipartName = "tbl", + EscapedMultipartName = TableName, IsMemoryOptimized = isMemoryOptimized }; editTableMetadata.Extend(columnWrappers); @@ -133,11 +133,10 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData return new TestDbDataReader(new [] {testResultSet}, false); } - public static void AddCells(RowEditBase rc, bool includeIdentity) + public static void AddCells(RowEditBase rc, int colsToSkip) { // Skip the first column since if identity, since identity columns can't be updated - int start = includeIdentity ? 1 : 0; - for (int i = start; i < rc.AssociatedResultSet.Columns.Length; i++) + for (int i = colsToSkip; i < rc.AssociatedResultSet.Columns.Length; i++) { rc.SetCell(i, "123"); } diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowCreateTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowCreateTests.cs index dc3e6ed9..1e916d26 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowCreateTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowCreateTests.cs @@ -4,7 +4,9 @@ // using System; +using System.Collections.Generic; using System.Data.Common; +using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -38,39 +40,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData Assert.Equal(etm, rc.AssociatedObjectMetadata); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task GetScript(bool includeIdentity) - { - // Setup: Generate the parameters for the row create - const long rowId = 100; - DbColumn[] columns = Common.GetColumns(includeIdentity); - ResultSet rs = await Common.GetResultSet(columns, includeIdentity); - EditTableMetadata etm = Common.GetStandardMetadata(columns); - - // If: I ask for a script to be generated without an identity column - RowCreate rc = new RowCreate(rowId, rs, etm); - Common.AddCells(rc, includeIdentity); - string script = rc.GetScript(); - - // Then: - // ... The script should not be null, - Assert.NotNull(script); - - // ... It should be formatted as an insert script - Regex r = new Regex(@"INSERT INTO (.+)\((.*)\) VALUES \((.*)\)"); - var m = r.Match(script); - Assert.True(m.Success); - - // ... It should have 3 columns and 3 values (regardless of the presence of an identity col) - string tbl = m.Groups[1].Value; - string cols = m.Groups[2].Value; - string vals = m.Groups[3].Value; - Assert.Equal(etm.EscapedMultipartName, tbl); - Assert.Equal(3, cols.Split(',').Length); - Assert.Equal(3, vals.Split(',').Length); - } + #region GetScript Tests [Fact] public async Task GetScriptMissingCell() @@ -83,6 +53,82 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData Assert.Throws(() => rc.GetScript()); } + public static IEnumerable GetScriptData + { + get + { + yield return new object[] {true, 0, 1, new RegexExpectedOutput(3, 3, 0)}; // Has identity, no defaults, all values set + yield return new object[] {true, 2, 1, new RegexExpectedOutput(3, 3, 0)}; // Has identity, some defaults, all values set + yield return new object[] {true, 2, 2, new RegexExpectedOutput(2, 2, 0)}; // Has identity, some defaults, defaults not set + yield return new object[] {true, 4, 1, new RegexExpectedOutput(3, 3, 0)}; // Has identity, all defaults, all values set + yield return new object[] {true, 4, 4, null}; // Has identity, all defaults, defaults not set + yield return new object[] {false, 0, 0, new RegexExpectedOutput(3, 3, 0)}; // No identity, no defaults, all values set + yield return new object[] {false, 1, 0, new RegexExpectedOutput(3, 3, 0)}; // No identity, some defaults, all values set + yield return new object[] {false, 1, 1, new RegexExpectedOutput(2, 2, 0)}; // No identity, some defaults, defaults not set + yield return new object[] {false, 3, 0, new RegexExpectedOutput(3, 3, 0)}; // No identity, all defaults, all values set + yield return new object[] {false, 3, 3, null}; // No identity, all defaults, defaults not set + } + } + + [Theory] + [MemberData(nameof(GetScriptData))] + public async Task GetScript(bool includeIdentity, int defaultCols, int valuesToSkipSetting, RegexExpectedOutput expectedOutput) + { + // Setup: + // ... Generate the parameters for the row create + DbColumn[] columns = Common.GetColumns(includeIdentity); + ResultSet rs = await Common.GetResultSet(columns, includeIdentity); + EditTableMetadata etm = Common.GetStandardMetadata(columns, includeIdentity, defaultCols); + + // ... Create a row create and set the appropriate number of cells + RowCreate rc = new RowCreate(100, rs, etm); + Common.AddCells(rc, valuesToSkipSetting); + + // If: I ask for the script for the row insert + string script = rc.GetScript(); + + // Then: + // ... The script should not be null + Assert.NotNull(script); + + // ... The script should match the expected regex output + ValidateScriptAgainstRegex(script, expectedOutput); + } + + private static void ValidateScriptAgainstRegex(string sql, RegexExpectedOutput expectedOutput) + { + if (expectedOutput == null) + { + // If expected output was null make sure we match the default values reges + Regex r = new Regex(@"INSERT INTO (.+) DEFAULT VALUES"); + Match m = r.Match(sql); + Assert.True(m.Success); + + // Table name matches + Assert.Equal(Common.TableName, m.Groups[1].Value); + } + else + { + // Do the whole validation + Regex r = new Regex(@"INSERT INTO (.+)\((.+)\) VALUES \((.+)\)"); + Match m = r.Match(sql); + Assert.True(m.Success); + + // Table name matches + Assert.Equal(Common.TableName, m.Groups[1].Value); + + // In columns match + string cols = m.Groups[2].Value; + Assert.Equal(expectedOutput.ExpectedInColumns, cols.Split(',').Length); + + // In values match + string vals = m.Groups[3].Value; + Assert.Equal(expectedOutput.ExpectedInValues, vals.Split(',').Length); + } + } + + #endregion + [Theory] [InlineData(true)] [InlineData(false)] @@ -93,7 +139,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData const long rowId = 100; DbColumn[] columns = Common.GetColumns(includeIdentity); ResultSet rs = await Common.GetResultSet(columns, includeIdentity); - EditTableMetadata etm = Common.GetStandardMetadata(columns); + EditTableMetadata etm = Common.GetStandardMetadata(columns, includeIdentity); // ... Setup a db reader for the result of an insert var newRowReader = Common.GetNewRowDataReader(columns, includeIdentity); @@ -106,56 +152,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData Assert.Equal(2, rs.RowCount); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task GetCommand(bool includeIdentity) - { - // Setup: - // ... Create a row create with cell updates - const long rowId = 100; - var columns = Common.GetColumns(includeIdentity); - var rs = await Common.GetResultSet(columns, includeIdentity); - var etm = Common.GetStandardMetadata(columns); - RowCreate rc = new RowCreate(rowId, rs, etm); - Common.AddCells(rc, includeIdentity); - - // ... Mock db connection for building the command - var mockConn = new TestSqlConnection(null); - - // If: I attempt to get a command for the edit - DbCommand cmd = rc.GetCommand(mockConn); - - // Then: - // ... The command should not be null - Assert.NotNull(cmd); - - // ... There should be parameters in it - Assert.Equal(3, cmd.Parameters.Count); - - // ... It should be formatted into an insert script with output - Regex r = new Regex(@"INSERT INTO (.+)\((.+)\) OUTPUT (.+) VALUES \((.+)\)"); - var m = r.Match(cmd.CommandText); - Assert.True(m.Success); - - // ... There should be a table - string tbl = m.Groups[1].Value; - Assert.Equal(etm.EscapedMultipartName, tbl); - - // ... There should be 3 columns for input - string inCols = m.Groups[2].Value; - Assert.Equal(3, inCols.Split(',').Length); - - // ... There should be 3 OR 4 columns for output that are inserted. - string[] outCols = m.Groups[3].Value.Split(','); - Assert.Equal(includeIdentity ? 4 : 3, outCols.Length); - Assert.All(outCols, s => Assert.StartsWith("inserted.", s.Trim())); - - // ... There should be 3 parameters - string[] param = m.Groups[4].Value.Split(','); - Assert.Equal(3, param.Length); - Assert.All(param, s => Assert.StartsWith("@Value", s.Trim())); - } + #region GetCommand Tests [Fact] public async Task GetCommandNullConnection() @@ -163,14 +160,13 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData // Setup: Create a row create RowCreate rc = await GetStandardRowCreate(); - // If: I attempt to create a command with a null connection // Then: It should throw an exception Assert.Throws(() => rc.GetCommand(null)); } - + [Fact] - public async Task GetCommandMissingCell() + public async Task GetCommandMissingCellNoDefault() { // Setup: Generate the parameters for the row create RowCreate rc = await GetStandardRowCreate(); @@ -181,6 +177,101 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData Assert.Throws(() => rc.GetCommand(mockConn)); } + public static IEnumerable GetCommandData + { + get + { + yield return new object[] {true, 0, 1, new RegexExpectedOutput(3, 3, 4)}; // Has identity, no defaults, all values set + yield return new object[] {true, 2, 1, new RegexExpectedOutput(3, 3, 4)}; // Has identity, some defaults, all values set + yield return new object[] {true, 2, 2, new RegexExpectedOutput(2, 2, 4)}; // Has identity, some defaults, defaults not set + yield return new object[] {true, 4, 1, new RegexExpectedOutput(3, 3, 4)}; // Has identity, all defaults, all values set + yield return new object[] {true, 4, 4, new RegexExpectedOutput(0, 0, 4)}; // Has identity, all defaults, defaults not set + yield return new object[] {false, 0, 0, new RegexExpectedOutput(3, 3, 3)}; // No identity, no defaults, all values set + yield return new object[] {false, 1, 0, new RegexExpectedOutput(3, 3, 3)}; // No identity, some defaults, all values set + yield return new object[] {false, 1, 1, new RegexExpectedOutput(2, 2, 3)}; // No identity, some defaults, defaults not set + yield return new object[] {false, 3, 0, new RegexExpectedOutput(3, 3, 3)}; // No identity, all defaults, all values set + yield return new object[] {false, 3, 3, new RegexExpectedOutput(0, 0, 3)}; // No identity, all defaults, defaults not set + } + } + + [Theory] + [MemberData(nameof(GetCommandData))] + public async Task GetCommand(bool includeIdentity, int defaultCols, int valuesToSkipSetting, RegexExpectedOutput expectedOutput) + { + // Setup: + // ... Generate the parameters for the row create + DbColumn[] columns = Common.GetColumns(includeIdentity); + ResultSet rs = await Common.GetResultSet(columns, includeIdentity); + EditTableMetadata etm = Common.GetStandardMetadata(columns, includeIdentity, defaultCols); + + // ... Mock db connection for building the command + var mockConn = new TestSqlConnection(null); + + // ... Create a row create and set the appropriate number of cells + RowCreate rc = new RowCreate(100, rs, etm); + Common.AddCells(rc, valuesToSkipSetting); + + // If: I ask for the command for the row insert + DbCommand cmd = rc.GetCommand(mockConn); + + // Then: + // ... The command should not be null + Assert.NotNull(cmd); + + // ... There should be parameters in it + Assert.Equal(expectedOutput.ExpectedInValues, cmd.Parameters.Count); + + // ... The script should match the expected regex output + ValidateCommandAgainstRegex(cmd.CommandText, expectedOutput); + } + + private static void ValidateCommandAgainstRegex(string sql, RegexExpectedOutput expectedOutput) + { + if (expectedOutput.ExpectedInColumns == 0 || expectedOutput.ExpectedInValues == 0) + { + // If expected output was null make sure we match the default values reges + Regex r = new Regex(@"INSERT INTO (.+) OUTPUT (.+) DEFAULT VALUES"); + Match m = r.Match(sql); + Assert.True(m.Success); + + // Table name matches + Assert.Equal(Common.TableName, m.Groups[1].Value); + + // Output columns match + string[] outCols = m.Groups[2].Value.Split(", "); + Assert.Equal(expectedOutput.ExpectedOutColumns, outCols.Length); + Assert.All(outCols, col => Assert.StartsWith("inserted.", col)); + } + else + { + // Do the whole validation + Regex r = new Regex(@"INSERT INTO (.+)\((.+)\) OUTPUT (.+) VALUES \((.+)\)"); + Match m = r.Match(sql); + Assert.True(m.Success); + + // Table name matches + Assert.Equal(Common.TableName, m.Groups[1].Value); + + // Output columns match + string[] outCols = m.Groups[3].Value.Split(", "); + Assert.Equal(expectedOutput.ExpectedOutColumns, outCols.Length); + Assert.All(outCols, col => Assert.StartsWith("inserted.", col)); + + // In columns match + string[] inCols = m.Groups[2].Value.Split(", "); + Assert.Equal(expectedOutput.ExpectedInColumns, inCols.Length); + + // In values match + string[] inVals = m.Groups[4].Value.Split(", "); + Assert.Equal(expectedOutput.ExpectedInValues, inVals.Length); + Assert.All(inVals, val => Assert.Matches(@"@.+\d+", val)); + } + } + + #endregion + + #region GetEditRow Tests + [Fact] public async Task GetEditRowNoAdditions() { @@ -208,12 +299,81 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData }); } + [Fact] + public async Task GetEditRowWithDefaultValue() + { + // Setup: Generate a row create with default values + const long rowId = 100; + DbColumn[] columns = Common.GetColumns(false); + ResultSet rs = await Common.GetResultSet(columns, false); + EditTableMetadata etm = Common.GetStandardMetadata(columns, false, columns.Length); + RowCreate rc = new RowCreate(rowId, rs, etm); + + // If: I request an edit row from the row create + EditRow er = rc.GetEditRow(null); + + // Then: + // ... The row should not be null + Assert.NotNull(er); + + // ... The row should not be clean + Assert.True(er.IsDirty); + Assert.Equal(EditRow.EditRowState.DirtyInsert, er.State); + + // ... The row sould have a bunch of default values (equal to number of columns) and all are dirty + Assert.Equal(rc.newCells.Length, er.Cells.Length); + Assert.All(er.Cells, ec => + { + Assert.Equal(Common.DefaultValue, ec.DisplayValue); + Assert.False(ec.IsNull); // TODO: Update when we support null default values better + Assert.True(ec.IsDirty); + }); + } + + [Fact] + public async Task GetEditRowWithCalculatedValue() + { + // Setup: Generate a row create with an identity column + const long rowId = 100; + DbColumn[] columns = Common.GetColumns(true); + ResultSet rs = await Common.GetResultSet(columns, true); + EditTableMetadata etm = Common.GetStandardMetadata(columns, true); + RowCreate rc = new RowCreate(rowId, rs, etm); + + // If: I request an edit row from the row created + EditRow er = rc.GetEditRow(null); + + // Then: + // ... The row should not be null + Assert.NotNull(er); + Assert.Equal(er.Id, rowId); + + // ... The row should not be clean + Assert.True(er.IsDirty); + Assert.Equal(EditRow.EditRowState.DirtyInsert, er.State); + + // ... The row should have a TBD for the identity column + Assert.Equal(rc.newCells.Length, er.Cells.Length); + Assert.Equal(SR.EditDataComputedColumnPlaceholder, er.Cells[0].DisplayValue); + Assert.False(er.Cells[0].IsNull); + Assert.True(er.Cells[0].IsDirty); + + // ... The rest of the cells should have empty display values + Assert.All(er.Cells.Skip(1), ec => + { + Assert.Equal(string.Empty, ec.DisplayValue); + Assert.False(ec.IsNull); + Assert.True(ec.IsDirty); + }); + } + [Fact] public async Task GetEditRowWithAdditions() { // Setp: Generate a row create with a cell added to it RowCreate rc = await GetStandardRowCreate(); - rc.SetCell(0, "foo"); + const string setValue = "foo"; + rc.SetCell(0, setValue); // If: I request an edit row from the row create EditRow er = rc.GetEditRow(null); @@ -228,7 +388,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData Assert.Equal(EditRow.EditRowState.DirtyInsert, er.State); // ... The row should have a single non-empty cell at the beginning that is dirty - Assert.Equal("foo", er.Cells[0].DisplayValue); + Assert.Equal(setValue, er.Cells[0].DisplayValue); Assert.False(er.Cells[0].IsNull); Assert.True(er.Cells[0].IsDirty); @@ -242,6 +402,10 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData } } + #endregion + + #region SetCell Tests + [Theory] [InlineData(-1)] // Negative [InlineData(3)] // At edge of acceptable values @@ -262,13 +426,14 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData RowCreate rc = await GetStandardRowCreate(); // If: I set a cell in the newly created row to something that doesn't need changing - EditUpdateCellResult eucr = rc.SetCell(0, "1"); + const string updateValue = "1"; + EditUpdateCellResult eucr = rc.SetCell(0, updateValue); // Then: // ... The returned value should be equal to what we provided Assert.NotNull(eucr); Assert.NotNull(eucr.Cell); - Assert.Equal("1", eucr.Cell.DisplayValue); + Assert.Equal(updateValue, eucr.Cell.DisplayValue); Assert.False(eucr.Cell.IsNull); // ... The returned value should be dirty @@ -330,13 +495,14 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData RowCreate rc = await GetStandardRowCreate(); // If: I set a cell in the newly created row to null - EditUpdateCellResult eucr = rc.SetCell(0, "NULL"); + const string nullValue = "NULL"; + EditUpdateCellResult eucr = rc.SetCell(0, nullValue); // Then: // ... The returned value should be equal to what we provided Assert.NotNull(eucr); Assert.NotNull(eucr.Cell); - Assert.NotEmpty(eucr.Cell.DisplayValue); + Assert.Equal(nullValue, eucr.Cell.DisplayValue); Assert.True(eucr.Cell.IsNull); // ... The returned value should be dirty @@ -349,6 +515,10 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData Assert.NotNull(rc.newCells[0]); } + #endregion + + #region RevertCell Tests + [Theory] [InlineData(-1)] // Negative [InlineData(3)] // At edge of acceptable values @@ -363,11 +533,17 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData Assert.Throws(() => rc.RevertCell(columnId)); } - [Fact] - public async Task RevertCellNotSet() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task RevertCellNotSet(bool hasDefaultValues) { - // Setup: Generate the row create - RowCreate rc = await GetStandardRowCreate(); + // Setup: + // ... Generate the parameters for the row create + DbColumn[] columns = Common.GetColumns(false); + ResultSet rs = await Common.GetResultSet(columns, false); + EditTableMetadata etm = Common.GetStandardMetadata(columns, false, hasDefaultValues ? 1 : 0); + RowCreate rc = new RowCreate(100, rs, etm); // If: I attempt to revert a cell that has not been set EditRevertCellResult result = rc.RevertCell(0); @@ -376,9 +552,11 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData // ... We should get a result back Assert.NotNull(result); - // ... We should get a null cell back - // @TODO: Check for a default value when we support it - Assert.Null(result.Cell); + // ... We should get back an edit cell with a value based on the default value + string expectedDisplayValue = hasDefaultValues ? Common.DefaultValue : string.Empty; + Assert.NotNull(result.Cell); + Assert.Equal(expectedDisplayValue, result.Cell.DisplayValue); + Assert.False(result.Cell.IsNull); // TODO: Modify to support null defaults // ... The row should be dirty Assert.True(result.IsRowDirty); @@ -387,11 +565,17 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData Assert.Null(rc.newCells[0]); } - [Fact] - public async Task RevertCellThatWasSet() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task RevertCellThatWasSet(bool hasDefaultValues) { - // Setup: Generate the row create - RowCreate rc = await GetStandardRowCreate(); + // Setup: + // ... Generate the parameters for the row create + DbColumn[] columns = Common.GetColumns(false); + ResultSet rs = await Common.GetResultSet(columns, false); + EditTableMetadata etm = Common.GetStandardMetadata(columns, false, hasDefaultValues ? 1 : 0); + RowCreate rc = new RowCreate(100, rs, etm); rc.SetCell(0, "1"); // If: I attempt to revert a cell that was set @@ -401,9 +585,11 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData // ... We should get a result back Assert.NotNull(result); - // ... We should get a null cell back - // @TODO: Check for a default value when we support it - Assert.Null(result.Cell); + // ... We should get back an edit cell with a value based on the default value + string expectedDisplayValue = hasDefaultValues ? Common.DefaultValue : string.Empty; + Assert.NotNull(result.Cell); + Assert.Equal(expectedDisplayValue, result.Cell.DisplayValue); + Assert.False(result.Cell.IsNull); // TODO: Modify to support null defaults // ... The row should be dirty Assert.True(result.IsRowDirty); @@ -412,6 +598,8 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData Assert.Null(rc.newCells[0]); } + #endregion + private static async Task GetStandardRowCreate() { var cols = Common.GetColumns(false); @@ -419,5 +607,19 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData var etm = Common.GetStandardMetadata(cols); return new RowCreate(100, rs, etm); } + + public class RegexExpectedOutput + { + public RegexExpectedOutput(int expectedInColumns, int expectedInValues, int expectedOutColumns) + { + ExpectedInColumns = expectedInColumns; + ExpectedInValues = expectedInValues; + ExpectedOutColumns = expectedOutColumns; + } + + public int ExpectedInColumns { get; set; } + public int ExpectedInValues { get; set; } + public int ExpectedOutColumns { get; set; } + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowUpdateTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowUpdateTests.cs index 09a30034..ac7f8f8d 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowUpdateTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowUpdateTests.cs @@ -128,7 +128,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData // If: // ... I add updates to all the cells in the row RowUpdate ru = new RowUpdate(0, rs, etm); - Common.AddCells(ru, true); + Common.AddCells(ru, 1); // ... Then I update a cell back to it's old value var eucr = ru.SetCell(1, (string) rs.GetRow(0)[1].RawObject); @@ -204,7 +204,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData // If: I ask for a script to be generated for update RowUpdate ru = new RowUpdate(0, rs, etm); - Common.AddCells(ru, true); + Common.AddCells(ru, 1); string script = ru.GetScript(); // Then: @@ -241,7 +241,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData var rs = await Common.GetResultSet(columns, includeIdentity); var etm = Common.GetStandardMetadata(columns, isMemoryOptimized); RowUpdate ru = new RowUpdate(0, rs, etm); - Common.AddCells(ru, includeIdentity); + Common.AddCells(ru, includeIdentity ? 1 : 0); // ... Mock db connection for building the command var mockConn = new TestSqlConnection(null);