public async Task DoesNotUpsertAnObject_IfItDoesNotHaveAnId() { var query = new MobileServiceTableQueryDescription("test"); var action = new PullAction(this.table.Object, MobileServiceTableKind.Table, this.context.Object, null, query, null, null, this.opQueue.Object, this.settings.Object, this.store.Object, MobileServiceRemoteTableOptions.All, null, CancellationToken.None); var itemWithId = new JObject() { { "id", "abc" }, { "text", "has id" } }; var itemWithoutId = new JObject() { { "text", "no id" } }; var result = new JArray(new[]{ itemWithId, itemWithoutId }); this.opQueue.Setup(q => q.LockTableAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult<IDisposable>(null)); this.opQueue.Setup(q => q.CountPending(It.IsAny<string>())).Returns(Task.FromResult(0L)); this.table.SetupSequence(t => t.ReadAsync(It.IsAny<string>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<MobileServiceFeatures>())) .Returns(Task.FromResult(QueryResult.Parse(result, null, false))) .Returns(Task.FromResult(QueryResult.Parse(new JArray(), null, false))); this.store.Setup(s => s.UpsertAsync("test", It.IsAny<IEnumerable<JObject>>(), true)) .Returns(Task.FromResult(0)) .Callback<string, IEnumerable<JObject>, bool>((tableName, items, fromServer) => { Assert.AreEqual(1, items.Count()); Assert.AreEqual(itemWithId, items.First()); }); await action.ExecuteAsync(); store.VerifyAll(); opQueue.VerifyAll(); table.VerifyAll(); store.Verify(s => s.DeleteAsync("test", It.IsAny<IEnumerable<string>>()), Times.Never(), "There shouldn't be any call to delete"); }
/// <summary> /// Pulls all items that match the given query from the associated remote table. /// </summary> /// <param name="tableName">The name of table to pull</param> /// <param name="tableKind">The kind of table</param> /// <param name="queryId">A string that uniquely identifies this query and is used to keep track of its sync state.</param> /// <param name="query">An OData query that determines which items to /// pull from the remote table.</param> /// <param name="options">An instance of <see cref="MobileServiceRemoteTableOptions"/></param> /// <param name="parameters">A dictionary of user-defined parameters and values to include in /// the request URI query string.</param> /// <param name="relatedTables"> /// List of tables that may have related records that need to be push before this table is pulled down. /// When no table is specified, all tables are considered related. /// </param> /// <param name="reader">An instance of <see cref="MobileServiceObjectReader"/></param> /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> token to observe /// </param> /// <param name="pullOptions"> /// PullOptions that determine how to pull data from the remote table /// </param> /// <returns> /// A task that completes when pull operation has finished. /// </returns> public async Task PullAsync(string tableName, MobileServiceTableKind tableKind, string queryId, string query, MobileServiceRemoteTableOptions options, IDictionary<string, string> parameters, IEnumerable<string> relatedTables, MobileServiceObjectReader reader, CancellationToken cancellationToken, PullOptions pullOptions) { await this.EnsureInitializedAsync(); if (parameters != null) { if (parameters.Keys.Any(k => k.Equals(MobileServiceTable.IncludeDeletedParameterName, StringComparison.OrdinalIgnoreCase))) { throw new ArgumentException("The key '{0}' is reserved and cannot be specified as a query parameter.".FormatInvariant(MobileServiceTable.IncludeDeletedParameterName)); } } var table = await this.GetTable(tableName); var queryDescription = MobileServiceTableQueryDescription.Parse(this.client.MobileAppUri, tableName, query); // local schema should be same as remote schema otherwise push can't function if (queryDescription.Selection.Any() || queryDescription.Projections.Any()) { throw new ArgumentException("Pull query with select clause is not supported.", "query"); } bool isIncrementalSync = !String.IsNullOrEmpty(queryId); if (isIncrementalSync) { if (queryDescription.Ordering.Any()) { throw new ArgumentException("Incremental pull query must not have orderby clause.", "query"); } if (queryDescription.Top.HasValue || queryDescription.Skip.HasValue) { throw new ArgumentException("Incremental pull query must not have skip or top specified.", "query"); } } if (!options.HasFlag(MobileServiceRemoteTableOptions.OrderBy) && queryDescription.Ordering.Any()) { throw new ArgumentException("The supported table options does not include orderby.", "query"); } if (!options.HasFlag(MobileServiceRemoteTableOptions.Skip) && queryDescription.Skip.HasValue) { throw new ArgumentException("The supported table options does not include skip.", "query"); } if (!options.HasFlag(MobileServiceRemoteTableOptions.Top) && queryDescription.Top.HasValue) { throw new ArgumentException("The supported table options does not include top.", "query"); } // let us not burden the server to calculate the count when we don't need it for pull queryDescription.IncludeTotalCount = false; using (var store = StoreChangeTrackerFactory.CreateTrackedStore(this.Store, StoreOperationSource.ServerPull, this.storeTrackingOptions, this.client.EventManager, this.settings)) { var action = new PullAction(table, tableKind, this, queryId, queryDescription, parameters, relatedTables, this.opQueue, this.settings, store, options, pullOptions, reader, cancellationToken); await this.ExecuteSyncAction(action); } }
/// <summary> /// Pulls all items that match the given query from the associated remote table. /// </summary> /// <param name="tableName">The name of table to pull</param> /// <param name="tableKind">The kind of table</param> /// <param name="queryId">A string that uniquely identifies this query and is used to keep track of its sync state.</param> /// <param name="query">An OData query that determines which items to /// pull from the remote table.</param> /// <param name="options">An instance of <see cref="MobileServiceRemoteTableOptions"/></param> /// <param name="parameters">A dictionary of user-defined parameters and values to include in /// the request URI query string.</param> /// <param name="relatedTables"> /// List of tables that may have related records that need to be push before this table is pulled down. /// When no table is specified, all tables are considered related. /// </param> /// <param name="reader">An instance of <see cref="MobileServiceObjectReader"/></param> /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> token to observe /// </param> /// <param name="pullOptions"> /// PullOptions that determine how to pull data from the remote table /// </param> /// <returns> /// A task that completes when pull operation has finished. /// </returns> public async Task PullAsync(string tableName, MobileServiceTableKind tableKind, string queryId, string query, MobileServiceRemoteTableOptions options, IDictionary <string, string> parameters, IEnumerable <string> relatedTables, MobileServiceObjectReader reader, CancellationToken cancellationToken, PullOptions pullOptions) { await this.EnsureInitializedAsync(); if (parameters != null) { if (parameters.Keys.Any(k => k.Equals(MobileServiceTable.IncludeDeletedParameterName, StringComparison.OrdinalIgnoreCase))) { throw new ArgumentException("The key '{0}' is reserved and cannot be specified as a query parameter.".FormatInvariant(MobileServiceTable.IncludeDeletedParameterName)); } } var table = await this.GetTable(tableName); var queryDescription = MobileServiceTableQueryDescription.Parse(this.client.MobileAppUri, tableName, query); // local schema should be same as remote schema otherwise push can't function if (queryDescription.Selection.Any() || queryDescription.Projections.Any()) { throw new ArgumentException("Pull query with select clause is not supported.", "query"); } bool isIncrementalSync = !String.IsNullOrEmpty(queryId); if (isIncrementalSync) { if (queryDescription.Ordering.Any()) { throw new ArgumentException("Incremental pull query must not have orderby clause.", "query"); } if (queryDescription.Top.HasValue || queryDescription.Skip.HasValue) { throw new ArgumentException("Incremental pull query must not have skip or top specified.", "query"); } } if (!options.HasFlag(MobileServiceRemoteTableOptions.OrderBy) && queryDescription.Ordering.Any()) { throw new ArgumentException("The supported table options does not include orderby.", "query"); } if (!options.HasFlag(MobileServiceRemoteTableOptions.Skip) && queryDescription.Skip.HasValue) { throw new ArgumentException("The supported table options does not include skip.", "query"); } if (!options.HasFlag(MobileServiceRemoteTableOptions.Top) && queryDescription.Top.HasValue) { throw new ArgumentException("The supported table options does not include top.", "query"); } // let us not burden the server to calculate the count when we don't need it for pull queryDescription.IncludeTotalCount = false; using (var store = StoreChangeTrackerFactory.CreateTrackedStore(this.Store, StoreOperationSource.ServerPull, this.storeTrackingOptions, this.client.EventManager, this.settings)) { var action = new PullAction(table, tableKind, this, queryId, queryDescription, parameters, relatedTables, this.opQueue, this.settings, store, options, pullOptions, reader, cancellationToken); await this.ExecuteSyncAction(action); } }
private async Task TestIncrementalSync(MobileServiceTableQueryDescription query, JArray result, DateTime maxUpdatedAt, bool savesMax, string firstQuery, string secondQuery) { var action = new PullAction(this.table.Object, MobileServiceTableKind.Table, this.context.Object, "latestItems", query, null, null, this.opQueue.Object, this.settings.Object, this.store.Object, MobileServiceRemoteTableOptions.All, null, CancellationToken.None); this.opQueue.Setup(q => q.LockTableAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult<IDisposable>(null)); this.opQueue.Setup(q => q.CountPending(It.IsAny<string>())).Returns(Task.FromResult(0L)); this.table.Setup(t => t.ReadAsync(firstQuery, It.IsAny<IDictionary<string, string>>(), It.IsAny<MobileServiceFeatures>())) .Returns(Task.FromResult(QueryResult.Parse(result, null, false))); if (result.Any()) { this.table.Setup(t => t.ReadAsync(secondQuery, It.IsAny<IDictionary<string, string>>(), It.IsAny<MobileServiceFeatures>())) .Returns(Task.FromResult(QueryResult.Parse(new JArray(), null, false))); } if (result.Any()) { this.store.Setup(s => s.UpsertAsync("test", It.IsAny<IEnumerable<JObject>>(), true)).Returns(Task.FromResult(0)); } this.settings.Setup(s => s.GetDeltaTokenAsync("test", "latestItems")).Returns(Task.FromResult(new DateTimeOffset(2013, 1, 1, 0, 0, 0, TimeSpan.Zero))); if (savesMax) { this.settings.Setup(s => s.SetDeltaTokenAsync("test", "latestItems", maxUpdatedAt)).Returns(Task.FromResult(0)); } await action.ExecuteAsync(); this.store.VerifyAll(); this.opQueue.VerifyAll(); this.table.VerifyAll(); this.settings.VerifyAll(); store.Verify(s => s.DeleteAsync("test", It.IsAny<IEnumerable<string>>()), Times.Never(), "There shouldn't be any call to delete"); }
/// <summary> /// Pulls all items that match the given query from the associated remote table. /// </summary> /// <param name="tableName">The name of table to pull</param> /// <param name="tableKind">The kind of table</param> /// <param name="queryId">A string that uniquely identifies this query and is used to keep track of its sync state.</param> /// <param name="query">An OData query that determines which items to /// pull from the remote table.</param> /// <param name="options">An instance of <see cref="MobileServiceRemoteTableOptions"/></param> /// <param name="parameters">A dictionary of user-defined parameters and values to include in /// the request URI query string.</param> /// <param name="relatedTables"> /// List of tables that may have related records that need to be push before this table is pulled down. /// When no table is specified, all tables are considered related. /// </param> /// <param name="reader">An instance of <see cref="MobileServiceObjectReader"/></param> /// <param name="cancellationToken">The <see cref="System.Threading.CancellationToken"/> token to observe /// </param> /// <returns> /// A task that completes when pull operation has finished. /// </returns> public async Task PullAsync(string tableName, MobileServiceTableKind tableKind, string queryId, string query, MobileServiceRemoteTableOptions options, IDictionary <string, string> parameters, IEnumerable <string> relatedTables, MobileServiceObjectReader reader, CancellationToken cancellationToken) { await this.EnsureInitializedAsync(); if (parameters != null) { if (parameters.Keys.Any(k => k.Equals(MobileServiceTable.IncludeDeletedParameterName, StringComparison.OrdinalIgnoreCase))) { throw new ArgumentException(Resources.Pull_Cannot_Use_Reserved_Key.FormatInvariant(MobileServiceTable.IncludeDeletedParameterName)); } if (parameters.Keys.Any(k => k.Equals(MobileServiceTable.SystemPropertiesQueryParameterName, StringComparison.OrdinalIgnoreCase))) { throw new ArgumentException(Resources.Pull_Cannot_Use_Reserved_Key.FormatInvariant(MobileServiceTable.SystemPropertiesQueryParameterName)); } } var table = await this.GetTable(tableName); var queryDescription = MobileServiceTableQueryDescription.Parse(this.client.ApplicationUri, tableName, query); // local schema should be same as remote schema otherwise push can't function if (queryDescription.Selection.Any() || queryDescription.Projections.Any()) { throw new ArgumentException(Resources.MobileServiceSyncTable_PullWithSelectNotSupported, "query"); } bool isIncrementalSync = !String.IsNullOrEmpty(queryId); if (isIncrementalSync) { if (queryDescription.Ordering.Any()) { throw new ArgumentException(Resources.MobileServiceSyncTable_IncrementalPullWithOrderNotAllowed, "query"); } if (queryDescription.Top.HasValue || queryDescription.Skip.HasValue) { throw new ArgumentException(Resources.MobileServiceSyncTable_IncrementalPullWithSkipTopNotSupported, "query"); } } if (!options.HasFlag(MobileServiceRemoteTableOptions.OrderBy) && queryDescription.Ordering.Any()) { throw new ArgumentException(Resources.MobileServiceSyncTable_OrderByNotAllowed, "query"); } if (!options.HasFlag(MobileServiceRemoteTableOptions.Skip) && queryDescription.Skip.HasValue) { throw new ArgumentException(Resources.MobileServiceSyncTable_SkipNotAllowed, "query"); } if (!options.HasFlag(MobileServiceRemoteTableOptions.Top) && queryDescription.Top.HasValue) { throw new ArgumentException(Resources.MobileServiceSyncTable_TopNotAllowed, "query"); } // let us not burden the server to calculate the count when we don't need it for pull queryDescription.IncludeTotalCount = false; var action = new PullAction(table, tableKind, this, queryId, queryDescription, parameters, relatedTables, this.opQueue, this.settings, this.Store, options, reader, cancellationToken); await this.ExecuteSyncAction(action); }
public async Task DoesNotUpsertAnObject_IfRecordIsPresentInOperationQueue() { var query = new MobileServiceTableQueryDescription("test"); var action = new PullAction(this.table.Object, MobileServiceTableKind.Table, this.context.Object, null, query, null, null, this.opQueue.Object, this.settings.Object, this.store.Object, MobileServiceRemoteTableOptions.All, pullOptions: null, reader: null, cancellationToken: CancellationToken.None); //// item with insert operation from server var insertItemWithPendingOperation = new JObject() { { "id", "abc" }, { "text", "has pending operation" } }; //// item with update operation from server var updateItemWithPendingOperation = new JObject() { { "id", "abc2" }, { "text", "has pending operation" } }; //// item with delete operation from server var deleteItemWithPendingOperation = new JObject() { { "id", "abc3" }, { "text", "has pending operation" } }; //// item with no pending operation from server var itemWithNoPendingOperation = new JObject() { { "id", "abc4" }, { "text", "has no pending operation" } }; var result = new JArray(new[]{ insertItemWithPendingOperation, updateItemWithPendingOperation, deleteItemWithPendingOperation, itemWithNoPendingOperation }); this.opQueue.Setup(q => q.LockTableAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult<IDisposable>(null)); this.opQueue.Setup(q => q.CountPending(It.IsAny<string>())).Returns(Task.FromResult(0L)); this.opQueue.Setup(q => q.GetOperationByItemIdAsync(It.IsAny<string>(), It.IsAny<string>())).Returns((string tableName, string id) => { if (id.Equals("abc")) return Task.FromResult<MobileServiceTableOperation>(new InsertOperation(tableName, MobileServiceTableKind.Table, id)); else if (id.Equals("abc2")) return Task.FromResult<MobileServiceTableOperation>(new UpdateOperation(tableName, MobileServiceTableKind.Table, id)); else if (id.Equals("abc3")) return Task.FromResult<MobileServiceTableOperation>(new DeleteOperation(tableName, MobileServiceTableKind.Table, id)); else return Task.FromResult<MobileServiceTableOperation>(null); }); //// below two reads correspond to first and second page from the server this.table.SetupSequence(t => t.ReadAsync(It.IsAny<string>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<MobileServiceFeatures>())) .Returns(Task.FromResult(QueryResult.Parse(result, null, false))) .Returns(Task.FromResult(QueryResult.Parse(new JArray(), null, false))); this.store.Setup(s => s.UpsertAsync("test", It.IsAny<IEnumerable<JObject>>(), true)) .Returns(Task.FromResult(0)) .Callback<string, IEnumerable<JObject>, bool>((tableName, items, fromServer) => { Assert.AreEqual(1, items.Count()); Assert.AreEqual(itemWithNoPendingOperation, items.First()); }); await action.ExecuteAsync(); store.VerifyAll(); opQueue.VerifyAll(); table.VerifyAll(); store.Verify(s => s.DeleteAsync("test", It.IsAny<IEnumerable<string>>()), Times.Never(), "There shouldn't be any call to delete"); }