@powersync/service-core 0.0.0-dev-20260223080959 → 0.0.0-dev-20260224151854

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.
@@ -827,6 +827,206 @@ config:
827
827
  });
828
828
  });
829
829
  });
830
+
831
+ describe('parameter query result logging', () => {
832
+ test('logs parameter query results for dynamic buckets', async () => {
833
+ const storage = new MockBucketChecksumStateStorage();
834
+ storage.updateTestChecksum({ bucket: 'by_project[1]', checksum: 1, count: 1 });
835
+ storage.updateTestChecksum({ bucket: 'by_project[2]', checksum: 1, count: 1 });
836
+ storage.updateTestChecksum({ bucket: 'by_project[3]', checksum: 1, count: 1 });
837
+
838
+ const logMessages: string[] = [];
839
+ const logData: any[] = [];
840
+ const mockLogger = {
841
+ info: (message: string, data: any) => {
842
+ logMessages.push(message);
843
+ logData.push(data);
844
+ },
845
+ error: () => {},
846
+ warn: () => {},
847
+ debug: () => {}
848
+ };
849
+
850
+ const state = new BucketChecksumState({
851
+ syncContext,
852
+ tokenPayload: new JwtPayload({ sub: 'u1' }),
853
+ syncRequest,
854
+ syncRules: SYNC_RULES_DYNAMIC,
855
+ bucketStorage: storage,
856
+ logger: mockLogger as any
857
+ });
858
+
859
+ const line = (await state.buildNextCheckpointLine({
860
+ base: storage.makeCheckpoint(1n, (lookups) => {
861
+ return [{ id: 1 }, { id: 2 }, { id: 3 }];
862
+ }),
863
+ writeCheckpoint: null,
864
+ update: CHECKPOINT_INVALIDATE_ALL
865
+ }))!;
866
+ line.advance();
867
+
868
+ expect(logMessages[0]).toContain('param_results: 3');
869
+ expect(logData[0].parameter_query_results).toBe(3);
870
+ });
871
+
872
+ test('throws error with breakdown when parameter query limit is exceeded', async () => {
873
+ const SYNC_RULES_MULTI = SqlSyncRules.fromYaml(
874
+ `
875
+ bucket_definitions:
876
+ projects:
877
+ parameters: select id from projects where user_id = request.user_id()
878
+ data: []
879
+ tasks:
880
+ parameters: select id from tasks where user_id = request.user_id()
881
+ data: []
882
+ comments:
883
+ parameters: select id from comments where user_id = request.user_id()
884
+ data: []
885
+ `,
886
+ { defaultSchema: 'public' }
887
+ ).config.hydrate({ hydrationState: versionedHydrationState(4) });
888
+
889
+ const storage = new MockBucketChecksumStateStorage();
890
+
891
+ const errorMessages: string[] = [];
892
+ const errorData: any[] = [];
893
+ const mockLogger = {
894
+ info: () => {},
895
+ error: (message: string, data: any) => {
896
+ errorMessages.push(message);
897
+ errorData.push(data);
898
+ },
899
+ warn: () => {},
900
+ debug: () => {}
901
+ };
902
+
903
+ const smallContext = new SyncContext({
904
+ maxBuckets: 100,
905
+ maxParameterQueryResults: 50,
906
+ maxDataFetchConcurrency: 10
907
+ });
908
+
909
+ const state = new BucketChecksumState({
910
+ syncContext: smallContext,
911
+ tokenPayload: new JwtPayload({ sub: 'u1' }),
912
+ syncRequest,
913
+ syncRules: SYNC_RULES_MULTI,
914
+ bucketStorage: storage,
915
+ logger: mockLogger as any
916
+ });
917
+
918
+ // Create 60 total results: 30 projects + 20 tasks + 10 comments
919
+ const projectIds = Array.from({ length: 30 }, (_, i) => ({ id: i + 1 }));
920
+ const taskIds = Array.from({ length: 20 }, (_, i) => ({ id: i + 1 }));
921
+ const commentIds = Array.from({ length: 10 }, (_, i) => ({ id: i + 1 }));
922
+
923
+ for (let i = 1; i <= 30; i++) {
924
+ storage.updateTestChecksum({ bucket: `projects[${i}]`, checksum: 1, count: 1 });
925
+ }
926
+ for (let i = 1; i <= 20; i++) {
927
+ storage.updateTestChecksum({ bucket: `tasks[${i}]`, checksum: 1, count: 1 });
928
+ }
929
+ for (let i = 1; i <= 10; i++) {
930
+ storage.updateTestChecksum({ bucket: `comments[${i}]`, checksum: 1, count: 1 });
931
+ }
932
+
933
+ await expect(
934
+ state.buildNextCheckpointLine({
935
+ base: storage.makeCheckpoint(1n, (lookups) => {
936
+ const lookup = lookups[0];
937
+ const lookupName = lookup.values[0];
938
+ if (lookupName === 'projects') {
939
+ return projectIds;
940
+ } else if (lookupName === 'tasks') {
941
+ return taskIds;
942
+ } else {
943
+ return commentIds;
944
+ }
945
+ }),
946
+ writeCheckpoint: null,
947
+ update: CHECKPOINT_INVALIDATE_ALL
948
+ })
949
+ ).rejects.toThrow('Too many parameter query results: 60 (limit of 50)');
950
+
951
+ // Verify error log includes breakdown
952
+ expect(errorMessages[0]).toContain('Parameter query results by definition:');
953
+ expect(errorMessages[0]).toContain('projects: 30');
954
+ expect(errorMessages[0]).toContain('tasks: 20');
955
+ expect(errorMessages[0]).toContain('comments: 10');
956
+
957
+ expect(errorData[0].parameter_query_results).toBe(60);
958
+ expect(errorData[0].parameter_query_results_by_definition).toEqual({
959
+ projects: 30,
960
+ tasks: 20,
961
+ comments: 10
962
+ });
963
+ });
964
+
965
+ test('limits breakdown to top 10 definitions', async () => {
966
+ // Create sync rules with 15 different definitions with dynamic parameters
967
+ let yamlDefinitions = 'bucket_definitions:\n';
968
+ for (let i = 1; i <= 15; i++) {
969
+ yamlDefinitions += ` def${i}:\n`;
970
+ yamlDefinitions += ` parameters: select id from def${i}_table where user_id = request.user_id()\n`;
971
+ yamlDefinitions += ` data: []\n`;
972
+ }
973
+
974
+ const SYNC_RULES_MANY = SqlSyncRules.fromYaml(yamlDefinitions, { defaultSchema: 'public' }).config.hydrate({
975
+ hydrationState: versionedHydrationState(5)
976
+ });
977
+
978
+ const storage = new MockBucketChecksumStateStorage();
979
+
980
+ const errorMessages: string[] = [];
981
+ const mockLogger = {
982
+ info: () => {},
983
+ error: (message: string) => {
984
+ errorMessages.push(message);
985
+ },
986
+ warn: () => {},
987
+ debug: () => {}
988
+ };
989
+
990
+ const smallContext = new SyncContext({
991
+ maxBuckets: 100,
992
+ maxParameterQueryResults: 10,
993
+ maxDataFetchConcurrency: 10
994
+ });
995
+
996
+ const state = new BucketChecksumState({
997
+ syncContext: smallContext,
998
+ tokenPayload: new JwtPayload({ sub: 'u1' }),
999
+ syncRequest,
1000
+ syncRules: SYNC_RULES_MANY,
1001
+ bucketStorage: storage,
1002
+ logger: mockLogger as any
1003
+ });
1004
+
1005
+ // Each definition creates one bucket, total 15 buckets
1006
+ for (let i = 1; i <= 15; i++) {
1007
+ storage.updateTestChecksum({ bucket: `def${i}[${i}]`, checksum: 1, count: 1 });
1008
+ }
1009
+
1010
+ await expect(
1011
+ state.buildNextCheckpointLine({
1012
+ base: storage.makeCheckpoint(1n, (lookups) => {
1013
+ // Return one result for each definition
1014
+ return [{ id: 1 }];
1015
+ }),
1016
+ writeCheckpoint: null,
1017
+ update: CHECKPOINT_INVALIDATE_ALL
1018
+ })
1019
+ ).rejects.toThrow('Too many parameter query results: 15 (limit of 10)');
1020
+
1021
+ // Verify only top 10 are shown
1022
+ const errorMessage = errorMessages[0];
1023
+ expect(errorMessage).toContain('... and 5 more results from 5 definitions');
1024
+
1025
+ // Count how many definitions are listed (should be exactly 10)
1026
+ const defMatches = errorMessage.match(/def\d+:/g);
1027
+ expect(defMatches?.length).toBe(10);
1028
+ });
1029
+ });
830
1030
  });
831
1031
 
832
1032
  class MockBucketChecksumStateStorage implements BucketChecksumStateStorage {