@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 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
- * `INSERT OR REPLACE … SELECT` — `_syncular_version` lands in
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
- return `INSERT OR REPLACE INTO ${quoteIdent(table.name)} (${names.join(', ')}) VALUES (${placeholders})`;
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
- * `INSERT OR REPLACE … SELECT` — `_syncular_version` lands in
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 replace-or-upsert.
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 OR REPLACE INTO ${quoteIdent(table.name)}
249
- (${[...names, quoteIdent(SYNC_VERSION_COLUMN)].join(', ')})
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.13.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.13.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.13.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
- return `INSERT OR REPLACE INTO ${quoteIdent(table.name)} (${names.join(', ')}) VALUES (${placeholders})`;
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
- * `INSERT OR REPLACE … SELECT` — `_syncular_version` lands in
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 replace-or-upsert.
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 OR REPLACE INTO ${quoteIdent(table.name)}
376
- (${[...names, quoteIdent(SYNC_VERSION_COLUMN)].join(', ')})
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)}`,