@syncular/client 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apply.d.ts +1 -1
- package/dist/apply.js +18 -6
- package/package.json +3 -3
- package/src/apply.ts +18 -6
package/dist/apply.d.ts
CHANGED
|
@@ -61,7 +61,7 @@ export interface SqliteSegmentDescriptor {
|
|
|
61
61
|
* the in-file metadata against the descriptor, validate the data table's
|
|
62
62
|
* column names/order against the generated schema, run the §5.6
|
|
63
63
|
* first-page clear when fresh, then copy every row with a single
|
|
64
|
-
*
|
|
64
|
+
* one primary-key upsert — `_syncular_version` lands in
|
|
65
65
|
* `_sync_version` exactly like a rows segment's per-row `serverVersion`.
|
|
66
66
|
*/
|
|
67
67
|
export declare function applySqliteSegment(db: ClientDatabase, schema: CompiledClientSchema, table: CompiledClientTable, bytes: Uint8Array, descriptor: SqliteSegmentDescriptor, options: {
|
package/dist/apply.js
CHANGED
|
@@ -12,7 +12,12 @@ function upsertSql(table) {
|
|
|
12
12
|
quoteIdent(SYNC_VERSION_COLUMN),
|
|
13
13
|
];
|
|
14
14
|
const placeholders = names.map(() => '?').join(', ');
|
|
15
|
-
|
|
15
|
+
const updates = names
|
|
16
|
+
.filter((name) => name !== quoteIdent(table.primaryKey))
|
|
17
|
+
.map((name) => `${name}=excluded.${name}`)
|
|
18
|
+
.join(', ');
|
|
19
|
+
return `INSERT INTO ${quoteIdent(table.name)} (${names.join(', ')}) VALUES (${placeholders})
|
|
20
|
+
ON CONFLICT (${quoteIdent(table.primaryKey)}) DO UPDATE SET ${updates}`;
|
|
16
21
|
}
|
|
17
22
|
export function upsertLocalRow(db, table, values, syncVersion) {
|
|
18
23
|
db.exec(upsertSql(table), [...values.map(toSqlValue), syncVersion]);
|
|
@@ -182,7 +187,7 @@ function imageInvalid(detail) {
|
|
|
182
187
|
* the in-file metadata against the descriptor, validate the data table's
|
|
183
188
|
* column names/order against the generated schema, run the §5.6
|
|
184
189
|
* first-page clear when fresh, then copy every row with a single
|
|
185
|
-
*
|
|
190
|
+
* one primary-key upsert — `_syncular_version` lands in
|
|
186
191
|
* `_sync_version` exactly like a rows segment's per-row `serverVersion`.
|
|
187
192
|
*/
|
|
188
193
|
export function applySqliteSegment(db, schema, table, bytes, descriptor, options) {
|
|
@@ -239,16 +244,23 @@ export function applySqliteSegment(db, schema, table, bytes, descriptor, options
|
|
|
239
244
|
expected.some((name, index) => actual[index] !== name)) {
|
|
240
245
|
throw new ClientSyncError('sync.schema_mismatch', `sqlite segment for ${JSON.stringify(table.name)} does not match the generated schema: columns [${actual.join(', ')}] (§5.3)`);
|
|
241
246
|
}
|
|
242
|
-
// 3. One transaction: fresh-bootstrap clear, then
|
|
247
|
+
// 3. One transaction: fresh-bootstrap clear, then primary-key upsert.
|
|
243
248
|
const names = table.columns.map((column) => quoteIdent(column.name));
|
|
249
|
+
const insertNames = [...names, quoteIdent(SYNC_VERSION_COLUMN)];
|
|
250
|
+
const updates = insertNames
|
|
251
|
+
.filter((name) => name !== quoteIdent(table.primaryKey))
|
|
252
|
+
.map((name) => `${name}=excluded.${name}`)
|
|
253
|
+
.join(', ');
|
|
244
254
|
return (options.transaction ?? ((fn) => db.transaction(fn)))(() => {
|
|
245
255
|
if (options.clearFirst) {
|
|
246
256
|
deleteScopedRows(db, table, options.effective);
|
|
247
257
|
}
|
|
248
|
-
db.exec(`INSERT
|
|
249
|
-
(${
|
|
258
|
+
db.exec(`INSERT INTO ${quoteIdent(table.name)}
|
|
259
|
+
(${insertNames.join(', ')})
|
|
250
260
|
SELECT ${[...names, quoteIdent('_syncular_version')].join(', ')}
|
|
251
|
-
FROM ${IMAGE_ALIAS}.${quoteIdent(table.name)}
|
|
261
|
+
FROM ${IMAGE_ALIAS}.${quoteIdent(table.name)}
|
|
262
|
+
WHERE true
|
|
263
|
+
ON CONFLICT (${quoteIdent(table.primaryKey)}) DO UPDATE SET ${updates}`);
|
|
252
264
|
const counted = db.query(`SELECT count(*) AS n FROM ${IMAGE_ALIAS}.${quoteIdent(table.name)}`)[0];
|
|
253
265
|
const applied = Number(counted?.n ?? 0);
|
|
254
266
|
if (applied !== descriptor.rowCount) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Syncular TypeScript client core — offline-first sync over SQLite (WASM/OPFS, Bun, Node)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
83
|
"@sqlite.org/sqlite-wasm": "^3.53.0-build1",
|
|
84
|
-
"@syncular/core": "0.
|
|
84
|
+
"@syncular/core": "0.14.0"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"better-sqlite3": ">=11"
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
|
-
"@syncular/server": "0.
|
|
95
|
+
"@syncular/server": "0.14.0",
|
|
96
96
|
"@types/better-sqlite3": "^7.6.13",
|
|
97
97
|
"better-sqlite3": "^12.11.1"
|
|
98
98
|
}
|
package/src/apply.ts
CHANGED
|
@@ -31,7 +31,12 @@ function upsertSql(table: CompiledClientTable): string {
|
|
|
31
31
|
quoteIdent(SYNC_VERSION_COLUMN),
|
|
32
32
|
];
|
|
33
33
|
const placeholders = names.map(() => '?').join(', ');
|
|
34
|
-
|
|
34
|
+
const updates = names
|
|
35
|
+
.filter((name) => name !== quoteIdent(table.primaryKey))
|
|
36
|
+
.map((name) => `${name}=excluded.${name}`)
|
|
37
|
+
.join(', ');
|
|
38
|
+
return `INSERT INTO ${quoteIdent(table.name)} (${names.join(', ')}) VALUES (${placeholders})
|
|
39
|
+
ON CONFLICT (${quoteIdent(table.primaryKey)}) DO UPDATE SET ${updates}`;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
export function upsertLocalRow(
|
|
@@ -285,7 +290,7 @@ function imageInvalid(detail: string): never {
|
|
|
285
290
|
* the in-file metadata against the descriptor, validate the data table's
|
|
286
291
|
* column names/order against the generated schema, run the §5.6
|
|
287
292
|
* first-page clear when fresh, then copy every row with a single
|
|
288
|
-
*
|
|
293
|
+
* one primary-key upsert — `_syncular_version` lands in
|
|
289
294
|
* `_sync_version` exactly like a rows segment's per-row `serverVersion`.
|
|
290
295
|
*/
|
|
291
296
|
export function applySqliteSegment(
|
|
@@ -365,17 +370,24 @@ export function applySqliteSegment(
|
|
|
365
370
|
);
|
|
366
371
|
}
|
|
367
372
|
|
|
368
|
-
// 3. One transaction: fresh-bootstrap clear, then
|
|
373
|
+
// 3. One transaction: fresh-bootstrap clear, then primary-key upsert.
|
|
369
374
|
const names = table.columns.map((column) => quoteIdent(column.name));
|
|
375
|
+
const insertNames = [...names, quoteIdent(SYNC_VERSION_COLUMN)];
|
|
376
|
+
const updates = insertNames
|
|
377
|
+
.filter((name) => name !== quoteIdent(table.primaryKey))
|
|
378
|
+
.map((name) => `${name}=excluded.${name}`)
|
|
379
|
+
.join(', ');
|
|
370
380
|
return (options.transaction ?? ((fn) => db.transaction(fn)))(() => {
|
|
371
381
|
if (options.clearFirst) {
|
|
372
382
|
deleteScopedRows(db, table, options.effective);
|
|
373
383
|
}
|
|
374
384
|
db.exec(
|
|
375
|
-
`INSERT
|
|
376
|
-
(${
|
|
385
|
+
`INSERT INTO ${quoteIdent(table.name)}
|
|
386
|
+
(${insertNames.join(', ')})
|
|
377
387
|
SELECT ${[...names, quoteIdent('_syncular_version')].join(', ')}
|
|
378
|
-
FROM ${IMAGE_ALIAS}.${quoteIdent(table.name)}
|
|
388
|
+
FROM ${IMAGE_ALIAS}.${quoteIdent(table.name)}
|
|
389
|
+
WHERE true
|
|
390
|
+
ON CONFLICT (${quoteIdent(table.primaryKey)}) DO UPDATE SET ${updates}`,
|
|
379
391
|
);
|
|
380
392
|
const counted = db.query(
|
|
381
393
|
`SELECT count(*) AS n FROM ${IMAGE_ALIAS}.${quoteIdent(table.name)}`,
|