Skip to content
This repository was archived by the owner on Feb 29, 2020. It is now read-only.

Commit c5b0b38

Browse files
committed
[client][managed][offline] Implement true upsert
1 parent f02584a commit c5b0b38

File tree

3 files changed

+156
-65
lines changed

3 files changed

+156
-65
lines changed

sdk/Managed/src/Microsoft.WindowsAzure.MobileServices.SQLiteStore/MobileServiceSQLiteStore.cs

+124-52
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class MobileServiceSQLiteStore : MobileServiceLocalStore
2525
/// Note: The default maximum number of parameters allowed by sqlite is 999
2626
/// See: http://www.sqlite.org/limits.html#max_variable_number
2727
/// </summary>
28-
private const int MaxParametersPerUpsertQuery = 800;
28+
private const int MaxParametersPerQuery = 800;
2929

3030
private Dictionary<string, TableDefinition> tableMap = new Dictionary<string, TableDefinition>(StringComparer.OrdinalIgnoreCase);
3131
private SQLiteConnection connection;
@@ -183,64 +183,17 @@ private Task UpsertAsyncInternal(string tableName, IEnumerable<JObject> items, b
183183
return Task.FromResult(0);
184184
}
185185

186-
// Generate the prepared insert statement
187-
string sqlBase = String.Format(
188-
"INSERT OR REPLACE INTO {0} ({1}) VALUES ",
189-
SqlHelpers.FormatTableName(tableName),
190-
String.Join(", ", columns.Select(c => c.Name).Select(SqlHelpers.FormatMember))
191-
);
192-
193-
// Use int division to calculate how many times this record will fit into our parameter quota
194-
int batchSize = MaxParametersPerUpsertQuery / columns.Count;
195-
if (batchSize == 0)
196-
{
197-
throw new InvalidOperationException(string.Format(Properties.Resources.SQLiteStore_TooManyColumns, MaxParametersPerUpsertQuery));
198-
}
199186

200-
foreach (var batch in items.Split(maxLength: batchSize))
201-
{
202-
var sql = new StringBuilder(sqlBase);
203-
var parameters = new Dictionary<string, object>();
187+
this.ExecuteNonQuery("BEGIN TRANSACTION", null);
204188

205-
foreach (JObject item in batch)
206-
{
207-
AppendInsertValuesSql(sql, parameters, columns, item);
208-
sql.Append(",");
209-
}
189+
BatchInsert(tableName, items, columns.Where(c => c.Name.Equals(MobileServiceSystemColumns.Id)).Take(1).ToList());
190+
BatchUpdate(tableName, items, columns);
210191

211-
if (parameters.Any())
212-
{
213-
sql.Remove(sql.Length - 1, 1); // remove the trailing comma
214-
this.ExecuteNonQuery(sql.ToString(), parameters);
215-
}
216-
}
192+
this.ExecuteNonQuery("COMMIT TRANSACTION", null);
217193

218194
return Task.FromResult(0);
219195
}
220196

221-
private static void AppendInsertValuesSql(StringBuilder sql, Dictionary<string, object> parameters, List<ColumnDefinition> columns, JObject item)
222-
{
223-
sql.Append("(");
224-
int colCount = 0;
225-
foreach (var column in columns)
226-
{
227-
if (colCount > 0)
228-
sql.Append(",");
229-
230-
JToken rawValue = item.GetValue(column.Name, StringComparison.OrdinalIgnoreCase);
231-
object value = SqlHelpers.SerializeValue(rawValue, column.StoreType, column.JsonType);
232-
233-
//The paramname for this field must be unique within this statement
234-
string paramName = "@p" + parameters.Count;
235-
236-
sql.Append(paramName);
237-
parameters[paramName] = value;
238-
239-
colCount++;
240-
}
241-
sql.Append(")");
242-
}
243-
244197
/// <summary>
245198
/// Deletes items from local table that match the given query.
246199
/// </summary>
@@ -373,6 +326,110 @@ private void CreateAllTables()
373326
}
374327
}
375328

