Skip to content

Commit 3a25a90

Browse files
committed
Implemented ExecuteBatch.
1 parent e8de1f2 commit 3a25a90

File tree

5 files changed

+122
-19
lines changed

5 files changed

+122
-19
lines changed

PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ public interface IPowerSyncDatabase : IEventStream<PowerSyncDBEvent>
6060

6161
Task<NonQueryResult> Execute(string query, object?[]? parameters = null);
6262

63+
Task<NonQueryResult> ExecuteBatch(string query, object?[][]? parameters = null);
64+
6365
Task<T[]> GetAll<T>(string sql, object?[]? parameters = null);
6466

6567
Task<T?> GetOptional<T>(string sql, object?[]? parameters = null);
@@ -555,6 +557,12 @@ public async Task<NonQueryResult> Execute(string query, object?[]? parameters =
555557
return await Database.Execute(query, parameters);
556558
}
557559

560+
public async Task<NonQueryResult> ExecuteBatch(string query, object?[][]? parameters = null)
561+
{
562+
await WaitForReady();
563+
return await Database.ExecuteBatch(query, parameters);
564+
}
565+
558566
public async Task<T[]> GetAll<T>(string query, object?[]? parameters = null)
559567
{
560568
await WaitForReady();

PowerSync/PowerSync.Common/DB/IDBAdapter.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ public interface ILockContext : IDBGetUtils
4343
{
4444
// Execute a single write statement.
4545
Task<NonQueryResult> Execute(string query, object?[]? parameters = null);
46+
47+
// Execute a batch of write statements.
48+
Task<NonQueryResult> ExecuteBatch(string query, object?[][]? parameters = null);
4649
}
4750

4851
public interface ITransaction : ILockContext
@@ -114,11 +117,6 @@ public interface IDBAdapter : IEventStream<DBAdapterEvent>, ILockContext
114117
/// </summary>
115118
new void Close();
116119

117-
/// <summary>
118-
/// Execute a batch of write statements.
119-
/// </summary>
120-
Task<QueryResult> ExecuteBatch(string query, object?[][]? parameters = null);
121-
122120
/// <summary>
123121
/// The name of the adapter.
124122
/// </summary>

PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,9 @@ public async Task<NonQueryResult> Execute(string query, object?[]? parameters =
149149
return await WriteLock((ctx) => ctx.Execute(query, parameters));
150150
}
151151

152-
public Task<QueryResult> ExecuteBatch(string query, object?[][]? parameters = null)
152+
public async Task<NonQueryResult> ExecuteBatch(string query, object?[][]? parameters = null)
153153
{
154-
// https://learn.microsoft.com/en-gb/dotnet/standard/data/sqlite/batching
155-
throw new NotImplementedException();
154+
return await WriteTransaction((ctx) => ctx.ExecuteBatch(query, parameters));
156155
}
157156

158157
public async Task<T> Get<T>(string sql, object?[]? parameters = null)
@@ -307,6 +306,11 @@ public Task<NonQueryResult> Execute(string query, object?[]? parameters = null)
307306
return connection.Execute(query, parameters);
308307
}
309308

309+
public Task<NonQueryResult> ExecuteBatch(string query, object?[][]? parameters = null)
310+
{
311+
return connection.ExecuteBatch(query, parameters);
312+
}
313+
310314
public Task<T> Get<T>(string sql, object?[]? parameters = null)
311315
{
312316
return connection.Get<T>(sql, parameters);

PowerSync/PowerSync.Common/MDSQLite/MDSQLiteConnection.cs

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,25 +72,29 @@ public void FlushUpdates()
7272
Emit(new DBAdapterEvent { TablesUpdated = batchedUpdate });
7373
}
7474

