Skip to content

Commit a53f2e5

Browse files
committed
fix(modules): address targeted RESP compatibility regressions
- Fix GEO float reply handling across geosearch-compatible paths. - Fix Bloom CF.INSERTNX status handling and Search PROFILE parsing edge cases. - Fix TimeSeries MRANGE selected-label/groupby compatibility behavior.
1 parent dc5a4e0 commit a53f2e5

File tree

7 files changed

+109
-20
lines changed

7 files changed

+109
-20
lines changed

packages/bloom/lib/commands/cuckoo/INSERTNX.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Command } from '@redis/client/dist/lib/RESP/types';
1+
import { ArrayReply, Command, NumberReply } from '@redis/client/dist/lib/RESP/types';
22
import INSERT, { parseCfInsertArguments } from './INSERT';
33

44
/**
@@ -16,5 +16,5 @@ export default {
1616
args[0].push('CF.INSERTNX');
1717
parseCfInsertArguments(...args);
1818
},
19-
transformReply: INSERT.transformReply
19+
transformReply: undefined as unknown as () => ArrayReply<NumberReply<-1 | 0 | 1>>
2020
} as const satisfies Command;

packages/client/lib/commands/GEOSEARCH_WITH.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { CommandParser } from '../client/parser';
2-
import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, NumberReply, DoubleReply, UnwrapReply, Command } from '../RESP/types';
2+
import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, NumberReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '../RESP/types';
33
import GEOSEARCH, { GeoSearchBy, GeoSearchFrom, GeoSearchOptions } from './GEOSEARCH';
4+
import { transformDoubleReply } from './generic-transformers';
45

56
export const GEO_REPLY_WITH = {
67
DISTANCE: 'WITHDIST',
@@ -12,7 +13,7 @@ export type GeoReplyWith = typeof GEO_REPLY_WITH[keyof typeof GEO_REPLY_WITH];
1213

1314
export interface GeoReplyWithMember {
1415
member: BlobStringReply;
15-
distance?: BlobStringReply;
16+
distance?: DoubleReply;
1617
hash?: NumberReply;
1718
coordinates?: {
1819
longitude: DoubleReply;
@@ -45,14 +46,23 @@ export default {
4546
},
4647
transformReply(
4748
reply: UnwrapReply<ArrayReply<TuplesReply<[BlobStringReply, ...Array<any>]>>>,
48-
replyWith: Array<GeoReplyWith>
49+
replyWith: Array<GeoReplyWith>,
50+
typeMapping?: TypeMapping
4951
) {
5052
const replyWithSet = new Set(replyWith);
5153
let index = 0;
5254
const distanceIndex = replyWithSet.has(GEO_REPLY_WITH.DISTANCE) && ++index,
5355
hashIndex = replyWithSet.has(GEO_REPLY_WITH.HASH) && ++index,
5456
coordinatesIndex = replyWithSet.has(GEO_REPLY_WITH.COORDINATES) && ++index;
55-
57+
58+
const parseDouble = (value: unknown) => {
59+
return (
60+
typeof value === 'number' ?
61+
value as unknown as DoubleReply :
62+
transformDoubleReply[2](value as BlobStringReply, undefined, typeMapping)
63+
);
64+
};
65+
5666
return reply.map(raw => {
5767
const unwrapped = raw as unknown as UnwrapReply<typeof raw>;
5868

@@ -61,18 +71,18 @@ export default {
6171
};
6272

6373
if (distanceIndex) {
64-
item.distance = unwrapped[distanceIndex];
74+
item.distance = parseDouble(unwrapped[distanceIndex]);
6575
}
66-
76+
6777
if (hashIndex) {
6878
item.hash = unwrapped[hashIndex];
6979
}
70-
80+
7181
if (coordinatesIndex) {
7282
const [longitude, latitude] = unwrapped[coordinatesIndex];
7383
item.coordinates = {
74-
longitude,
75-
latitude
84+
longitude: parseDouble(longitude),
85+
latitude: parseDouble(latitude)
7686
};
7787
}
7888

packages/search/lib/commands/PROFILE_AGGREGATE.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { CommandParser } from '@redis/client/dist/lib/client/parser';
22
import { Command, ReplyUnion, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
33
import AGGREGATE, { AggregateRawReply, FtAggregateOptions, parseAggregateOptions } from './AGGREGATE';
4-
import { ProfileOptions, ProfileRawReplyResp2, ProfileReplyResp2, } from './PROFILE_SEARCH';
4+
import {
5+
ProfileOptions,
6+
ProfileRawReplyResp2,
7+
ProfileReplyResp2,
8+
extractProfileResultsReply,
9+
transformProfileReply
10+
} from './PROFILE_SEARCH';
511

612
export default {
713
NOT_KEYED_COMMAND: true,
@@ -38,6 +44,13 @@ export default {
3844
profile: reply[1]
3945
}
4046
},
41-
3: (reply: ReplyUnion): ReplyUnion => reply
47+
3: (reply: ReplyUnion): ProfileReplyResp2 => {
48+
return {
49+
results: AGGREGATE.transformReply[3](
50+
extractProfileResultsReply(reply)
51+
),
52+
profile: transformProfileReply(reply)
53+
};
54+
}
4255
},
4356
} as const satisfies Command;

packages/search/lib/commands/PROFILE_SEARCH.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser';
22
import { ArrayReply, Command, RedisArgument, ReplyUnion, TuplesReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
33
import { AggregateReply } from './AGGREGATE';
44
import SEARCH, { FtSearchOptions, SearchRawReply, SearchReply, parseSearchOptions } from './SEARCH';
5+
import { getMapValue, mapLikeEntries, mapLikeToObject, normalizeProfileReply } from './reply-transformers';
56

67
export type ProfileRawReplyResp2<T> = TuplesReply<[
78
T,
@@ -19,6 +20,73 @@ export interface ProfileOptions {
1920
LIMITED?: true;
2021
}
2122

23+
export function extractProfileResultsReply(reply: ReplyUnion): ReplyUnion {
24+
const replyObject = mapLikeToObject(reply);
25+
26+
// Redis 8+ wraps results under `Results`.
27+
if (Object.hasOwn(replyObject, 'Results')) {
28+
return replyObject['Results'] as ReplyUnion;
29+
}
30+
31+
// Redis 7.4 RESP3 returns search/aggregate payload directly at top-level.
32+
if (
33+
(Object.hasOwn(replyObject, 'total_results') || Object.hasOwn(replyObject, 'total')) &&
34+
Object.hasOwn(replyObject, 'results')
35+
) {
36+
return reply;
37+
}
38+
39+
if (Object.hasOwn(replyObject, 'results')) {
40+
return replyObject['results'] as ReplyUnion;
41+
}
42+
43+
return (getMapValue(replyObject, ['results']) ?? reply) as ReplyUnion;
44+
}
45+
46+
function normalizeLegacyProfileReply(profile: ReplyUnion): ReplyUnion {
47+
return mapLikeEntries(profile).map(([key, value]) => {
48+
// Redis 7.4 often wraps iterator profiles as a single-element array containing an object.
49+
// Tests expect the inner object normalized directly as a flat key/value list.
50+
if (Array.isArray(value) && value.length === 1) {
51+
const first = value[0];
52+
if (Object.keys(mapLikeToObject(first)).length > 0) {
53+
return [key, normalizeProfileReply(first)];
54+
}
55+
}
56+
57+
return [key, normalizeProfileReply(value)];
58+
}) as unknown as ReplyUnion;
59+
}
60+
61+
export function transformProfileReply(reply: ReplyUnion): ReplyUnion {
62+
const replyObject = mapLikeToObject(reply);
63+
const profile = (
64+
Object.hasOwn(replyObject, 'Profile') ?
65+
replyObject['Profile'] :
66+
Object.hasOwn(replyObject, 'profile') ?
67+
replyObject['profile'] :
68+
getMapValue(replyObject, ['Profile', 'profile'])
69+
) as ReplyUnion;
70+
71+
const profileObject = mapLikeToObject(profile);
72+
73+
// Redis 7.2 - 7.4 profile payload is a plain map keyed by timing labels.
74+
if (Object.hasOwn(profileObject, 'Total profile time')) {
75+
return normalizeLegacyProfileReply(profile);
76+
}
77+
78+
return normalizeProfileReply(profile) as ReplyUnion;
79+
}
80+
81+
function transformProfileSearchReplyResp3(reply: ReplyUnion): ProfileReplyResp2 {
82+
return {
83+
results: SEARCH.transformReply[3](
84+
extractProfileResultsReply(reply)
85+
),
86+
profile: transformProfileReply(reply)
87+
};
88+
}
89+
2290
export default {
2391
NOT_KEYED_COMMAND: true,
2492
IS_READ_ONLY: true,
@@ -54,6 +122,6 @@ export default {
54122
profile: reply[1]
55123
};
56124
},
57-
3: (reply: ReplyUnion): ReplyUnion => reply
125+
3: transformProfileSearchReplyResp3
58126
},
59127
} as const satisfies Command;

packages/time-series/lib/commands/MRANGE_GROUPBY.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,8 @@ export default {
122122
}, typeMapping);
123123
},
124124
3(reply: TsMRangeGroupByRawReply3) {
125-
return resp3MapToValue(reply, ([_labels, _metadata1, metadata2, samples]) => {
125+
return resp3MapToValue(reply, ([_labels, _metadata1, _metadata2, samples]) => {
126126
return {
127-
sources: extractResp3MRangeSources(metadata2),
128127
samples: transformSamplesReply[3](samples)
129128
};
130129
});

packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export default {
7474
}, typeMapping);
7575
},
7676
3(reply: TsMRangeSelectedLabelsRawReply3) {
77-
return resp3MapToValue(reply, ([_key, labels, samples]) => {
77+
return resp3MapToValue(reply, ([labels, _metadata, samples]) => {
7878
return {
7979
labels,
8080
samples: transformSamplesReply[3](samples)

packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Command, ArrayReply, BlobStringReply, MapReply, TuplesReply, RedisArgum
33
import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
44
import { parseSelectedLabelsArguments, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from './helpers';
55
import { TsRangeOptions, parseRangeArguments } from './RANGE';
6-
import { extractResp3MRangeSources, parseGroupByArguments, TsMRangeGroupBy, TsMRangeGroupByRawMetadataReply3 } from './MRANGE_GROUPBY';
6+
import { parseGroupByArguments, TsMRangeGroupBy, TsMRangeGroupByRawMetadataReply3 } from './MRANGE_GROUPBY';
77
import { parseFilterArgument } from './MGET';
88
import MRANGE_SELECTED_LABELS from './MRANGE_SELECTED_LABELS';
99

@@ -65,10 +65,9 @@ export default {
6565
transformReply: {
6666
2: MRANGE_SELECTED_LABELS.transformReply[2],
6767
3(reply: TsMRangeWithLabelsGroupByRawReply3) {
68-
return resp3MapToValue(reply, ([labels, _metadata, metadata2, samples]) => {
68+
return resp3MapToValue(reply, ([labels, _metadata, _metadata2, samples]) => {
6969
return {
7070
labels,
71-
sources: extractResp3MRangeSources(metadata2),
7271
samples: transformSamplesReply[3](samples)
7372
};
7473
});

0 commit comments

Comments
 (0)