@syncular/server 0.33.0 → 0.34.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.
@@ -45,7 +45,7 @@ import { StorageQueryError } from './storage-errors.js';
45
45
  import { decodeRow } from '@syncular/core';
46
46
  import { bindAuthoritativePartition, prepareAuthoritativeQuery, } from './authoritative-query.js';
47
47
  import { syncError } from './errors.js';
48
- import { assertAppendOnlyMigration, assertPhysicalColumns, assertStoredLayouts, commitWindowPageSql, latestScopedChangeSeqSql, deleteRowSql, deleteSqliteRowScopesSql, dropTableDdl, indexRowPageStatement, layoutsOf, migratePayload, parseLayouts, quoteIdent, retiredTableNames, rewritePlan, rewriteRowSql, rewriteValues, SCHEMA_META_DDL_SQLITE, SYNC_COLUMN_VERSIONS_COLUMN, SYNC_PARTITION_COLUMN, SYNC_PAYLOAD_COLUMN, SYNC_ROW_ID_COLUMN, SYNC_SCOPES_COLUMN, SYNC_VERSION_COLUMN, scanRowPageSql, schemaDdl, selectRowScopesSql, selectRowSql, selectRowsForRewriteSql, tableColumnNames, toSqlValue, upsertSql, upsertValues, } from './relational-rows.js';
48
+ import { assertAppendOnlyMigration, assertPhysicalColumns, assertStoredLayouts, commitWindowPageSql, LATEST_SCOPED_CHANGE_SEQ_SQLITE_SQL, deleteRowSql, deleteSqliteRowScopesSql, dropTableDdl, indexRowPageStatement, layoutsOf, migratePayload, parseLayouts, quoteIdent, retiredTableNames, rewritePlan, rewriteRowSql, rewriteValues, SCHEMA_META_DDL_SQLITE, SYNC_COLUMN_VERSIONS_COLUMN, SYNC_PARTITION_COLUMN, SYNC_PAYLOAD_COLUMN, SYNC_ROW_ID_COLUMN, SYNC_SCOPES_COLUMN, SYNC_VERSION_COLUMN, scanRowPageSql, schemaDdl, selectRowScopesSql, selectRowSql, selectRowsForRewriteSql, tableColumnNames, toSqlValue, upsertSql, upsertValues, } from './relational-rows.js';
49
49
  import { matchesEffective } from './scopes.js';
50
50
  import { asUint8Array, collectCommitWindowPage, deserializePushResult, placeholders, serializePushResult, sqliteDdlStatements, toStoredChange, toStoredRow, } from './sqlite-dialect.js';
51
51
  import { isD1ConstraintError, StorageConstraintError } from './storage-errors.js';
@@ -1456,7 +1456,7 @@ export class D1ServerStorage {
1456
1456
  if (firstVariable === undefined || values.length === 0)
1457
1457
  return 0;
1458
1458
  // Concurrent reads of a pull share one `db.batch` round trip.
1459
- const [row] = await this.#batchedAll(latestScopedChangeSeqSql(values.length), values.flatMap((value) => [partition, query.table, firstVariable, value]));
1459
+ const [row] = await this.#batchedAll(LATEST_SCOPED_CHANGE_SEQ_SQLITE_SQL, [partition, query.table, firstVariable, JSON.stringify(values)]);
1460
1460
  return row?.seq ?? 0;
1461
1461
  }
