@powersync/service-core 1.20.5 → 1.22.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/CHANGELOG.md +66 -0
- package/dist/api/RouteAPI.d.ts +3 -3
- package/dist/api/diagnostics.d.ts +1 -1
- package/dist/api/diagnostics.js +19 -3
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/auth/RemoteJWKSCollector.js +3 -2
- package/dist/auth/RemoteJWKSCollector.js.map +1 -1
- package/dist/entry/commands/teardown-action.js +1 -1
- package/dist/entry/commands/teardown-action.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/AbstractModule.d.ts +1 -1
- package/dist/replication/AbstractReplicationJob.js +1 -1
- package/dist/replication/AbstractReplicationJob.js.map +1 -1
- package/dist/replication/AbstractReplicator.d.ts +6 -6
- package/dist/replication/AbstractReplicator.js +21 -21
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/replication/RelationCache.d.ts +9 -2
- package/dist/replication/RelationCache.js +21 -2
- package/dist/replication/RelationCache.js.map +1 -1
- package/dist/routes/configure-fastify.js +3 -1
- package/dist/routes/configure-fastify.js.map +1 -1
- package/dist/routes/endpoints/admin.js +16 -8
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/checkpointing.js +1 -1
- package/dist/routes/endpoints/checkpointing.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +1 -1
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js +8 -8
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +2 -2
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/routes/route-register.d.ts +2 -0
- package/dist/routes/route-register.js +65 -3
- package/dist/routes/route-register.js.map +1 -1
- package/dist/runner/teardown.js +4 -4
- package/dist/runner/teardown.js.map +1 -1
- package/dist/storage/BucketStorage.d.ts +9 -9
- package/dist/storage/BucketStorage.js +9 -9
- package/dist/storage/BucketStorageBatch.d.ts +29 -0
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/BucketStorageFactory.d.ts +27 -18
- package/dist/storage/BucketStorageFactory.js +13 -12
- package/dist/storage/BucketStorageFactory.js.map +1 -1
- package/dist/storage/PersistedSyncRulesContent.d.ts +6 -4
- package/dist/storage/PersistedSyncRulesContent.js +15 -8
- package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/SourceEntity.d.ts +8 -1
- package/dist/storage/SourceTable.d.ts +32 -11
- package/dist/storage/SourceTable.js +41 -15
- package/dist/storage/SourceTable.js.map +1 -1
- package/dist/storage/StorageVersionConfig.d.ts +1 -1
- package/dist/storage/StorageVersionConfig.js +1 -1
- package/dist/storage/SyncRulesBucketStorage.d.ts +63 -18
- package/dist/storage/SyncRulesBucketStorage.js +14 -0
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/storage/WriteCheckpointAPI.d.ts +6 -6
- package/dist/storage/WriteCheckpointAPI.js +1 -1
- package/dist/storage/bson.d.ts +0 -1
- package/dist/storage/bson.js +0 -4
- package/dist/storage/bson.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +6 -9
- package/dist/sync/BucketChecksumState.js +117 -58
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/sync.d.ts +2 -2
- package/dist/sync/sync.js.map +1 -1
- package/dist/tracing/PerformanceTracer.d.ts +60 -0
- package/dist/tracing/PerformanceTracer.js +105 -0
- package/dist/tracing/PerformanceTracer.js.map +1 -0
- package/dist/tracing/TraceWriter.d.ts +22 -0
- package/dist/tracing/TraceWriter.js +63 -0
- package/dist/tracing/TraceWriter.js.map +1 -0
- package/dist/util/checkpointing.js +1 -1
- package/dist/util/config/compound-config-collector.d.ts +1 -1
- package/dist/util/config/compound-config-collector.js +2 -2
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
- package/dist/util/env.js +1 -1
- package/dist/util/protocol-types.d.ts +1 -1
- package/dist/util/protocol-types.js +1 -1
- package/dist/util/util-index.d.ts +1 -0
- package/dist/util/util-index.js +1 -0
- package/dist/util/util-index.js.map +1 -1
- package/dist/util/utils.d.ts +5 -0
- package/dist/util/utils.js +7 -0
- package/dist/util/utils.js.map +1 -1
- package/package.json +11 -11
- package/src/api/RouteAPI.ts +3 -3
- package/src/api/diagnostics.ts +29 -6
- package/src/auth/RemoteJWKSCollector.ts +3 -1
- package/src/entry/commands/teardown-action.ts +1 -1
- package/src/index.ts +2 -0
- package/src/modules/AbstractModule.ts +1 -1
- package/src/replication/AbstractReplicationJob.ts +1 -1
- package/src/replication/AbstractReplicator.ts +23 -23
- package/src/replication/RelationCache.ts +23 -4
- package/src/routes/configure-fastify.ts +8 -1
- package/src/routes/endpoints/admin.ts +17 -8
- package/src/routes/endpoints/checkpointing.ts +1 -1
- package/src/routes/endpoints/socket-route.ts +1 -1
- package/src/routes/endpoints/sync-rules.ts +8 -8
- package/src/routes/endpoints/sync-stream.ts +2 -2
- package/src/routes/route-register.ts +73 -4
- package/src/runner/teardown.ts +4 -4
- package/src/storage/BucketStorage.ts +9 -9
- package/src/storage/BucketStorageBatch.ts +32 -0
- package/src/storage/BucketStorageFactory.ts +35 -23
- package/src/storage/PersistedSyncRulesContent.ts +20 -12
- package/src/storage/SourceEntity.ts +9 -1
- package/src/storage/SourceTable.ts +56 -22
- package/src/storage/StorageVersionConfig.ts +1 -1
- package/src/storage/SyncRulesBucketStorage.ts +74 -22
- package/src/storage/WriteCheckpointAPI.ts +6 -6
- package/src/storage/bson.ts +0 -5
- package/src/sync/BucketChecksumState.ts +142 -78
- package/src/sync/sync.ts +4 -4
- package/src/tracing/PerformanceTracer.ts +149 -0
- package/src/tracing/TraceWriter.ts +67 -0
- package/src/util/checkpointing.ts +1 -1
- package/src/util/config/compound-config-collector.ts +3 -3
- package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +1 -1
- package/src/util/env.ts +1 -1
- package/src/util/protocol-types.ts +1 -1
- package/src/util/util-index.ts +1 -0
- package/src/util/utils.ts +8 -0
- package/test/src/auth.test.ts +120 -1
- package/test/src/diagnostics.test.ts +155 -0
- package/test/src/routes/error-handler.integration.test.ts +275 -0
- package/test/src/routes/stream.test.ts +15 -4
- package/test/src/storage/SourceTable.test.ts +89 -0
- package/test/src/sync/BucketChecksumState.test.ts +244 -80
- package/test/tsconfig.json +0 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import * as storage from '../../../src/storage/storage-index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a SourceTable with the current `ref`-based options shape.
|
|
6
|
+
* Data/parameter sources are not relevant to the storeCurrentData behaviour, so they default to empty.
|
|
7
|
+
*/
|
|
8
|
+
function makeTable(options: Partial<storage.SourceTableOptions> = {}): storage.SourceTable {
|
|
9
|
+
return new storage.SourceTable({
|
|
10
|
+
id: 'test-id',
|
|
11
|
+
ref: { connectionTag: 'test', schema: 'public', name: 'test_table' },
|
|
12
|
+
objectId: 123,
|
|
13
|
+
replicaIdColumns: [{ name: 'id' }],
|
|
14
|
+
snapshotComplete: true,
|
|
15
|
+
bucketDataSources: [],
|
|
16
|
+
parameterLookupSources: [],
|
|
17
|
+
...options
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('SourceTable', () => {
|
|
22
|
+
describe('storeCurrentData property', () => {
|
|
23
|
+
test('defaults to true', () => {
|
|
24
|
+
const table = makeTable();
|
|
25
|
+
expect(table.storeCurrentData).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('can be set to false', () => {
|
|
29
|
+
const table = makeTable();
|
|
30
|
+
table.storeCurrentData = false;
|
|
31
|
+
expect(table.storeCurrentData).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('is preserved when cloning', () => {
|
|
35
|
+
const table = makeTable();
|
|
36
|
+
table.storeCurrentData = false;
|
|
37
|
+
const cloned = table.clone();
|
|
38
|
+
|
|
39
|
+
expect(cloned.storeCurrentData).toBe(false);
|
|
40
|
+
expect(cloned).not.toBe(table);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('clone preserves all properties including storeCurrentData', () => {
|
|
44
|
+
const table = makeTable({
|
|
45
|
+
replicaIdColumns: [{ name: 'id', type: 'int4' }],
|
|
46
|
+
snapshotComplete: false
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
table.syncData = false;
|
|
50
|
+
table.syncParameters = false;
|
|
51
|
+
table.syncEvent = false;
|
|
52
|
+
table.storeCurrentData = false;
|
|
53
|
+
table.snapshotStatus = {
|
|
54
|
+
totalEstimatedCount: 100,
|
|
55
|
+
replicatedCount: 50,
|
|
56
|
+
lastKey: Buffer.from('test')
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const cloned = table.clone();
|
|
60
|
+
|
|
61
|
+
expect(cloned.syncData).toBe(false);
|
|
62
|
+
expect(cloned.syncParameters).toBe(false);
|
|
63
|
+
expect(cloned.syncEvent).toBe(false);
|
|
64
|
+
expect(cloned.storeCurrentData).toBe(false);
|
|
65
|
+
expect(cloned.snapshotStatus).toEqual(table.snapshotStatus);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('integration with other properties', () => {
|
|
70
|
+
test('storeCurrentData does not affect syncAny', () => {
|
|
71
|
+
const table = makeTable();
|
|
72
|
+
|
|
73
|
+
table.storeCurrentData = false;
|
|
74
|
+
table.syncData = true;
|
|
75
|
+
table.syncParameters = false;
|
|
76
|
+
table.syncEvent = false;
|
|
77
|
+
|
|
78
|
+
expect(table.syncAny).toBe(true); // Should still be true
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('storeCurrentData is independent of snapshot status', () => {
|
|
82
|
+
const table = makeTable({ snapshotComplete: false });
|
|
83
|
+
|
|
84
|
+
table.storeCurrentData = false;
|
|
85
|
+
expect(table.snapshotComplete).toBe(false);
|
|
86
|
+
expect(table.storeCurrentData).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ChecksumMap,
|
|
8
8
|
InternalOpId,
|
|
9
9
|
JwtPayload,
|
|
10
|
+
ParameterSetLimitExceededError,
|
|
10
11
|
ReplicationCheckpoint,
|
|
11
12
|
StreamingSyncRequest,
|
|
12
13
|
SyncContext,
|
|
@@ -14,34 +15,39 @@ import {
|
|
|
14
15
|
} from '@/index.js';
|
|
15
16
|
import { JSONBig } from '@powersync/service-jsonbig';
|
|
16
17
|
import {
|
|
18
|
+
nodeSqlite,
|
|
17
19
|
ParameterIndexLookupCreator,
|
|
20
|
+
ParameterLookupDefinitionId,
|
|
21
|
+
ParameterLookupRows,
|
|
22
|
+
ParameterLookupScope,
|
|
18
23
|
ScopedParameterLookup,
|
|
19
|
-
|
|
20
|
-
SqliteJsonRow,
|
|
21
|
-
SqliteRow,
|
|
24
|
+
SourceTableRef,
|
|
22
25
|
SqlSyncRules,
|
|
23
26
|
TablePattern,
|
|
24
27
|
versionedHydrationState
|
|
25
28
|
} from '@powersync/service-sync-rules';
|
|
26
|
-
import
|
|
29
|
+
import * as sqlite from 'node:sqlite';
|
|
27
30
|
import { beforeEach, describe, expect, test } from 'vitest';
|
|
28
31
|
|
|
29
32
|
describe('BucketChecksumState', () => {
|
|
30
33
|
const LOOKUP_SOURCE: ParameterIndexLookupCreator = {
|
|
31
|
-
get
|
|
34
|
+
get sourceId(): ParameterLookupDefinitionId {
|
|
32
35
|
return {
|
|
33
36
|
lookupName: 'lookup',
|
|
34
|
-
queryId: '0'
|
|
35
|
-
source: LOOKUP_SOURCE
|
|
37
|
+
queryId: '0'
|
|
36
38
|
};
|
|
37
39
|
},
|
|
38
40
|
getSourceTables(): Set<TablePattern> {
|
|
39
41
|
return new Set();
|
|
40
42
|
},
|
|
41
|
-
|
|
42
|
-
return
|
|
43
|
+
createEvaluator() {
|
|
44
|
+
return {
|
|
45
|
+
evaluateParameterRow() {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
};
|
|
43
49
|
},
|
|
44
|
-
tableSyncsParameters(_table:
|
|
50
|
+
tableSyncsParameters(_table: SourceTableRef): boolean {
|
|
45
51
|
return false;
|
|
46
52
|
}
|
|
47
53
|
};
|
|
@@ -59,7 +65,7 @@ bucket_definitions:
|
|
|
59
65
|
data: []
|
|
60
66
|
`,
|
|
61
67
|
{ defaultSchema: 'public' }
|
|
62
|
-
).config.hydrate({ hydrationState: versionedHydrationState(1) });
|
|
68
|
+
).config.hydrate({ hydrationState: versionedHydrationState(1), sqlite: nodeSqlite(sqlite) });
|
|
63
69
|
|
|
64
70
|
// global[1] and global[2]
|
|
65
71
|
const SYNC_RULES_GLOBAL_TWO = SqlSyncRules.fromYaml(
|
|
@@ -72,7 +78,7 @@ bucket_definitions:
|
|
|
72
78
|
data: []
|
|
73
79
|
`,
|
|
74
80
|
{ defaultSchema: 'public' }
|
|
75
|
-
).config.hydrate({ hydrationState: versionedHydrationState(2) });
|
|
81
|
+
).config.hydrate({ hydrationState: versionedHydrationState(2), sqlite: nodeSqlite(sqlite) });
|
|
76
82
|
|
|
77
83
|
// by_project[n]
|
|
78
84
|
const SYNC_RULES_DYNAMIC = SqlSyncRules.fromYaml(
|
|
@@ -83,7 +89,7 @@ bucket_definitions:
|
|
|
83
89
|
data: []
|
|
84
90
|
`,
|
|
85
91
|
{ defaultSchema: 'public' }
|
|
86
|
-
).config.hydrate({ hydrationState: versionedHydrationState(3) });
|
|
92
|
+
).config.hydrate({ hydrationState: versionedHydrationState(3), sqlite: nodeSqlite(sqlite) });
|
|
87
93
|
|
|
88
94
|
const syncContext = new SyncContext({
|
|
89
95
|
maxBuckets: 100,
|
|
@@ -545,7 +551,7 @@ bucket_definitions:
|
|
|
545
551
|
const line = (await state.buildNextCheckpointLine({
|
|
546
552
|
base: storage.makeCheckpoint(1n, (lookups) => {
|
|
547
553
|
expect(lookups).toEqual([ScopedParameterLookup.direct(lookupScope('by_project', '1'), ['u1'])]);
|
|
548
|
-
return [{ id: 1 }, { id: 2 }];
|
|
554
|
+
return [{ lookup: lookups[0], rows: [{ id: 1 }, { id: 2 }] }];
|
|
549
555
|
}),
|
|
550
556
|
writeCheckpoint: null,
|
|
551
557
|
update: CHECKPOINT_INVALIDATE_ALL
|
|
@@ -606,7 +612,7 @@ bucket_definitions:
|
|
|
606
612
|
const line2 = (await state.buildNextCheckpointLine({
|
|
607
613
|
base: storage.makeCheckpoint(2n, (lookups) => {
|
|
608
614
|
expect(lookups).toEqual([ScopedParameterLookup.direct(lookupScope('by_project', '1'), ['u1'])]);
|
|
609
|
-
return [{ id: 1 }, { id: 2 }, { id: 3 }];
|
|
615
|
+
return [{ lookup: lookups[0], rows: [{ id: 1 }, { id: 2 }, { id: 3 }] }];
|
|
610
616
|
}),
|
|
611
617
|
writeCheckpoint: null,
|
|
612
618
|
update: {
|
|
@@ -654,7 +660,7 @@ config:
|
|
|
654
660
|
|
|
655
661
|
const rules = SqlSyncRules.fromYaml(source, {
|
|
656
662
|
defaultSchema: 'public'
|
|
657
|
-
}).config.hydrate({ hydrationState: versionedHydrationState(1) });
|
|
663
|
+
}).config.hydrate({ hydrationState: versionedHydrationState(1), sqlite: nodeSqlite(sqlite) });
|
|
658
664
|
|
|
659
665
|
return new BucketChecksumState({
|
|
660
666
|
syncContext,
|
|
@@ -889,7 +895,8 @@ config:
|
|
|
889
895
|
|
|
890
896
|
const line = (await state.buildNextCheckpointLine({
|
|
891
897
|
base: storage.makeCheckpoint(1n, (lookups) => {
|
|
892
|
-
|
|
898
|
+
expect(lookups).toHaveLength(1);
|
|
899
|
+
return [{ lookup: lookups[0], rows: [{ id: 1 }, { id: 2 }, { id: 3 }] }];
|
|
893
900
|
}),
|
|
894
901
|
writeCheckpoint: null,
|
|
895
902
|
update: CHECKPOINT_INVALIDATE_ALL
|
|
@@ -900,22 +907,197 @@ config:
|
|
|
900
907
|
expect(logData[0].parameter_query_results).toBe(3);
|
|
901
908
|
});
|
|
902
909
|
|
|
903
|
-
test('throws error with breakdown when
|
|
910
|
+
test('throws error with breakdown when too many parameters were fetched', async () => {
|
|
911
|
+
const syncRules = SqlSyncRules.fromYaml(
|
|
912
|
+
`
|
|
913
|
+
config:
|
|
914
|
+
edition: 3
|
|
915
|
+
|
|
916
|
+
streams:
|
|
917
|
+
a:
|
|
918
|
+
auto_subscribe: true
|
|
919
|
+
query: SELECT * FROM a WHERE id IN (SELECT id FROM magic_sequence WHERE count0 = auth.parameter('a'))
|
|
920
|
+
b:
|
|
921
|
+
auto_subscribe: true
|
|
922
|
+
query: SELECT * FROM a WHERE id IN (SELECT id FROM magic_sequence WHERE count1 = auth.parameter('b'))
|
|
923
|
+
c:
|
|
924
|
+
auto_subscribe: true
|
|
925
|
+
query: SELECT * FROM a WHERE id IN (SELECT id FROM magic_sequence WHERE count1 = auth.parameter('c'))
|
|
926
|
+
`,
|
|
927
|
+
{ defaultSchema: 'public' }
|
|
928
|
+
).config.hydrate({
|
|
929
|
+
hydrationState: versionedHydrationState(1),
|
|
930
|
+
sqlite: nodeSqlite(sqlite)
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
const storage = new MockBucketChecksumStateStorage();
|
|
934
|
+
|
|
935
|
+
const errorMessages: string[] = [];
|
|
936
|
+
const errorData: any[] = [];
|
|
937
|
+
const mockLogger = {
|
|
938
|
+
info: () => {},
|
|
939
|
+
error: (message: string, data: any) => {
|
|
940
|
+
errorMessages.push(message);
|
|
941
|
+
errorData.push(data);
|
|
942
|
+
},
|
|
943
|
+
warn: () => {},
|
|
944
|
+
debug: () => {}
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
const smallContext = new SyncContext({
|
|
948
|
+
maxBuckets: 100,
|
|
949
|
+
maxParameterQueryResults: 55,
|
|
950
|
+
maxDataFetchConcurrency: 10
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
const state = new BucketChecksumState({
|
|
954
|
+
syncContext: smallContext,
|
|
955
|
+
tokenPayload: new JwtPayload({
|
|
956
|
+
sub: 'u1',
|
|
957
|
+
// Create 60 total results: 30 a + 20 b + 10 c
|
|
958
|
+
a: BigInt(30),
|
|
959
|
+
b: BigInt(20),
|
|
960
|
+
c: BigInt(10)
|
|
961
|
+
}),
|
|
962
|
+
syncRequest,
|
|
963
|
+
syncRules,
|
|
964
|
+
bucketStorage: storage,
|
|
965
|
+
logger: mockLogger as any
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
await expect(
|
|
969
|
+
state.buildNextCheckpointLine({
|
|
970
|
+
base: storage.makeCheckpoint(1n, (lookups, limit) => {
|
|
971
|
+
expect(lookups).toHaveLength(1);
|
|
972
|
+
const [{ values }] = lookups;
|
|
973
|
+
expect(values[0]).toStrictEqual('lookup');
|
|
974
|
+
|
|
975
|
+
const count = Number(values[2]); // The count parameter from streams
|
|
976
|
+
if (count > limit) {
|
|
977
|
+
throw new ParameterSetLimitExceededError(limit);
|
|
978
|
+
}
|
|
979
|
+
return [{ lookup: lookups[0], rows: Array.from({ length: count }, (_, i) => ({ '0': BigInt(i) })) }];
|
|
980
|
+
}),
|
|
981
|
+
writeCheckpoint: null,
|
|
982
|
+
update: CHECKPOINT_INVALIDATE_ALL
|
|
983
|
+
})
|
|
984
|
+
).rejects.toThrow('Too many parameter query results (limit of 55)');
|
|
985
|
+
|
|
986
|
+
// Verify error log includes breakdown
|
|
987
|
+
expect(errorMessages[0]).toContain('Invoked parameter queries by definition:');
|
|
988
|
+
expect(errorMessages[0]).toContain('Stream a evaluating parameter on magic_sequence: 30 results.');
|
|
989
|
+
expect(errorMessages[0]).toContain('Stream b evaluating parameter on magic_sequence: 20 results.');
|
|
990
|
+
expect(errorMessages[0]).toContain(
|
|
991
|
+
'Stream c evaluating parameter on magic_sequence exceeded the remaining limit of 5 available results.'
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
expect(errorData[0].checkpoint).toEqual(1n);
|
|
995
|
+
expect(errorData[0].parameterResults).toEqual([
|
|
996
|
+
{
|
|
997
|
+
definition: 'Stream a evaluating parameter on magic_sequence',
|
|
998
|
+
didExceedLimit: false,
|
|
999
|
+
resultsOrLimit: 30
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
definition: 'Stream b evaluating parameter on magic_sequence',
|
|
1003
|
+
didExceedLimit: false,
|
|
1004
|
+
resultsOrLimit: 20
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
definition: 'Stream c evaluating parameter on magic_sequence',
|
|
1008
|
+
didExceedLimit: true,
|
|
1009
|
+
resultsOrLimit: 5
|
|
1010
|
+
}
|
|
1011
|
+
]);
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
test('throws error when too many lookups are requested at once', async () => {
|
|
1015
|
+
const syncRules = SqlSyncRules.fromYaml(
|
|
1016
|
+
`
|
|
1017
|
+
config:
|
|
1018
|
+
edition: 3
|
|
1019
|
+
|
|
1020
|
+
streams:
|
|
1021
|
+
a:
|
|
1022
|
+
auto_subscribe: true
|
|
1023
|
+
query: SELECT * FROM a WHERE x IN (SELECT x FROM b WHERE y IN auth.parameter('p'))
|
|
1024
|
+
`,
|
|
1025
|
+
{ defaultSchema: 'public' }
|
|
1026
|
+
).config.hydrate({
|
|
1027
|
+
hydrationState: versionedHydrationState(1),
|
|
1028
|
+
sqlite: nodeSqlite(sqlite)
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
const storage = new MockBucketChecksumStateStorage();
|
|
1032
|
+
|
|
1033
|
+
const errorData: any[] = [];
|
|
1034
|
+
const mockLogger = {
|
|
1035
|
+
info: () => {},
|
|
1036
|
+
error: (_message: string, data: any) => {
|
|
1037
|
+
errorData.push(data);
|
|
1038
|
+
},
|
|
1039
|
+
warn: () => {},
|
|
1040
|
+
debug: () => {}
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
const smallContext = new SyncContext({
|
|
1044
|
+
maxBuckets: 100,
|
|
1045
|
+
maxParameterQueryResults: 10,
|
|
1046
|
+
maxDataFetchConcurrency: 10
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
const state = new BucketChecksumState({
|
|
1050
|
+
syncContext: smallContext,
|
|
1051
|
+
tokenPayload: new JwtPayload({
|
|
1052
|
+
sub: 'u1',
|
|
1053
|
+
p: Array.from({ length: 100 }, (_, i) => i)
|
|
1054
|
+
}),
|
|
1055
|
+
syncRequest,
|
|
1056
|
+
syncRules,
|
|
1057
|
+
bucketStorage: storage,
|
|
1058
|
+
logger: mockLogger as any
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
await expect(
|
|
1062
|
+
state.buildNextCheckpointLine({
|
|
1063
|
+
base: storage.makeCheckpoint(1n, () => {
|
|
1064
|
+
throw new Error('should not get called');
|
|
1065
|
+
}),
|
|
1066
|
+
writeCheckpoint: null,
|
|
1067
|
+
update: CHECKPOINT_INVALIDATE_ALL
|
|
1068
|
+
})
|
|
1069
|
+
).rejects.toThrow('Attempted to fetch 100 lookups at once, a maximum of 10 lookups are allowed');
|
|
1070
|
+
|
|
1071
|
+
expect(errorData).toStrictEqual([
|
|
1072
|
+
{
|
|
1073
|
+
user_id: 'u1',
|
|
1074
|
+
checkpoint: 1n,
|
|
1075
|
+
cause: 'Stream a evaluating parameter on b'
|
|
1076
|
+
}
|
|
1077
|
+
]);
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
test('throws error with breakdown when bucket limit is exceeded', async () => {
|
|
1081
|
+
// These streams are designed to return buckets without consuming too many parameters (as exceeding that limit is
|
|
1082
|
+
// a different error).
|
|
904
1083
|
const SYNC_RULES_MULTI = SqlSyncRules.fromYaml(
|
|
905
1084
|
`
|
|
906
|
-
|
|
1085
|
+
config:
|
|
1086
|
+
edition: 3
|
|
1087
|
+
|
|
1088
|
+
streams:
|
|
907
1089
|
projects:
|
|
908
|
-
|
|
909
|
-
|
|
1090
|
+
auto_subscribe: true
|
|
1091
|
+
query: SELECT id FROM projects WHERE p IN auth.parameter('a')
|
|
910
1092
|
tasks:
|
|
911
|
-
|
|
912
|
-
|
|
1093
|
+
auto_subscribe: true
|
|
1094
|
+
query: SELECT id FROM tasks WHERE p IN auth.parameter('b')
|
|
913
1095
|
comments:
|
|
914
|
-
|
|
915
|
-
|
|
1096
|
+
auto_subscribe: true
|
|
1097
|
+
query: SELECT id FROM comments WHERE p IN auth.parameter('c')
|
|
916
1098
|
`,
|
|
917
1099
|
{ defaultSchema: 'public' }
|
|
918
|
-
).config.hydrate({ hydrationState: versionedHydrationState(4) });
|
|
1100
|
+
).config.hydrate({ hydrationState: versionedHydrationState(4), sqlite: nodeSqlite(sqlite) });
|
|
919
1101
|
|
|
920
1102
|
const storage = new MockBucketChecksumStateStorage();
|
|
921
1103
|
|
|
@@ -932,79 +1114,66 @@ bucket_definitions:
|
|
|
932
1114
|
};
|
|
933
1115
|
|
|
934
1116
|
const smallContext = new SyncContext({
|
|
935
|
-
maxBuckets:
|
|
936
|
-
maxParameterQueryResults:
|
|
1117
|
+
maxBuckets: 50,
|
|
1118
|
+
maxParameterQueryResults: 10,
|
|
937
1119
|
maxDataFetchConcurrency: 10
|
|
938
1120
|
});
|
|
939
1121
|
|
|
940
1122
|
const state = new BucketChecksumState({
|
|
941
1123
|
syncContext: smallContext,
|
|
942
|
-
tokenPayload: new JwtPayload({
|
|
1124
|
+
tokenPayload: new JwtPayload({
|
|
1125
|
+
sub: 'u1',
|
|
1126
|
+
// Create 60 total results: 30 projects + 20 tasks + 10 comments
|
|
1127
|
+
a: Array.from({ length: 30 }, (_, i) => BigInt(i)),
|
|
1128
|
+
b: Array.from({ length: 20 }, (_, i) => BigInt(i)),
|
|
1129
|
+
c: Array.from({ length: 10 }, (_, i) => BigInt(i))
|
|
1130
|
+
}),
|
|
943
1131
|
syncRequest,
|
|
944
1132
|
syncRules: SYNC_RULES_MULTI,
|
|
945
1133
|
bucketStorage: storage,
|
|
946
1134
|
logger: mockLogger as any
|
|
947
1135
|
});
|
|
948
1136
|
|
|
949
|
-
// Create 60 total results: 30 projects + 20 tasks + 10 comments
|
|
950
|
-
const projectIds = Array.from({ length: 30 }, (_, i) => ({ id: i + 1 }));
|
|
951
|
-
const taskIds = Array.from({ length: 20 }, (_, i) => ({ id: i + 1 }));
|
|
952
|
-
const commentIds = Array.from({ length: 10 }, (_, i) => ({ id: i + 1 }));
|
|
953
|
-
|
|
954
|
-
for (let i = 1; i <= 30; i++) {
|
|
955
|
-
storage.updateTestChecksum({ bucket: `projects[${i}]`, checksum: 1, count: 1 });
|
|
956
|
-
}
|
|
957
|
-
for (let i = 1; i <= 20; i++) {
|
|
958
|
-
storage.updateTestChecksum({ bucket: `tasks[${i}]`, checksum: 1, count: 1 });
|
|
959
|
-
}
|
|
960
|
-
for (let i = 1; i <= 10; i++) {
|
|
961
|
-
storage.updateTestChecksum({ bucket: `comments[${i}]`, checksum: 1, count: 1 });
|
|
962
|
-
}
|
|
963
|
-
|
|
964
1137
|
await expect(
|
|
965
1138
|
state.buildNextCheckpointLine({
|
|
966
|
-
base: storage.makeCheckpoint(1n,
|
|
967
|
-
const lookup = lookups[0];
|
|
968
|
-
const lookupName = lookup.values[0];
|
|
969
|
-
if (lookupName === 'projects') {
|
|
970
|
-
return projectIds;
|
|
971
|
-
} else if (lookupName === 'tasks') {
|
|
972
|
-
return taskIds;
|
|
973
|
-
} else {
|
|
974
|
-
return commentIds;
|
|
975
|
-
}
|
|
976
|
-
}),
|
|
1139
|
+
base: storage.makeCheckpoint(1n),
|
|
977
1140
|
writeCheckpoint: null,
|
|
978
1141
|
update: CHECKPOINT_INVALIDATE_ALL
|
|
979
1142
|
})
|
|
980
|
-
).rejects.toThrow('Too many
|
|
1143
|
+
).rejects.toThrow('Too many buckets: 60 (limit of 50');
|
|
981
1144
|
|
|
982
1145
|
// Verify error log includes breakdown
|
|
983
|
-
expect(errorMessages[0]).toContain('
|
|
1146
|
+
expect(errorMessages[0]).toContain('Buckets by definition:');
|
|
984
1147
|
expect(errorMessages[0]).toContain('projects: 30');
|
|
985
1148
|
expect(errorMessages[0]).toContain('tasks: 20');
|
|
986
1149
|
expect(errorMessages[0]).toContain('comments: 10');
|
|
987
1150
|
|
|
988
1151
|
expect(errorData[0].checkpoint).toEqual(1n);
|
|
989
|
-
expect(errorData[0].
|
|
990
|
-
expect(errorData[0].
|
|
1152
|
+
expect(errorData[0].buckets).toBe(60);
|
|
1153
|
+
expect(errorData[0].buckets_by_definition).toEqual({
|
|
991
1154
|
projects: 30,
|
|
992
1155
|
tasks: 20,
|
|
993
1156
|
comments: 10
|
|
994
1157
|
});
|
|
995
1158
|
});
|
|
996
1159
|
|
|
997
|
-
test('limits breakdown to top 10 definitions', async () => {
|
|
998
|
-
// Create sync
|
|
999
|
-
let yamlDefinitions =
|
|
1160
|
+
test('limits bucket breakdown to top 10 definitions', async () => {
|
|
1161
|
+
// Create sync streams with 15 different definitions with dynamic parameters
|
|
1162
|
+
let yamlDefinitions = `
|
|
1163
|
+
config:
|
|
1164
|
+
edition: 3
|
|
1165
|
+
|
|
1166
|
+
streams:
|
|
1167
|
+
`;
|
|
1000
1168
|
for (let i = 1; i <= 15; i++) {
|
|
1001
1169
|
yamlDefinitions += ` def${i}:\n`;
|
|
1002
|
-
yamlDefinitions += `
|
|
1003
|
-
yamlDefinitions += `
|
|
1170
|
+
yamlDefinitions += ` auto_subscribe: true\n`;
|
|
1171
|
+
yamlDefinitions += ` query: SELECT * FROM tbl WHERE b = auth.parameter('${i}')\n`;
|
|
1004
1172
|
}
|
|
1005
1173
|
|
|
1006
1174
|
const SYNC_RULES_MANY = SqlSyncRules.fromYaml(yamlDefinitions, { defaultSchema: 'public' }).config.hydrate({
|
|
1007
|
-
hydrationState: versionedHydrationState(5)
|
|
1175
|
+
hydrationState: versionedHydrationState(5),
|
|
1176
|
+
sqlite: nodeSqlite(sqlite)
|
|
1008
1177
|
});
|
|
1009
1178
|
|
|
1010
1179
|
const storage = new MockBucketChecksumStateStorage();
|
|
@@ -1020,35 +1189,30 @@ bucket_definitions:
|
|
|
1020
1189
|
};
|
|
1021
1190
|
|
|
1022
1191
|
const smallContext = new SyncContext({
|
|
1023
|
-
maxBuckets:
|
|
1192
|
+
maxBuckets: 10,
|
|
1024
1193
|
maxParameterQueryResults: 10,
|
|
1025
1194
|
maxDataFetchConcurrency: 10
|
|
1026
1195
|
});
|
|
1027
1196
|
|
|
1028
1197
|
const state = new BucketChecksumState({
|
|
1029
1198
|
syncContext: smallContext,
|
|
1030
|
-
tokenPayload: new JwtPayload({
|
|
1199
|
+
tokenPayload: new JwtPayload({
|
|
1200
|
+
sub: 'u1',
|
|
1201
|
+
...Object.fromEntries(Array.from({ length: 30 }, (_, i) => [i.toString(), BigInt(i)]))
|
|
1202
|
+
}),
|
|
1031
1203
|
syncRequest,
|
|
1032
1204
|
syncRules: SYNC_RULES_MANY,
|
|
1033
1205
|
bucketStorage: storage,
|
|
1034
1206
|
logger: mockLogger as any
|
|
1035
1207
|
});
|
|
1036
1208
|
|
|
1037
|
-
// Each definition creates one bucket, total 15 buckets
|
|
1038
|
-
for (let i = 1; i <= 15; i++) {
|
|
1039
|
-
storage.updateTestChecksum({ bucket: `def${i}[${i}]`, checksum: 1, count: 1 });
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
1209
|
await expect(
|
|
1043
1210
|
state.buildNextCheckpointLine({
|
|
1044
|
-
base: storage.makeCheckpoint(1n,
|
|
1045
|
-
// Return one result for each definition
|
|
1046
|
-
return [{ id: 1 }];
|
|
1047
|
-
}),
|
|
1211
|
+
base: storage.makeCheckpoint(1n),
|
|
1048
1212
|
writeCheckpoint: null,
|
|
1049
1213
|
update: CHECKPOINT_INVALIDATE_ALL
|
|
1050
1214
|
})
|
|
1051
|
-
).rejects.toThrow('Too many
|
|
1215
|
+
).rejects.toThrow('Too many buckets: 15 (limit of 10)');
|
|
1052
1216
|
|
|
1053
1217
|
// Verify only top 10 are shown
|
|
1054
1218
|
const errorMessage = errorMessages[0];
|
|
@@ -1094,16 +1258,16 @@ class MockBucketChecksumStateStorage implements BucketChecksumStateStorage {
|
|
|
1094
1258
|
|
|
1095
1259
|
makeCheckpoint(
|
|
1096
1260
|
opId: InternalOpId,
|
|
1097
|
-
parameters?: (lookups: ScopedParameterLookup[]) =>
|
|
1261
|
+
parameters?: (lookups: ScopedParameterLookup[], limit: number) => ParameterLookupRows[]
|
|
1098
1262
|
): ReplicationCheckpoint {
|
|
1099
1263
|
return {
|
|
1100
1264
|
checkpoint: opId,
|
|
1101
1265
|
lsn: String(opId),
|
|
1102
|
-
getParameterSets: async (lookups: ScopedParameterLookup[]) => {
|
|
1266
|
+
getParameterSets: async (lookups: ScopedParameterLookup[], limit) => {
|
|
1103
1267
|
if (parameters == null) {
|
|
1104
1268
|
throw new Error(`getParametersSets not defined for checkpoint ${opId}`);
|
|
1105
1269
|
}
|
|
1106
|
-
return parameters(lookups);
|
|
1270
|
+
return parameters(lookups, limit);
|
|
1107
1271
|
}
|
|
1108
1272
|
};
|
|
1109
1273
|
}
|