Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 10 additions & 32 deletions src/CoreSync.Sqlite/SqliteSyncProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -848,49 +848,27 @@ public async Task RemoveProvisionAsync(CancellationToken cancellationToken = def
using var connection = new SqliteConnection(Configuration.ConnectionString);
await connection.OpenAsync(cancellationToken);

//1. discover tables
using var cmd = connection.CreateCommand();
var listOfTables = new List<string>();
foreach (SqliteSyncTable table in Configuration.Tables)
{
cmd.CommandText = $"PRAGMA table_info('{table.Name}')";
using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
/*
cid name type notnull dflt_value pk
---------- ---------- ---------- ---------- ---------- ----------
*/
while (await reader.ReadAsync(cancellationToken))
{
var colName = reader.GetString(1);

if (string.CompareOrdinal(colName, "__OP") == 0)
{
continue;
}

listOfTables.Add(colName);
}
}

//2. drop ct table
//1. drop ct table
cmd.CommandText = $"DROP TABLE IF EXISTS __CORE_SYNC_CT";
await cmd.ExecuteNonQueryAsync();

//3. drop remote anchor table
//2. drop remote anchor table
cmd.CommandText = $"DROP TABLE IF EXISTS __CORE_SYNC_REMOTE_ANCHOR";
await cmd.ExecuteNonQueryAsync();

//4. drop local anchor table
//3. drop local anchor table
cmd.CommandText = $"DROP TABLE IF EXISTS __CORE_SYNC_LOCAL_ID";
await cmd.ExecuteNonQueryAsync();

//5. drop triggers
foreach (var tableName in listOfTables)
//4. drop triggers — iterate table names directly; DROP TRIGGER IF EXISTS is safe even if never provisioned
foreach (SqliteSyncTable table in Configuration.Tables)
{
await DisableChangeTrackingForTable(cmd, tableName, cancellationToken);
await DisableChangeTrackingForTable(cmd, table.Name, cancellationToken);
}
}

public async Task<SyncVersion> GetSyncVersionAsync(CancellationToken cancellationToken = default)
{
await InitializeStoreAsync(cancellationToken);
Expand Down Expand Up @@ -992,7 +970,7 @@ public async Task EnableChangeTrackingForTable(string name, CancellationToken ca
await SetupTableForUpdatesOrDeletesOnly(table, cmd, cancellationToken);
}
}

public async Task DisableChangeTrackingForTable(string name, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(name))
Expand All @@ -1009,7 +987,7 @@ public async Task DisableChangeTrackingForTable(string name, CancellationToken c
}