1462
1462
  async readCommitWindow(partition, query) {
@@ -1,6 +1,6 @@
1
1
  import { type AuthoritativeRelationPlan } from './authoritative-query.js';
2
- import { type RemoteOperationResponse, type RowValue } from '@syncular/core';
3
- import type { SyncRequestContext } from './context.js';
2
+ import { type RemoteOperationRequest, type RemoteOperationResponse, type RowValue } from '@syncular/core';
3
+ import type { ResolveScopes, SyncRequestContext } from './context.js';
4
4
  import type { AuthoritativeQueryRequest, AuthoritativeQueryResult, AuthoritativeQueryValue } from './storage.js';
5
5
  import { type ValidateRow } from './validate.js';
6
6
  export interface RemoteQueryDependency {
@@ -101,5 +101,22 @@ export declare class RemoteOperationRegistry {
101
101
  constructor(operations: readonly RegisteredRemoteOperation[]);
102
102
  get(id: string): RegisteredRemoteOperation | undefined;
103
103
  }
104
- export declare function handleRemoteOperation(bytes: Uint8Array, ctx: SyncRequestContext, registry: RemoteOperationRegistry): Promise<Uint8Array>;
104
+ /**
105
+ * The registry and identity one remote operation request runs with
106
+ * (REMOTE.md §5.1). `actorId` is the commit's actor and defaults to the
107
+ * authenticated actor; `resolveScopes` replaces the configured resolver for
108
+ * this request.
109
+ */
110
+ export interface RemoteOperationBinding {
111
+ readonly registry: RemoteOperationRegistry;
112
+ readonly actorId?: string;
113
+ readonly resolveScopes?: ResolveScopes;
114
+ }
115
+ /**
116
+ * Chooses the binding of one decoded request (REMOTE.md §5.1). `ctx` carries
117
+ * the authenticated actor; the resolver runs after the client-id binding
118
+ * check and may throw a `SyncError` or a `ValidationRejection`.
119
+ */
120
+ export type RemoteOperationResolver = (request: RemoteOperationRequest, ctx: SyncRequestContext) => RemoteOperationBinding | Promise<RemoteOperationBinding>;
121
+ export declare function handleRemoteOperation(bytes: Uint8Array, authenticated: SyncRequestContext, operations: RemoteOperationRegistry | RemoteOperationResolver): Promise<Uint8Array>;
105
122
  export {};
@@ -6,7 +6,7 @@ import { processPushOperationsWithTrace } from './push.js';
6
6
  import { compileSchema } from './schema.js';
7
7
  import { authorizeWrite, renderScopeValue, } from './scopes.js';
8
8
  import { transactionQuery } from './storage-query.js';
9
- import { toValidateRow } from './validate.js';
9
+ import { toValidateRow, ValidationRejection, } from './validate.js';
10
10
  function scopeAllowed(allowed, variable, value) {
11
11
  const values = allowed[variable];
12
12
  return (values !== undefined && (values.includes('*') || values.includes(value)));
@@ -353,6 +353,15 @@ export class RemoteOperationRegistry {
353
353
  }
354
354
  }
355
355
  function encodeRemoteOperationError(error, fallbackCode) {
356
+ if (error instanceof ValidationRejection) {
357
+ return encodeRemoteOperationResponse({
358
+ revision: 1,
359
+ kind: 'error',
360
+ code: error.code,
361
+ message: error.message,
362
+ retryable: false,
363
+ });
364
+ }
356
365
  const sync = error instanceof SyncError
357
366
  ? error
358
367
  : syncError(fallbackCode, fallbackCode === 'operation.invalid_request'
@@ -366,7 +375,7 @@ function encodeRemoteOperationError(error, fallbackCode) {
366
375
  retryable: sync.retryable,
367
376
  });
368
377
  }
369
- export async function handleRemoteOperation(bytes, ctx, registry) {
378
+ export async function handleRemoteOperation(bytes, authenticated, operations) {
370
379
  let request;
371
380
  try {
372
381
  request = decodeRemoteOperationRequest(bytes);
@@ -384,6 +393,7 @@ export async function handleRemoteOperation(bytes, ctx, registry) {
384
393
  catch (error) {
385
394
  return encodeRemoteOperationError(error, 'operation.invalid_request');
386
395
  }
396
+ let ctx = authenticated;
387
397
  try {
388
398
  await touchAuthenticatedPartition(ctx);
389
399
  if (request.clientId.startsWith(REMOTE_COMMAND_CLIENT_ID_PREFIX)) {
@@ -397,6 +407,16 @@ export async function handleRemoteOperation(bytes, ctx, registry) {
397
407
  (typeof request.requestId !== 'string' || request.requestId.length === 0)) {
398
408
  throw syncError('operation.invalid_request');
399
409
  }
410
+ let registry = operations;
411
+ if (typeof registry === 'function') {
412
+ const binding = await registry(request, ctx);
413
+ registry = binding.registry;
414
+ ctx = {
415
+ ...ctx,
416
+ actorId: binding.actorId ?? ctx.actorId,
417
+ resolveScopes: binding.resolveScopes ?? ctx.resolveScopes,
418
+ };
419
+ }
400
420
  const operation = registry.get(request.operationId);
401
421
  if (operation === undefined) {
402
422
  throw syncError('operation.unknown');
@@ -411,7 +431,7 @@ export async function handleRemoteOperation(bytes, ctx, registry) {
411
431
  return encodeRemoteOperationResponse(await operation.run(ctx, request.clientId, request.requestId, request.params));
412
432
  }
413
433
  catch (error) {
414
- if (!(error instanceof SyncError)) {
434
+ if (!(error instanceof SyncError || error instanceof ValidationRejection)) {
415
435
  reportError(error, ctx.onError, 'operations');
416
436
  }
417
437
  return encodeRemoteOperationError(error, 'operation.execution_failed');
@@ -259,11 +259,14 @@ export declare function prefetchRowsSql(table: CompiledTable): string;
259
259
  * SQLite/D1: the newest `commit_seq` in the inverted change-scope index for
260
260
  * one variable's values, as one index-only max per value (a `(partition,
261
261
  * tbl, var, value)` prefix), so the cost is independent of the table's
262
- * history. Bind order: [partition, tbl, var, value] per value.
262
+ * history. The values arrive as one JSON array, so the statement has no
263
+ * compound SELECT and four bound parameters for any value count (D1 caps a
264
+ * compound SELECT at 5 terms and a statement at 100 parameters).
265
+ * Params: [partition, tbl, var, values as JSON array text].
263
266
  */
264
- export declare function latestScopedChangeSeqSql(valueCount: number): string;
267
+ export declare const LATEST_SCOPED_CHANGE_SEQ_SQLITE_SQL = "SELECT max((SELECT max(commit_seq) FROM sync_change_scopes\n WHERE partition=? AND tbl=? AND var=? AND value=v.value)) AS seq\n FROM json_each(?) AS v";
265
268
  /**
266
- * PostgreSQL set-based form of `latestScopedChangeSeqSql` for the
269
+ * PostgreSQL set-based form of `LATEST_SCOPED_CHANGE_SEQ_SQLITE_SQL` for the
267
270
  * subscriptions of one table in a pull. `$2` is a JSON array of
268
271
  * `{i, var, vals}` records. Params: [partition, specs (JSON text), tbl].
269
272
  */
@@ -610,17 +610,16 @@ export function prefetchRowsSql(table) {
610
610
  * SQLite/D1: the newest `commit_seq` in the inverted change-scope index for
611
611
  * one variable's values, as one index-only max per value (a `(partition,
612
612
  * tbl, var, value)` prefix), so the cost is independent of the table's
613
- * history. Bind order: [partition, tbl, var, value] per value.
613
+ * history. The values arrive as one JSON array, so the statement has no
614
+ * compound SELECT and four bound parameters for any value count (D1 caps a
615
+ * compound SELECT at 5 terms and a statement at 100 parameters).
616
+ * Params: [partition, tbl, var, values as JSON array text].
614
617
  */
615
- export function latestScopedChangeSeqSql(valueCount) {
616
- const perValue = [];
617
- for (let i = 0; i < valueCount; i++) {
618
- perValue.push('SELECT (SELECT max(commit_seq) FROM sync_change_scopes WHERE partition=? AND tbl=? AND var=? AND value=?) AS seq');
619
- }
620
- return `SELECT max(seq) AS seq FROM (${perValue.join(' UNION ALL ')})`;
621
- }
618
+ export const LATEST_SCOPED_CHANGE_SEQ_SQLITE_SQL = `SELECT max((SELECT max(commit_seq) FROM sync_change_scopes
619
+ WHERE partition=? AND tbl=? AND var=? AND value=v.value)) AS seq
620
+ FROM json_each(?) AS v`;
622
621
  /**
623
- * PostgreSQL set-based form of `latestScopedChangeSeqSql` for the
622
+ * PostgreSQL set-based form of `LATEST_SCOPED_CHANGE_SEQ_SQLITE_SQL` for the
624
623
  * subscriptions of one table in a pull. `$2` is a JSON array of
625
624
  * `{i, var, vals}` records. Params: [partition, specs (JSON text), tbl].
626
625
  */
@@ -13,7 +13,7 @@ import { serveNotReadyError } from './readiness.js';
13
13
  */
14
14
  import { bindAuthoritativePartition, prepareAuthoritativeQuery, } from './authoritative-query.js';
15
15
  import { syncError } from './errors.js';
16
- import { assertPhysicalColumns, assertStoredLayouts, commitWindowPageSql, latestScopedChangeSeqSql, deleteRowSql, deleteSqliteRowScopesSql, dropTableDdl, indexRowPageStatement, layoutsOf, migratePayload, parseLayouts, quoteIdent, retiredTableNames, rewritePlan, rewriteRowSql, rewriteValues, SCHEMA_META_DDL_SQLITE, SYNC_COLUMN_VERSIONS_COLUMN, SYNC_PARTITION_COLUMN, SYNC_PAYLOAD_COLUMN, SYNC_ROW_ID_COLUMN, SYNC_SCOPES_COLUMN, SYNC_VERSION_COLUMN, scanRowPageSql, schemaDdl, selectRowScopesSql, selectRowSql, selectRowsForRewriteSql, upsertSql, upsertValues, } from './relational-rows.js';
16
+ import { assertPhysicalColumns, assertStoredLayouts, commitWindowPageSql, LATEST_SCOPED_CHANGE_SEQ_SQLITE_SQL, deleteRowSql, deleteSqliteRowScopesSql, dropTableDdl, indexRowPageStatement, layoutsOf, migratePayload, parseLayouts, quoteIdent, retiredTableNames, rewritePlan, rewriteRowSql, rewriteValues, SCHEMA_META_DDL_SQLITE, SYNC_COLUMN_VERSIONS_COLUMN, SYNC_PARTITION_COLUMN, SYNC_PAYLOAD_COLUMN, SYNC_ROW_ID_COLUMN, SYNC_SCOPES_COLUMN, SYNC_VERSION_COLUMN, scanRowPageSql, schemaDdl, selectRowScopesSql, selectRowSql, selectRowsForRewriteSql, upsertSql, upsertValues, } from './relational-rows.js';
17
17
  import { matchesEffective } from './scopes.js';
18
18
  import { collectCommitWindowPage, deserializePushResult, placeholders, SQLITE_DDL, serializePushResult, toStoredChange, toStoredRow, } from './sqlite-dialect.js';
19
19
  import { SqliteAdapterRequiredError, } from './sqlite-driver.js';
@@ -1034,13 +1034,8 @@ END`);
1034
1034
  if (firstVariable === undefined || values.length === 0)
1035
1035
  return 0;
1036
1036
  const row = this.db
1037
- .query(latestScopedChangeSeqSql(values.length))
1038
- .get(...values.flatMap((value) => [
1039
- partition,
1040
- query.table,
1041
- firstVariable,
1042
- value,
1043
- ]));
1037
+ .query(LATEST_SCOPED_CHANGE_SEQ_SQLITE_SQL)
1038
+ .get(partition, query.table, firstVariable, JSON.stringify(values));
1044
1039
  return row?.seq ?? 0;
1045
1040
  }
1046
1041
  async readCommitWindow(partition, query) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/server",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "Syncular server: handleSyncRequest + storage/auth interfaces for the sync protocol",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -68,7 +68,7 @@
68
68
  "verify:node": "cd ../.. && bun build ./packages/server/test/sqlite-runtime/verify-node.mjs --target=node --conditions=bun --outfile=./packages/server/.verify-node.built.mjs && node ./packages/server/.verify-node.built.mjs"
69
69
  },
70
70
  "dependencies": {
71
- "@syncular/core": "0.33.0"
71
+ "@syncular/core": "0.34.0"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@electric-sql/pglite": "^0.5.4"
package/src/d1-storage.ts CHANGED
@@ -54,7 +54,7 @@ import {
54
54
  assertPhysicalColumns,
55
55
  assertStoredLayouts,
56
56
  commitWindowPageSql,
57
- latestScopedChangeSeqSql,
57
+ LATEST_SCOPED_CHANGE_SEQ_SQLITE_SQL,
58
58
  deleteRowSql,
59
59
  deleteSqliteRowScopesSql,
60
60
  dropTableDdl,
@@ -2189,8 +2189,8 @@ export class D1ServerStorage implements ServerStorage {
2189
2189
  if (firstVariable === undefined || values.length === 0) return 0;
2190
2190
  // Concurrent reads of a pull share one `db.batch` round trip.
2191
2191
  const [row] = await this.#batchedAll<{ seq: number | null }>(
2192
- latestScopedChangeSeqSql(values.length),
2193
- values.flatMap((value) => [partition, query.table, firstVariable, value]),
2192
+ LATEST_SCOPED_CHANGE_SEQ_SQLITE_SQL,
2193
+ [partition, query.table, firstVariable, JSON.stringify(values)],
2194
2194
  );
2195
2195
  return row?.seq ?? 0;
2196
2196
  }
package/src/operations.ts CHANGED
@@ -8,11 +8,12 @@ import {
8
8
  encodeRemoteOperationResponse,
9
9
  encodeSparseRow,
10
10
  type PushOperation,
11
+ type RemoteOperationRequest,
11
12
  type RemoteOperationResponse,
12
13
  type RowValue,
13
14
  type ScopeMap,
14
15
  } from '@syncular/core';
15
- import type { SyncRequestContext } from './context';
16
+ import type { ResolveScopes, SyncRequestContext } from './context';
16
17
  import {
17
18
  REMOTE_COMMAND_CLIENT_ID_PREFIX,
18
19
  RESOLVER_OUTAGE,
@@ -33,7 +34,11 @@ import type {
33
34
  } from './storage';
34
35
  import type { StorageTransaction } from './storage';
35
36
  import { transactionQuery } from './storage-query';
36
- import { toValidateRow, type ValidateRow } from './validate';
37
+ import {
38
+ toValidateRow,
39
+ ValidationRejection,
40
+ type ValidateRow,
41
+ } from './validate';
37
42
 
38
43
  export interface RemoteQueryDependency {
39
44
  readonly table: string;
@@ -683,10 +688,41 @@ export class RemoteOperationRegistry {
683
688
  }
684
689
  }
685
690
 
691
+ /**
692
+ * The registry and identity one remote operation request runs with
693
+ * (REMOTE.md §5.1). `actorId` is the commit's actor and defaults to the
694
+ * authenticated actor; `resolveScopes` replaces the configured resolver for
695
+ * this request.
696
+ */
697
+ export interface RemoteOperationBinding {
698
+ readonly registry: RemoteOperationRegistry;
699
+ readonly actorId?: string;
700
+ readonly resolveScopes?: ResolveScopes;
701
+ }
702
+
703
+ /**
704
+ * Chooses the binding of one decoded request (REMOTE.md §5.1). `ctx` carries
705
+ * the authenticated actor; the resolver runs after the client-id binding
706
+ * check and may throw a `SyncError` or a `ValidationRejection`.
707
+ */
708
+ export type RemoteOperationResolver = (
709
+ request: RemoteOperationRequest,
710
+ ctx: SyncRequestContext,
711
+ ) => RemoteOperationBinding | Promise<RemoteOperationBinding>;
712
+
686
713
  function encodeRemoteOperationError(
687
714
  error: unknown,
688
715
  fallbackCode: 'operation.invalid_request' | 'operation.execution_failed',
689
716
  ): Uint8Array {
717
+ if (error instanceof ValidationRejection) {
718
+ return encodeRemoteOperationResponse({
719
+ revision: 1,
720
+ kind: 'error',
721
+ code: error.code,
722
+ message: error.message,
723
+ retryable: false,
724
+ });
725
+ }
690
726
  const sync =
691
727
  error instanceof SyncError
692
728
  ? error
@@ -707,8 +743,8 @@ function encodeRemoteOperationError(
707
743
 
708
744
  export async function handleRemoteOperation(
709
745
  bytes: Uint8Array,
710
- ctx: SyncRequestContext,
711
- registry: RemoteOperationRegistry,
746
+ authenticated: SyncRequestContext,
747
+ operations: RemoteOperationRegistry | RemoteOperationResolver,
712
748
  ): Promise<Uint8Array> {
713
749
  let request;
714
750
  try {
@@ -728,6 +764,7 @@ export async function handleRemoteOperation(
728
764
  } catch (error) {
729
765
  return encodeRemoteOperationError(error, 'operation.invalid_request');
730
766
  }
767
+ let ctx = authenticated;
731
768
  try {
732
769
  await touchAuthenticatedPartition(ctx);
733
770
  if (request.clientId.startsWith(REMOTE_COMMAND_CLIENT_ID_PREFIX)) {
@@ -752,6 +789,16 @@ export async function handleRemoteOperation(
752
789
  ) {
753
790
  throw syncError('operation.invalid_request');
754
791
  }
792
+ let registry = operations;
793
+ if (typeof registry === 'function') {
794
+ const binding = await registry(request, ctx);
795
+ registry = binding.registry;
796
+ ctx = {
797
+ ...ctx,
798
+ actorId: binding.actorId ?? ctx.actorId,
799
+ resolveScopes: binding.resolveScopes ?? ctx.resolveScopes,
800
+ };
801
+ }
755
802
  const operation = registry.get(request.operationId);
756
803
  if (operation === undefined) {
757
804
  throw syncError('operation.unknown');
@@ -772,7 +819,7 @@ export async function handleRemoteOperation(
772
819
  ),
773
820
  );
774
821
  } catch (error) {
775
- if (!(error instanceof SyncError)) {
822
+ if (!(error instanceof SyncError || error instanceof ValidationRejection)) {
776
823
  reportError(error, ctx.onError, 'operations');
777
824
  }
778
825
  return encodeRemoteOperationError(error, 'operation.execution_failed');
@@ -709,20 +709,17 @@ export function prefetchRowsSql(table: CompiledTable): string {
709
709
  * SQLite/D1: the newest `commit_seq` in the inverted change-scope index for
710
710
  * one variable's values, as one index-only max per value (a `(partition,
711
711
  * tbl, var, value)` prefix), so the cost is independent of the table's
712
- * history. Bind order: [partition, tbl, var, value] per value.
712
+ * history. The values arrive as one JSON array, so the statement has no
713
+ * compound SELECT and four bound parameters for any value count (D1 caps a
714
+ * compound SELECT at 5 terms and a statement at 100 parameters).
715
+ * Params: [partition, tbl, var, values as JSON array text].
713
716
  */
714
- export function latestScopedChangeSeqSql(valueCount: number): string {
715
- const perValue: string[] = [];
716
- for (let i = 0; i < valueCount; i++) {
717
- perValue.push(
718
- 'SELECT (SELECT max(commit_seq) FROM sync_change_scopes WHERE partition=? AND tbl=? AND var=? AND value=?) AS seq',
719
- );
720
- }
721
- return `SELECT max(seq) AS seq FROM (${perValue.join(' UNION ALL ')})`;
722
- }
717
+ export const LATEST_SCOPED_CHANGE_SEQ_SQLITE_SQL = `SELECT max((SELECT max(commit_seq) FROM sync_change_scopes
718
+ WHERE partition=? AND tbl=? AND var=? AND value=v.value)) AS seq
719
+ FROM json_each(?) AS v`;
723
720
 
724
721
  /**
725
- * PostgreSQL set-based form of `latestScopedChangeSeqSql` for the
722
+ * PostgreSQL set-based form of `LATEST_SCOPED_CHANGE_SEQ_SQLITE_SQL` for the
726
723
  * subscriptions of one table in a pull. `$2` is a JSON array of
727
724
  * `{i, var, vals}` records. Params: [partition, specs (JSON text), tbl].
728
725
  */
@@ -22,7 +22,7 @@ import {
22
22
  assertPhysicalColumns,
23
23
  assertStoredLayouts,
24
24
  commitWindowPageSql,
25
- latestScopedChangeSeqSql,
25
+ LATEST_SCOPED_CHANGE_SEQ_SQLITE_SQL,
26
26
  deleteRowSql,
27
27
  deleteSqliteRowScopesSql,
28
28
  dropTableDdl,
@@ -1639,16 +1639,9 @@ END`);
1639
1639
  if (firstVariable === undefined || values.length === 0) return 0;
1640
1640
  const row = this.db
1641
1641
  .query<{ seq: number | null }, string[]>(
1642
- latestScopedChangeSeqSql(values.length),
1642
+ LATEST_SCOPED_CHANGE_SEQ_SQLITE_SQL,
1643
1643
  )
1644
- .get(
1645
- ...values.flatMap((value) => [
1646
- partition,
1647
- query.table,
1648
- firstVariable,
1649
- value,
1650
- ]),
1651
- );
1644
+ .get(partition, query.table, firstVariable, JSON.stringify(values));
1652
1645
  return row?.seq ?? 0;
1653
1646
  }
1654
1647