From d7ecfb1a8747c62cde23fce5b1a01c25abe47183 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 21 Mar 2017 15:14:04 -0700 Subject: [PATCH] feature/edit/subset (#283) * Changing query/subset API to only use Result on success, Error on error * Creating an interservice API for getting query result subsets * Updates to subset API * RowStartIndex is now long * Output of query/subset is a 2D array of DbCellValue * Adding LongSkip method to LongList to allow skipping ahead by longs * Moving LongList back to ServiceLayer utilities. Move refactoring * Stubbing out request for edit/subset * Initial implementation of getting edit rows * Unit tests for RowEdit and RowDelete .GetEditRow * Fixing major bugs in LongList implementation, adding much more thorough tests * Adding some more unit tests and fixes to make unit tests pass * Fixing comment --- .../Utility/Validate.cs | 6 +- .../EditData/Contracts/EditRow.cs | 45 +++ .../EditData/Contracts/EditSubsetRequest.cs | 52 +++ .../EditData/EditDataService.cs | 15 + .../EditData/EditSession.cs | 55 +++ .../EditData/UpdateManagement/CellUpdate.cs | 16 + .../EditData/UpdateManagement/RowCreate.cs | 20 + .../EditData/UpdateManagement/RowDelete.cs | 19 + .../EditData/UpdateManagement/RowEdit.cs | 8 + .../EditData/UpdateManagement/RowUpdate.cs | 24 ++ .../QueryExecution/Batch.cs | 2 +- .../Contracts/ResultSetSubset.cs | 2 +- .../QueryExecution/Contracts/SubsetRequest.cs | 7 +- .../QueryExecution/Query.cs | 2 +- .../QueryExecution/QueryExecutionService.cs | 57 ++- .../QueryExecution/ResultSet.cs | 21 +- .../Utility/LongList.cs | 130 ++++--- .../EditData/CellUpdateTests.cs | 27 ++ .../EditData/RowCreateTests.cs | 59 +++ .../EditData/RowDeleteTests.cs | 89 +++-- .../EditData/RowEditBaseTests.cs | 6 + .../EditData/RowUpdateTests.cs | 75 +++- .../EditData/ServiceIntegrationTests.cs | 32 ++ .../EditData/SessionTests.cs | 167 ++++++++ .../QueryExecution/SubsetTests.cs | 27 +- .../Utility/LongListTests.cs | 367 +++++++++++++++++- 26 files changed, 1165 insertions(+), 165 deletions(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/EditData/Contracts/EditRow.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/EditData/Contracts/EditSubsetRequest.cs rename src/{Microsoft.SqlTools.Hosting => Microsoft.SqlTools.ServiceLayer}/Utility/LongList.cs (72%) diff --git a/src/Microsoft.SqlTools.Hosting/Utility/Validate.cs b/src/Microsoft.SqlTools.Hosting/Utility/Validate.cs index 2f25bf6d..1b3d4074 100644 --- a/src/Microsoft.SqlTools.Hosting/Utility/Validate.cs +++ b/src/Microsoft.SqlTools.Hosting/Utility/Validate.cs @@ -37,9 +37,9 @@ namespace Microsoft.SqlTools.Utility /// The upper limit which the value should not be greater than. public static void IsWithinRange( string parameterName, - int valueToCheck, - int lowerLimit, - int upperLimit) + long valueToCheck, + long lowerLimit, + long upperLimit) { // TODO: Debug assert here if lowerLimit >= upperLimit diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/Contracts/EditRow.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/Contracts/EditRow.cs new file mode 100644 index 00000000..2042bea0 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/Contracts/EditRow.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.EditData.Contracts +{ + /// + /// A way to return a row in a result set that is being edited. It contains state about whether + /// or not the row is dirty + /// + public class EditRow + { + public enum EditRowState + { + Clean = 0, + DirtyInsert = 1, + DirtyDelete = 2, + DirtyUpdate = 3 + } + + /// + /// The cells in the row. If the row has pending changes, they will be represented in + /// this list + /// + public DbCellValue[] Cells { get; set; } + + /// + /// Internal ID of the row. This should be used whenever referencing a row in row edit operations. + /// + public long Id { get; set; } + + /// + /// Whether or not the row has changes pending + /// + public bool IsDirty => State != EditRowState.Clean; + + /// + /// What type of dirty state (or lack thereof) the row is + /// + public EditRowState State { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/Contracts/EditSubsetRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/Contracts/EditSubsetRequest.cs new file mode 100644 index 00000000..623098e4 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/Contracts/EditSubsetRequest.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.EditData.Contracts +{ + /// + /// Parameters for a subset retrieval request + /// + public class EditSubsetParams : SessionOperationParams + { + /// + /// Beginning index of the rows to return from the selected resultset. This index will be + /// included in the results. + /// + public long RowStartIndex { get; set; } + + /// + /// Number of rows to include in the result of this request. If the number of the rows + /// exceeds the number of rows available after the start index, all available rows after + /// the start index will be returned. + /// + public int RowCount { get; set; } + } + + /// + /// Parameters for the result of a subset retrieval request + /// + public class EditSubsetResult + { + /// + /// The number of rows returned from result set, useful for determining if less rows were + /// returned than requested. + /// + public int RowCount { get; set; } + + /// + /// The requested subset of rows, with information about whether or not the rows are dirty + /// + public EditRow[] Subset { get; set; } + } + + public class EditSubsetRequest + { + public static readonly + RequestType Type = + RequestType.Create("edit/subset"); + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/EditDataService.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/EditDataService.cs index de1867d1..e5e67731 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/EditData/EditDataService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/EditDataService.cs @@ -91,6 +91,7 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData serviceHost.SetRequestHandler(EditInitializeRequest.Type, HandleInitializeRequest); serviceHost.SetRequestHandler(EditRevertCellRequest.Type, HandleRevertCellRequest); serviceHost.SetRequestHandler(EditRevertRowRequest.Type, HandleRevertRowRequest); + serviceHost.SetRequestHandler(EditSubsetRequest.Type, HandleSubsetRequest); serviceHost.SetRequestHandler(EditUpdateCellRequest.Type, HandleUpdateCellRequest); } @@ -241,6 +242,20 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData }); } + internal Task HandleSubsetRequest(EditSubsetParams subsetParams, + RequestContext requestContext) + { + return HandleSessionRequest(subsetParams, requestContext, session => + { + EditRow[] rows = session.GetRows(subsetParams.RowStartIndex, subsetParams.RowCount).Result; + return new EditSubsetResult + { + RowCount = rows.Length, + Subset = rows + }; + }); + } + internal Task HandleUpdateCellRequest(EditUpdateCellParams updateParams, RequestContext requestContext) { diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/EditSession.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/EditSession.cs index bccd3aa5..bad9f4d4 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/EditData/EditSession.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/EditSession.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.EditData.Contracts; using Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement; using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.Utility; namespace Microsoft.SqlTools.ServiceLayer.EditData @@ -186,6 +187,60 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData } } + /// + /// Retrieves a subset of rows with the pending updates applied. If more rows than exist + /// are requested, only the rows that exist will be returned. + /// + /// Index to start returning rows from + /// The number of rows to return. + /// An array of rows with pending edits applied + public async Task GetRows(long startIndex, int rowCount) + { + // Get the cached rows from the result set + ResultSetSubset cachedRows = startIndex < associatedResultSet.RowCount + ? await associatedResultSet.GetSubset(startIndex, rowCount) + : new ResultSetSubset + { + RowCount = 0, + Rows = new DbCellValue[][] { } + }; + + // Convert the rows into EditRows and apply the changes we have + List editRows = new List(); + for (int i = 0; i < cachedRows.RowCount; i++) + { + long rowId = i + startIndex; + RowEditBase edr; + if (EditCache.TryGetValue(rowId, out edr)) + { + // Ask the edit object to generate an edit row + editRows.Add(edr.GetEditRow(cachedRows.Rows[i])); + } + else + { + // Package up the existing row into a clean edit row + EditRow er = new EditRow + { + Id = rowId, + Cells = cachedRows.Rows[i], + State = EditRow.EditRowState.Clean + }; + editRows.Add(er); + } + } + + // If the requested range of rows was at the end of the original cell set and we have + // added new rows, we need to reflect those changes + if (rowCount > cachedRows.RowCount) + { + long endIndex = startIndex + cachedRows.RowCount; + var newRows = EditCache.Where(edit => edit.Key >= endIndex).Take(rowCount - cachedRows.RowCount); + editRows.AddRange(newRows.Select(newRow => newRow.Value.GetEditRow(null))); + } + + return editRows.ToArray(); + } + /// /// Reverts a cell in a pending edit /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/CellUpdate.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/CellUpdate.cs index a24e5d4e..9aea0fd5 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/CellUpdate.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/CellUpdate.cs @@ -85,6 +85,22 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement #region Properties + /// + /// Converts the cell update to a DbCellValue + /// + public DbCellValue AsDbCellValue + { + get + { + return new DbCellValue + { + DisplayValue = ValueAsString, + IsNull = Value == DBNull.Value, + RawObject = Value + }; + } + } + /// /// The column that the cell will be placed in /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowCreate.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowCreate.cs index db9fbf29..b5292fa0 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowCreate.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowCreate.cs @@ -122,6 +122,26 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement return command; } + /// + /// Generates a edit row that represents a row pending insertion + /// + /// Original, cached cell contents. (Should be null in this case) + /// EditRow of pending update + public override EditRow GetEditRow(DbCellValue[] cachedRow) + { + // Iterate over the new cells. If they are null, generate a blank value + DbCellValue[] editCells = newCells.Select(cell => cell == null + ? new DbCellValue {DisplayValue = string.Empty, IsNull = false, RawObject = null} + : cell.AsDbCellValue) + .ToArray(); + return new EditRow + { + Id = RowId, + Cells = editCells, + State = EditRow.EditRowState.DirtyInsert + }; + } + /// /// Generates the INSERT INTO statement that will apply the row creation /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowDelete.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowDelete.cs index fc92e4af..64b0ddc9 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowDelete.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowDelete.cs @@ -9,6 +9,7 @@ using System.Globalization; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.EditData.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.Utility; namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement @@ -72,6 +73,24 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement return command; } + /// + /// Generates a edit row that represents a row pending deletion. All the original cells are + /// intact but the state is dirty. + /// + /// Original, cached cell contents + /// EditRow that is pending deletion + public override EditRow GetEditRow(DbCellValue[] cachedRow) + { + Validate.IsNotNull(nameof(cachedRow), cachedRow); + + return new EditRow + { + Id = RowId, + Cells = cachedRow, + State = EditRow.EditRowState.DirtyDelete + }; + } + /// /// Generates a DELETE statement to delete this row /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowEdit.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowEdit.cs index 6260a39d..9df74c50 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowEdit.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowEdit.cs @@ -85,6 +85,14 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement /// Command to commit the change to the db public abstract DbCommand GetCommand(DbConnection connection); + /// + /// Generates a row that has the pending update applied. The dirty status of the row is + /// reflected in the returned EditRow. + /// + /// The original, cached row values + /// An EditRow with the pending changes applied + public abstract EditRow GetEditRow(DbCellValue[] cachedRow); + /// /// Converts the row edit into a SQL statement /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowUpdate.cs b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowUpdate.cs index 6e43aa29..3342f1d7 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowUpdate.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/EditData/UpdateManagement/RowUpdate.cs @@ -113,6 +113,30 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement return command; } + /// + /// Generates a edit row that represents a row with pending update. The cells pending + /// updates are merged into the unchanged cells. + /// + /// Original, cached cell contents + /// EditRow with pending updates + public override EditRow GetEditRow(DbCellValue[] cachedRow) + { + Validate.IsNotNull(nameof(cachedRow), cachedRow); + + // For each cell that is pending update, replace the db cell value with a new one + foreach (var cellUpdate in cellUpdates) + { + cachedRow[cellUpdate.Key] = cellUpdate.Value.AsDbCellValue; + } + + return new EditRow + { + Id = RowId, + Cells = cachedRow, + State = EditRow.EditRowState.DirtyUpdate + }; + } + /// /// Constructs an update statement to change the associated row. /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs index a912d30f..58ab4ccf 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs @@ -355,7 +355,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// The starting row of the results /// How many rows to retrieve /// A subset of results - public Task GetSubset(int resultSetIndex, int startRow, int rowCount) + public Task GetSubset(int resultSetIndex, long startRow, int rowCount) { ResultSet targetResultSet; lock (resultSets) diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSubset.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSubset.cs index 62308824..d416753a 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSubset.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/ResultSetSubset.cs @@ -19,6 +19,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// /// 2D array of the cell values requested from result set /// - public string[][] Rows { get; set; } + public DbCellValue[][] Rows { get; set; } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/SubsetRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/SubsetRequest.cs index aec9ee07..f61fd559 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/SubsetRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/SubsetRequest.cs @@ -31,7 +31,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// Beginning index of the rows to return from the selected resultset. This index will be /// included in the results. /// - public int RowsStartIndex { get; set; } + public long RowsStartIndex { get; set; } /// /// Number of rows to include in the result of this request. If the number of the rows @@ -46,11 +46,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts /// public class SubsetResult { - /// - /// Subset request error messages. Optional, can be set to null to indicate no errors - /// - public string Message { get; set; } - /// /// The requested subset of results. Optional, can be set to null to indicate an error /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs index 3a62c9f9..e7cd81eb 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs @@ -280,7 +280,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// The starting row of the results /// How many rows to retrieve /// A subset of results - public Task GetSubset(int batchIndex, int resultSetIndex, int startRow, int rowCount) + public Task GetSubset(int batchIndex, int resultSetIndex, long startRow, int rowCount) { // Sanity check to make sure that the batch is within bounds if (batchIndex < 0 || batchIndex >= Batches.Length) diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs index a9f0a012..54fa8f36 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs @@ -176,42 +176,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { try { - // Attempt to load the query - Query query; - if (!ActiveQueries.TryGetValue(subsetParams.OwnerUri, out query)) - { - await requestContext.SendResult(new SubsetResult - { - Message = SR.QueryServiceRequestsNoQuery - }); - return; - } - - // Retrieve the requested subset and return it + ResultSetSubset subset = await InterServiceResultSubset(subsetParams); var result = new SubsetResult { - Message = null, - ResultSubset = await query.GetSubset(subsetParams.BatchIndex, - subsetParams.ResultSetIndex, subsetParams.RowsStartIndex, subsetParams.RowsCount) + ResultSubset = subset }; await requestContext.SendResult(result); } - catch (InvalidOperationException ioe) - { - // Return the error as a result - await requestContext.SendResult(new SubsetResult - { - Message = ioe.Message - }); - } - catch (ArgumentOutOfRangeException aoore) - { - // Return the error as a result - await requestContext.SendResult(new SubsetResult - { - Message = aoore.Message - }); - } catch (Exception e) { // This was unexpected, so send back as error @@ -415,7 +386,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// The identifier of the query to be disposed /// Action to perform on success /// Action to perform on failure - /// public async Task InterServiceDisposeQuery(string ownerUri, Func successAction, Func failureAction) { @@ -444,6 +414,29 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution } } + /// + /// Retrieves the requested subset of rows from the requested result set. Intended to be + /// called by another service. + /// + /// Parameters for the subset to retrieve + /// The requested subset + /// The requested query does not exist + public async Task InterServiceResultSubset(SubsetParams subsetParams) + { + Validate.IsNotNullOrEmptyString(nameof(subsetParams.OwnerUri), subsetParams.OwnerUri); + + // Attempt to load the query + Query query; + if (!ActiveQueries.TryGetValue(subsetParams.OwnerUri, out query)) + { + throw new ArgumentOutOfRangeException(SR.QueryServiceRequestsNoQuery); + } + + // Retrieve the requested subset and return it + return await query.GetSubset(subsetParams.BatchIndex, subsetParams.ResultSetIndex, + subsetParams.RowsStartIndex, subsetParams.RowsCount); + } + #endregion #region Private Helpers diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs index 55747e4e..2c7747d1 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/ResultSet.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage; +using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.Utility; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution @@ -223,7 +224,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution /// The starting row of the results /// How many rows to retrieve /// A subset of results - public Task GetSubset(int startRow, int rowCount) + public Task GetSubset(long startRow, int rowCount) { // Sanity check to make sure that the results have been read beforehand if (!hasBeenRead) @@ -244,7 +245,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution return Task.Factory.StartNew(() => { - string[][] rows; + DbCellValue[][] rows; using (IFileStreamReader fileStreamReader = fileStreamFactory.GetReader(outputFileName)) { @@ -255,19 +256,23 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution // Iterate over all the rows and process them into a list of string builders // ReSharper disable once AccessToDisposedClosure The lambda is used immediately in string.Join call IEnumerable rowValues = fileOffsets.Select(rowOffset => fileStreamReader.ReadRow(rowOffset, Columns)[0].DisplayValue); - rows = new[] { new[] { string.Join(string.Empty, rowValues) } }; + string singleString = string.Join(string.Empty, rowValues); + DbCellValue cellValue = new DbCellValue + { + DisplayValue = singleString, + IsNull = false, + RawObject = singleString + }; + rows = new[] { new[] { cellValue } }; } else { // Figure out which rows we need to read back - IEnumerable rowOffsets = fileOffsets.Skip(startRow).Take(rowCount); + IEnumerable rowOffsets = fileOffsets.LongSkip(startRow).Take(rowCount); // Iterate over the rows we need and process them into output // ReSharper disable once AccessToDisposedClosure The lambda is used immediately in .ToArray call - rows = rowOffsets.Select(rowOffset => fileStreamReader.ReadRow(rowOffset, Columns) - .Select(cell => cell.DisplayValue).ToArray()) - .ToArray(); - + rows = rowOffsets.Select(rowOffset => fileStreamReader.ReadRow(rowOffset, Columns).ToArray()).ToArray(); } } // Retrieve the subset of the results as per the request diff --git a/src/Microsoft.SqlTools.Hosting/Utility/LongList.cs b/src/Microsoft.SqlTools.ServiceLayer/Utility/LongList.cs similarity index 72% rename from src/Microsoft.SqlTools.Hosting/Utility/LongList.cs rename to src/Microsoft.SqlTools.ServiceLayer/Utility/LongList.cs index d3d0cc10..25d66110 100644 --- a/src/Microsoft.SqlTools.Hosting/Utility/LongList.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Utility/LongList.cs @@ -6,8 +6,9 @@ using System; using System.Collections; using System.Collections.Generic; +using Microsoft.SqlTools.Utility; -namespace Microsoft.SqlTools.Utility +namespace Microsoft.SqlTools.ServiceLayer.Utility { /// /// Collection class that permits storage of over int.MaxValue items. This is performed @@ -22,8 +23,7 @@ namespace Microsoft.SqlTools.Utility public class LongList : IEnumerable { #region Member Variables - - private int expandListSize = int.MaxValue; + private List> expandedList; private readonly List shortList; @@ -35,6 +35,7 @@ namespace Microsoft.SqlTools.Utility public LongList() { shortList = new List(); + ExpandListSize = int.MaxValue; Count = 0; } @@ -45,11 +46,15 @@ namespace Microsoft.SqlTools.Utility /// public long Count { get; private set; } + /// + /// Used to get or set the value at a given index in the list + /// + /// Index into the list to access public T this[long index] { - get - { - return GetItem(index); + get + { + return GetItem(index); } set @@ -58,17 +63,10 @@ namespace Microsoft.SqlTools.Utility } } - public int ExpandListSize - { - get - { - return this.expandListSize; - } - internal set - { - this.expandListSize = value; - } - } + /// + /// The number of elements to store in a single list before expanding into multiple lists + /// + public int ExpandListSize { get; internal set; } #endregion @@ -81,7 +79,7 @@ namespace Microsoft.SqlTools.Utility /// Index of the item that was just added public long Add(T val) { - if (Count <= this.ExpandListSize) + if (Count < this.ExpandListSize) { shortList.Add(val); } @@ -118,21 +116,22 @@ namespace Microsoft.SqlTools.Utility /// The item at the index specified public T GetItem(long index) { - T val = default(T); + Validate.IsWithinRange(nameof(index), index, 0, Count - 1); - if (Count <= this.ExpandListSize) + T val = default(T); + if (Count < this.ExpandListSize) { int i32Index = Convert.ToInt32(index); val = shortList[i32Index]; } else { - int iArray32Index = (int) (Count / this.ExpandListSize); + int iArray32Index = (int) (index / this.ExpandListSize); if (expandedList.Count > iArray32Index) { List arr = expandedList[iArray32Index]; - int i32Index = (int) (Count % this.ExpandListSize); + int i32Index = (int) (index % this.ExpandListSize); if (arr.Count > i32Index) { val = arr[i32Index]; @@ -142,6 +141,25 @@ namespace Microsoft.SqlTools.Utility return val; } + /// + /// Skips ahead the number of elements requested and returns the elements after that many elements + /// + /// The number of elements to skip + /// All elements after the number of elements to skip + public IEnumerable LongSkip(long start) + { + Validate.IsWithinRange(nameof(start), start, 0, Count - 1); + + // Generate an enumerator over this list and jump ahead to the position we want + LongListEnumerator longEnumerator = new LongListEnumerator(this) {Index = start - 1}; + + // While there are results to get, yield return them + while (longEnumerator.MoveNext()) + { + yield return longEnumerator.Current; + } + } + /// /// Sets the item at the specified index /// @@ -158,10 +176,10 @@ namespace Microsoft.SqlTools.Utility } else { - int iArray32Index = (int) (Count / this.ExpandListSize); + int iArray32Index = (int) (index / this.ExpandListSize); List arr = expandedList[iArray32Index]; - int i32Index = (int)(Count % this.ExpandListSize); + int i32Index = (int)(index % this.ExpandListSize); arr[i32Index] = value; } } @@ -173,6 +191,8 @@ namespace Microsoft.SqlTools.Utility /// The index to remove from the list public void RemoveAt(long index) { + Validate.IsWithinRange(nameof(index), index, 0, Count - 1); + if (Count <= this.ExpandListSize) { int iArray32MemberIndex = Convert.ToInt32(index); // 0 based @@ -189,14 +209,19 @@ namespace Microsoft.SqlTools.Utility arr.RemoveAt(iArray32MemberIndex); // now shift members of the array back one - int iArray32TotalIndex = (int) (Count / this.ExpandListSize); - for (int i = arrayIndex + 1; i < iArray32TotalIndex; i++) + //int iArray32TotalIndex = (int) (Count / this.ExpandListSize); + for (int i = arrayIndex + 1; i < expandedList.Count; i++) { List arr1 = expandedList[i - 1]; List arr2 = expandedList[i]; - arr1.Add(arr2[this.ExpandListSize - 1]); + arr1.Add(arr2[0]); arr2.RemoveAt(0); + + if (arr2.Count == 0) + { + expandedList.RemoveAt(i); + } } } --Count; @@ -228,19 +253,15 @@ namespace Microsoft.SqlTools.Utility public class LongListEnumerator : IEnumerator { - #region Member Variables - - /// - /// The index into the list of the item that is the current item - /// - private long index; - /// /// The current list that we're iterating over. /// private readonly LongList localList; - #endregion + /// + /// The current index into the list + /// + private long index; /// /// Constructs a new enumerator for a given LongList @@ -249,10 +270,27 @@ namespace Microsoft.SqlTools.Utility public LongListEnumerator(LongList list) { localList = list; - index = 0; + index = -1; Current = default(TEt); } + /// + /// The index into the list of the item that is the current item. Upon setting, + /// will be updated if the index is in range. Otherwise, + /// default() will be used. + /// + public long Index + { + get { return index; } + set + { + index = value; + Current = value >= localList.Count || value < 0 + ? default(TEt) + : localList[index]; + } + } + #region IEnumerator Implementation /// @@ -260,10 +298,10 @@ namespace Microsoft.SqlTools.Utility /// public TEt Current { get; private set; } - object IEnumerator.Current - { - get { return Current; } - } + /// + /// Returns the current item in the enumeration + /// + object IEnumerator.Current => Current; /// /// Moves to the next item in the list we're iterating over @@ -271,14 +309,8 @@ namespace Microsoft.SqlTools.Utility /// Whether or not the move was successful public bool MoveNext() { - if (index < localList.Count) - { - Current = localList[index]; - index++; - return true; - } - Current = default(TEt); - return false; + Index++; + return Index < localList.Count; } /// @@ -286,7 +318,7 @@ namespace Microsoft.SqlTools.Utility /// public void Reset() { - index = 0; + Index = 0; Current = default(TEt); } diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/CellUpdateTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/CellUpdateTests.cs index 4ed3c557..a61b4ed7 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/CellUpdateTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/CellUpdateTests.cs @@ -216,6 +216,33 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData } } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AsDbCellValue(bool isNull) + { + // Setup: Create a cell update + var value = isNull ? "NULL" : "foo"; + var col = GetWrapper("NTEXT"); + CellUpdate cu = new CellUpdate(col, value); + + // If: I convert it to a DbCellvalue + DbCellValue dbc = cu.AsDbCellValue; + + // Then: + // ... It should not be null + Assert.NotNull(dbc); + + // ... The display value should be the same as the value we supplied + Assert.Equal(value, dbc.DisplayValue); + + // ... The null-ness of the value should be the same as what we supplied + Assert.Equal(isNull, dbc.IsNull); + + // ... We don't care *too* much about the raw value, but we'll check it anyhow + Assert.Equal(isNull ? (object)DBNull.Value : value, dbc.RawObject); + } + private static DbColumnWrapper GetWrapper(string dataTypeName, bool allowNull = true) { return new DbColumnWrapper(new CellUpdateTestDbColumn(typeof(T), dataTypeName, allowNull)); diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowCreateTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowCreateTests.cs index 80ac1a66..4c49121b 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowCreateTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowCreateTests.cs @@ -12,6 +12,7 @@ using Microsoft.SqlTools.ServiceLayer.EditData; using Microsoft.SqlTools.ServiceLayer.EditData.Contracts; using Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement; using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.Test.Common; using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility; using Xunit; @@ -181,6 +182,64 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData Assert.Throws(() => rc.GetCommand(mockConn)); } + [Fact] + public void GetEditRowNoAdditions() + { + // Setup: Generate a standard row create + RowCreate rc = GetStandardRowCreate(); + + // 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 should have a bunch of empty cells (equal to number of columns) + Assert.Equal(rc.newCells.Length, er.Cells.Length); + Assert.All(er.Cells, dbc => + { + Assert.Equal(string.Empty, dbc.DisplayValue); + Assert.False(dbc.IsNull); + }); + } + + [Fact] + public void GetEditRowWithAdditions() + { + // Setp: Generate a row create with a cell added to it + RowCreate rc = GetStandardRowCreate(); + rc.SetCell(0, "foo"); + + // If: I request an edit row from the row create + EditRow er = rc.GetEditRow(null); + + // Then: + // ... The row should not be null and contain the same number of cells as columns + Assert.NotNull(er); + Assert.Equal(EditRow.EditRowState.DirtyInsert, er.State); + + // ... The row should not be clean + Assert.True(er.IsDirty); + Assert.Equal(EditRow.EditRowState.DirtyInsert, er.State); + + // ... The row should have a single non-empty cell at the beginning + Assert.Equal("foo", er.Cells[0].DisplayValue); + Assert.False(er.Cells[0].IsNull); + + // ... The rest of the cells should be blank + for (int i = 1; i < er.Cells.Length; i++) + { + DbCellValue dbc = er.Cells[i]; + Assert.Equal(string.Empty, dbc.DisplayValue); + Assert.False(dbc.IsNull); + } + } + [Theory] [InlineData(-1)] // Negative [InlineData(3)] // At edge of acceptable values diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowDeleteTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowDeleteTests.cs index dfa4e647..941e14ed 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowDeleteTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowDeleteTests.cs @@ -5,11 +5,14 @@ using System; using System.Data.Common; +using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.EditData; +using Microsoft.SqlTools.ServiceLayer.EditData.Contracts; using Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement; using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility; using Xunit; @@ -21,15 +24,15 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData public void RowDeleteConstruction() { // Setup: Create the values to store - const long rowId = 100; - ResultSet rs = QueryExecution.Common.GetBasicExecutedBatch().ResultSets[0]; - IEditTableMetadata etm = Common.GetStandardMetadata(rs.Columns); + DbColumn[] columns = Common.GetColumns(true); + ResultSet rs = Common.GetResultSet(columns, true); + IEditTableMetadata etm = Common.GetStandardMetadata(columns, false); // If: I create a RowCreate instance - RowCreate rc = new RowCreate(rowId, rs, etm); + RowDelete rc = new RowDelete(100, rs, etm); // Then: The values I provided should be available - Assert.Equal(rowId, rc.RowId); + Assert.Equal(100, rc.RowId); Assert.Equal(rs, rc.AssociatedResultSet); Assert.Equal(etm, rc.AssociatedObjectMetadata); } @@ -64,14 +67,12 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData public async Task ApplyChanges() { // Setup: Generate the parameters for the row delete object - // We don't care about the values besides the row ID - const long rowId = 0; var columns = Common.GetColumns(false); var rs = Common.GetResultSet(columns, false); var etm = Common.GetStandardMetadata(columns); // If: I ask for the change to be applied - RowDelete rd = new RowDelete(rowId, rs, etm); + RowDelete rd = new RowDelete(0, rs, etm); await rd.ApplyChanges(null); // Reader not used, can be null // Then : The result set should have one less row in it @@ -87,11 +88,10 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData { // Setup: // ... Create a row delete - const long rowId = 0; var columns = Common.GetColumns(includeIdentity); var rs = Common.GetResultSet(columns, includeIdentity); var etm = Common.GetStandardMetadata(columns, !includeIdentity, isMemoryOptimized); - RowDelete rd = new RowDelete(rowId, rs, etm); + RowDelete rd = new RowDelete(0, rs, etm); // ... Mock db connection for building the command var mockConn = new TestSqlConnection(null); @@ -131,26 +131,66 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData public void GetCommandNullConnection() { // Setup: Create a row delete - var columns = Common.GetColumns(false); - var rs = Common.GetResultSet(columns, false); - var etm = Common.GetStandardMetadata(columns); - RowDelete rd = new RowDelete(0, rs, etm); + RowDelete rd = GetStandardRowDelete(); // If: I attempt to create a command with a null connection // Then: It should throw an exception Assert.Throws(() => rd.GetCommand(null)); } + [Fact] + public void GetEditRow() + { + // Setup: Create a row delete + var columns = Common.GetColumns(false); + var rs = Common.GetResultSet(columns, false); + var etm = Common.GetStandardMetadata(columns); + RowDelete rd = new RowDelete(0, rs, etm); + + // If: I attempt to get an edit row + DbCellValue[] cells = rs.GetRow(0).ToArray(); + EditRow er = rd.GetEditRow(cells); + + // Then: + // ... The state should be dirty + Assert.True(er.IsDirty); + Assert.Equal(EditRow.EditRowState.DirtyDelete, er.State); + + // ... The ID should be the same as the one provided + Assert.Equal(0, er.Id); + + // ... The row should match the cells that were given + Assert.Equal(cells.Length, er.Cells.Length); + for (int i = 0; i < cells.Length; i++) + { + DbCellValue originalCell = cells[i]; + DbCellValue outputCell = er.Cells[i]; + + Assert.Equal(originalCell.DisplayValue, outputCell.DisplayValue); + Assert.Equal(originalCell.IsNull, outputCell.IsNull); + // Note: No real need to check the RawObject property + } + } + + [Fact] + public void GetEditNullRow() + { + // Setup: Create a row delete + RowDelete rd = GetStandardRowDelete(); + + // If: I attempt to get an edit row with a null cached row + // Then: I should get an exception + Assert.Throws(() => rd.GetEditRow(null)); + } + [Fact] public void SetCell() { - DbColumn[] columns = Common.GetColumns(true); - ResultSet rs = Common.GetResultSet(columns, true); - IEditTableMetadata etm = Common.GetStandardMetadata(columns, false); + // Setup: Create a row delete + RowDelete rd = GetStandardRowDelete(); // If: I set a cell on a delete row edit // Then: It should throw as invalid operation - RowDelete rd = new RowDelete(0, rs, etm); Assert.Throws(() => rd.SetCell(0, null)); } @@ -158,14 +198,19 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData public void RevertCell() { // Setup: Create a row delete - DbColumn[] cols = Common.GetColumns(false); - ResultSet rs = Common.GetResultSet(cols, false); - IEditTableMetadata etm = Common.GetStandardMetadata(cols); - RowDelete rd = new RowDelete(0, rs, etm); + RowDelete rd = GetStandardRowDelete(); // If: I revert a cell on a delete row edit // Then: It should throw Assert.Throws(() => rd.RevertCell(0)); } + + private RowDelete GetStandardRowDelete() + { + var cols = Common.GetColumns(false); + var rs = Common.GetResultSet(cols, false); + var etm = Common.GetStandardMetadata(cols); + return new RowDelete(0, rs, etm); + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowEditBaseTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowEditBaseTests.cs index 19f98728..1da82c9c 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowEditBaseTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowEditBaseTests.cs @@ -14,6 +14,7 @@ using Microsoft.SqlTools.ServiceLayer.EditData; using Microsoft.SqlTools.ServiceLayer.EditData.Contracts; using Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement; using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.Test.Common; using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility; using Xunit; @@ -267,6 +268,11 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData throw new NotImplementedException(); } + public override EditRow GetEditRow(DbCellValue[] cells) + { + throw new NotImplementedException(); + } + public override string RevertCell(int columnId) { throw new NotImplementedException(); diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowUpdateTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowUpdateTests.cs index 2fe050d7..5a0ed1c2 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowUpdateTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/RowUpdateTests.cs @@ -5,6 +5,7 @@ using System; using System.Data.Common; +using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -12,6 +13,7 @@ using Microsoft.SqlTools.ServiceLayer.EditData; using Microsoft.SqlTools.ServiceLayer.EditData.Contracts; using Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement; using Microsoft.SqlTools.ServiceLayer.QueryExecution; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.Test.Common; using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility; using Xunit; @@ -41,10 +43,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData public void SetCell() { // Setup: Create a row update - var columns = Common.GetColumns(false); - var rs = Common.GetResultSet(columns, false); - var etm = Common.GetStandardMetadata(columns); - RowUpdate ru = new RowUpdate(0, rs, etm); + RowUpdate ru = GetStandardRowUpdate(); // If: I set a cell that can be updated EditUpdateCellResult eucr = ru.SetCell(0, "col1"); @@ -234,15 +233,63 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData [Fact] public void GetCommandNullConnection() { - // Setup: Create a row create - var columns = Common.GetColumns(false); - var rs = Common.GetResultSet(columns, false); - var etm = Common.GetStandardMetadata(columns); - RowUpdate rc = new RowUpdate(0, rs, etm); + // Setup: Create a row update + RowUpdate ru = GetStandardRowUpdate(); // If: I attempt to create a command with a null connection // Then: It should throw an exception - Assert.Throws(() => rc.GetCommand(null)); + Assert.Throws(() => ru.GetCommand(null)); + } + + [Fact] + public void GetEditRow() + { + // Setup: Create a row update with a cell set + var columns = Common.GetColumns(false); + var rs = Common.GetResultSet(columns, false); + var etm = Common.GetStandardMetadata(columns); + RowUpdate ru = new RowUpdate(0, rs, etm); + ru.SetCell(0, "foo"); + + // If: I attempt to get an edit row + DbCellValue[] cells = rs.GetRow(0).ToArray(); + EditRow er = ru.GetEditRow(cells); + + // Then: + // ... The state should be dirty + Assert.True(er.IsDirty); + Assert.Equal(EditRow.EditRowState.DirtyUpdate, er.State); + + // ... The ID should be the same as the one provided + Assert.Equal(0, er.Id); + + // ... The row should match the cells that were given, except for the updated cell + Assert.Equal(cells.Length, er.Cells.Length); + for (int i = 1; i < cells.Length; i++) + { + DbCellValue originalCell = cells[i]; + DbCellValue outputCell = er.Cells[i]; + + Assert.Equal(originalCell.DisplayValue, outputCell.DisplayValue); + Assert.Equal(originalCell.IsNull, outputCell.IsNull); + // Note: No real need to check the RawObject property + } + + // ... The updated cell should match what it was set to + DbCellValue newCell = er.Cells[0]; + Assert.Equal(newCell.DisplayValue, "foo"); + Assert.Equal(newCell.IsNull, false); + } + + [Fact] + public void GetEditNullRow() + { + // Setup: Create a row update + RowUpdate ru = GetStandardRowUpdate(); + + // If: I attempt to get an edit row with a null cached row + // Then: I should get an exception + Assert.Throws(() => ru.GetEditRow(null)); } [Theory] @@ -344,5 +391,13 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData // ... The cell should no longer be set Assert.DoesNotContain(0, ru.cellUpdates.Keys); } + + private RowUpdate GetStandardRowUpdate() + { + var columns = Common.GetColumns(false); + var rs = Common.GetResultSet(columns, false); + var etm = Common.GetStandardMetadata(columns); + return new RowUpdate(0, rs, etm); + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/ServiceIntegrationTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/ServiceIntegrationTests.cs index f9674c25..e83221ba 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/ServiceIntegrationTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/ServiceIntegrationTests.cs @@ -64,6 +64,8 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData efv.Validate(); } + + #endregion #region Dispose Tests @@ -215,6 +217,36 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData edit.Verify(e => e.SetCell(It.IsAny(), It.IsAny()), Times.Once); } + [Fact] + public async Task GetRowsSuccess() + { + // Setup: Create an edit data service with a session + // Setup: Create an edit data service with a session + var eds = new EditDataService(null, null, null); + var session = GetDefaultSession(); + eds.ActiveSessions[Constants.OwnerUri] = session; + + // If: I validly ask for rows + var efv = new EventFlowValidator() + .AddResultValidation(esr => + { + Assert.NotNull(esr); + Assert.NotEmpty(esr.Subset); + Assert.NotEqual(0, esr.RowCount); + }) + .Complete(); + await eds.HandleSubsetRequest(new EditSubsetParams + { + OwnerUri = Constants.OwnerUri, + RowCount = 10, + RowStartIndex = 0 + }, efv.Object); + + // Then: + // ... It should be successful + efv.Validate(); + } + [Theory] [InlineData(null, "table", "table")] // Null owner URI [InlineData(Common.OwnerUri, null, "table")] // Null object name diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/SessionTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/SessionTests.cs index fb2092cf..59ed75f8 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/SessionTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/EditData/SessionTests.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Data.Common; using System.IO; +using System.Linq; using System.Threading.Tasks; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.EditData; @@ -392,6 +393,172 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData #endregion + #region SubSet Tests + + [Fact] + public async Task GetRowsNoEdits() + { + // Setup: Create a session with a proper query and metadata + Query q = QueryExecution.Common.GetBasicExecutedQuery(); + ResultSet rs = q.Batches[0].ResultSets[0]; + IEditTableMetadata etm = Common.GetStandardMetadata(rs.Columns); + EditSession s = new EditSession(rs, etm); + + // If: I ask for 3 rows from session skipping the first + EditRow[] rows = await s.GetRows(1, 3); + + // Then: + // ... I should get back 3 rows + Assert.Equal(3, rows.Length); + + // ... Each row should... + for (int i = 0; i < rows.Length; i++) + { + EditRow er = rows[i]; + + // ... Have properly set IDs + Assert.Equal(i + 1, er.Id); + + // ... Have cells equal to the cells in the result set + DbCellValue[] cachedRow = rs.GetRow(i + 1).ToArray(); + Assert.Equal(cachedRow.Length, er.Cells.Length); + for (int j = 0; j < cachedRow.Length; j++) + { + Assert.Equal(cachedRow[j].DisplayValue, er.Cells[j].DisplayValue); + Assert.Equal(cachedRow[j].IsNull, er.Cells[j].IsNull); + } + + // ... Be clean, since we didn't apply any updates + Assert.Equal(EditRow.EditRowState.Clean, er.State); + Assert.False(er.IsDirty); + } + } + + [Fact] + public async Task GetRowsPendingUpdate() + { + // Setup: + // ... Create a session with a proper query and metadata + Query q = QueryExecution.Common.GetBasicExecutedQuery(); + ResultSet rs = q.Batches[0].ResultSets[0]; + IEditTableMetadata etm = Common.GetStandardMetadata(rs.Columns); + EditSession s = new EditSession(rs, etm); + + // ... Add a cell update to it + s.UpdateCell(1, 0, "foo"); + + // If: I ask for 3 rows from the session, skipping the first, including the updated one + EditRow[] rows = await s.GetRows(1, 3); + + // Then: + // ... I should get back 3 rows + Assert.Equal(3, rows.Length); + + // ... The first row should reflect that there is an update pending + // (More in depth testing is done in the RowUpdate class tests) + var updatedRow = rows[0]; + Assert.Equal(EditRow.EditRowState.DirtyUpdate, updatedRow.State); + Assert.Equal("foo", updatedRow.Cells[0].DisplayValue); + + // ... The other rows should be clean + for (int i = 1; i < rows.Length; i++) + { + Assert.Equal(EditRow.EditRowState.Clean, rows[i].State); + } + } + + [Fact] + public async Task GetRowsPendingDeletion() + { + // Setup: + // ... Create a session with a proper query and metadata + Query q = QueryExecution.Common.GetBasicExecutedQuery(); + ResultSet rs = q.Batches[0].ResultSets[0]; + IEditTableMetadata etm = Common.GetStandardMetadata(rs.Columns); + EditSession s = new EditSession(rs, etm); + + // ... Add a row deletion + s.DeleteRow(1); + + // If: I ask for 3 rows from the session, skipping the first, including the updated one + EditRow[] rows = await s.GetRows(1, 3); + + // Then: + // ... I should get back 3 rows + Assert.Equal(3, rows.Length); + + // ... The first row should reflect that there is an update pending + // (More in depth testing is done in the RowUpdate class tests) + var updatedRow = rows[0]; + Assert.Equal(EditRow.EditRowState.DirtyDelete, updatedRow.State); + Assert.NotEmpty(updatedRow.Cells[0].DisplayValue); + + // ... The other rows should be clean + for (int i = 1; i < rows.Length; i++) + { + Assert.Equal(EditRow.EditRowState.Clean, rows[i].State); + } + } + + [Fact] + public async Task GetRowsPendingInsertion() + { + // Setup: + // ... Create a session with a proper query and metadata + Query q = QueryExecution.Common.GetBasicExecutedQuery(); + ResultSet rs = q.Batches[0].ResultSets[0]; + IEditTableMetadata etm = Common.GetStandardMetadata(rs.Columns); + EditSession s = new EditSession(rs, etm); + + // ... Add a row creation + s.CreateRow(); + + // If: I ask for the rows including the new rows + EditRow[] rows = await s.GetRows(0, 6); + + // Then: + // ... I should get back 6 rows + Assert.Equal(6, rows.Length); + + // ... The last row should reflect that there's a new row + var updatedRow = rows[5]; + Assert.Equal(EditRow.EditRowState.DirtyInsert, updatedRow.State); + + // ... The other rows should be clean + for (int i = 0; i < rows.Length - 1; i++) + { + Assert.Equal(EditRow.EditRowState.Clean, rows[i].State); + } + } + + [Fact] + public async Task GetRowsAllNew() + { + // Setup: + // ... Create a session with a query and metadata + Query q = QueryExecution.Common.GetBasicExecutedQuery(); + ResultSet rs = q.Batches[0].ResultSets[0]; + IEditTableMetadata etm = Common.GetStandardMetadata(rs.Columns); + EditSession s = new EditSession(rs, etm); + + // ... Add a few row creations + s.CreateRow(); + s.CreateRow(); + s.CreateRow(); + + // If: I ask for the rows included the new rows + EditRow[] rows = await s.GetRows(5, 5); + + // Then: + // ... I should get back 3 rows back + Assert.Equal(3, rows.Length); + + // ... All the rows should be new + Assert.All(rows, r => Assert.Equal(EditRow.EditRowState.DirtyInsert, r.State)); + } + + #endregion + #region Script Edits Tests [Theory] diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/QueryExecution/SubsetTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/QueryExecution/SubsetTests.cs index 60929b49..b01792f2 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/QueryExecution/SubsetTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/QueryExecution/SubsetTests.cs @@ -142,8 +142,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution var subsetRequest = new EventFlowValidator() .AddResultValidation(r => { - // Then: Messages should be null and subset should not be null - Assert.Null(r.Message); + // Then: Subset should not be null Assert.NotNull(r.ResultSubset); }).Complete(); await queryService.HandleResultSubsetRequest(subsetParams, subsetRequest.Object); @@ -159,12 +158,8 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution var queryService = Common.GetPrimedExecutionService(null, true, false, workspaceService); var subsetParams = new SubsetParams { OwnerUri = Constants.OwnerUri, RowsCount = 1, ResultSetIndex = 0, RowsStartIndex = 0 }; var subsetRequest = new EventFlowValidator() - .AddResultValidation(r => - { - // Then: Messages should not be null and the subset should be null - Assert.NotNull(r.Message); - Assert.Null(r.ResultSubset); - }).Complete(); + .AddErrorValidation(Assert.NotEmpty) + .Complete(); await queryService.HandleResultSubsetRequest(subsetParams, subsetRequest.Object); subsetRequest.Validate(); } @@ -185,12 +180,8 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution // ... And I then ask for a valid set of results from it var subsetParams = new SubsetParams { OwnerUri = Constants.OwnerUri, RowsCount = 1, ResultSetIndex = 0, RowsStartIndex = 0 }; var subsetRequest = new EventFlowValidator() - .AddResultValidation(r => - { - // Then: There should not be a subset and message should not be null - Assert.NotNull(r.Message); - Assert.Null(r.ResultSubset); - }).Complete(); + .AddErrorValidation(Assert.NotEmpty) + .Complete(); await queryService.HandleResultSubsetRequest(subsetParams, subsetRequest.Object); subsetRequest.Validate(); } @@ -210,12 +201,8 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution // ... And I then ask for a set of results from it var subsetParams = new SubsetParams { OwnerUri = Constants.OwnerUri, RowsCount = 1, ResultSetIndex = 0, RowsStartIndex = 0 }; var subsetRequest = new EventFlowValidator() - .AddResultValidation(r => - { - // Then: There should be an error message and no subset - Assert.NotNull(r.Message); - Assert.Null(r.ResultSubset); - }).Complete(); + .AddErrorValidation(Assert.NotEmpty) + .Complete(); await queryService.HandleResultSubsetRequest(subsetParams, subsetRequest.Object); subsetRequest.Validate(); } diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Utility/LongListTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Utility/LongListTests.cs index 5fe0a89d..e165507f 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Utility/LongListTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Utility/LongListTests.cs @@ -3,7 +3,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.SqlTools.Utility; +using System; +using System.Linq; +using Microsoft.SqlTools.ServiceLayer.Utility; using Xunit; namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Utility @@ -13,27 +15,368 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Utility /// public class LongListTests { - /// - /// Add and remove and item in a LongList - /// [Fact] - public void LongListTest() + public void LongListConstruction() { - var longList = new LongList(); - longList.Add('.'); - Assert.True(longList.Count == 1); - longList.RemoveAt(0); - Assert.True(longList.Count == 0); + // If: I construct a new long list + LongList ll = new LongList(); + + // Then: + // ... There should be no values in the list + Assert.Equal(0, ll.Count); } + #region GetItem / Add Tests + + [Theory] + [InlineData(-1L)] // Negative index + [InlineData(0L)] // Index equal to count of elements + [InlineData(100L)] // Index larger than elements + public void GetItemOutOfRange(long index) + { + // If: I construct a new long list + LongList ll = new LongList(); + + // Then: + // ... There should be no values in the list + Assert.Throws(() => ll[index]); + Assert.Throws(() => ll.GetItem(index)); + } + + [Theory] + [InlineData(0)] // Element at beginning + [InlineData(1)] // Element in middle + [InlineData(2)] // Element at end + public void GetItemNotExpanded(long index) + { + // If: I construct a new long list with a couple items in it + LongList ll = new LongList {0, 1, 2}; + + // Then: I can read back the value from the list + Assert.Equal(3, ll.Count); + Assert.Equal(index, ll[index]); + Assert.Equal(index, ll.GetItem(index)); + } + + [Fact] + public void GetItemExanded() + { + // If: I construct a new long list that is guaranteed to have been expanded + LongList ll = new LongList {ExpandListSize = 2}; + for (int i = 0; i < 10; i++) + { + ll.Add(i); + } + + // Then: + // ... All the added values should be accessible + Assert.Equal(10, ll.Count); + for (int i = 0; i < 10; i++) + { + Assert.Equal(i, ll[i]); + Assert.Equal(i, ll.GetItem(i)); + } + } + + #endregion + + #region SetItem Tests + + [Theory] + [InlineData(-1L)] // Negative index + [InlineData(0L)] // Index equal to count of elements + [InlineData(100L)] // Index larger than elements + public void SetItemOutOfRange(long index) + { + // If: I construct a new long list + LongList ll = new LongList(); + + // Then: + // ... There should be no values in the list + Assert.Throws(() => ll[index] = 8); + Assert.Throws(() => ll.SetItem(index, 8)); + } + + [Fact] + public void SetItemNotExpanded() + { + // If: + // ... I construct a new long list with a few items in it + // ... And I set all values to new values + LongList ll = new LongList {0, 1, 2}; + for (int i = 0; i < ll.Count; i++) + { + ll.SetItem(i, 8); + } + + // Then: All values in the list should be 8 + Assert.All(ll, i => Assert.Equal(8, i)); + } + + [Fact] + public void SetItemIndexerNotExpanded() + { + // If: + // ... I construct a new long list with a few items in it + // ... And I set all values to new values + LongList ll = new LongList {0, 1, 2}; + for (int i = 0; i < ll.Count; i++) + { + ll[i] = 8; + } + + // Then: All values in the list should be 8 + Assert.All(ll, i => Assert.Equal(8, i)); + } + + [Fact] + public void SetItemExpanded() + { + // If: + // ... I construct a new long list that is guaranteed to have been expanded + LongList ll = new LongList {ExpandListSize = 2}; + for (int i = 0; i < 10; i++) + { + ll.Add(i); + } + + // ... And reset all the values to 8 + for (int i = 0; i < 10; i++) + { + ll.SetItem(i, 8); + } + + // Then: All values in the list should be 8 + Assert.All(ll, i => Assert.Equal(8, i)); + } + + [Fact] + public void SetItemIndexerExpanded() + { + // If: + // ... I construct a new long list that is guaranteed to have been expanded + LongList ll = new LongList {ExpandListSize = 2}; + for (int i = 0; i < 10; i++) + { + ll.Add(i); + } + + // ... And reset all the values to 8 + for (int i = 0; i < 10; i++) + { + ll[i] = 8; + } + + // Then: All values in the list should be 8 + Assert.All(ll, i => Assert.Equal(8, i)); + } + + #endregion + + #region RemoveAt Tests + + [Theory] + [InlineData(-1L)] // Negative index + [InlineData(0L)] // Index equal to count of elements + [InlineData(100L)] // Index larger than elements + public void RemoveOutOfRange(long index) + { + // If: I construct a new long list + LongList ll = new LongList(); + + // Then: + // ... There should be no values in the list + Assert.Throws(() => ll.RemoveAt(index)); + } + + [Theory] + [InlineData(0)] // Remove at beginning of list + [InlineData(2)] // Remove from middle of list + [InlineData(4)] // Remove at end of list + public void RemoveAtNotExpanded(long index) + { + // If: + // ... I create a long list with a few elements in it (and one element that will be removed) + LongList ll = new LongList(); + for (int i = 0; i < 5; i++) + { + ll.Add(i == index ? 1 : 8); + } + + // ... And I delete an element + ll.RemoveAt(index); + + // Then: + // ... The count should have subtracted + Assert.Equal(4, ll.Count); + + // ... All values should be 8 since we removed the 1 + Assert.All(ll, i => Assert.Equal(8, i)); + } + + [Fact] + public void RemoveAtExpanded() + { + // If: + // ... I create a long list that is guaranteed to be expanded + // (Created with 2x the values, evaluate the ) + LongList ll = new LongList {ExpandListSize = 2}; + for (int j = 0; j < 2; j++) + { + for (int i = 0; i < 10; i++) + { + ll.Add(i); + } + } + + // ... And I delete all of the first half of values + // (we're doing this backwards to make sure remove works at different points in the list) + for (int i = 9; i >= 0; i--) + { + ll.RemoveAt(i); + } + + // Then: + // ... The second half of the values should still remain + for (int i = 0; i < 10; i++) + { + Assert.Equal(i, ll[i]); + } + + // If: + // ... I then proceed to add elements onto the end again + for (int i = 0; i < 10; i++) + { + ll.Add(i); + } + + // Then: All the elements should be there, in order + for (int j = 0; j < 2; j++) + { + for (int i = 0; i < 10; i++) + { + int index = j * 10 + i; + Assert.Equal(i, ll[index]); + } + } + } + + #endregion + + #region IEnumerable Tests + + [Fact] + public void GetEnumerator() + { + // Setup: Create a long list with a handful of elements + LongList ll = new LongList(); + for (int i = 0; i < 5; i++) + { + ll.Add(i); + } + + // If: I get iterate over the list via GetEnumerator + // Then: All the elements should be returned, in order + int val = 0; + foreach (int element in ll) + { + Assert.Equal(val++, element); + } + } + + [Fact] + public void GetEnumeratorExpanded() + { + // Setup: Create a long list with a handful of elements + LongList ll = new LongList {ExpandListSize = 2}; + for (int i = 0; i < 5; i++) + { + ll.Add(i); + } + + // If: I get iterate over the list via GetEnumerator + // Then: All the elements should be returned, in order + int val = 0; + foreach (int element in ll) + { + Assert.Equal(val++, element); + } + } + + [Theory] + [InlineData(-1)] // Negative + [InlineData(5)] // Equal to count + [InlineData(100)] // Far too large + public void LongSkipOutOfRange(long index) + { + // Setup: Create a long list with a handful of elements + LongList ll = new LongList {ExpandListSize = 2}; + for (int i = 0; i < 5; i++) + { + ll.Add(i); + } + + // If: I attempt to skip ahead by a value that is out of range + // Then: I should get an exception + // NOTE: We must do the .ToList in order to evaluate the LongSkip since it is implemented + // with a yield return + Assert.Throws(() => ll.LongSkip(index).ToArray()); + } + + [Theory] + [InlineData(0)] // Don't actually skip anything + [InlineData(2)] // Skip within the short list + public void LongSkip(long index) + { + // Setup: Create a long list with a handful of elements + LongList ll = new LongList(); + for (int i = 0; i < 5; i++) + { + ll.Add(i); + } + + // If: I skip ahead by a few elements and get all elements in an array + int[] values = ll.LongSkip(index).ToArray(); + + // Then: The elements including the skip start index should be in the output + for (int i = 0; i < values.Length; i++) + { + Assert.Equal(ll[i+index], values[i]); + } + } + + [Theory] + [InlineData(0)] // Don't actually skip anything + [InlineData(1)] // Skip within the short list + [InlineData(3)] // Skip across expanded lists + public void LongSkipExpanded(long index) + { + // Setup: Create a long list with a handful of elements + LongList ll = new LongList {ExpandListSize = 2}; + for (int i = 0; i < 5; i++) + { + ll.Add(i); + } + + // If: I skip ahead by a few elements and get all elements in an array + int[] values = ll.LongSkip(index).ToArray(); + + // Then: The elements including the skip start index should be in the output + for (int i = 0; i < values.Length; i++) + { + Assert.Equal(ll[i+index], values[i]); + } + } + + #endregion + /// /// Add and remove and item in a LongList causing an expansion /// [Fact] public void LongListExpandTest() { - var longList = new LongList(); - longList.ExpandListSize = 3; + var longList = new LongList {ExpandListSize = 3}; for (int i = 0; i < 6; ++i) { longList.Add(i);