329+
private void BatchUpdate(string tableName, IEnumerable<JObject> items, List<ColumnDefinition> columns)
330+
{
331+
if (columns.Count <= 1)
332+
{
333+
return; // For update to work there has to be at least once column besides Id that needs to be updated
334+
}
335+
336+
ValidateParameterCount(columns.Count);
337+
338+
string sqlBase = String.Format("UPDATE {0} SET ", SqlHelpers.FormatTableName(tableName));
339+
340+
foreach (JObject item in items)
341+
{
342+
var sql = new StringBuilder(sqlBase);
343+
var parameters = new Dictionary<string, object>();
344+
345+
ColumnDefinition idColumn = columns.FirstOrDefault(c => c.Name.Equals(MobileServiceSystemColumns.Id));
346+
if (idColumn == null)
347+
{
348+
continue;
349+
}
350+
351+
foreach (var column in columns.Where(c => c != idColumn))
352+
{
353+
string paramName = AddParameter(item, parameters, column);
354+
355+
sql.AppendFormat("{0} = {1}", SqlHelpers.FormatMember(column.Name), paramName);
356+
sql.Append(",");
357+
}
358+
359+
if (parameters.Any())
360+
{
361+
sql.Remove(sql.Length - 1, 1); // remove the trailing comma
362+
363+
}
364+
365+
sql.AppendFormat(" WHERE {0} = {1}", SqlHelpers.FormatMember(MobileServiceSystemColumns.Id), AddParameter(item, parameters, idColumn));
366+
367+
this.ExecuteNonQuery(sql.ToString(), parameters);
368+
}
369+
}
370+
371+
private void BatchInsert(string tableName, IEnumerable<JObject> items, List<ColumnDefinition> columns)
372+
{
373+
if (columns.Count == 0) // we need to have some columns to insert the item
374+
{
375+
return;
376+
}
377+
378+
// Generate the prepared insert statement
379+
string sqlBase = String.Format(
380+
"INSERT OR IGNORE INTO {0} ({1}) VALUES ",
381+
SqlHelpers.FormatTableName(tableName),
382+
String.Join(", ", columns.Select(c => c.Name).Select(SqlHelpers.FormatMember))
383+
);
384+
385+
// Use int division to calculate how many times this record will fit into our parameter quota
386+
int batchSize = ValidateParameterCount(columns.Count);
387+
388+
foreach (var batch in items.Split(maxLength: batchSize))
389+
{
390+
var sql = new StringBuilder(sqlBase);
391+
var parameters = new Dictionary<string, object>();
392+
393+
foreach (JObject item in batch)
394+
{
395+
AppendInsertValuesSql(sql, parameters, columns, item);
396+
sql.Append(",");
397+
}
398+
399+
if (parameters.Any())
400+
{
401+
sql.Remove(sql.Length - 1, 1); // remove the trailing comma
402+
this.ExecuteNonQuery(sql.ToString(), parameters);
403+
}
404+
}
405+
}
406+
407+
private static int ValidateParameterCount(int parametersCount)
408+
{
409+
int batchSize = MaxParametersPerQuery / parametersCount;
410+
if (batchSize == 0)
411+
{
412+
throw new InvalidOperationException(string.Format(Properties.Resources.SQLiteStore_TooManyColumns, MaxParametersPerQuery));
413+
}
414+
return batchSize;
415+
}
416+
417+
private static void AppendInsertValuesSql(StringBuilder sql, Dictionary<string, object> parameters, List<ColumnDefinition> columns, JObject item)
418+
{
419+
sql.Append("(");
420+
int colCount = 0;
421+
foreach (var column in columns)
422+
{
423+
if (colCount > 0)
424+
sql.Append(",");
425+
426+
sql.Append(AddParameter(item, parameters, column));
427+
428+
colCount++;
429+
}
430+
sql.Append(")");
431+
}
432+
376433
internal virtual void CreateTableFromObject(string tableName, IEnumerable<ColumnDefinition> columns)
377434
{
378435
ColumnDefinition idColumn = columns.FirstOrDefault(c => c.Name.Equals(MobileServiceSystemColumns.Id));
@@ -404,6 +461,21 @@ internal virtual void CreateTableFromObject(string tableName, IEnumerable<Column
404461
// NOTE: In SQLite you cannot drop columns, only add them.
405462
}
406463

464+
private static string AddParameter(JObject item, Dictionary<string, object> parameters, ColumnDefinition column)
465+
{
466+
JToken rawValue = item.GetValue(column.Name, StringComparison.OrdinalIgnoreCase);
467+
object value = SqlHelpers.SerializeValue(rawValue, column.StoreType, column.JsonType);
468+
string paramName = CreateParameter(parameters, value);
469+
return paramName;
470+
}
471+
472+
private static string CreateParameter(Dictionary<string, object> parameters, object value)
473+
{
474+
string paramName = "@p" + parameters.Count;
475+
parameters[paramName] = value;
476+
return paramName;
477+
}
478+
407479
/// <summary>
408480
/// Executes a sql statement on a given table in local SQLite database.
409481
/// </summary>

sdk/Managed/test/Microsoft.WindowsAzure.MobileServices.SQLiteStore.Test/UnitTests/SQLiteStoreTests.Query.cs

+8-10
Original file line numberDiff line numberDiff line change
@@ -274,13 +274,19 @@ private static async Task<MobileServiceSQLiteStore> SetupMathTestTable(JObject[]
274274
var store = new MobileServiceSQLiteStore(TestDbName);
275275
store.DefineTable(MathTestTable, new JObject()
276276
{
277+
{ "id", String.Empty },
277278
{ "val", 0f },
278279
{ "expected", 0f }
279280
});
280281

281282
await store.InitializeAsync();
282283

283-
await InsertAll(store, MathTestTable, mathTestData);
284+
foreach (JObject item in mathTestData)
285+
{
286+
item[MobileServiceSystemColumns.Id] = Guid.NewGuid().ToString();
287+
}
288+
289+
await store.UpsertAsync(MathTestTable, mathTestData, fromServer: false);
284290

285291
return store;
286292
}
@@ -308,7 +314,7 @@ private static async Task<MobileServiceSQLiteStore> SetupTestTable()
308314

309315
if (!queryTableInitialized)
310316
{
311-
await InsertAll(store, TestTable, testData);
317+
await store.UpsertAsync(TestTable, testData, fromServer: false);
312318
}
313319

314320
queryTableInitialized = true;
@@ -349,13 +355,5 @@ private static async Task<T> Query<T>(MobileServiceSQLiteStore store, string tab
349355
{
350356
return (T)await store.ReadAsync(MobileServiceTableQueryDescription.Parse(tableName, query));
351357
}
352-
353-
private async static Task InsertAll(MobileServiceSQLiteStore store, string tableName, JObject[] items)
354-
{
355-
foreach (JObject item in items)
356-
{
357-
await store.UpsertAsync(tableName, new[] { item }, fromServer: false);
358-
}
359-
}
360358
}
361359
}

