@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.
- package/dist/d1-storage.js +2 -2
- package/dist/operations.d.ts +20 -3
- package/dist/operations.js +23 -3
- package/dist/relational-rows.d.ts +6 -3
- package/dist/relational-rows.js +8 -9
- package/dist/sqlite-storage.js +3 -8
- package/package.json +2 -2
- package/src/d1-storage.ts +3 -3
- package/src/operations.ts +52 -5
- package/src/relational-rows.ts +8 -11
- package/src/sqlite-storage.ts +3 -10
package/dist/d1-storage.js
CHANGED
|
@@ -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,
|
|
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(
|
|
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) {
|
package/dist/operations.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 {};
|
package/dist/operations.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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
|
|
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 `
|
|
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
|
*/
|
package/dist/relational-rows.js
CHANGED
|
@@ -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.
|
|
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
|
|
616
|
-
|
|
617
|
-
|
|
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 `
|
|
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
|
*/
|
package/dist/sqlite-storage.js
CHANGED
|
@@ -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,
|
|
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(
|
|
1038
|
-
.get(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2193
|
-
|
|
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 {
|
|
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
|
-
|
|
711
|
-
|
|
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');
|
package/src/relational-rows.ts
CHANGED
|
@@ -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.
|
|
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
|
|
715
|
-
|
|
716
|
-
|
|
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 `
|
|
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
|
*/
|
package/src/sqlite-storage.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
assertPhysicalColumns,
|
|
23
23
|
assertStoredLayouts,
|
|
24
24
|
commitWindowPageSql,
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|