Skip to content

SqliteSyncProvider always reports a conflict when applying changes where no tracking row exists #1373

@kmgallahan

Description

@kmgallahan

As per this discussion: #1372, it was discovered that the SqliteSyncProvider will always report conflicts when:

  • A row exists in a client SQLite DB
  • The row is updated elsewhere and synced to the client
  • There is no _tracking entry for the row

With much digging and AI assistance the faulty code was identified:

stringBuilder.AppendLine($"WHERE ([side].[timestamp] < @sync_min_timestamp OR [side].[update_scope_id] = @sync_scope_id) ");
stringBuilder.Append($"OR ({SqliteManagementUtils.WhereColumnIsNull(this.TableDescription.PrimaryKeys, "[base]")} ");
stringBuilder.AppendLine($"AND ([side].[timestamp] < @sync_min_timestamp OR [side].[timestamp] IS NULL)) ");
stringBuilder.Append($"OR @sync_force_write = 1");

This creates a WHERE clause:

LEFT JOIN [Row_tracking] AS [side] ON [side].[Id] = @Id
LEFT JOIN [Row] AS [base] ON [base].[Id] = @Id
WHERE ([side].[timestamp] <= @sync_min_timestamp OR [side].[update_scope_id] = @sync_scope_id) 
OR ([base].[Id] IS NULL  AND ([side].[timestamp] <= @sync_min_timestamp OR [side].[timestamp] IS NULL)) 

Which has the following issue:

Scenario A: The Row DOES NOT exist (New Record)
If [base].[Id] is NULL (it's a new insert):

[base].[Id] IS NULL is TRUE.
Inside the inner parentheses:
[side].[timestamp] <= ... is FALSE.
[side].[timestamp] IS NULL is TRUE (Because there is no tracking row).
The logic becomes: TRUE AND (FALSE OR TRUE) -> TRUE.
Outcome: The row IS selected. The sync proceeds to insert the new row.

Scenario B: The Row ALREADY exists
If [base].[Id] is NOT NULL (the row is there):

[base].[Id] IS NULL is FALSE.
The AND condition immediately fails.
Outcome: The row IS NOT selected. The sync skips this update (unless the @sync_force_write flag from the wider query was set to 1).

This is resolved by modifying the SQL builder to handle cases correctly:

// 1. Standard Update: Only allow if row exists (NOT NULL) AND Tracking conditions are met
// We verify [base] is NOT NULL to ensure we don't implicitly resurrect a tombstoned row here.
stringBuilder.Append($"WHERE (NOT ({SqliteManagementUtils.WhereColumnIsNull(this.TableDescription.PrimaryKeys, "[base]")}) ");
stringBuilder.AppendLine($"AND ([side].[timestamp] <= @sync_min_timestamp OR [side].[update_scope_id] = @sync_scope_id)) ");

// 2. Missing Tracking: Allow write if Tracking row is missing (New Inserts or Data Update)
stringBuilder.AppendLine($"OR ({SqliteManagementUtils.WhereColumnIsNull(this.TableDescription.PrimaryKeys, "[side]")}) ");

// 3. Force Write
stringBuilder.Append($"OR @sync_force_write = 1");

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions