You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: add block-level LWW for fine-grained text conflict resolution (#16)
* feat: add block-level LWW for fine-grained text conflict resolution
Implements block-level Last-Writer-Wins for text columns across SQLite
and PostgreSQL. Text is split into blocks (lines by default) and each
block is tracked independently, so concurrent edits to different parts
of the same text are preserved after sync.
- Add block.c/block.h with split, diff, position, and materialize logic
- Add fractional-indexing submodule for stable block ordering
- Add cloudsync_set_column() and cloudsync_text_materialize() functions
- Add cross-platform SQL abstractions for blocks table (SQLite/PostgreSQL)
- Add block handling to PG insert, update, col_value, and set_column
- Move network code to src/network/ directory
- Bump version to 0.9.200
- Add 36 SQLite block LWW unit tests and 7 PostgreSQL test files
- Update README and API docs with block-level LWW documentation
* fix(ci): checkout submodules in GitHub Actions workflow
**Description:** Configures per-column settings for a synchronized table. This function is primarily used to enable **block-level LWW** on text columns, allowing fine-grained conflict resolution at the line (or paragraph) level instead of the entire cell.
184
+
185
+
When block-level LWW is enabled on a column, INSERT and UPDATE operations automatically split the text into blocks using a delimiter (default: newline `\n`) and track each block independently. During sync, changes are merged block-by-block, so concurrent edits to different parts of the same text are preserved.
186
+
187
+
**Parameters:**
188
+
189
+
-`table_name` (TEXT): The name of the synchronized table.
190
+
-`col_name` (TEXT): The name of the text column to configure.
191
+
-`key` (TEXT): The setting key. Supported keys:
192
+
-`'algo'` — Set the column algorithm. Use value `'block'` to enable block-level LWW.
193
+
-`'delimiter'` — Set the block delimiter string. Only applies to columns with block-level LWW enabled.
194
+
-`value` (TEXT): The setting value.
195
+
196
+
**Returns:** None.
197
+
198
+
**Example:**
199
+
200
+
```sql
201
+
-- Enable block-level LWW on a column (splits text by newline by default)
**Description:** Reconstructs the full text of a block-level LWW column from its individual blocks and writes the result back to the base table column. This is useful after a merge operation to ensure the column contains the up-to-date materialized text.
215
+
216
+
After a sync/merge, the column is updated automatically. This function is primarily useful for manual materialization or debugging.
217
+
218
+
**Parameters:**
219
+
220
+
-`table_name` (TEXT): The name of the table.
221
+
-`col_name` (TEXT): The name of the block-level LWW column.
222
+
-`pk_values...` (variadic): The primary key values identifying the row. For composite primary keys, pass each key value as a separate argument in declaration order.
@@ -32,6 +34,7 @@ In simple terms, CRDTs make it possible for multiple users to **edit shared data
32
34
33
35
-**Offline-First by Design**: Works seamlessly even when devices are offline. Changes are queued locally and synced automatically when connectivity is restored.
34
36
-**CRDT-Based Conflict Resolution**: Merges updates deterministically and efficiently, ensuring eventual consistency across all replicas without the need for complex merge logic.
37
+
-**Block-Level LWW for Text**: Fine-grained conflict resolution for text columns. Instead of overwriting the entire cell, changes are tracked and merged at the line (or paragraph) level, so concurrent edits to different parts of the same text are preserved.
35
38
-**Embedded Network Layer**: No external libraries or sync servers required. SQLiteSync handles connection setup, message encoding, retries, and state reconciliation internally.
36
39
-**Drop-in Simplicity**: Just load the extension into SQLite and start syncing. No need to implement custom protocols or state machines.
37
40
-**Efficient and Resilient**: Optimized binary encoding, automatic batching, and robust retry logic make synchronization fast and reliable even on flaky networks.
@@ -69,6 +72,30 @@ For example:
69
72
70
73
For more information, see the [SQLite Cloud RLS documentation](https://docs.sqlitecloud.io/docs/rls).
71
74
75
+
## Block-Level LWW
76
+
77
+
Standard CRDT sync resolves conflicts at the **cell level**: if two devices edit the same column of the same row, one value wins entirely. This works well for short values like names or statuses, but for longer text content — documents, notes, descriptions — it means the entire text is replaced even if the edits were in different parts.
78
+
79
+
**Block-Level LWW** (Last-Writer-Wins) solves this by splitting text columns into **blocks** (lines by default) and tracking each block independently. When two devices edit different lines of the same text, **both edits are preserved** after sync. Only when two devices edit the *same* line does LWW conflict resolution apply.
80
+
81
+
### How It Works
82
+
83
+
1.**Enable block tracking** on a text column using `cloudsync_set_column()`.
84
+
2. On INSERT or UPDATE, SQLite Sync automatically splits the text into blocks using the configured delimiter (default: newline `\n`).
85
+
3. Each block gets a unique fractional index position, enabling insertions between existing blocks without reindexing.
86
+
4. During sync, changes are merged block-by-block rather than replacing the whole cell.
87
+
5. Use `cloudsync_text_materialize()` to reconstruct the full text from blocks on demand, or read the column directly (it is updated automatically after merge).
88
+
89
+
### Key Properties
90
+
91
+
-**Non-conflicting edits are preserved**: Two users editing different lines of the same document both see their changes after sync.
92
+
-**Same-line conflicts use LWW**: If two users edit the same line, the last writer wins — consistent with standard CRDT behavior.
93
+
-**Custom delimiters**: Use paragraph separators (`\n\n`), sentence boundaries, or any string as the block delimiter.
94
+
-**Mixed columns**: A table can have both regular LWW columns and block-level LWW columns side by side.
95
+
-**Transparent reads**: The base column always contains the current full text. Block tracking is an internal mechanism; your queries work unchanged.
96
+
97
+
For setup instructions and a complete example, see [Block-Level LWW Example](#block-level-lww-example). For API details, see the [API Reference](./API.md).
98
+
72
99
### What Can You Build with SQLite Sync?
73
100
74
101
SQLite Sync is ideal for building collaborative and distributed apps across web, mobile, desktop, and edge platforms. Some example use cases include:
@@ -108,6 +135,7 @@ SQLite Sync is ideal for building collaborative and distributed apps across web,
108
135
For detailed information on all available functions, their parameters, and examples, refer to the [comprehensive API Reference](./API.md). The API includes:
109
136
110
137
-**Configuration Functions** — initialize, enable, and disable sync on tables
138
+
-**Block-Level LWW Functions** — configure block tracking on text columns and materialize text from blocks
111
139
-**Helper Functions** — version info, site IDs, UUID generation
112
140
-**Schema Alteration Functions** — safely alter synced tables
113
141
-**Network Functions** — connect, authenticate, send/receive changes, and monitor sync status
See the [examples](./examples/simple-todo-db/) directory for a comprehensive walkthrough including:
354
382
- Multi-device collaboration
355
-
- Offline scenarios
383
+
- Offline scenarios
356
384
- Row-level security setup
357
385
- Conflict resolution demonstrations
358
386
387
+
## Block-Level LWW Example
388
+
389
+
This example shows how to enable block-level text sync on a notes table, so that concurrent edits to different lines are merged instead of overwritten.
390
+
391
+
### Setup
392
+
393
+
```sql
394
+
-- Load the extension
395
+
.load ./cloudsync
396
+
397
+
-- Create a table with a text column for long-form content
After this setup, every INSERT or UPDATE to the `body` column automatically splits the text into blocks (one per line) and tracks each block independently.
412
+
413
+
### Two-Device Scenario
414
+
415
+
```sql
416
+
-- Device A: create a note
417
+
INSERT INTO notes (id, title, body) VALUES (
418
+
'note-001',
419
+
'Meeting Notes',
420
+
'Line 1: Welcome
421
+
Line 2: Agenda
422
+
Line 3: Action items'
423
+
);
424
+
425
+
-- Sync Device A -> Cloud -> Device B
426
+
-- (Both devices now have the same 3-line note)
427
+
```
428
+
429
+
```sql
430
+
-- Device A (offline): edit line 1
431
+
UPDATE notes SET body ='Line 1: Welcome everyone
432
+
Line 2: Agenda
433
+
Line 3: Action items'WHERE id ='note-001';
434
+
435
+
-- Device B (offline): edit line 3
436
+
UPDATE notes SET body ='Line 1: Welcome
437
+
Line 2: Agenda
438
+
Line 3: Action items - DONE'WHERE id ='note-001';
439
+
```
440
+
441
+
```sql
442
+
-- After both devices sync, the merged result is:
443
+
-- 'Line 1: Welcome everyone
444
+
-- Line 2: Agenda
445
+
-- Line 3: Action items - DONE'
446
+
--
447
+
-- Both edits are preserved because they affected different lines.
448
+
```
449
+
450
+
### Custom Delimiter
451
+
452
+
For paragraph-level tracking (useful for long-form documents), set a custom delimiter:
453
+
454
+
```sql
455
+
-- Use double newline as delimiter (paragraph separator)
0 commit comments