sdk/Managed/test/Microsoft.WindowsAzure.MobileServices.SQLiteStore.Test/UnitTests/SQLiteStoreTests.cs

+24-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public class SQLiteStoreTests : TestBase
2424
[AsyncTestMethod]
2525
public async Task InitializeAsync_InitializesTheStore()
2626
{
27+
TestUtilities.DropTestTable(TestDbName, TestTable);
28+
2729
var store = new MobileServiceSQLiteStore(TestDbName);
2830
store.DefineTable(TestTable, new JObject()
2931
{
@@ -221,6 +223,8 @@ public void UpsertAsync_Throws_WhenStoreIsNotInitialized()
221223
[AsyncTestMethod]
222224
public async Task UpsertAsync_Throws_WhenColumnInItemIsNotDefinedAndItIsLocal()
223225
{
226+
TestUtilities.DropTestTable(TestDbName, TestTable);
227+
224228
using (var store = new MobileServiceSQLiteStore(TestDbName))
225229
{
226230
store.DefineTable(TestTable, new JObject()
@@ -240,6 +244,8 @@ public async Task UpsertAsync_Throws_WhenColumnInItemIsNotDefinedAndItIsLocal()
240244
[AsyncTestMethod]
241245
public async Task UpsertAsync_DoesNotThrow_WhenColumnInItemIsNotDefinedAndItIsFromServer()
242246
{
247+
TestUtilities.DropTestTable(TestDbName, TestTable);
248+
243249
using (var store = new MobileServiceSQLiteStore(TestDbName))
244250
{
245251
store.DefineTable(TestTable, new JObject()
@@ -250,13 +256,20 @@ public async Task UpsertAsync_DoesNotThrow_WhenColumnInItemIsNotDefinedAndItIsFr
250256

251257
await store.InitializeAsync();
252258

253-
await store.UpsertAsync(TestTable, new[] { new JObject() { { "notDefined", "okok" }, { "dob", DateTime.UtcNow } } }, fromServer: true);
259+
await store.UpsertAsync(TestTable, new[] { new JObject()
260+
{
261+
{ "id", "abc" },
262+
{ "notDefined", "okok" },
263+
{ "dob", DateTime.UtcNow }
264+
} }, fromServer: true);
254265
}
255266
}
256267

257268
[AsyncTestMethod]
258269
public async Task UpsertAsync_DoesNotThrow_WhenItemIsEmpty()
259270
{
271+
TestUtilities.DropTestTable(TestDbName, TestTable);
272+
260273
using (var store = new MobileServiceSQLiteStore(TestDbName))
261274
{
262275
store.DefineTable(TestTable, new JObject()
@@ -347,6 +360,7 @@ public async Task UpsertAsync_UpdatesTheRow_WhenItExists()
347360
await store.UpsertAsync(TestTable, new[]{new JObject()
348361
{
349362
{ "id", "abc" },
363+
{ "text", "xyz" },
350364
{ "__createdAt", DateTime.Now }
351365
}}, fromServer: false);
352366

@@ -355,6 +369,12 @@ await store.UpsertAsync(TestTable, new[]{new JObject()
355369
{ "id", "abc" },
356370
{ "__createdAt", new DateTime(200,1,1) }
357371
}}, fromServer: false);
372+
373+
JObject result = await store.LookupAsync(TestTable, "abc");
374+
375+
Assert.AreEqual(result.Value<string>("id"), "abc");
376+
Assert.AreEqual(result.Value<string>("text"), "xyz");
377+
Assert.AreEqual(result.Value<string>("__createdAt"), "01/01/0200 00:00:00");
358378
}
359379
long count = TestUtilities.CountRows(TestDbName, TestTable);
360380
Assert.AreEqual(count, 1L);
@@ -532,8 +552,9 @@ public static void DefineTestTable(MobileServiceSQLiteStore store)
532552
{
533553
store.DefineTable(TestTable, new JObject()
534554
{
535-
{"id", String.Empty },
536-
{"__createdAt", DateTime.Now}
555+
{ "id", String.Empty },
556+
{ "text", String.Empty },
557+
{ "__createdAt", DateTime.Now }
537558
});
538559
}
539560
}

0 commit comments

Comments
 (0)