private async Task DisableChangeTrackingForTable(SqliteCommand cmd, string tableName, CancellationToken cancellationToken)
{
{
var commandTextBase = new Func<string, string>((op) => $@"DROP TRIGGER IF EXISTS [__{tableName}_ct-{op}__]");
cmd.CommandText = commandTextBase("INSERT");
await cmd.ExecuteNonQueryAsync();
Expand All @@ -1018,7 +996,7 @@ private async Task DisableChangeTrackingForTable(SqliteCommand cmd, string table
await cmd.ExecuteNonQueryAsync();

cmd.CommandText = commandTextBase("DELETE");
await cmd.ExecuteNonQueryAsync();
await cmd.ExecuteNonQueryAsync();
}

}
Expand Down
147 changes: 147 additions & 0 deletions src/CoreSync.Tests/SqliteRemoveProvisionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
using CoreSync.Sqlite;
using CoreSync.Tests.Data;
using Microsoft.Data.Sqlite;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
using SQLite;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace CoreSync.Tests;

/// <summary>
/// Covers <see cref="SqliteSyncProvider.RemoveProvisionAsync"/>: after a de-provision the database
/// must be left exactly as it was before <see cref="SqliteSyncProvider.ApplyProvisionAsync"/> ran,
/// i.e. no change-tracking triggers and no __CORE_SYNC_* tables.
/// </summary>
[TestClass]
public class SqliteRemoveProvisionTests
{
private static string CreateDatabase(string testName)
{
var dbFile = Path.Combine(Path.GetTempPath(), $"{testName}_{Guid.NewGuid():N}.sqlite");

using (var db = new SQLiteConnection(dbFile))
{
db.CreateTable<Stock>();
db.CreateTable<Valuation>();
}

SqliteConnection.ClearAllPools();

return dbFile;
}

private static SqliteSyncProvider CreateProvider(string dbFile)
=> new(new SqliteSyncConfigurationBuilder($"Data Source={dbFile}")
.Table<Stock>()
.Table<Valuation>()
.Build(), logger: new ConsoleLogger("LOC"));

private static List<string> GetObjectNames(string dbFile, string type)
{
var names = new List<string>();

using var connection = new SqliteConnection($"Data Source={dbFile}");
connection.Open();

using var cmd = connection.CreateCommand();
cmd.CommandText = $"SELECT name FROM sqlite_master WHERE type = '{type}' ORDER BY name";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
names.Add(reader.GetString(0));
}

return names;
}

[TestMethod]
public async Task RemoveProvisionAsync_RemovesChangeTrackingTriggersAndTables()
{
var dbFile = CreateDatabase(nameof(RemoveProvisionAsync_RemovesChangeTrackingTriggersAndTables));
var syncProvider = CreateProvider(dbFile);

await syncProvider.ApplyProvisionAsync();

//provisioning creates 3 triggers per table (INSERT/UPDATE/DELETE) plus the sync tables
GetObjectNames(dbFile, "trigger").ShouldBe(
[
"__Stock_ct-DELETE__",
"__Stock_ct-INSERT__",
"__Stock_ct-UPDATE__",
"__Valuation_ct-DELETE__",
"__Valuation_ct-INSERT__",
"__Valuation_ct-UPDATE__",
]);
GetObjectNames(dbFile, "table").ShouldContain("__CORE_SYNC_CT");

await syncProvider.RemoveProvisionAsync();

GetObjectNames(dbFile, "trigger").ShouldBeEmpty();

var tables = GetObjectNames(dbFile, "table");
tables.ShouldNotContain("__CORE_SYNC_CT");
tables.ShouldNotContain("__CORE_SYNC_REMOTE_ANCHOR");
tables.ShouldNotContain("__CORE_SYNC_LOCAL_ID");

//user tables must survive the de-provision
tables.ShouldContain("Stock");
tables.ShouldContain("Valuation");
}

[TestMethod]
public async Task RemoveProvisionAsync_LeavesDatabaseWritable()
{
var dbFile = CreateDatabase(nameof(RemoveProvisionAsync_LeavesDatabaseWritable));
var syncProvider = CreateProvider(dbFile);

await syncProvider.ApplyProvisionAsync();
await syncProvider.RemoveProvisionAsync();

//triggers left behind by a partial de-provision still target the (now dropped) __CORE_SYNC_CT
//table, so any write to a tracked table would fail with "no such table: main.__CORE_SYNC_CT"
using var db = new SQLiteConnection(dbFile);
db.Insert(new Stock { Id = Guid.NewGuid(), Symbol = "MY_SYMBOL" });

db.Table<Stock>().Count().ShouldBe(1);
}

[TestMethod]
public async Task RemoveProvisionAsync_CanBeCalledOnANotProvisionedDatabase()
{
var dbFile = CreateDatabase(nameof(RemoveProvisionAsync_CanBeCalledOnANotProvisionedDatabase));
var syncProvider = CreateProvider(dbFile);

await syncProvider.RemoveProvisionAsync();

GetObjectNames(dbFile, "trigger").ShouldBeEmpty();
}

[TestMethod]
public async Task RemoveProvisionAsync_RemovesTriggersOfATableMissingFromTheDatabase()
{
var dbFile = CreateDatabase(nameof(RemoveProvisionAsync_RemovesTriggersOfATableMissingFromTheDatabase));
var syncProvider = CreateProvider(dbFile);

await syncProvider.ApplyProvisionAsync();

//simulate a table dropped outside of CoreSync: its triggers go away with it, but the
//de-provision must not choke on the missing table nor skip the remaining ones
using (var connection = new SqliteConnection($"Data Source={dbFile}"))
{
connection.Open();
using var cmd = connection.CreateCommand();
cmd.CommandText = "DROP TABLE [Valuation]";
cmd.ExecuteNonQuery();
}

SqliteConnection.ClearAllPools();

await syncProvider.RemoveProvisionAsync();

GetObjectNames(dbFile, "trigger").ShouldBeEmpty();
}
}
Loading