Skip to content

Commit 1e2368a

Browse files
authored
Merge pull request #156 from snowfrogdev/sqlite-adapter
Add Sqlite support with SqliteDbAdapter implementation
2 parents f95f552 + ad1db44 commit 1e2368a

File tree

5 files changed

+403
-1
lines changed

5 files changed

+403
-1
lines changed

Respawn.DatabaseTests/Respawn.DatabaseTests.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
<PropertyGroup>
44
<TargetFramework>net8.0</TargetFramework>
55
<NoWarn>$(NoWarn);NU1902;NU1903;NU1904</NoWarn>
6-
</PropertyGroup>
6+
</PropertyGroup>
77
<ItemGroup>
88
<PackageReference Include="IBM.Data.DB2.Core" Version="3.1.0.500" />
9+
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.5" />
910
<PackageReference Include="MySql.Data" Version="9.2.0" />
1011
<PackageReference Include="Npgsql" Version="9.0.3" />
1112
<PackageReference Include="NPoco.SqlServer" Version="5.7.1" />
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
using System;
2+
using System.IO;
3+
using System.Threading.Tasks;
4+
using Microsoft.Data.Sqlite;
5+
using NPoco;
6+
using Respawn.Graph;
7+
using Shouldly;
8+
using Xunit;
9+
using Xunit.Abstractions;
10+
11+
namespace Respawn.DatabaseTests
12+
{
13+
public class SqliteTests : IAsyncLifetime
14+
{
15+
private readonly ITestOutputHelper _output;
16+
private SqliteConnection _connection;
17+
private Database _database;
18+
private string _dbFileName;
19+
20+
public SqliteTests(ITestOutputHelper output) => _output = output;
21+
22+
public async Task InitializeAsync()
23+
{
24+
_dbFileName = Path.Combine(Path.GetTempPath(), $"respawn_test_{Guid.NewGuid():N}.db");
25+
var connectionString = $"Data Source={_dbFileName};";
26+
27+
_connection = new SqliteConnection(connectionString);
28+
await _connection.OpenAsync();
29+
30+
_database = new Database(_connection);
31+
}
32+
33+
public Task DisposeAsync()
34+
{
35+
// Close and dispose of the connection before attempting to delete the file
36+
_connection?.Close();
37+
_connection?.Dispose();
38+
_connection = null;
39+
40+
try
41+
{
42+
if (File.Exists(_dbFileName))
43+
{
44+
File.Delete(_dbFileName);
45+
}
46+
}
47+
catch
48+
{
49+
// Ignore deletion errors
50+
}
51+
52+
return Task.CompletedTask;
53+
}
54+
55+
[Fact]
56+
public async Task ShouldDeleteData()
57+
{
58+
await _database.ExecuteAsync("CREATE TABLE foo (value INTEGER)");
59+
60+
for (int i = 0; i < 100; i++)
61+
{
62+
await _database.ExecuteAsync("INSERT INTO foo VALUES (@0)", i);
63+
}
64+
65+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM foo").ShouldBe(100);
66+
67+
var checkpoint = await Respawner.CreateAsync(_connection);
68+
await checkpoint.ResetAsync(_connection);
69+
70+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM foo").ShouldBe(0);
71+
}
72+
73+
[Fact]
74+
public async Task ShouldIgnoreTables()
75+
{
76+
await _database.ExecuteAsync("CREATE TABLE foo (value INTEGER)");
77+
await _database.ExecuteAsync("CREATE TABLE bar (value INTEGER)");
78+
79+
for (int i = 0; i < 100; i++)
80+
{
81+
await _database.ExecuteAsync("INSERT INTO foo VALUES (@0)", i);
82+
await _database.ExecuteAsync("INSERT INTO bar VALUES (@0)", i);
83+
}
84+
85+
var checkpoint = await Respawner.CreateAsync(_connection, new RespawnerOptions
86+
{
87+
TablesToIgnore = new Table[] { "foo" }
88+
});
89+
await checkpoint.ResetAsync(_connection);
90+
91+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM foo").ShouldBe(100);
92+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM bar").ShouldBe(0);
93+
}
94+
95+
[Fact]
96+
public async Task ShouldIncludeTables()
97+
{
98+
await _database.ExecuteAsync("CREATE TABLE foo (value INTEGER)");
99+
await _database.ExecuteAsync("CREATE TABLE bar (value INTEGER)");
100+
101+
for (int i = 0; i < 100; i++)
102+
{
103+
await _database.ExecuteAsync("INSERT INTO foo VALUES (@0)", i);
104+
await _database.ExecuteAsync("INSERT INTO bar VALUES (@0)", i);
105+
}
106+
107+
var checkpoint = await Respawner.CreateAsync(_connection, new RespawnerOptions
108+
{
109+
TablesToInclude = new Table[] { "foo" }
110+
});
111+
await checkpoint.ResetAsync(_connection);
112+
113+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM foo").ShouldBe(0);
114+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM bar").ShouldBe(100);
115+
}
116+
117+
[Fact]
118+
public async Task ShouldHandleRelationships()
119+
{
120+
await _database.ExecuteAsync("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, value INTEGER)");
121+
await _database.ExecuteAsync("CREATE TABLE bar (id INTEGER PRIMARY KEY AUTOINCREMENT, foo_id INTEGER, FOREIGN KEY(foo_id) REFERENCES foo(id))");
122+
123+
for (int i = 0; i < 100; i++)
124+
{
125+
await _database.ExecuteAsync("INSERT INTO foo (value) VALUES (@0)", i);
126+
await _database.ExecuteAsync("INSERT INTO bar (foo_id) VALUES (@0)", i + 1);
127+
}
128+
129+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM foo").ShouldBe(100);
130+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM bar").ShouldBe(100);
131+
132+
var checkpoint = await Respawner.CreateAsync(_connection);
133+
await checkpoint.ResetAsync(_connection);
134+
135+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM foo").ShouldBe(0);
136+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM bar").ShouldBe(0);
137+
}
138+
139+
[Fact]
140+
public async Task ShouldReseedIds()
141+
{
142+
await _database.ExecuteAsync("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, value INTEGER)");
143+
144+
for (int i = 0; i < 100; i++)
145+
{
146+
await _database.ExecuteAsync("INSERT INTO foo (value) VALUES (@0)", i);
147+
}
148+
149+
_database.ExecuteScalar<int>("SELECT MAX(id) FROM foo").ShouldBe(100);
150+
151+
var checkpoint = await Respawner.CreateAsync(_connection, new RespawnerOptions
152+
{
153+
WithReseed = true
154+
});
155+
await checkpoint.ResetAsync(_connection);
156+
157+
await _database.ExecuteAsync("INSERT INTO foo (value) VALUES (@0)", 1);
158+
_database.ExecuteScalar<int>("SELECT MAX(id) FROM foo").ShouldBe(1);
159+
}
160+
161+
[Fact]
162+
public async Task ShouldHandleSelfRelationships()
163+
{
164+
await _database.ExecuteAsync("CREATE TABLE foo (id INTEGER PRIMARY KEY, parentid INTEGER NULL)");
165+
await _database.ExecuteAsync("CREATE INDEX IX_foo_parentid ON foo(parentid)");
166+
await _database.ExecuteAsync("CREATE TRIGGER fk_foo_self BEFORE DELETE ON foo FOR EACH ROW BEGIN " +
167+
"DELETE FROM foo WHERE parentid = OLD.id; END");
168+
169+
await _database.ExecuteAsync("INSERT INTO foo (id) VALUES (@0)", 1);
170+
for (int i = 1; i < 100; i++)
171+
{
172+
await _database.ExecuteAsync("INSERT INTO foo VALUES (@0, @1)", i + 1, i);
173+
}
174+
175+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM foo").ShouldBe(100);
176+
177+
var checkpoint = await Respawner.CreateAsync(_connection);
178+
await checkpoint.ResetAsync(_connection);
179+
180+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM foo").ShouldBe(0);
181+
}
182+
183+
[Fact]
184+
public async Task ShouldHandleCircularRelationships()
185+
{
186+
await _database.ExecuteAsync("CREATE TABLE parent (id INTEGER PRIMARY KEY, childid INTEGER NULL)");
187+
await _database.ExecuteAsync("CREATE TABLE child (id INTEGER PRIMARY KEY, parentid INTEGER NULL)");
188+
189+
// In SQLite, we need to create triggers to enforce the foreign key relationships
190+
await _database.ExecuteAsync("CREATE TRIGGER fk_parent_child BEFORE DELETE ON parent FOR EACH ROW BEGIN " +
191+
"UPDATE child SET parentid = NULL WHERE parentid = OLD.id; END");
192+
await _database.ExecuteAsync("CREATE TRIGGER fk_child_parent BEFORE DELETE ON child FOR EACH ROW BEGIN " +
193+
"UPDATE parent SET childid = NULL WHERE childid = OLD.id; END");
194+
195+
for (int i = 0; i < 100; i++)
196+
{
197+
await _database.ExecuteAsync("INSERT INTO parent VALUES (@0, null)", i);
198+
await _database.ExecuteAsync("INSERT INTO child VALUES (@0, null)", i);
199+
}
200+
201+
await _database.ExecuteAsync("UPDATE parent SET childid = 0");
202+
await _database.ExecuteAsync("UPDATE child SET parentid = 1");
203+
204+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM parent").ShouldBe(100);
205+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM child").ShouldBe(100);
206+
207+
var checkpoint = await Respawner.CreateAsync(_connection);
208+
await checkpoint.ResetAsync(_connection);
209+
210+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM parent").ShouldBe(0);
211+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM child").ShouldBe(0);
212+
}
213+
214+
[Fact]
215+
public async Task ShouldHandleComplexCycles()
216+
{
217+
await _database.ExecuteAsync("PRAGMA foreign_keys = ON");
218+
219+
await _database.ExecuteAsync("CREATE TABLE a (id INTEGER PRIMARY KEY, b_id INTEGER NULL)");
220+
await _database.ExecuteAsync("CREATE TABLE b (id INTEGER PRIMARY KEY, a_id INTEGER NULL, c_id INTEGER NULL, d_id INTEGER NULL)");
221+
await _database.ExecuteAsync("CREATE TABLE c (id INTEGER PRIMARY KEY, d_id INTEGER NULL)");
222+
await _database.ExecuteAsync("CREATE TABLE d (id INTEGER PRIMARY KEY)");
223+
await _database.ExecuteAsync("CREATE TABLE e (id INTEGER PRIMARY KEY, a_id INTEGER NULL)");
224+
await _database.ExecuteAsync("CREATE TABLE f (id INTEGER PRIMARY KEY, b_id INTEGER NULL)");
225+
226+
// Create the foreign key constraints
227+
await _database.ExecuteAsync("CREATE INDEX IX_a_b_id ON a(b_id)");
228+
await _database.ExecuteAsync("CREATE INDEX IX_b_a_id ON b(a_id)");
229+
await _database.ExecuteAsync("CREATE INDEX IX_b_c_id ON b(c_id)");
230+
await _database.ExecuteAsync("CREATE INDEX IX_b_d_id ON b(d_id)");
231+
await _database.ExecuteAsync("CREATE INDEX IX_c_d_id ON c(d_id)");
232+
await _database.ExecuteAsync("CREATE INDEX IX_e_a_id ON e(a_id)");
233+
await _database.ExecuteAsync("CREATE INDEX IX_f_b_id ON f(b_id)");
234+
235+
await _database.ExecuteAsync("INSERT INTO d (id) VALUES (1)");
236+
await _database.ExecuteAsync("INSERT INTO c (id, d_id) VALUES (1, 1)");
237+
await _database.ExecuteAsync("INSERT INTO a (id) VALUES (1)");
238+
await _database.ExecuteAsync("INSERT INTO b (id, c_id, d_id) VALUES (1, 1, 1)");
239+
await _database.ExecuteAsync("INSERT INTO e (id, a_id) VALUES (1, 1)");
240+
await _database.ExecuteAsync("INSERT INTO f (id, b_id) VALUES (1, 1)");
241+
await _database.ExecuteAsync("UPDATE a SET b_id = 1");
242+
await _database.ExecuteAsync("UPDATE b SET a_id = 1");
243+
244+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM a").ShouldBe(1);
245+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM b").ShouldBe(1);
246+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM c").ShouldBe(1);
247+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM d").ShouldBe(1);
248+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM e").ShouldBe(1);
249+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM f").ShouldBe(1);
250+
251+
var checkpoint = await Respawner.CreateAsync(_connection);
252+
await checkpoint.ResetAsync(_connection);
253+
254+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM a").ShouldBe(0);
255+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM b").ShouldBe(0);
256+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM c").ShouldBe(0);
257+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM d").ShouldBe(0);
258+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM e").ShouldBe(0);
259+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM f").ShouldBe(0);
260+
}
261+
262+
[Fact]
263+
public async Task ShouldDeleteDataWithRelationships()
264+
{
265+
await _database.ExecuteAsync("PRAGMA foreign_keys = ON");
266+
267+
await _database.ExecuteAsync("CREATE TABLE bob (bobvalue INTEGER PRIMARY KEY)");
268+
await _database.ExecuteAsync("CREATE TABLE foo (foovalue INTEGER PRIMARY KEY, bobvalue INTEGER NOT NULL, " +
269+
"FOREIGN KEY(bobvalue) REFERENCES bob(bobvalue))");
270+
await _database.ExecuteAsync("CREATE TABLE bar (barvalue INTEGER PRIMARY KEY, " +
271+
"FOREIGN KEY(barvalue) REFERENCES foo(foovalue))");
272+
273+
for (int i = 0; i < 100; i++)
274+
{
275+
await _database.ExecuteAsync("INSERT INTO bob VALUES (@0)", i);
276+
await _database.ExecuteAsync("INSERT INTO foo VALUES (@0, @0)", i);
277+
await _database.ExecuteAsync("INSERT INTO bar VALUES (@0)", i);
278+
}
279+
280+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM foo").ShouldBe(100);
281+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM bar").ShouldBe(100);
282+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM bob").ShouldBe(100);
283+
284+
var checkpoint = await Respawner.CreateAsync(_connection);
285+
await checkpoint.ResetAsync(_connection);
286+
287+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM foo").ShouldBe(0);
288+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM bar").ShouldBe(0);
289+
_database.ExecuteScalar<int>("SELECT COUNT(1) FROM bob").ShouldBe(0);
290+
}
291+
}
292+
}

Respawn/DbAdapter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public static class DbAdapter
88
public static readonly IDbAdapter Oracle = new OracleDbAdapter();
99
public static readonly IDbAdapter Informix = new InformixDbAdapter();
1010
public static readonly IDbAdapter DB2 = new DB2DbAdapter();
11+
public static readonly IDbAdapter Sqlite = new SqliteDbAdapter();
1112
public static readonly IDbAdapter Snowflake = new SnowflakeDbAdapter();
1213
}
1314
}

Respawn/Respawner.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public static async Task<Respawner> CreateAsync(DbConnection connection, Respawn
5656
"MySqlConnection" => DbAdapter.MySql,
5757
"OracleConnection" => DbAdapter.Oracle,
5858
"DB2Connection" or "IfxConnection" => DbAdapter.Informix,
59+
"SqliteConnection" => DbAdapter.Sqlite,
5960
"SnowflakeDbConnection" => DbAdapter.Snowflake,
6061
_ => throw new ArgumentException("The database adapter could not be inferred from the DbConnection. Please pass an explicit database adapter in the options.", nameof(options))
6162
};

0 commit comments

Comments
 (0)