@syncular/client 0.30.10 → 0.30.11

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.
@@ -1,7 +1,21 @@
1
1
  import { ClientSyncError } from './errors';
2
2
  import type { ClientDatabase } from './database';
3
- import type { CompiledClientSchema, JsonRowValue } from './schema';
4
- import { jsonToRowValue, quoteIdent } from './schema';
3
+ import type {
4
+ CompiledClientSchema,
5
+ CompiledClientTable,
6
+ JsonRowValue,
7
+ } from './schema';
8
+ import {
9
+ mapRowValues,
10
+ fromSqlValue,
11
+ jsonToRowValue,
12
+ quoteIdent,
13
+ rowValueToJson,
14
+ SYNC_VERSION_COLUMN,
15
+ toSqlValue,
16
+ } from './schema';
17
+ import type { RowValue } from '@syncular/core';
18
+ import type { RetainedCommitRow } from './outcomes';
5
19
  import type {
6
20
  OutboxCommit,
7
21
  OutboxBeforeImage,
@@ -11,29 +25,78 @@ import { deleteLocalRow, upsertLocalRow } from './apply';
11
25
 
12
26
  /** Protected failure state. The server base advances independently of intent. */
13
27
  export function ensureFailedOverlaySchema(db: ClientDatabase): void {
14
- db.exec(`CREATE TABLE IF NOT EXISTS _syncular_failed_rows(
15
- commit_id TEXT NOT NULL, idx INTEGER NOT NULL,
16
- tbl TEXT NOT NULL, id TEXT NOT NULL, at INTEGER NOT NULL,
17
- op TEXT NOT NULL, intent TEXT, base TEXT, version INTEGER,
18
- PRIMARY KEY(commit_id, idx))`);
28
+ db.exec(
29
+ 'CREATE TABLE IF NOT EXISTS _syncular_failed_rows(commit_id TEXT NOT NULL,idx INTEGER NOT NULL,tbl TEXT NOT NULL,id TEXT NOT NULL,at INTEGER NOT NULL,op TEXT NOT NULL,intent TEXT,base TEXT,version INTEGER,unique_conflicts TEXT,PRIMARY KEY(commit_id,idx))',
30
+ );
31
+ if (
32
+ !db
33
+ .query('PRAGMA table_info(_syncular_failed_rows)')
34
+ .some((row) => row.name === 'unique_conflicts')
35
+ )
36
+ db.exec(
37
+ 'ALTER TABLE _syncular_failed_rows ADD COLUMN unique_conflicts TEXT',
38
+ );
19
39
  }
20
40
 
21
41
  export function retainedBaseWrite(
22
42
  db: ClientDatabase,
23
- table: string,
43
+ table: CompiledClientTable,
24
44
  rowId: string,
25
45
  values?: Readonly<Record<string, JsonRowValue>>,
26
46
  version?: number,
27
47
  ): void {
28
48
  db.exec(
29
- 'UPDATE _syncular_failed_rows SET base = ?, version = ? WHERE tbl = ? AND id = ?',
49
+ 'UPDATE _syncular_failed_rows SET base=?,version=? WHERE tbl=? AND id=?',
30
50
  [
31
51
  values === undefined ? null : JSON.stringify(values),
32
52
  version ?? null,
33
- table,
53
+ table.name,
34
54
  rowId,
35
55
  ],
36
56
  );
57
+ if (!table.indexes.some((index) => index.unique)) return;
58
+ for (const retained of db.query(
59
+ 'SELECT commit_id,idx,intent,unique_conflicts FROM _syncular_failed_rows WHERE tbl=?',
60
+ [table.name],
61
+ )) {
62
+ const prior: readonly (Omit<
63
+ NonNullable<RetainedCommitRow['uniqueConflicts']>[number],
64
+ 'serverRow'
65
+ > & { serverRow: Readonly<Record<string, JsonRowValue>> })[] =
66
+ retained.unique_conflicts === null
67
+ ? []
68
+ : JSON.parse(String(retained.unique_conflicts));
69
+ const conflicts = prior.filter((conflict) => conflict.rowId !== rowId);
70
+ if (values !== undefined && retained.intent !== null) {
71
+ const intent: Record<string, JsonRowValue> = JSON.parse(
72
+ String(retained.intent),
73
+ );
74
+ conflicts.push(
75
+ ...uniqueConflicts(db, table, mapRowValues(intent, jsonToRowValue))
76
+ .filter((conflict) => conflict.rowId === rowId)
77
+ .map((conflict) => ({
78
+ ...conflict,
79
+ serverRow: mapRowValues(conflict.serverRow, rowValueToJson),
80
+ })),
81
+ );
82
+ }
83
+ db.exec(
84
+ 'UPDATE _syncular_failed_rows SET unique_conflicts=? WHERE commit_id=? AND idx=?',
85
+ [
86
+ conflicts.length
87
+ ? JSON.stringify(
88
+ conflicts.sort(
89
+ (a, b) =>
90
+ table.indexes.findIndex((index) => index.name === a.index) -
91
+ table.indexes.findIndex((index) => index.name === b.index),
92
+ ),
93
+ )
94
+ : null,
95
+ String(retained.commit_id),
96
+ Number(retained.idx),
97
+ ],
98
+ );
99
+ }
37
100
  }
38
101
 
39
102
  export function retainFailedRows(
@@ -53,8 +116,7 @@ export function retainFailedRows(
53
116
  'retained intent has no before-image',
54
117
  );
55
118
  db.exec(
56
- `INSERT INTO _syncular_failed_rows(commit_id, idx, tbl, id, at, op, intent, base, version)
57
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
119
+ 'INSERT INTO _syncular_failed_rows(commit_id,idx,tbl,id,at,op,intent,base,version)VALUES(?,?,?,?,?,?,?,?,?)',
58
120
  [
59
121
  commit.clientCommitId,
60
122
  index,
@@ -70,6 +132,40 @@ export function retainFailedRows(
70
132
  }
71
133
  }
72
134
 
135
+ /** Match declared keys through SQLite, preserving index affinity and NULL semantics. */
136
+ export function uniqueConflicts(
137
+ db: ClientDatabase,
138
+ table: CompiledClientTable,
139
+ values: Readonly<Record<string, RowValue>>,
140
+ ): NonNullable<RetainedCommitRow['uniqueConflicts']> {
141
+ return table.indexes
142
+ .filter((index) => index.unique)
143
+ .flatMap((index) =>
144
+ db
145
+ .query(
146
+ `SELECT * FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)} !=? AND ${index.columns.map((column) => `${quoteIdent(column)} = ?`).join(' AND ')}`,
147
+ [
148
+ toSqlValue(values[table.primaryKey] ?? null),
149
+ ...index.columns.map((column) =>
150
+ toSqlValue(values[column] ?? null),
151
+ ),
152
+ ],
153
+ )
154
+ .map((row) => ({
155
+ index: index.name,
156
+ columns: index.columns,
157
+ rowId: String(row[table.primaryKey]),
158
+ serverRow: Object.fromEntries(
159
+ table.columns.map((column) => [
160
+ column.name,
161
+ fromSqlValue(column, row[column.name] ?? null),
162
+ ]),
163
+ ),
164
+ serverVersion: Number(row[SYNC_VERSION_COLUMN]),
165
+ })),
166
+ );
167
+ }
168
+
73
169
  export function restoreFailedBases(
74
170
  db: ClientDatabase,
75
171
  schema: CompiledClientSchema,
@@ -77,7 +173,7 @@ export function restoreFailedBases(
77
173
  ): void {
78
174
  const seen = new Set<string>();
79
175
  for (const row of db.query(
80
- 'SELECT * FROM _syncular_failed_rows ORDER BY at, commit_id, idx',
176
+ 'SELECT * FROM _syncular_failed_rows ORDER BY at,commit_id,idx',
81
177
  )) {
82
178
  const key = JSON.stringify([row.tbl, row.id]);
83
179
  if (!absentIntent && seen.has(key)) continue;
@@ -89,11 +185,48 @@ export function restoreFailedBases(
89
185
  'retained intent references an unknown table',
90
186
  );
91
187
  const json = absentIntent ? row.intent : row.base;
188
+ if (absentIntent && row.intent !== null) {
189
+ const values: Record<string, JsonRowValue> = JSON.parse(
190
+ String(row.intent),
191
+ );
192
+ const conflicts = uniqueConflicts(
193
+ db,
194
+ table,
195
+ mapRowValues(values, jsonToRowValue),
196
+ );
197
+ // Hydrate a pre-0.30.11 failure after its winner was already imported.
198
+ // Pending and retained overlays cannot supply authoritative row evidence.
199
+ if (row.unique_conflicts === null) {
200
+ const authorized = conflicts.filter(
201
+ (conflict) =>
202
+ conflict.serverVersion >= 0 &&
203
+ !db.query(
204
+ "SELECT 1 FROM _syncular_failed_rows WHERE tbl=? AND id=? UNION ALL SELECT 1 FROM _syncular_outbox,json_each(operations)WHERE json_extract(json_each.value,'$.table')=? AND json_extract(json_each.value,'$.rowId')=? LIMIT 1",
205
+ [table.name, conflict.rowId, table.name, conflict.rowId],
206
+ ).length,
207
+ );
208
+ if (authorized.length)
209
+ db.exec(
210
+ 'UPDATE _syncular_failed_rows SET unique_conflicts=? WHERE commit_id=? AND idx=?',
211
+ [
212
+ JSON.stringify(
213
+ authorized.map((conflict) => ({
214
+ ...conflict,
215
+ serverRow: mapRowValues(conflict.serverRow, rowValueToJson),
216
+ })),
217
+ ),
218
+ String(row.commit_id),
219
+ Number(row.idx),
220
+ ],
221
+ );
222
+ }
223
+ if (conflicts.length) continue;
224
+ }
92
225
  if (
93
226
  absentIntent &&
94
227
  (json === null ||
95
228
  db.query(
96
- `SELECT 1 FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)} = ?`,
229
+ `SELECT 1 FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}=?`,
97
230
  [String(row.id)],
98
231
  ).length)
99
232
  )
@@ -134,7 +267,7 @@ export function failedOverlayCommits(db: ClientDatabase): OutboxCommit[] {
134
267
  }
135
268
  >();
136
269
  for (const row of db.query(
137
- 'SELECT * FROM _syncular_failed_rows ORDER BY at, commit_id, idx',
270
+ 'SELECT * FROM _syncular_failed_rows ORDER BY at,commit_id,idx',
138
271
  )) {
139
272
  const id = String(row.commit_id);
140
273
  const operation: OutboxOperation = JSON.parse(String(row.op));
@@ -168,7 +301,12 @@ export function dropFailedRows(
168
301
  const dropped = failedOverlayCommits(db).filter((commit) =>
169
302
  ids.has(commit.clientCommitId),
170
303
  );
171
- for (const id of ids)
172
- db.exec('DELETE FROM _syncular_failed_rows WHERE commit_id = ?', [id]);
304
+ for (const id of ids) {
305
+ db.exec('DELETE FROM _syncular_failed_rows WHERE commit_id=?', [id]);
306
+ db.exec(
307
+ "UPDATE _syncular_commit_outcomes SET operations=NULL,results='[]' WHERE client_commit_id=?",
308
+ [id],
309
+ );
310
+ }
173
311
  return dropped;
174
312
  }
package/src/outbox.ts CHANGED
@@ -66,15 +66,12 @@ export function appendOutboxCommit(
66
66
  );
67
67
  }
68
68
  db.exec(
69
- `INSERT INTO _syncular_outbox(client_commit_id, created_at_ms, operations)
70
- VALUES (?, ?, ?)`,
69
+ 'INSERT INTO _syncular_outbox(client_commit_id,created_at_ms,operations)VALUES(?,?,?)',
71
70
  [clientCommitId, nowMs, JSON.stringify(operations)],
72
71
  );
73
72
  for (const image of beforeImages) {
74
73
  db.exec(
75
- `INSERT INTO _syncular_outbox_before_images(
76
- client_commit_id, op_index, existed, sync_version, values_json
77
- ) VALUES (?, ?, ?, ?, ?)`,
74
+ 'INSERT INTO _syncular_outbox_before_images(client_commit_id,op_index,existed,sync_version,values_json)VALUES(?,?,?,?,?)',
78
75
  [
79
76
  clientCommitId,
80
77
  image.opIndex,
@@ -90,8 +87,7 @@ export function appendOutboxCommit(
90
87
  export function listOutbox(db: ClientDatabase): OutboxCommit[] {
91
88
  return db
92
89
  .query(
93
- `SELECT seq, client_commit_id, created_at_ms, operations
94
- FROM _syncular_outbox ORDER BY seq ASC`,
90
+ 'SELECT seq, client_commit_id, created_at_ms, operations FROM _syncular_outbox ORDER BY seq ASC',
95
91
  )
96
92
  .map(decodeOutboxRow);
97
93
  }
@@ -113,8 +109,7 @@ export function* iterateOutbox(
113
109
  let afterSeq = 0;
114
110
  while (afterSeq < throughSeq) {
115
111
  const rows = db.query(
116
- `SELECT seq, client_commit_id, created_at_ms, operations FROM _syncular_outbox
117
- WHERE seq > ? AND seq <= ? ORDER BY seq ASC LIMIT 32`,
112
+ 'SELECT seq, client_commit_id, created_at_ms, operations FROM _syncular_outbox WHERE seq > ? AND seq <= ? ORDER BY seq ASC LIMIT 32',
118
113
  [afterSeq, throughSeq],
119
114
  );
120
115
  if (rows.length === 0) return;
@@ -128,7 +123,7 @@ export function* iterateOutbox(
128
123
 
129
124
  /** Routine status reads never load operation bodies. */
130
125
  export function countOutbox(db: ClientDatabase): number {
131
- return db.query('SELECT COUNT(*) AS count FROM _syncular_outbox')[0]!
126
+ return db.query('SELECT COUNT(*)AS count FROM _syncular_outbox')[0]!
132
127
  .count as number;
133
128
  }
134
129
 
@@ -136,14 +131,14 @@ export function deleteOutboxCommit(
136
131
  db: ClientDatabase,
137
132
  clientCommitId: string,
138
133
  ): void {
139
- db.exec('DELETE FROM _syncular_blob_commit_refs WHERE commit_id = ?', [
134
+ db.exec('DELETE FROM _syncular_blob_commit_refs WHERE commit_id=?', [
140
135
  clientCommitId,
141
136
  ]);
142
137
  db.exec(
143
- 'DELETE FROM _syncular_outbox_before_images WHERE client_commit_id = ?',
138
+ 'DELETE FROM _syncular_outbox_before_images WHERE client_commit_id=?',
144
139
  [clientCommitId],
145
140
  );
146
- db.exec('DELETE FROM _syncular_outbox WHERE client_commit_id = ?', [
141
+ db.exec('DELETE FROM _syncular_outbox WHERE client_commit_id=?', [
147
142
  clientCommitId,
148
143
  ]);
149
144
  }
@@ -154,9 +149,7 @@ export function listOutboxBeforeImages(
154
149
  ): OutboxBeforeImage[] {
155
150
  return db
156
151
  .query(
157
- `SELECT op_index, existed, sync_version, values_json
158
- FROM _syncular_outbox_before_images
159
- WHERE client_commit_id = ? ORDER BY op_index`,
152
+ 'SELECT op_index,existed,sync_version,values_json FROM _syncular_outbox_before_images WHERE client_commit_id=? ORDER BY op_index',
160
153
  [clientCommitId],
161
154
  )
162
155
  .map((row) => ({
@@ -182,14 +175,11 @@ export function replaceOutboxBeforeImages(
182
175
  ): void {
183
176
  for (const image of replacements) {
184
177
  db.exec(
185
- `DELETE FROM _syncular_outbox_before_images
186
- WHERE client_commit_id = ? AND op_index = ?`,
178
+ 'DELETE FROM _syncular_outbox_before_images WHERE client_commit_id=? AND op_index=?',
187
179
  [clientCommitId, image.opIndex],
188
180
  );
189
181
  db.exec(
190
- `INSERT INTO _syncular_outbox_before_images(
191
- client_commit_id, op_index, existed, sync_version, values_json
192
- ) VALUES (?, ?, ?, ?, ?)`,
182
+ 'INSERT INTO _syncular_outbox_before_images(client_commit_id,op_index,existed,sync_version,values_json)VALUES(?,?,?,?,?)',
193
183
  [
194
184
  clientCommitId,
195
185
  image.opIndex,
package/src/outcomes.ts CHANGED
@@ -10,7 +10,12 @@ import type { RejectionDetails, RowValue } from '@syncular/core';
10
10
  import type { ClientDatabase } from './database';
11
11
  import { ClientSyncError } from './errors';
12
12
  import type { OutboxOperation } from './outbox';
13
- import { type JsonRowValue, jsonToRowValue, rowValueToJson } from './schema';
13
+ import {
14
+ mapRowValues,
15
+ type JsonRowValue,
16
+ jsonToRowValue,
17
+ rowValueToJson,
18
+ } from './schema';
14
19
 
15
20
  export interface ConflictRecord {
16
21
  readonly clientCommitId: string;
@@ -70,6 +75,14 @@ export type CommitOperationOutcome =
70
75
  };
71
76
 
72
77
  export interface RetainedCommitRow {
78
+ /** Authorized different-primary-key server rows matching intended unique keys. */
79
+ readonly uniqueConflicts?: readonly {
80
+ readonly index: string;
81
+ readonly columns: readonly string[];
82
+ readonly rowId: string;
83
+ readonly serverRow: Readonly<Record<string, RowValue>>;
84
+ readonly serverVersion: number;
85
+ }[];
73
86
  readonly table: string;
74
87
  readonly rowId: string;
75
88
  /** Complete intended row, or null for a local deletion. */
@@ -132,12 +145,7 @@ function encodeResults(results: readonly CommitOperationOutcome[]): string {
132
145
  status: 'conflict',
133
146
  conflict: {
134
147
  ...result.conflict,
135
- serverRow: Object.fromEntries(
136
- Object.entries(result.conflict.serverRow).map(([key, value]) => [
137
- key,
138
- rowValueToJson(value),
139
- ]),
140
- ),
148
+ serverRow: mapRowValues(result.conflict.serverRow, rowValueToJson),
141
149
  },
142
150
  };
143
151
  });
@@ -154,12 +162,7 @@ function decodeResults(raw: string): CommitOperationOutcome[] {
154
162
  ...result.conflict,
155
163
  // Pre-column-version journal entries carry no conflictColumns.
156
164
  conflictColumns: result.conflict.conflictColumns ?? [],
157
- serverRow: Object.fromEntries(
158
- Object.entries(result.conflict.serverRow).map(([key, value]) => [
159
- key,
160
- jsonToRowValue(value),
161
- ]),
162
- ),
165
+ serverRow: mapRowValues(result.conflict.serverRow, jsonToRowValue),
163
166
  },
164
167
  };
165
168
  });
@@ -171,7 +174,7 @@ function parseOutcome(
171
174
  ): CommitOutcome {
172
175
  const retainedRows: RetainedCommitRow[] = db
173
176
  .query(
174
- 'SELECT tbl, id, intent, base, version FROM _syncular_failed_rows WHERE commit_id = ? ORDER BY idx',
177
+ 'SELECT tbl,id,intent,base,version,unique_conflicts FROM _syncular_failed_rows WHERE commit_id=? ORDER BY idx',
175
178
  [String(row.client_commit_id)],
176
179
  )
177
180
  .map((retained) => {
@@ -187,6 +190,18 @@ function parseOutcome(
187
190
  table: String(retained.tbl),
188
191
  rowId: String(retained.id),
189
192
  localRow: decode(retained.intent),
193
+ ...(retained.unique_conflicts === null
194
+ ? {}
195
+ : {
196
+ uniqueConflicts: (
197
+ JSON.parse(String(retained.unique_conflicts)) as NonNullable<
198
+ RetainedCommitRow['uniqueConflicts']
199
+ >
200
+ ).map((conflict) => ({
201
+ ...conflict,
202
+ serverRow: decode(JSON.stringify(conflict.serverRow))!,
203
+ })),
204
+ }),
190
205
  serverRow: decode(retained.base),
191
206
  serverVersion:
192
207
  retained.version === null ? null : Number(retained.version),
@@ -217,9 +232,7 @@ export function recordCommitOutcome(
217
232
  outcome: Omit<CommitOutcome, 'sequence' | 'resolution'>,
218
233
  ): CommitOutcome {
219
234
  db.exec(
220
- `INSERT INTO _syncular_commit_outcomes(
221
- client_commit_id, status, recorded_at_ms, results, operations, resolution
222
- ) VALUES (?, ?, ?, ?, ?, 'active')`,
235
+ "INSERT INTO _syncular_commit_outcomes(client_commit_id,status,recorded_at_ms,results,operations,resolution)VALUES(?,?,?,?,?,'active')",
223
236
  [
224
237
  outcome.clientCommitId,
225
238
  outcome.status,
@@ -238,9 +251,7 @@ export function commitOutcome(
238
251
  clientCommitId: string,
239
252
  ): CommitOutcome | undefined {
240
253
  const row = db.query(
241
- `SELECT seq, client_commit_id, status, recorded_at_ms, results, operations,
242
- resolution, resolved_at_ms, replacement_client_commit_id
243
- FROM _syncular_commit_outcomes WHERE client_commit_id = ?`,
254
+ 'SELECT seq,client_commit_id,status,recorded_at_ms,results,operations,resolution,resolved_at_ms,replacement_client_commit_id FROM _syncular_commit_outcomes WHERE client_commit_id=?',
244
255
  [clientCommitId],
245
256
  )[0];
246
257
  return row === undefined ? undefined : parseOutcome(db, row);
@@ -261,10 +272,7 @@ export function listCommitOutcomes(
261
272
  ? "WHERE resolution = 'active' AND status IN ('conflict', 'rejected')"
262
273
  : '';
263
274
  const rows = db.query(
264
- `SELECT seq, client_commit_id, status, recorded_at_ms, results, operations,
265
- resolution, resolved_at_ms, replacement_client_commit_id
266
- FROM _syncular_commit_outcomes ${where}
267
- ORDER BY seq DESC${limit === undefined ? '' : ' LIMIT ?'}`,
275
+ `SELECT seq,client_commit_id,status,recorded_at_ms,results,operations,resolution,resolved_at_ms,replacement_client_commit_id FROM _syncular_commit_outcomes ${where} ORDER BY seq DESC${limit === undefined ? '' : ' LIMIT ?'}`,
268
276
  limit === undefined ? [] : [limit],
269
277
  );
270
278
  return rows.map((row) => parseOutcome(db, row));
@@ -276,9 +284,7 @@ export function persistCommitOutcomeResolution(
276
284
  nowMs: number,
277
285
  ): CommitOutcome | undefined {
278
286
  db.exec(
279
- `UPDATE _syncular_commit_outcomes
280
- SET resolution = ?, resolved_at_ms = ?, replacement_client_commit_id = ?
281
- WHERE client_commit_id = ? AND resolution = 'active'`,
287
+ "UPDATE _syncular_commit_outcomes SET resolution=?,resolved_at_ms=?,replacement_client_commit_id=? WHERE client_commit_id=? AND resolution='active'",
282
288
  [
283
289
  input.resolution,
284
290
  nowMs,
@@ -309,13 +315,11 @@ export function pruneCommitOutcomes(
309
315
  const excess = Math.max(0, (count ?? 0) - maxEntries);
310
316
  if (excess === 0) return 0;
311
317
  const candidates = db.query(
312
- `SELECT seq FROM _syncular_commit_outcomes
313
- WHERE status IN ('applied', 'cached') OR resolution != 'active'
314
- ORDER BY seq ASC LIMIT ?`,
318
+ "SELECT seq FROM _syncular_commit_outcomes WHERE status IN('applied','cached')OR resolution !='active' ORDER BY seq ASC LIMIT ?",
315
319
  [excess],
316
320
  );
317
321
  for (const candidate of candidates) {
318
- db.exec('DELETE FROM _syncular_commit_outcomes WHERE seq = ?', [
322
+ db.exec('DELETE FROM _syncular_commit_outcomes WHERE seq=?', [
319
323
  candidate.seq as number,
320
324
  ]);
321
325
  }
@@ -453,12 +453,12 @@ export function readPreviousVersionContainer(
453
453
  db: ClientDatabase,
454
454
  ): PreviousVersionRecord | undefined {
455
455
  const meta = db.query(
456
- "SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = ?",
456
+ "SELECT 1 AS present FROM sqlite_master WHERE type='table' AND name=?",
457
457
  [CONTAINER_META_TABLE],
458
458
  );
459
459
  if (meta.length === 0) return undefined;
460
460
  const raw = db.query(
461
- `SELECT record FROM ${quoteIdent(CONTAINER_META_TABLE)} WHERE id = 1`,
461
+ `SELECT record FROM ${quoteIdent(CONTAINER_META_TABLE)} WHERE id=1`,
462
462
  )[0]?.record;
463
463
  if (raw === undefined) return undefined;
464
464
  try {
@@ -504,13 +504,11 @@ function decodeRecord(value: string): PreviousVersionRecord {
504
504
 
505
505
  function writeRecord(db: ClientDatabase, record: PreviousVersionRecord): void {
506
506
  db.exec(
507
- `CREATE TABLE IF NOT EXISTS ${quoteIdent(CONTAINER_META_TABLE)} (
508
- id INTEGER PRIMARY KEY CHECK (id = 1),
509
- record TEXT NOT NULL)`,
507
+ `CREATE TABLE IF NOT EXISTS ${quoteIdent(CONTAINER_META_TABLE)}(id INTEGER PRIMARY KEY CHECK(id=1),record TEXT NOT NULL)`,
510
508
  );
511
509
  db.exec(`DELETE FROM ${quoteIdent(CONTAINER_META_TABLE)}`);
512
510
  db.exec(
513
- `INSERT INTO ${quoteIdent(CONTAINER_META_TABLE)}(id, record) VALUES (1, ?)`,
511
+ `INSERT INTO ${quoteIdent(CONTAINER_META_TABLE)}(id,record)VALUES(1,?)`,
514
512
  [JSON.stringify(record)],
515
513
  );
516
514
  }
@@ -541,9 +539,7 @@ export function capturePreviousVersion(
541
539
  const discovered = new Set(
542
540
  replica
543
541
  .query(
544
- `SELECT name FROM sqlite_master WHERE type = 'table'
545
- AND name NOT LIKE '_syncular_%'
546
- AND name NOT LIKE 'sqlite_%'`,
542
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '_syncular_%' AND name NOT LIKE 'sqlite_%'",
547
543
  )
548
544
  .map((row) => String(row.name)),
549
545
  );
@@ -567,7 +563,7 @@ export function capturePreviousVersion(
567
563
  for (const table of tables) {
568
564
  const probeRows = Number(
569
565
  replica.query(
570
- `SELECT COUNT(*) AS count FROM (SELECT 1 FROM ${quoteIdent(table.name)} LIMIT ?)`,
566
+ `SELECT COUNT(*)AS count FROM(SELECT 1 FROM ${quoteIdent(table.name)} LIMIT ?)`,
571
567
  [config.maxRows + 1],
572
568
  )[0]?.count ?? 0,
573
569
  );
@@ -584,15 +580,12 @@ export function capturePreviousVersion(
584
580
  .join(' + ');
585
581
  if (table.columns.length === 0) continue;
586
582
  const oversized = replica.query(
587
- `SELECT 1 AS hit FROM ${quoteIdent(table.name)}
588
- WHERE (${byteSum}) > ? LIMIT 1`,
583
+ `SELECT 1 AS hit FROM ${quoteIdent(table.name)} WHERE(${byteSum})>? LIMIT 1`,
589
584
  [config.maxRowBytes],
590
585
  );
591
586
  if (oversized.length > 0) return over(measuredRows, measuredBytes);
592
587
  const total = replica.query(
593
- `SELECT COALESCE(SUM(bytes), 0) AS total FROM (
594
- SELECT (${byteSum}) AS bytes FROM ${quoteIdent(table.name)} LIMIT ?
595
- )`,
588
+ `SELECT COALESCE(SUM(bytes),0)AS total FROM(SELECT(${byteSum})AS bytes FROM ${quoteIdent(table.name)} LIMIT ?)`,
596
589
  [perTableRows[index] ?? 0],
597
590
  )[0]?.total;
598
591
  measuredBytes += Number(total ?? 0);
@@ -604,11 +597,7 @@ export function capturePreviousVersion(
604
597
  container.transaction(() => {
605
598
  dropPreviousVersionContainer(container);
606
599
  container.exec(
607
- `CREATE TABLE ${quoteIdent(CONTAINER_TABLE)} (
608
- tbl TEXT NOT NULL,
609
- row_id TEXT NOT NULL,
610
- payload TEXT NOT NULL,
611
- PRIMARY KEY (tbl, row_id))`,
600
+ `CREATE TABLE ${quoteIdent(CONTAINER_TABLE)}(tbl TEXT NOT NULL,row_id TEXT NOT NULL,payload TEXT NOT NULL,PRIMARY KEY(tbl,row_id))`,
612
601
  );
613
602
  for (const table of tables) {
614
603
  const selectColumns = table.columns
@@ -617,8 +606,7 @@ export function capturePreviousVersion(
617
606
  let lastRowId = -1;
618
607
  for (;;) {
619
608
  const rows = replica.query(
620
- `SELECT rowid AS _rid, ${selectColumns} FROM ${quoteIdent(table.name)}
621
- WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`,
609
+ `SELECT rowid AS _rid,${selectColumns} FROM ${quoteIdent(table.name)} WHERE rowid>? ORDER BY rowid ASC LIMIT ?`,
622
610
  [lastRowId, COPY_BATCH_ROWS],
623
611
  );
624
612
  if (rows.length === 0) break;
@@ -632,8 +620,7 @@ export function capturePreviousVersion(
632
620
  payload[column.name] = rowValueToJson(value);
633
621
  }
634
622
  container.exec(
635
- `INSERT INTO ${quoteIdent(CONTAINER_TABLE)}(tbl, row_id, payload)
636
- VALUES (?, ?, ?)`,
623
+ `INSERT INTO ${quoteIdent(CONTAINER_TABLE)}(tbl,row_id,payload)VALUES(?,?,?)`,
637
624
  [table.name, rowIdText(table, row), JSON.stringify(payload)],
638
625
  );
639
626
  }
@@ -703,8 +690,7 @@ export function readPreviousVersionRows(
703
690
  ? ''
704
691
  : ` AND row_id IN (${rowIds.map(() => '?').join(', ')})`;
705
692
  const rows = db.query(
706
- `SELECT row_id, payload FROM ${quoteIdent(CONTAINER_TABLE)}
707
- WHERE tbl = ?${where} ORDER BY row_id ASC LIMIT ?`,
693
+ `SELECT row_id,payload FROM ${quoteIdent(CONTAINER_TABLE)} WHERE tbl=?${where} ORDER BY row_id ASC LIMIT ?`,
708
694
  [table.name, ...rowIds, limit + 1],
709
695
  );
710
696
  const truncated = rows.length > limit;
@@ -738,7 +724,7 @@ export function writePreviousVersionRefusal(
738
724
 
739
725
  /** Delete the durable refusal. Part of the orphan sweep and of a successful capture. */
740
726
  export function clearPreviousVersionRefusal(db: ClientDatabase): void {
741
- db.exec('DELETE FROM _syncular_meta WHERE key = ?', [
727
+ db.exec('DELETE FROM _syncular_meta WHERE key=?', [
742
728
  PREVIOUS_VERSION_CONTEXT_KEY,
743
729
  ]);
744
730
  }
@@ -906,7 +892,7 @@ export function writePreviousVersionAudit(
906
892
 
907
893
  /** Delete the advisory audit. Part of a discard, never of a normal bump. */
908
894
  export function clearPreviousVersionAudit(db: ClientDatabase): void {
909
- db.exec('DELETE FROM _syncular_meta WHERE key = ?', [
895
+ db.exec('DELETE FROM _syncular_meta WHERE key=?', [
910
896
  PREVIOUS_VERSION_AUDIT_KEY,
911
897
  ]);
912
898
  }