75-
private static void PrepareCommand(SqliteCommand command, string query, object?[]? parameters)
75+
/// <summary>
76+
/// Replaces ? placeholders with named parameters and sets up the command.
77+
/// Returns the parameter names for reference.
78+
/// </summary>
79+
private static List<string> PrepareCommandParameters(SqliteCommand command, string query, int parameterCount)
7680
{
77-
if (parameters == null || parameters.Length == 0)
81+
var parameterNames = new List<string>();
82+
83+
if (parameterCount == 0)
7884
{
7985
command.CommandText = query;
80-
return;
86+
return parameterNames;
8187
}
8288

83-
var parameterNames = new List<string>();
84-
8589
// Count placeholders
8690
int placeholderCount = query.Count(c => c == '?');
87-
if (placeholderCount != parameters.Length)
91+
if (placeholderCount != parameterCount)
8892
{
89-
throw new ArgumentException("Number of provided parameters does not match the number of `?` placeholders in the query.");
93+
throw new ArgumentException($"Number of parameters ({parameterCount}) does not match the number of `?` placeholders ({placeholderCount}) in the query.");
9094
}
9195

9296
// Replace `?` sequentially with named parameters
93-
for (int i = 0; i < parameters.Length; i++)
97+
for (int i = 0; i < parameterCount; i++)
9498
{
9599
string paramName = $"@param{i}";
96100
parameterNames.Add(paramName);
@@ -102,17 +106,38 @@ private static void PrepareCommand(SqliteCommand command, string query, object?[
102106
}
103107

104108
query = string.Concat(query.Substring(0, index), paramName, query.Substring(index + 1));
109+
105110
}
106111

107112
command.CommandText = query;
108113

109-
// Add parameters to the command
110-
for (int i = 0; i < parameters.Length; i++)
114+
// Create empty parameter objects
115+
foreach (var paramName in parameterNames)
111116
{
112-
command.Parameters.AddWithValue(parameterNames[i], parameters[i] ?? DBNull.Value);
117+
var parameter = command.CreateParameter();
118+
parameter.ParameterName = paramName;
119+
command.Parameters.Add(parameter);
113120
}
121+
122+
return parameterNames;
114123
}
115124

125+
private static void PrepareCommand(SqliteCommand command, string query, object?[]? parameters)
126+
{
127+
int paramCount = parameters?.Length ?? 0;
128+
PrepareCommandParameters(command, query, paramCount);
129+
130+
// Set the values
131+
if (parameters != null)
132+
{
133+
for (int i = 0; i < parameters.Length; i++)
134+
{
135+
command.Parameters[i].Value = parameters[i] ?? DBNull.Value;
136+
}
137+
}
138+
}
139+
140+
116141
public async Task<NonQueryResult> Execute(string query, object?[]? parameters = null)
117142
{
118143
using var command = Db.CreateCommand();
@@ -127,6 +152,44 @@ public async Task<NonQueryResult> Execute(string query, object?[]? parameters =
127152
};
128153
}
129154

155+
public async Task<NonQueryResult> ExecuteBatch(string query, object?[][]? parameters = null)
156+
{
157+
parameters ??= [];
158+
159+
if (parameters.Length == 0)
160+
{
161+
return new NonQueryResult { RowsAffected = 0 };
162+
}
163+
164+
int totalRowsAffected = 0;
165+
166+
var command = Db.CreateCommand();
167+
168+
// Prepare command once with parameter placeholders
169+
int paramCount = parameters[0]?.Length ?? 0;
170+
PrepareCommandParameters(command, query, paramCount);
171+
172+
// Execute for each parameter set (reuses compiled statement)
173+
foreach (var paramSet in parameters)
174+
{
175+
if (paramSet != null)
176+
{
177+
for (int i = 0; i < paramSet.Length; i++)
178+
{
179+
command.Parameters[i].Value = paramSet[i] ?? DBNull.Value;
180+
}
181+
}
182+
183+
totalRowsAffected += await command.ExecuteNonQueryAsync();
184+
}
185+
186+
return new NonQueryResult
187+
{
188+
RowsAffected = totalRowsAffected,
189+
InsertId = raw.sqlite3_last_insert_rowid(Db.Handle)
190+
};
191+
}
192+
130193
public async Task<QueryResult> ExecuteQuery(string query, object?[]? parameters = null)
131194
{
132195
var result = new QueryResult();

Tests/PowerSync/PowerSync.Common.Tests/Client/PowerSyncDatabaseTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,36 @@ public async Task ReadWhileWriteIsRunningTest()
290290
Assert.Equal(42, result);
291291
}
292292

293+
[Fact]
294+
public async Task BatchExecuteTest()
295+
{
296+
var id1 = Guid.NewGuid().ToString();
297+
var description1 = "Asset 1";
298+
var make1 = "Make 1";
299+
300+
var id2 = Guid.NewGuid().ToString();
301+
var description2 = "Asset 2";
302+
var make2 = "Make 2";
303+
304+
var sql = "INSERT INTO assets (id, description, make) VALUES(?, ?, ?)";
305+
object?[][] parameters = [
306+
[id1, description1, make1],
307+
[id2, description2, make2]
308+
];
309+
310+
await db.ExecuteBatch(sql, parameters);
311+
312+
var result = await db.GetAll<AssetResult>("SELECT id, description, make FROM assets ORDER BY description");
313+
314+
Assert.Equal(2, result.Length);
315+
Assert.Equal(id1, result[0].id);
316+
Assert.Equal(description1, result[0].description);
317+
Assert.Equal(make1, result[0].make);
318+
Assert.Equal(id2, result[1].id);
319+
Assert.Equal(description2, result[1].description);
320+
Assert.Equal(make2, result[1].make);
321+
}
322+
293323
[Fact(Timeout = 2000)]
294324
public async Task QueueSimultaneousExecutionsTest()
295325
{

0 commit comments

Comments
 (0)