Skip to content

Commit b7cffa3

Browse files
authored
Edit Data: Fix to work with tables with triggers (#576)
* Moving logic for adding default values to new rows * Fixing implementation of script generation to handle default values all around * Unit tests! * WIP * Reworking row create script/command generation to work more cleanly and work on triggered tables * Addressing some bugs with the create row implementation * Implementing the trigger table fix for row updates Some small improvements to the create/update tests.
1 parent 4d4e0b1 commit b7cffa3

File tree

7 files changed

+477
-331
lines changed

7 files changed

+477
-331
lines changed

src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowCreate.cs

Lines changed: 119 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement
2424
/// </summary>
2525
public sealed class RowCreate : RowEditBase
2626
{
27-
private const string InsertScriptStart = "INSERT INTO {0}";
28-
private const string InsertScriptColumns = "({0})";
29-
private const string InsertScriptOut = " OUTPUT {0}";
30-
private const string InsertScriptDefault = " DEFAULT VALUES";
31-
private const string InsertScriptValues = " VALUES ({0})";
27+
private const string DeclareStatement = "DECLARE {0} TABLE ({1})";
28+
private const string InsertOutputDefaultStatement = "INSERT INTO {0} OUTPUT {1} INTO {2} DEFAULT VALUES";
29+
private const string InsertOutputValuesStatement = "INSERT INTO {0}({1}) OUTPUT {2} INTO {3} VALUES ({4})";
30+
private const string InsertScriptDefaultStatement = "INSERT INTO {0} DEFAULT VALUES";
31+
private const string InsertScriptValuesStatement = "INSERT INTO {0}({1}) VALUES ({2})";
32+
private const string SelectStatement = "SELECT {0} FROM {1}";
3233

3334
internal readonly CellUpdate[] newCells;
3435

@@ -88,13 +89,72 @@ public override DbCommand GetCommand(DbConnection connection)
8889
{
8990
Validate.IsNotNull(nameof(connection), connection);
9091

91-
// Build the script and generate a command
92-
ScriptBuildResult result = BuildInsertScript(forCommand: true);
92+
// Process the cells and columns
93+
List<string> declareColumns = new List<string>();
94+
List<string> inColumnNames = new List<string>();
95+
List<string> outClauseColumnNames = new List<string>();
96+
List<string> inValues = new List<string>();
97+
List<SqlParameter> inParameters = new List<SqlParameter>();
98+
List<string> selectColumns = new List<string>();
99+
for(int i = 0; i < AssociatedObjectMetadata.Columns.Length; i++)
100+
{
101+
DbColumnWrapper column = AssociatedResultSet.Columns[i];
102+
EditColumnMetadata metadata = AssociatedObjectMetadata.Columns[i];
103+
CellUpdate cell = newCells[i];
104+
105+
// Add the output columns regardless of whether the column is read only
106+
outClauseColumnNames.Add($"inserted.{metadata.EscapedName}");
107+
declareColumns.Add($"{metadata.EscapedName} {ToSqlScript.FormatColumnType(column, useSemanticEquivalent: true)}");
108+
selectColumns.Add(metadata.EscapedName);
109+
110+
// Continue if we're not inserting a value for this column
111+
if (!IsCellValueProvided(column, cell, DefaultValues[i]))
112+
{
113+
continue;
114+
}
115+
116+
// Add the input column
117+
inColumnNames.Add(metadata.EscapedName);
118+
119+
// Add the input values as parameters
120+
string paramName = $"@Value{RowId}_{i}";
121+
inValues.Add(paramName);
122+
inParameters.Add(new SqlParameter(paramName, column.SqlDbType) {Value = cell.Value});
123+
}
124+
125+
// Put everything together into a single query
126+
// Step 1) Build a temp table for inserting output values into
127+
string tempTableName = $"@Insert{RowId}Output";
128+
string declareStatement = string.Format(DeclareStatement, tempTableName, string.Join(", ", declareColumns));
129+
130+
// Step 2) Build the insert statement
131+
string joinedOutClauseNames = string.Join(", ", outClauseColumnNames);
132+
string insertStatement = inValues.Count > 0
133+
? string.Format(InsertOutputValuesStatement,
134+
AssociatedObjectMetadata.EscapedMultipartName,
135+
string.Join(", ", inColumnNames),
136+
joinedOutClauseNames,
137+
tempTableName,
138+
string.Join(", ", inValues))
139+
: string.Format(InsertOutputDefaultStatement,
140+
AssociatedObjectMetadata.EscapedMultipartName,
141+
joinedOutClauseNames,
142+
tempTableName);
143+
144+
// Step 3) Build the select statement
145+
string selectStatement = string.Format(SelectStatement, string.Join(", ", selectColumns), tempTableName);
93146

147+
// Step 4) Put it all together into a results object
148+
StringBuilder query = new StringBuilder();
149+
query.AppendLine(declareStatement);
150+
query.AppendLine(insertStatement);
151+
query.Append(selectStatement);
152+
153+
// Build the command
94154
DbCommand command = connection.CreateCommand();
95-
command.CommandText = result.ScriptText;
155+
command.CommandText = query.ToString();
96156
command.CommandType = CommandType.Text;
97-
command.Parameters.AddRange(result.ScriptParameters);
157+
command.Parameters.AddRange(inParameters.ToArray());
98158

99159
return command;
100160
}
@@ -123,7 +183,32 @@ public override EditRow GetEditRow(DbCellValue[] cachedRow)
123183
/// <returns>INSERT INTO statement</returns>
124184
public override string GetScript()
125185
{
126-
return BuildInsertScript(forCommand: false).ScriptText;
186+
// Process the cells and columns
187+
List<string> inColumns = new List<string>();
188+
List<string> inValues = new List<string>();
189+
for (int i = 0; i < AssociatedObjectMetadata.Columns.Length; i++)
190+
{
191+
DbColumnWrapper column = AssociatedResultSet.Columns[i];
192+
CellUpdate cell = newCells[i];
193+
194+
// Continue if we're not inserting a value for this column
195+
if (!IsCellValueProvided(column, cell, DefaultValues[i]))
196+
{
197+
continue;
198+
}
199+
200+
// Column is provided
201+
inColumns.Add(AssociatedObjectMetadata.Columns[i].EscapedName);
202+
inValues.Add(ToSqlScript.FormatValue(cell.AsDbCellValue, column));
203+
}
204+
205+
// Build the insert statement
206+
return inValues.Count > 0
207+
? string.Format(InsertScriptValuesStatement,
208+
AssociatedObjectMetadata.EscapedMultipartName,
209+
string.Join(", ", inColumns),
210+
string.Join(", ", inValues))
211+
: string.Format(InsertScriptDefaultStatement, AssociatedObjectMetadata.EscapedMultipartName);
127212
}
128213

129214
/// <summary>
@@ -173,111 +258,40 @@ public override EditUpdateCellResult SetCell(int columnId, string newValue)
173258
#endregion
174259

175260
/// <summary>
176-
/// Generates an INSERT script that will insert this row
261+
/// Verifies the column and cell, ensuring a column that needs a value has one.
177262
/// </summary>
178-
/// <param name="forCommand">
179-
/// If <c>true</c> the script will be generated with an OUTPUT clause for returning all
180-
/// values in the inserted row (including computed values). The script will also generate
181-
/// parameters for inserting the values.
182-
/// If <c>false</c> the script will not have an OUTPUT clause and will have the values
183-
/// directly inserted into the script (with proper escaping, of course).
184-
/// </param>
185-
/// <returns>A script build result object with the script text and any parameters</returns>
263+
/// <param name="column">Column that will be inserted into</param>
264+
/// <param name="cell">Current cell value for this row</param>
265+
/// <param name="defaultCell">Default value for the column in this row</param>
186266
/// <exception cref="InvalidOperationException">
187-
/// Thrown if there are columns that are not readonly, do not have default values, and were
188-
/// not assigned values.
267+
/// Thrown if the column needs a value but it is not provided
189268
/// </exception>
190-
private ScriptBuildResult BuildInsertScript(bool forCommand)
191-
{
192-
// Process all the columns in this table
193-
List<string> inValues = new List<string>();
194-
List<string> inColumns = new List<string>();
195-
List<string> outColumns = new List<string>();
196-
List<SqlParameter> sqlParameters = new List<SqlParameter>();
197-
for (int i = 0; i < AssociatedObjectMetadata.Columns.Length; i++)
269+
/// <returns>
270+
/// <c>true</c> If the column has a value provided
271+
/// <c>false</c> If the column does not have a value provided (column is read-only, has default, etc)
272+
/// </returns>
273+
private static bool IsCellValueProvided(DbColumnWrapper column, CellUpdate cell, string defaultCell)
274+
{
275+
// Skip columns that cannot be updated
276+
if (!column.IsUpdatable)
198277
{
199-
DbColumnWrapper column = AssociatedResultSet.Columns[i];
200-
CellUpdate cell = newCells[i];
201-
202-
// Add an out column if we're doing this for a command
203-
if (forCommand)
204-
{
205-
outColumns.Add($"inserted.{ToSqlScript.FormatIdentifier(column.ColumnName)}");
206-
}
278+
return false;
279+
}
207280

208-
// Skip columns that cannot be updated
209-
if (!column.IsUpdatable)
281+
// Make sure a value was provided for the cell
282+
if (cell == null)
283+
{
284+
// If the column is not nullable and there is not default defined, then fail
285+
if (!column.AllowDBNull.HasTrue() && defaultCell == null)
210286
{
211-
continue;
287+
throw new InvalidOperationException(SR.EditDataCreateScriptMissingValue(column.ColumnName));
212288
}
213-
214-
// Make sure a value was provided for the cell
215-
if (cell == null)
216-
{
217-
// If the column is not nullable and there is no default defined, then fail
218-
if (!column.AllowDBNull.HasTrue() && DefaultValues[i] == null)
219-
{
220-
throw new InvalidOperationException(SR.EditDataCreateScriptMissingValue(column.ColumnName));
221-
}
222289

223-
// There is a default value (or omitting the value is fine), so trust the db will apply it correctly
224-
continue;
225-
}
226-
227-
// Add the input values
228-
if (forCommand)
229-
{
230-
// Since this script is for command use, add parameter for the input value to the list
231-
string paramName = $"@Value{RowId}_{i}";
232-
inValues.Add(paramName);
233-
234-
SqlParameter param = new SqlParameter(paramName, cell.Column.SqlDbType) {Value = cell.Value};
235-
sqlParameters.Add(param);
236-
}
237-
else
238-
{
239-
// This script isn't for command use, add the value, formatted for insertion
240-
inValues.Add(ToSqlScript.FormatValue(cell.Value, column));
241-
}
242-
243-
// Add the column to the in columns
244-
inColumns.Add(ToSqlScript.FormatIdentifier(column.ColumnName));
245-
}
246-
247-
// Begin the script (ie, INSERT INTO blah)
248-
StringBuilder queryBuilder = new StringBuilder();
249-
queryBuilder.AppendFormat(InsertScriptStart, AssociatedObjectMetadata.EscapedMultipartName);
250-
251-
// Add the input columns (if there are any)
252-
if (inColumns.Count > 0)
253-
{
254-
string joinedInColumns = string.Join(", ", inColumns);
255-
queryBuilder.AppendFormat(InsertScriptColumns, joinedInColumns);
256-
}
257-
258-
// Add the output columns (this will be empty if we are not building for command)
259-
if (outColumns.Count > 0)
260-
{
261-
string joinedOutColumns = string.Join(", ", outColumns);
262-
queryBuilder.AppendFormat(InsertScriptOut, joinedOutColumns);
263-
}
264-
265-
// Add the input values (if there any) or use the default values
266-
if (inValues.Count > 0)
267-
{
268-
string joinedInValues = string.Join(", ", inValues);
269-
queryBuilder.AppendFormat(InsertScriptValues, joinedInValues);
270-
}
271-
else
272-
{
273-
queryBuilder.AppendFormat(InsertScriptDefault);
290+
// There is a default value (or omitting the value is fine), so trust the db will apply it correctly
291+
return false;
274292
}
275293

276-
return new ScriptBuildResult
277-
{
278-
ScriptText = queryBuilder.ToString(),
279-
ScriptParameters = sqlParameters.ToArray()
280-
};
294+
return true;
281295
}
282296

283297
private EditCell GetEditCell(CellUpdate cell, int index)
@@ -301,11 +315,5 @@ private EditCell GetEditCell(CellUpdate cell, int index)
301315
}
302316
return new EditCell(dbCell, isDirty: true);
303317
}
304-
305-
private class ScriptBuildResult
306-
{
307-
public string ScriptText { get; set; }
308-
public SqlParameter[] ScriptParameters { get; set; }
309-
}
310318
}
311319
}

0 commit comments

Comments
 (0)