/// <summary> /// Create a new BPI, and serialize the changeset if not in memory /// </summary> internal static BatchPartInfo CreateBatchPartInfo(int batchIndex, SyncSet set, string fileName, string directoryFullPath, bool isLastBatch) { BatchPartInfo bpi = null; // Create a batch part // The batch part creation process will serialize the changesSet to the disk // Serialize the file ! Serialize(set.GetContainerSet(), fileName, directoryFullPath); bpi = new BatchPartInfo { FileName = fileName }; bpi.Index = batchIndex; bpi.IsLastBatch = isLastBatch; // Even if the set is empty (serialized on disk), we should retain the tables names if (set != null) { bpi.Tables = set.Tables.Select(t => new BatchPartTableInfo(t.TableName, t.SchemaName)).ToArray(); } return(bpi); }
private void redo(List <Sync <T> > syncs) { List <Change <T> > changes = new List <Change <T> >(); { for (int syncCount = 0; syncCount < syncs.Count; syncCount++) { Sync <T> sync = syncs[syncCount]; switch (sync.getType()) { case Sync <T> .Type.Set: { SyncSet <T> syncSet = (SyncSet <T>)sync; // Make change ChangeSet <T> changeSet = new ChangeSet <T>(); { changeSet.index = syncSet.index; changeSet.values.AddRange(syncSet.news); } changes.Add(changeSet); } break; case Sync <T> .Type.Add: { SyncAdd <T> syncAdd = (SyncAdd <T>)sync; // Make change ChangeAdd <T> changeAdd = new ChangeAdd <T>(); { changeAdd.index = syncAdd.index; changeAdd.values.AddRange(syncAdd.values); } changes.Add(changeAdd); } break; case Sync <T> .Type.Remove: { SyncRemove <T> syncRemove = (SyncRemove <T>)sync; // Make change ChangeRemove <T> changeRemove = new ChangeRemove <T>(); { changeRemove.index = syncRemove.index; changeRemove.number = syncRemove.values.Count; } changes.Add(changeRemove); } break; default: Logger.LogError("unknown type: " + sync.getType() + "; " + this); break; } } } if (changes.Count > 0) { this.processChange(changes); } }
public async Task BaseOrchestrator_Provision_ShouldCreate_TrackingTable() { var dbName = HelperDatabase.GetRandomName("tcp_lo_"); await HelperDatabase.CreateDatabaseAsync(ProviderType.Sql, dbName, true); var cs = HelperDatabase.GetConnectionString(ProviderType.Sql, dbName); var sqlProvider = new SqlSyncProvider(cs); var scopeName = "scope"; var options = new SyncOptions(); var setup = new SyncSetup { TrackingTablesSuffix = "sync", TrackingTablesPrefix = "trck" }; var schema = new SyncSet(); var table = new SyncTable("Product", "SalesLT"); var colID = new SyncColumn("ID", typeof(Guid)); var colName = new SyncColumn("Name", typeof(string)); table.Columns.Add(colID); table.Columns.Add(colName); table.Columns.Add("Number", typeof(int)); table.PrimaryKeys.Add("ID"); //schema.TrackingTablesSuffix = "sync"; //schema.TrackingTablesPrefix = "trck"; schema.Tables.Add(table); // trackign table name is composed with prefix and suffix from setup var trackingTableName = $"{setup.TrackingTablesPrefix}{table.TableName}{setup.TrackingTablesSuffix}"; var localOrchestrator = new LocalOrchestrator(sqlProvider, options, setup, scopeName); var provision = SyncProvision.TrackingTable; await localOrchestrator.ProvisionAsync(schema, provision); using (var c = new SqlConnection(cs)) { await c.OpenAsync().ConfigureAwait(false); var tbl = await SqlManagementUtils.GetTableAsync(c, null, trackingTableName, "SalesLT"); var tblName = tbl.Rows[0]["TableName"].ToString(); var schName = tbl.Rows[0]["SchemaName"].ToString(); Assert.Equal(trackingTableName, tblName); Assert.Equal(table.SchemaName, schName); c.Close(); } HelperDatabase.DropDatabase(ProviderType.Sql, dbName); }
public static void getAddAndRemoveValues <T>(List <Sync <T> > syncs, List <T> addValues, List <T> removeValues) { for (int syncCount = 0; syncCount < syncs.Count; syncCount++) { Sync <T> sync = syncs[syncCount]; switch (sync.getType()) { case Sync <T> .Type.Set: { SyncSet <T> syncSet = (SyncSet <T>)sync; if (syncSet.olds.Count == syncSet.news.Count) { for (int i = 0; i < syncSet.olds.Count; i++) { // od value removeValues.Add(syncSet.olds[i]); // add new value addValues.Add(syncSet.news[i]); } } else { Logger.LogError("count error: " + syncSet.olds.Count + "; " + syncSet.news.Count); } } break; case Sync <T> .Type.Add: { SyncAdd <T> syncAdd = (SyncAdd <T>)sync; for (int i = 0; i < syncAdd.values.Count; i++) { T value = syncAdd.values[i]; addValues.Add(value); } } break; case Sync <T> .Type.Remove: { SyncRemove <T> syncRemove = (SyncRemove <T>)sync; for (int i = 0; i < syncRemove.values.Count; i++) { T value = syncRemove.values[i]; removeValues.Add(value); } } break; default: Logger.LogError("unknown type: " + sync.getType()); break; } } }
public static void replaceCallBack <T>(ValueChangeCallBack callBack, List <Sync <T> > syncs) { for (int syncCount = 0; syncCount < syncs.Count; syncCount++) { Sync <T> sync = syncs[syncCount]; switch (sync.getType()) { case Sync <T> .Type.Set: { SyncSet <T> syncSet = (SyncSet <T>)sync; if (syncSet.olds.Count == syncSet.news.Count) { for (int i = 0; i < syncSet.olds.Count; i++) { // od value RemoveCallBack(callBack, syncSet.olds[i]); // add new value AddCallBack(callBack, syncSet.news[i]); } } else { Logger.LogError("count error: " + syncSet.olds.Count + "; " + syncSet.news.Count); } } break; case Sync <T> .Type.Add: { SyncAdd <T> syncAdd = (SyncAdd <T>)sync; for (int i = 0; i < syncAdd.values.Count; i++) { T value = syncAdd.values[i]; AddCallBack(callBack, value); } } break; case Sync <T> .Type.Remove: { SyncRemove <T> syncRemove = (SyncRemove <T>)sync; for (int i = 0; i < syncRemove.values.Count; i++) { T value = syncRemove.values[i]; RemoveCallBack(callBack, value); } } break; default: Logger.LogError("unknown type: " + sync.getType()); break; } } }
/// <summary> /// Create a new BatchInfo, containing all BatchPartInfo /// </summary> public BatchInfo(SyncSet inSchema, string rootDirectory = null, string directoryName = null) { // We need to create a change table set, containing table with columns not readonly foreach (var table in inSchema.Tables) { DbSyncAdapter.CreateChangesTable(inSchema.Tables[table.TableName, table.SchemaName], this.SanitizedSchema); } this.DirectoryRoot = rootDirectory; this.BatchPartsInfo = new List <BatchPartInfo>(); this.DirectoryName = string.IsNullOrEmpty(directoryName) ? string.Concat(DateTime.UtcNow.ToString("yyyy_MM_dd_ss"), Path.GetRandomFileName().Replace(".", "")) : directoryName; }
public async Task BaseOrchestrator_Provision_SchemaCreated_If_SchemaHasColumnsDefinition() { var dbName = HelperDatabase.GetRandomName("tcp_lo_"); await HelperDatabase.CreateDatabaseAsync(ProviderType.Sql, dbName, true); var cs = HelperDatabase.GetConnectionString(ProviderType.Sql, dbName); var sqlProvider = new SqlSyncProvider(cs); var scopeName = "scope"; var options = new SyncOptions(); var setup = new SyncSetup(); var schema = new SyncSet(); var table = new SyncTable("Product", "SalesLT"); var colID = new SyncColumn("ID", typeof(Guid)); var colName = new SyncColumn("Name", typeof(string)); table.Columns.Add(colID); table.Columns.Add(colName); table.Columns.Add("Number", typeof(int)); table.PrimaryKeys.Add("ID"); schema.Tables.Add(table); var localOrchestrator = new LocalOrchestrator(sqlProvider, options, setup, scopeName); var provision = SyncProvision.Table | SyncProvision.TrackingTable | SyncProvision.StoredProcedures | SyncProvision.Triggers; await localOrchestrator.ProvisionAsync(schema, provision); using (var c = new SqlConnection(cs)) { await c.OpenAsync().ConfigureAwait(false); var tbl = await SqlManagementUtils.GetTableAsync(c, null, "Product", "SalesLT"); var tblName = tbl.Rows[0]["TableName"].ToString(); var schName = tbl.Rows[0]["SchemaName"].ToString(); Assert.Equal(table.TableName, tblName); Assert.Equal(table.SchemaName, schName); var cols = await SqlManagementUtils.GetColumnsForTableAsync(c, null, "Product", "SalesLT"); Assert.Equal(3, cols.Rows.Count); c.Close(); } HelperDatabase.DropDatabase(ProviderType.Sql, dbName); }
/// <summary> /// After deserializing all rows, call the converter for each row /// </summary> public void AfterDeserializedRows(SyncSet data, IConverter converter) { foreach (var table in data.Tables) { if (table.Rows.Count > 0) { foreach (var row in table.Rows) { converter.AfterDeserialized(row); } } } }
/// <summary> /// Create a new BatchInfo, containing all BatchPartInfo /// </summary> public BatchInfo(bool isInMemory, SyncSet inSchema, string rootDirectory = null, string directoryName = null) { this.InMemory = isInMemory; this.schema = inSchema.Clone(); // If not in memory, generate a directory name and initialize batch parts list if (!this.InMemory) { this.DirectoryRoot = rootDirectory; this.BatchPartsInfo = new List <BatchPartInfo>(); this.DirectoryName = string.IsNullOrEmpty(directoryName) ? string.Concat(DateTime.UtcNow.ToString("yyyy_MM_dd_ss"), Path.GetRandomFileName().Replace(".", "")) : directoryName; } }
/// <summary> /// Before serializing all rows, call the converter for each row /// </summary> public void BeforeSerializeRows(SyncSet data, IConverter converter) { foreach (var table in data.Tables) { if (table.Rows.Count > 0) { foreach (var row in table.Rows) { converter.BeforeSerialize(row); } } } }
/// <summary> /// Add changes to batch info. /// </summary> public void AddChanges(SyncSet changes, int batchIndex = 0, bool isLastBatch = true) { if (this.InMemory) { this.InMemoryData = changes; } else { var bpId = this.GenerateNewFileName(batchIndex.ToString()); //var fileName = Path.Combine(this.GetDirectoryFullPath(), bpId); var bpi = BatchPartInfo.CreateBatchPartInfo(batchIndex, changes, bpId, GetDirectoryFullPath(), isLastBatch); // add the batchpartinfo tp the current batchinfo this.BatchPartsInfo.Add(bpi); } }
/// <summary> /// Add changes to batch info. /// </summary> public async Task AddChangesAsync(SyncSet changes, int batchIndex = 0, bool isLastBatch = true, BaseOrchestrator orchestrator = null) { if (this.InMemory) { this.InMemoryData = changes; } else { var bpId = this.GenerateNewFileName(batchIndex.ToString()); //var fileName = Path.Combine(this.GetDirectoryFullPath(), bpId); var bpi = await BatchPartInfo.CreateBatchPartInfoAsync(batchIndex, changes, bpId, GetDirectoryFullPath(), isLastBatch, orchestrator).ConfigureAwait(false); // add the batchpartinfo tp the current batchinfo this.BatchPartsInfo.Add(bpi); } }
/// <summary> /// Add changes to batch info. /// </summary> public async Task AddChangesAsync(SyncSet changes, int batchIndex = 0, bool isLastBatch = true, ISerializerFactory serializerFactory = default, BaseOrchestrator orchestrator = null) { if (this.InMemory) { this.SerializerFactoryKey = null; this.InMemoryData = changes; } else { this.SerializerFactoryKey = serializerFactory.Key; var bpId = GenerateNewFileName(batchIndex.ToString()); var bpi = await BatchPartInfo.CreateBatchPartInfoAsync(batchIndex, changes, bpId, GetDirectoryFullPath(), isLastBatch, serializerFactory, orchestrator).ConfigureAwait(false); // add the batchpartinfo tp the current batchinfo this.BatchPartsInfo.Add(bpi); } }
public async Task RemoteOrchestrator_Provision_SchemaFail_If_SchemaHasColumnsDefinitionButNoPrimaryKey() { var dbName = HelperDatabase.GetRandomName("tcp_ro_"); await HelperDatabase.CreateDatabaseAsync(ProviderType.Sql, dbName, true); var cs = HelperDatabase.GetConnectionString(ProviderType.Sql, dbName); var sqlProvider = new SqlSyncProvider(cs); var ctx = new AdventureWorksContext((dbName, ProviderType.Sql, sqlProvider), true, false); await ctx.Database.EnsureCreatedAsync(); var scopeName = "scope"; var options = new SyncOptions(); var setup = new SyncSetup(new string[] { "SalesLT.Product" }); var schema = new SyncSet(); var table = new SyncTable("Product", "SalesLT"); var colID = new SyncColumn("ID", typeof(Guid)); var colName = new SyncColumn("Name", typeof(string)); table.Columns.Add(colID); table.Columns.Add(colName); table.Columns.Add("Number", typeof(int)); schema.Tables.Add(table); var remoteOrchestrator = new RemoteOrchestrator(sqlProvider, options); var scopeInfo = await remoteOrchestrator.GetServerScopeInfoAsync(scopeName, setup); // Overriding scope info to introduce a bad table with no primary key scopeInfo.Schema = schema; scopeInfo.Setup = setup; var provision = SyncProvision.Table | SyncProvision.TrackingTable | SyncProvision.StoredProcedures | SyncProvision.Triggers; var se = await Assert.ThrowsAsync <SyncException>( async() => await remoteOrchestrator.ProvisionAsync(scopeInfo, provision)); Assert.Equal(SyncStage.Provisioning, se.SyncStage); Assert.Equal(SyncSide.ServerSide, se.Side); Assert.Equal("MissingPrimaryKeyException", se.TypeName); HelperDatabase.DropDatabase(ProviderType.Sql, dbName); }
// TODO : Set the serializer to the one choosed by user /// <summary> /// Loads the batch file and import the rows in a SyncSet instance /// </summary> public void LoadBatch(SyncSet schema, string directoryFullPath) { if (string.IsNullOrEmpty(this.FileName)) { return; } // Clone the schema to get a unique instance var set = schema.Clone(); // Get a Batch part, and deserialise the file into a the BatchPartInfo Set property var data = Deserialize(this.FileName, directoryFullPath); // Import data in a typed Set set.ImportContainerSet(data, true); this.Data = set; }
public async Task Table_Exists() { var dbName = HelperDatabase.GetRandomName("tcp_lo_"); await HelperDatabase.CreateDatabaseAsync(ProviderType.Sql, dbName, true); var cs = HelperDatabase.GetConnectionString(ProviderType.Sql, dbName); var sqlProvider = new SqlSyncProvider(cs); var options = new SyncOptions(); var setup = new SyncSetup(new string[] { "SalesLT.Product", "SalesLT.ProductCategory" }); var table = new SyncTable("Product", "SalesLT"); var colID = new SyncColumn("ID", typeof(Guid)); var colName = new SyncColumn("Name", typeof(string)); table.Columns.Add(colID); table.Columns.Add(colName); table.Columns.Add("Number", typeof(int)); table.PrimaryKeys.Add("ID"); var schema = new SyncSet(); schema.Tables.Add(table); var localOrchestrator = new LocalOrchestrator(sqlProvider, options); var scopeInfo = await localOrchestrator.GetClientScopeInfoAsync(); scopeInfo.Setup = setup; scopeInfo.Schema = schema; await localOrchestrator.SaveClientScopeInfoAsync(scopeInfo); await localOrchestrator.CreateTableAsync(scopeInfo, "Product", "SalesLT"); var exists = await localOrchestrator.ExistTableAsync(scopeInfo, table.TableName, table.SchemaName).ConfigureAwait(false); Assert.True(exists); exists = await localOrchestrator.ExistTableAsync(scopeInfo, "ProductCategory", "SalesLT").ConfigureAwait(false); Assert.False(exists); HelperDatabase.DropDatabase(ProviderType.Sql, dbName); }
public async Task BaseOrchestrator_Provision_SchemaFail_If_SchemaHasColumnsDefinitionButNoPrimaryKey() { var dbName = HelperDatabase.GetRandomName("tcp_lo_"); await HelperDatabase.CreateDatabaseAsync(ProviderType.Sql, dbName, true); var cs = HelperDatabase.GetConnectionString(ProviderType.Sql, dbName); var sqlProvider = new SqlSyncProvider(cs); var scopeName = "scope"; var options = new SyncOptions(); var setup = new SyncSetup(); var schema = new SyncSet(); var table = new SyncTable("Product", "SalesLT"); var colID = new SyncColumn("ID", typeof(Guid)); var colName = new SyncColumn("Name", typeof(string)); table.Columns.Add(colID); table.Columns.Add(colName); table.Columns.Add("Number", typeof(int)); schema.Tables.Add(table); var localOrchestrator = new LocalOrchestrator(sqlProvider, options, setup, scopeName); var provision = SyncProvision.Table | SyncProvision.TrackingTable | SyncProvision.StoredProcedures | SyncProvision.Triggers; var se = await Assert.ThrowsAsync <SyncException>(async() => await localOrchestrator.ProvisionAsync(schema, provision)); Assert.Equal(SyncStage.Provisioning, se.SyncStage); Assert.Equal(SyncSide.ClientSide, se.Side); Assert.Equal("MissingPrimaryKeyException", se.TypeName); HelperDatabase.DropDatabase(ProviderType.Sql, dbName); }
/// <summary> /// Loads the batch file and import the rows in a SyncSet instance /// </summary> public async Task LoadBatchAsync(SyncSet sanitizedSchema, string directoryFullPath, BaseOrchestrator orchestrator = null) { if (this.Data != null) { return; } if (string.IsNullOrEmpty(this.FileName)) { return; } // Clone the schema to get a unique instance var set = sanitizedSchema.Clone(); // Get a Batch part, and deserialise the file into a the BatchPartInfo Set property var data = await DeserializeAsync(this.FileName, directoryFullPath, orchestrator); // Import data in a typed Set set.ImportContainerSet(data, true); this.Data = set; }
private void redo(List <Sync <T> > syncs) { for (int syncCount = 0; syncCount < syncs.Count; syncCount++) { Sync <T> sync = syncs[syncCount]; switch (sync.getType()) { case Sync <T> .Type.Set: { SyncSet <T> syncSet = (SyncSet <T>)sync; if (syncSet.olds.Count == syncSet.news.Count && syncSet.olds.Count == 1) { this.v = syncSet.news[0]; } else { Logger.LogError("count error: " + this); } } break; case Sync <T> .Type.Add: { } break; case Sync <T> .Type.Remove: { } break; default: Logger.LogError("unknown type: " + sync.getType() + "; " + this); break; } } }
/// <summary> /// Get changes from /// </summary> internal async Task <HttpMessageSendChangesResponse> ApplyThenGetChangesAsync( HttpMessageSendChangesRequest httpMessage, SessionCache sessionCache, int clientBatchSize, CancellationToken cancellationToken) { // Get if we need to serialize data or making everything in memory var clientWorkInMemory = clientBatchSize == 0; // Check schema. // If client has stored the schema, the EnsureScope will not be called on server. if (this.Schema == null || !this.Schema.HasTables || !this.Schema.HasColumns) { var(_, newSchema) = await this.EnsureSchemaAsync( httpMessage.SyncContext, this.Setup, cancellationToken).ConfigureAwait(false); newSchema.EnsureSchema(); this.Schema = newSchema; } // ------------------------------------------------------------ // FIRST STEP : receive client changes // ------------------------------------------------------------ // We are receiving changes from client // BatchInfo containing all BatchPartInfo objects // Retrieve batchinfo instance if exists // Get batch info from session cache if exists, otherwise create it if (sessionCache.ClientBatchInfo == null) { sessionCache.ClientBatchInfo = new BatchInfo(clientWorkInMemory, Schema, this.Options.BatchDirectory); } // create the in memory changes set var changesSet = new SyncSet(Schema.ScopeName); foreach (var table in httpMessage.Changes.Tables) { DbSyncAdapter.CreateChangesTable(Schema.Tables[table.TableName, table.SchemaName], changesSet); } changesSet.ImportContainerSet(httpMessage.Changes, false); // If client has made a conversion on each line, apply the reverse side of it if (this.ClientConverter != null && changesSet.HasRows) { AfterDeserializedRows(changesSet, this.ClientConverter); } // add changes to the batch info sessionCache.ClientBatchInfo.AddChanges(changesSet, httpMessage.BatchIndex, httpMessage.IsLastBatch); // Clear the httpMessage set if (!clientWorkInMemory && httpMessage.Changes != null) { httpMessage.Changes.Clear(); } // Until we don't have received all the batches, wait for more if (!httpMessage.IsLastBatch) { return new HttpMessageSendChangesResponse(httpMessage.SyncContext) { ServerStep = HttpStep.SendChangesInProgress } } ; // ------------------------------------------------------------ // SECOND STEP : apply then return server changes // ------------------------------------------------------------ // get changes var(context, remoteClientTimestamp, serverBatchInfo, policy, serverChangesSelected) = await this.ApplyThenGetChangesAsync(httpMessage.SyncContext, httpMessage.Scope, this.Schema, sessionCache.ClientBatchInfo, this.Options.DisableConstraintsOnApplyChanges, this.Options.UseBulkOperations, false, this.Options.CleanFolder, clientBatchSize, this.Options.BatchDirectory, this.Options.ConflictResolutionPolicy, cancellationToken).ConfigureAwait(false); // Save the server batch info object to cache if not working in memory if (!clientWorkInMemory) { sessionCache.RemoteClientTimestamp = remoteClientTimestamp; sessionCache.ServerBatchInfo = serverBatchInfo; sessionCache.ServerChangesSelected = serverChangesSelected; } // Get the firt response to send back to client return(GetChangesResponse(context, remoteClientTimestamp, serverBatchInfo, serverChangesSelected, 0, policy)); }
public MessageGetChangesBatch(Guid?excludingScopeId, Guid localScopeId, bool isNew, long lastTimestamp, SyncSet schema, int batchSize, string batchDirectory) { this.Schema = schema ?? throw new ArgumentNullException(nameof(schema)); this.BatchDirectory = batchDirectory ?? throw new ArgumentNullException(nameof(batchDirectory)); this.ExcludingScopeId = excludingScopeId; this.LocalScopeId = localScopeId; this.IsNew = isNew; this.LastTimestamp = lastTimestamp; this.BatchSize = batchSize; }
private static SyncSet CreateSchema() { var set = new SyncSet(); set.StoredProceduresPrefix = "spp"; set.StoredProceduresSuffix = "sps"; set.TrackingTablesPrefix = "ttp"; set.TrackingTablesSuffix = "tts"; set.TriggersPrefix = "tp"; set.TriggersSuffix = "ts"; var tbl = new SyncTable("ServiceTickets", null); tbl.OriginalProvider = "SqlServerProvider"; tbl.SyncDirection = Enumerations.SyncDirection.Bidirectional; set.Tables.Add(tbl); var c = SyncColumn.Create <int>("ServiceTicketID"); c.DbType = 8; c.AllowDBNull = true; c.IsAutoIncrement = true; c.AutoIncrementStep = 1; c.AutoIncrementSeed = 10; c.IsCompute = false; c.IsReadOnly = true; tbl.Columns.Add(c); tbl.Columns.Add(SyncColumn.Create <string>("Title")); tbl.Columns.Add(SyncColumn.Create <string>("Description")); tbl.Columns.Add(SyncColumn.Create <int>("StatusValue")); tbl.Columns.Add(SyncColumn.Create <int>("EscalationLevel")); tbl.Columns.Add(SyncColumn.Create <DateTime>("Opened")); tbl.Columns.Add(SyncColumn.Create <DateTime>("Closed")); tbl.Columns.Add(SyncColumn.Create <int>("CustomerID")); tbl.PrimaryKeys.Add("ServiceTicketID"); // Add Second tables var tbl2 = new SyncTable("Product", "SalesLT"); tbl2.SyncDirection = SyncDirection.UploadOnly; tbl2.Columns.Add(SyncColumn.Create <int>("Id")); tbl2.Columns.Add(SyncColumn.Create <string>("Title")); tbl2.PrimaryKeys.Add("Id"); set.Tables.Add(tbl2); // Add Filters var sf = new SyncFilter("Product", "SalesLT"); sf.Parameters.Add(new SyncFilterParameter { Name = "Title", DbType = DbType.String, MaxLength = 20, DefaultValue = "'Bikes'" }); sf.Parameters.Add(new SyncFilterParameter { Name = "LastName", TableName = "Customer", SchemaName = "SalesLT", AllowNull = true }); sf.Wheres.Add(new SyncFilterWhereSideItem { ColumnName = "Title", ParameterName = "Title", SchemaName = "SalesLT", TableName = "Product" }); sf.Joins.Add(new SyncFilterJoin { JoinEnum = Join.Right, TableName = "SalesLT.ProductCategory", LeftColumnName = "LCN", LeftTableName = "SalesLT.Product", RightColumnName = "RCN", RightTableName = "SalesLT.ProductCategory" }); sf.CustomWheres.Add("1 = 1"); set.Filters.Add(sf); // Add Relations var keys = new[] { new SyncColumnIdentifier("ProductId", "ServiceTickets") }; var parentKeys = new[] { new SyncColumnIdentifier("ProductId", "Product", "SalesLT") }; var rel = new SyncRelation("AdventureWorks_Product_ServiceTickets", keys, parentKeys); set.Relations.Add(rel); return(set); }
private void Assertions(SyncSet outSchema) { // Call the EnsureSchema to propagate schema to all entities outSchema.EnsureSchema(); Assert.NotNull(outSchema); Assert.Equal("spp", outSchema.StoredProceduresPrefix); Assert.Equal("sps", outSchema.StoredProceduresSuffix); Assert.Equal("ttp", outSchema.TrackingTablesPrefix); Assert.Equal("tts", outSchema.TrackingTablesSuffix); Assert.Equal("tp", outSchema.TriggersPrefix); Assert.Equal("ts", outSchema.TriggersSuffix); Assert.Equal(SyncOptions.DefaultScopeName, outSchema.ScopeName); Assert.NotEmpty(outSchema.Tables); Assert.NotEmpty(outSchema.Filters); Assert.NotEmpty(outSchema.Relations); Assert.Equal(2, outSchema.Tables.Count); Assert.Single(outSchema.Relations); Assert.Single(outSchema.Filters); var tbl1 = outSchema.Tables[0]; Assert.Equal("ServiceTickets", tbl1.TableName); Assert.Null(tbl1.SchemaName); Assert.Equal(SyncDirection.Bidirectional, tbl1.SyncDirection); Assert.NotNull(tbl1.Schema); Assert.Equal(outSchema, tbl1.Schema); Assert.Equal(8, tbl1.Columns.Count); Assert.Equal(tbl1, tbl1.Columns.Table); Assert.NotEmpty(tbl1.Columns.InnerCollection); var col = tbl1.Columns[0]; Assert.Equal("ServiceTicketID", col.ColumnName); Assert.True(col.AllowDBNull); Assert.Equal(10, col.AutoIncrementSeed); Assert.Equal(1, col.AutoIncrementStep); Assert.True(col.IsAutoIncrement); Assert.False(col.IsCompute); Assert.True(col.IsReadOnly); Assert.Equal(0, col.Ordinal); // check orders on others columns Assert.Equal(7, tbl1.Columns["CustomerID"].Ordinal); var tbl2 = outSchema.Tables[1]; Assert.Equal("Product", tbl2.TableName); Assert.Equal("SalesLT", tbl2.SchemaName); Assert.Equal(SyncDirection.UploadOnly, tbl2.SyncDirection); Assert.NotNull(tbl2.Schema); Assert.Equal(outSchema, tbl2.Schema); Assert.Equal(2, tbl2.Columns.Count); Assert.Equal(tbl2, tbl2.Columns.Table); Assert.NotEmpty(tbl2.Columns.InnerCollection); Assert.Single(tbl2.PrimaryKeys); Assert.Equal("Id", tbl2.PrimaryKeys[0]); // Check Filters Assert.NotEmpty(outSchema.Filters); var sf = outSchema.Filters[0]; Assert.Equal("Product", sf.TableName); Assert.Equal("SalesLT", sf.SchemaName); Assert.Equal(outSchema, sf.Schema); Assert.Equal(2, sf.Parameters.Count); Assert.Single(sf.Joins); Assert.Single(sf.CustomWheres); Assert.Single(sf.Wheres); // Parameter 01 Assert.Equal("Title", sf.Parameters[0].Name); Assert.Equal(20, sf.Parameters[0].MaxLength); Assert.Equal("'Bikes'", sf.Parameters[0].DefaultValue); Assert.False(sf.Parameters[0].AllowNull); Assert.Null(sf.Parameters[0].TableName); Assert.Null(sf.Parameters[0].SchemaName); Assert.Equal(outSchema, sf.Parameters[0].Schema); // Parameter 02 Assert.Equal("LastName", sf.Parameters[1].Name); Assert.Equal(0, sf.Parameters[1].MaxLength); Assert.Null(sf.Parameters[1].DefaultValue); Assert.True(sf.Parameters[1].AllowNull); Assert.Equal("Customer", sf.Parameters[1].TableName); Assert.Equal("SalesLT", sf.Parameters[1].SchemaName); Assert.Equal(outSchema, sf.Parameters[1].Schema); // Joins Assert.Equal(Join.Right, sf.Joins[0].JoinEnum); Assert.Equal("SalesLT.ProductCategory", sf.Joins[0].TableName); Assert.Equal("LCN", sf.Joins[0].LeftColumnName); Assert.Equal("SalesLT.Product", sf.Joins[0].LeftTableName); Assert.Equal("RCN", sf.Joins[0].RightColumnName); Assert.Equal("SalesLT.ProductCategory", sf.Joins[0].RightTableName); // Wheres Assert.Equal("Title", sf.Wheres[0].ColumnName); Assert.Equal("Title", sf.Wheres[0].ParameterName); Assert.Equal("SalesLT", sf.Wheres[0].SchemaName); Assert.Equal("Product", sf.Wheres[0].TableName); // Customer Wheres Assert.Equal("1 = 1", sf.CustomWheres[0]); // Check Relations Assert.NotEmpty(outSchema.Relations); var rel = outSchema.Relations[0]; Assert.Equal("AdventureWorks_Product_ServiceTickets", rel.RelationName); Assert.NotEmpty(rel.ParentKeys); Assert.NotEmpty(rel.Keys); var c = rel.Keys.ToList()[0]; Assert.Equal("ProductId", c.ColumnName); Assert.Equal("ServiceTickets", c.TableName); Assert.Null(c.SchemaName); var p = rel.ParentKeys.ToList()[0]; Assert.Equal("ProductId", p.ColumnName); Assert.Equal("Product", p.TableName); Assert.Equal("SalesLT", p.SchemaName); }
/// <summary> /// Create a new BPI, and serialize the changeset if not in memory /// </summary> internal static async Task <BatchPartInfo> CreateBatchPartInfoAsync(int batchIndex, SyncSet set, string fileName, string directoryFullPath, bool isLastBatch, BaseOrchestrator orchestrator = null) { BatchPartInfo bpi = null; // Create a batch part // The batch part creation process will serialize the changesSet to the disk // Serialize the file ! await SerializeAsync(set.GetContainerSet(), fileName, directoryFullPath, orchestrator); bpi = new BatchPartInfo { FileName = fileName }; bpi.Index = batchIndex; bpi.IsLastBatch = isLastBatch; // Even if the set is empty (serialized on disk), we should retain the tables names if (set != null) { bpi.Tables = set.Tables.Select(t => new BatchPartTableInfo(t.TableName, t.SchemaName, t.Rows.Count)).ToArray(); bpi.RowsCount = set.Tables.Sum(t => t.Rows.Count); } return(bpi); }
/// <summary> /// Metadatas are handled by Change Tracking /// So just do nothing here /// </summary> public override Task <(SyncContext, DatabaseMetadatasCleaned)> DeleteMetadatasAsync(SyncContext context, SyncSet schema, SyncSetup setup, long timestampLimit, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress = null) => Task.FromResult((context, new DatabaseMetadatasCleaned()));
/// <summary> /// Internally setting schema /// </summary> internal void SetSchema(SyncSet schema) => this.schema = schema;
/// <summary> /// Metadatas are handled by Change Tracking /// So just do nothing here /// </summary> public override Task <SyncContext> DeleteMetadatasAsync(SyncContext context, SyncSet schema, long timestampLimit, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress = null) => Task.FromResult(context);
/// <summary> /// Create a response message content based on a requested index in a server batch info /// </summary> private HttpMessageSendChangesResponse GetChangesResponse(SyncContext syncContext, long remoteClientTimestamp, BatchInfo serverBatchInfo, DatabaseChangesSelected serverChangesSelected, int batchIndexRequested, ConflictResolutionPolicy policy) { // 1) Create the http message content response var changesResponse = new HttpMessageSendChangesResponse(syncContext); changesResponse.ChangesSelected = serverChangesSelected; changesResponse.ServerStep = HttpStep.GetChanges; changesResponse.ConflictResolutionPolicy = policy; // If nothing to do, just send back if (serverBatchInfo.InMemory || serverBatchInfo.BatchPartsInfo.Count == 0) { if (this.ClientConverter != null && serverBatchInfo.InMemoryData.HasRows) { BeforeSerializeRows(serverBatchInfo.InMemoryData, this.ClientConverter); } changesResponse.Changes = serverBatchInfo.InMemoryData.GetContainerSet(); changesResponse.BatchIndex = 0; changesResponse.IsLastBatch = true; changesResponse.RemoteClientTimestamp = remoteClientTimestamp; return(changesResponse); } // Get the batch part index requested var batchPartInfo = serverBatchInfo.BatchPartsInfo.First(d => d.Index == batchIndexRequested); // if we are not in memory, we set the BI in session, to be able to get it back on next request // create the in memory changes set var changesSet = new SyncSet(Schema.ScopeName); foreach (var table in Schema.Tables) { DbSyncAdapter.CreateChangesTable(Schema.Tables[table.TableName, table.SchemaName], changesSet); } batchPartInfo.LoadBatch(changesSet, serverBatchInfo.GetDirectoryFullPath()); // if client request a conversion on each row, apply the conversion if (this.ClientConverter != null && batchPartInfo.Data.HasRows) { BeforeSerializeRows(batchPartInfo.Data, this.ClientConverter); } changesResponse.Changes = batchPartInfo.Data.GetContainerSet(); changesResponse.BatchIndex = batchIndexRequested; changesResponse.IsLastBatch = batchPartInfo.IsLastBatch; changesResponse.RemoteClientTimestamp = remoteClientTimestamp; changesResponse.ServerStep = batchPartInfo.IsLastBatch ? HttpStep.GetChanges : HttpStep.GetChangesInProgress; // If we have only one bpi, we can safely delete it if (batchPartInfo.IsLastBatch) { // delete the folder (not the BatchPartInfo, because we have a reference on it) if (this.Options.CleanFolder) { var shouldDeleteFolder = true; if (!string.IsNullOrEmpty(this.Options.SnapshotsDirectory)) { var dirInfo = new DirectoryInfo(serverBatchInfo.DirectoryRoot); var snapInfo = new DirectoryInfo(this.Options.SnapshotsDirectory); shouldDeleteFolder = dirInfo.FullName != snapInfo.FullName; } if (shouldDeleteFolder) { serverBatchInfo.TryRemoveDirectory(); } } } return(changesResponse); }
public void processChange(List <Change <T> > changes) { if (changes.Count > 0) { // Make list change List <Sync <T> > syncs = new List <Sync <T> >(); { for (int syncCount = 0; syncCount < changes.Count; syncCount++) { Change <T> change = changes[syncCount]; switch (change.getType()) { case Change <T> .Type.Set: { ChangeSet <T> changeSet = (ChangeSet <T>)change; // keep old SyncSet SyncSet <T> oldSyncSet = null; if (changeSet.index >= 0 && changeSet.index + changeSet.values.Count <= this.vs.Count) { for (int i = 0; i < changeSet.values.Count; i++) { int setIndex = changeSet.index + i; T oldValue = this.vs[setIndex]; // check is different bool isDifferent = true; { if (object.Equals(oldValue, changeSet.values[i])) { isDifferent = false; } else { if (((Data)(object)changeSet.values[i]).uid == ((Data)(object)oldValue).uid) { isDifferent = false; } } } // Make change if (isDifferent) { // Change { // Set parent { ((Data)(object)oldValue).p = null; ((Data)(object)changeSet.values[i]).p = this; } // add this.vs[setIndex] = changeSet.values[i]; } // Make Sync { // get changeSet SyncSet <T> syncSet = null; { // check old if (oldSyncSet != null) { if (oldSyncSet.index + oldSyncSet.olds.Count == setIndex) { syncSet = oldSyncSet; } } // make new if (syncSet == null) { syncSet = new SyncSet <T>(); { syncSet.index = setIndex; } syncs.Add(syncSet); // set new old oldSyncSet = syncSet; } } // add value { syncSet.olds.Add(oldValue); syncSet.news.Add(changeSet.values[i]); } } } else { // Debug.LogError("why the same: " + oldValue + "; " + changeSet.values[i]); } } } else { Logger.LogError("index error: " + changeSet.index + "; " + this.vs.Count + "; " + this); } } break; case Change <T> .Type.Add: { ChangeAdd <T> changeAdd = (ChangeAdd <T>)change; // Add if (changeAdd.index >= 0 && changeAdd.index <= this.vs.Count) { // Change { // set parent { foreach (T value in changeAdd.values) { if (value != null) { ((Data)(object)value).p = this; } else { Logger.LogError("why value null: " + this); } } } this.vs.InsertRange(changeAdd.index, changeAdd.values); } // Make Sync { SyncAdd <T> syncAdd = new SyncAdd <T>(); { syncAdd.index = changeAdd.index; syncAdd.values.AddRange(changeAdd.values); } syncs.Add(syncAdd); } } else { Logger.LogError("index error: " + changeAdd.index + "; " + this.vs.Count + "; " + this); } } break; case Change <T> .Type.Remove: { ChangeRemove <T> changeRemove = (ChangeRemove <T>)change; // Check index if (changeRemove.number > 0 && changeRemove.index >= 0 && changeRemove.index + changeRemove.number <= this.vs.Count) { // Make sync: phai make sync truoc moi lay duoc oldValues SyncRemove <T> syncRemove = new SyncRemove <T>(); { syncRemove.index = changeRemove.index; for (int i = 0; i < changeRemove.number; i++) { syncRemove.values.Add(this.vs[changeRemove.index + i]); } } syncs.Add(syncRemove); // Change { // set parent { foreach (T value in syncRemove.values) { if (value != null) { ((Data)(object)value).p = null; } else { Logger.LogError("why value null: " + this); } } } // Remove this.vs.RemoveRange(changeRemove.index, changeRemove.number); } } else { Logger.LogError("index error: " + this); } } break; default: Logger.LogError("unknown change type: " + change.getType() + "; " + this); break; } } } // CallBack if (syncs.Count > 0) { foreach (ValueChangeCallBack callBack in p.callBacks.ToArray()) { callBack.onUpdateSync(this, syncs); } } else { // Debug.LogError("why don't have syncCount: " + this); } } else { Logger.LogError("why don't have changes: " + this); } }
/// <summary> /// Applying changes message. /// Be careful policy could be differente from the schema (especially on client side, it's the reverse one, by default) /// </summary> public MessageApplyChanges(Guid localScopeId, Guid senderScopeId, bool isNew, long lastTimestamp, SyncSet schema, ConflictResolutionPolicy policy, bool disableConstraintsOnApplyChanges, bool useBulkOperations, bool cleanMetadatas, bool cleanFolder, BatchInfo changes) { this.LocalScopeId = localScopeId; this.SenderScopeId = senderScopeId; this.IsNew = isNew; this.LastTimestamp = lastTimestamp; this.Schema = schema ?? throw new ArgumentNullException(nameof(schema)); this.Policy = policy; this.DisableConstraintsOnApplyChanges = disableConstraintsOnApplyChanges; this.UseBulkOperations = useBulkOperations; this.CleanMetadatas = cleanMetadatas; this.CleanFolder = cleanFolder; this.Changes = changes ?? throw new ArgumentNullException(nameof(changes)); }