]>
Commit | Line | Data |
---|---|---|
1 | /* SORT command and helper functions. | |
2 | * | |
3 | * Copyright (c) 2009-2012, Salvatore Sanfilippo <antirez at gmail dot com> | |
4 | * All rights reserved. | |
5 | * | |
6 | * Redistribution and use in source and binary forms, with or without | |
7 | * modification, are permitted provided that the following conditions are met: | |
8 | * | |
9 | * * Redistributions of source code must retain the above copyright notice, | |
10 | * this list of conditions and the following disclaimer. | |
11 | * * Redistributions in binary form must reproduce the above copyright | |
12 | * notice, this list of conditions and the following disclaimer in the | |
13 | * documentation and/or other materials provided with the distribution. | |
14 | * * Neither the name of Redis nor the names of its contributors may be used | |
15 | * to endorse or promote products derived from this software without | |
16 | * specific prior written permission. | |
17 | * | |
18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
19 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
20 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
21 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE | |
22 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
23 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
24 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
25 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
26 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
27 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
28 | * POSSIBILITY OF SUCH DAMAGE. | |
29 | */ | |
30 | ||
31 | ||
32 | #include "redis.h" | |
33 | #include "pqsort.h" /* Partial qsort for SORT+LIMIT */ | |
34 | #include <math.h> /* isnan() */ | |
35 | ||
36 | zskiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank); | |
37 | ||
38 | redisSortOperation *createSortOperation(int type, robj *pattern) { | |
39 | redisSortOperation *so = zmalloc(sizeof(*so)); | |
40 | so->type = type; | |
41 | so->pattern = pattern; | |
42 | return so; | |
43 | } | |
44 | ||
45 | /* Return the value associated to the key with a name obtained using | |
46 | * the following rules: | |
47 | * | |
48 | * 1) The first occurence of '*' in 'pattern' is substituted with 'subst'. | |
49 | * | |
50 | * 2) If 'pattern' matches the "->" string, everything on the left of | |
51 | * the arrow is treated as the name of an hash field, and the part on the | |
52 | * left as the key name containing an hash. The value of the specified | |
53 | * field is returned. | |
54 | * | |
55 | * 3) If 'pattern' equals "#", the function simply returns 'subst' itself so | |
56 | * that the SORT command can be used like: SORT key GET # to retrieve | |
57 | * the Set/List elements directly. | |
58 | * | |
59 | * The returned object will always have its refcount increased by 1 | |
60 | * when it is non-NULL. */ | |
61 | robj *lookupKeyByPattern(redisDb *db, robj *pattern, robj *subst) { | |
62 | char *p, *f, *k; | |
63 | sds spat, ssub; | |
64 | robj *keyobj, *fieldobj = NULL, *o; | |
65 | int prefixlen, sublen, postfixlen, fieldlen; | |
66 | ||
67 | /* If the pattern is "#" return the substitution object itself in order | |
68 | * to implement the "SORT ... GET #" feature. */ | |
69 | spat = pattern->ptr; | |
70 | if (spat[0] == '#' && spat[1] == '\0') { | |
71 | incrRefCount(subst); | |
72 | return subst; | |
73 | } | |
74 | ||
75 | /* The substitution object may be specially encoded. If so we create | |
76 | * a decoded object on the fly. Otherwise getDecodedObject will just | |
77 | * increment the ref count, that we'll decrement later. */ | |
78 | subst = getDecodedObject(subst); | |
79 | ssub = subst->ptr; | |
80 | ||
81 | /* If we can't find '*' in the pattern we return NULL as to GET a | |
82 | * fixed key does not make sense. */ | |
83 | p = strchr(spat,'*'); | |
84 | if (!p) { | |
85 | decrRefCount(subst); | |
86 | return NULL; | |
87 | } | |
88 | ||
89 | /* Find out if we're dealing with a hash dereference. */ | |
90 | if ((f = strstr(p+1, "->")) != NULL && *(f+2) != '\0') { | |
91 | fieldlen = sdslen(spat)-(f-spat)-2; | |
92 | fieldobj = createStringObject(f+2,fieldlen); | |
93 | } else { | |
94 | fieldlen = 0; | |
95 | } | |
96 | ||
97 | /* Perform the '*' substitution. */ | |
98 | prefixlen = p-spat; | |
99 | sublen = sdslen(ssub); | |
100 | postfixlen = sdslen(spat)-(prefixlen+1)-(fieldlen ? fieldlen+2 : 0); | |
101 | keyobj = createStringObject(NULL,prefixlen+sublen+postfixlen); | |
102 | k = keyobj->ptr; | |
103 | memcpy(k,spat,prefixlen); | |
104 | memcpy(k+prefixlen,ssub,sublen); | |
105 | memcpy(k+prefixlen+sublen,p+1,postfixlen); | |
106 | decrRefCount(subst); /* Incremented by decodeObject() */ | |
107 | ||
108 | /* Lookup substituted key */ | |
109 | o = lookupKeyRead(db,keyobj); | |
110 | if (o == NULL) goto noobj; | |
111 | ||
112 | if (fieldobj) { | |
113 | if (o->type != REDIS_HASH) goto noobj; | |
114 | ||
115 | /* Retrieve value from hash by the field name. This operation | |
116 | * already increases the refcount of the returned object. */ | |
117 | o = hashTypeGetObject(o, fieldobj); | |
118 | } else { | |
119 | if (o->type != REDIS_STRING) goto noobj; | |
120 | ||
121 | /* Every object that this function returns needs to have its refcount | |
122 | * increased. sortCommand decreases it again. */ | |
123 | incrRefCount(o); | |
124 | } | |
125 | decrRefCount(keyobj); | |
126 | if (fieldobj) decrRefCount(fieldobj); | |
127 | return o; | |
128 | ||
129 | noobj: | |
130 | decrRefCount(keyobj); | |
131 | if (fieldlen) decrRefCount(fieldobj); | |
132 | return NULL; | |
133 | } | |
134 | ||
135 | /* sortCompare() is used by qsort in sortCommand(). Given that qsort_r with | |
136 | * the additional parameter is not standard but a BSD-specific we have to | |
137 | * pass sorting parameters via the global 'server' structure */ | |
138 | int sortCompare(const void *s1, const void *s2) { | |
139 | const redisSortObject *so1 = s1, *so2 = s2; | |
140 | int cmp; | |
141 | ||
142 | if (!server.sort_alpha) { | |
143 | /* Numeric sorting. Here it's trivial as we precomputed scores */ | |
144 | if (so1->u.score > so2->u.score) { | |
145 | cmp = 1; | |
146 | } else if (so1->u.score < so2->u.score) { | |
147 | cmp = -1; | |
148 | } else { | |
149 | /* Objects have the same score, but we don't want the comparison | |
150 | * to be undefined, so we compare objects lexicographycally. | |
151 | * This way the result of SORT is deterministic. */ | |
152 | cmp = compareStringObjects(so1->obj,so2->obj); | |
153 | } | |
154 | } else { | |
155 | /* Alphanumeric sorting */ | |
156 | if (server.sort_bypattern) { | |
157 | if (!so1->u.cmpobj || !so2->u.cmpobj) { | |
158 | /* At least one compare object is NULL */ | |
159 | if (so1->u.cmpobj == so2->u.cmpobj) | |
160 | cmp = 0; | |
161 | else if (so1->u.cmpobj == NULL) | |
162 | cmp = -1; | |
163 | else | |
164 | cmp = 1; | |
165 | } else { | |
166 | /* We have both the objects, use strcoll */ | |
167 | cmp = strcoll(so1->u.cmpobj->ptr,so2->u.cmpobj->ptr); | |
168 | } | |
169 | } else { | |
170 | /* Compare elements directly. */ | |
171 | cmp = compareStringObjects(so1->obj,so2->obj); | |
172 | } | |
173 | } | |
174 | return server.sort_desc ? -cmp : cmp; | |
175 | } | |
176 | ||
177 | /* The SORT command is the most complex command in Redis. Warning: this code | |
178 | * is optimized for speed and a bit less for readability */ | |
179 | void sortCommand(redisClient *c) { | |
180 | list *operations; | |
181 | unsigned int outputlen = 0; | |
182 | int desc = 0, alpha = 0; | |
183 | long limit_start = 0, limit_count = -1, start, end; | |
184 | int j, dontsort = 0, vectorlen; | |
185 | int getop = 0; /* GET operation counter */ | |
186 | int int_convertion_error = 0; | |
187 | robj *sortval, *sortby = NULL, *storekey = NULL; | |
188 | redisSortObject *vector; /* Resulting vector to sort */ | |
189 | ||
190 | /* Lookup the key to sort. It must be of the right types */ | |
191 | sortval = lookupKeyRead(c->db,c->argv[1]); | |
192 | if (sortval && sortval->type != REDIS_SET && | |
193 | sortval->type != REDIS_LIST && | |
194 | sortval->type != REDIS_ZSET) | |
195 | { | |
196 | addReply(c,shared.wrongtypeerr); | |
197 | return; | |
198 | } | |
199 | ||
200 | /* Create a list of operations to perform for every sorted element. | |
201 | * Operations can be GET/DEL/INCR/DECR */ | |
202 | operations = listCreate(); | |
203 | listSetFreeMethod(operations,zfree); | |
204 | j = 2; /* options start at argv[2] */ | |
205 | ||
206 | /* Now we need to protect sortval incrementing its count, in the future | |
207 | * SORT may have options able to overwrite/delete keys during the sorting | |
208 | * and the sorted key itself may get destroied */ | |
209 | if (sortval) | |
210 | incrRefCount(sortval); | |
211 | else | |
212 | sortval = createListObject(); | |
213 | ||
214 | /* The SORT command has an SQL-alike syntax, parse it */ | |
215 | while(j < c->argc) { | |
216 | int leftargs = c->argc-j-1; | |
217 | if (!strcasecmp(c->argv[j]->ptr,"asc")) { | |
218 | desc = 0; | |
219 | } else if (!strcasecmp(c->argv[j]->ptr,"desc")) { | |
220 | desc = 1; | |
221 | } else if (!strcasecmp(c->argv[j]->ptr,"alpha")) { | |
222 | alpha = 1; | |
223 | } else if (!strcasecmp(c->argv[j]->ptr,"limit") && leftargs >= 2) { | |
224 | if ((getLongFromObjectOrReply(c, c->argv[j+1], &limit_start, NULL) != REDIS_OK) || | |
225 | (getLongFromObjectOrReply(c, c->argv[j+2], &limit_count, NULL) != REDIS_OK)) return; | |
226 | j+=2; | |
227 | } else if (!strcasecmp(c->argv[j]->ptr,"store") && leftargs >= 1) { | |
228 | storekey = c->argv[j+1]; | |
229 | j++; | |
230 | } else if (!strcasecmp(c->argv[j]->ptr,"by") && leftargs >= 1) { | |
231 | sortby = c->argv[j+1]; | |
232 | /* If the BY pattern does not contain '*', i.e. it is constant, | |
233 | * we don't need to sort nor to lookup the weight keys. */ | |
234 | if (strchr(c->argv[j+1]->ptr,'*') == NULL) dontsort = 1; | |
235 | j++; | |
236 | } else if (!strcasecmp(c->argv[j]->ptr,"get") && leftargs >= 1) { | |
237 | listAddNodeTail(operations,createSortOperation( | |
238 | REDIS_SORT_GET,c->argv[j+1])); | |
239 | getop++; | |
240 | j++; | |
241 | } else { | |
242 | decrRefCount(sortval); | |
243 | listRelease(operations); | |
244 | addReply(c,shared.syntaxerr); | |
245 | return; | |
246 | } | |
247 | j++; | |
248 | } | |
249 | ||
250 | /* For the STORE option, or when SORT is called from a Lua script, | |
251 | * we want to force a specific ordering even when no explicit ordering | |
252 | * was asked (SORT BY nosort). This guarantees that replication / AOF | |
253 | * is deterministic. | |
254 | * | |
255 | * However in the case 'dontsort' is true, but the type to sort is a | |
256 | * sorted set, we don't need to do anything as ordering is guaranteed | |
257 | * in this special case. */ | |
258 | if ((storekey || c->flags & REDIS_LUA_CLIENT) && | |
259 | (dontsort && sortval->type != REDIS_ZSET)) | |
260 | { | |
261 | /* Force ALPHA sorting */ | |
262 | dontsort = 0; | |
263 | alpha = 1; | |
264 | sortby = NULL; | |
265 | } | |
266 | ||
267 | /* Destructively convert encoded sorted sets for SORT. */ | |
268 | if (sortval->type == REDIS_ZSET) | |
269 | zsetConvert(sortval, REDIS_ENCODING_SKIPLIST); | |
270 | ||
271 | /* Objtain the length of the object to sort. */ | |
272 | switch(sortval->type) { | |
273 | case REDIS_LIST: vectorlen = listTypeLength(sortval); break; | |
274 | case REDIS_SET: vectorlen = setTypeSize(sortval); break; | |
275 | case REDIS_ZSET: vectorlen = dictSize(((zset*)sortval->ptr)->dict); break; | |
276 | default: vectorlen = 0; redisPanic("Bad SORT type"); /* Avoid GCC warning */ | |
277 | } | |
278 | ||
279 | /* Perform LIMIT start,count sanity checking. */ | |
280 | start = (limit_start < 0) ? 0 : limit_start; | |
281 | end = (limit_count < 0) ? vectorlen-1 : start+limit_count-1; | |
282 | if (start >= vectorlen) { | |
283 | start = vectorlen-1; | |
284 | end = vectorlen-2; | |
285 | } | |
286 | if (end >= vectorlen) end = vectorlen-1; | |
287 | ||
288 | /* Optimization: | |
289 | * | |
290 | * 1) if the object to sort is a sorted set. | |
291 | * 2) There is nothing to sort as dontsort is true (BY <constant string>). | |
292 | * 3) We have a LIMIT option that actually reduces the number of elements | |
293 | * to fetch. | |
294 | * | |
295 | * In this case to load all the objects in the vector is a huge waste of | |
296 | * resources. We just allocate a vector that is big enough for the selected | |
297 | * range length, and make sure to load just this part in the vector. */ | |
298 | if (sortval->type == REDIS_ZSET && | |
299 | dontsort && | |
300 | (start != 0 || end != vectorlen-1)) | |
301 | { | |
302 | vectorlen = end-start+1; | |
303 | } | |
304 | ||
305 | /* Load the sorting vector with all the objects to sort */ | |
306 | vector = zmalloc(sizeof(redisSortObject)*vectorlen); | |
307 | j = 0; | |
308 | ||
309 | if (sortval->type == REDIS_LIST) { | |
310 | listTypeIterator *li = listTypeInitIterator(sortval,0,REDIS_TAIL); | |
311 | listTypeEntry entry; | |
312 | while(listTypeNext(li,&entry)) { | |
313 | vector[j].obj = listTypeGet(&entry); | |
314 | vector[j].u.score = 0; | |
315 | vector[j].u.cmpobj = NULL; | |
316 | j++; | |
317 | } | |
318 | listTypeReleaseIterator(li); | |
319 | } else if (sortval->type == REDIS_SET) { | |
320 | setTypeIterator *si = setTypeInitIterator(sortval); | |
321 | robj *ele; | |
322 | while((ele = setTypeNextObject(si)) != NULL) { | |
323 | vector[j].obj = ele; | |
324 | vector[j].u.score = 0; | |
325 | vector[j].u.cmpobj = NULL; | |
326 | j++; | |
327 | } | |
328 | setTypeReleaseIterator(si); | |
329 | } else if (sortval->type == REDIS_ZSET && dontsort) { | |
330 | /* Special handling for a sorted set, if 'dontsort' is true. | |
331 | * This makes sure we return elements in the sorted set original | |
332 | * ordering, accordingly to DESC / ASC options. | |
333 | * | |
334 | * Note that in this case we also handle LIMIT here in a direct | |
335 | * way, just getting the required range, as an optimization. */ | |
336 | ||
337 | zset *zs = sortval->ptr; | |
338 | zskiplist *zsl = zs->zsl; | |
339 | zskiplistNode *ln; | |
340 | robj *ele; | |
341 | int rangelen = vectorlen; | |
342 | ||
343 | /* Check if starting point is trivial, before doing log(N) lookup. */ | |
344 | if (desc) { | |
345 | long zsetlen = dictSize(((zset*)sortval->ptr)->dict); | |
346 | ||
347 | ln = zsl->tail; | |
348 | if (start > 0) | |
349 | ln = zslGetElementByRank(zsl,zsetlen-start); | |
350 | } else { | |
351 | ln = zsl->header->level[0].forward; | |
352 | if (start > 0) | |
353 | ln = zslGetElementByRank(zsl,start+1); | |
354 | } | |
355 | ||
356 | while(rangelen--) { | |
357 | redisAssertWithInfo(c,sortval,ln != NULL); | |
358 | ele = ln->obj; | |
359 | vector[j].obj = ele; | |
360 | vector[j].u.score = 0; | |
361 | vector[j].u.cmpobj = NULL; | |
362 | j++; | |
363 | ln = desc ? ln->backward : ln->level[0].forward; | |
364 | } | |
365 | /* The code producing the output does not know that in the case of | |
366 | * sorted set, 'dontsort', and LIMIT, we are able to get just the | |
367 | * range, already sorted, so we need to adjust "start" and "end" | |
368 | * to make sure start is set to 0. */ | |
369 | end -= start; | |
370 | start = 0; | |
371 | } else if (sortval->type == REDIS_ZSET) { | |
372 | dict *set = ((zset*)sortval->ptr)->dict; | |
373 | dictIterator *di; | |
374 | dictEntry *setele; | |
375 | di = dictGetIterator(set); | |
376 | while((setele = dictNext(di)) != NULL) { | |
377 | vector[j].obj = dictGetKey(setele); | |
378 | vector[j].u.score = 0; | |
379 | vector[j].u.cmpobj = NULL; | |
380 | j++; | |
381 | } | |
382 | dictReleaseIterator(di); | |
383 | } else { | |
384 | redisPanic("Unknown type"); | |
385 | } | |
386 | redisAssertWithInfo(c,sortval,j == vectorlen); | |
387 | ||
388 | /* Now it's time to load the right scores in the sorting vector */ | |
389 | if (dontsort == 0) { | |
390 | for (j = 0; j < vectorlen; j++) { | |
391 | robj *byval; | |
392 | if (sortby) { | |
393 | /* lookup value to sort by */ | |
394 | byval = lookupKeyByPattern(c->db,sortby,vector[j].obj); | |
395 | if (!byval) continue; | |
396 | } else { | |
397 | /* use object itself to sort by */ | |
398 | byval = vector[j].obj; | |
399 | } | |
400 | ||
401 | if (alpha) { | |
402 | if (sortby) vector[j].u.cmpobj = getDecodedObject(byval); | |
403 | } else { | |
404 | if (byval->encoding == REDIS_ENCODING_RAW) { | |
405 | char *eptr; | |
406 | ||
407 | vector[j].u.score = strtod(byval->ptr,&eptr); | |
408 | if (eptr[0] != '\0' || errno == ERANGE || | |
409 | isnan(vector[j].u.score)) | |
410 | { | |
411 | int_convertion_error = 1; | |
412 | } | |
413 | } else if (byval->encoding == REDIS_ENCODING_INT) { | |
414 | /* Don't need to decode the object if it's | |
415 | * integer-encoded (the only encoding supported) so | |
416 | * far. We can just cast it */ | |
417 | vector[j].u.score = (long)byval->ptr; | |
418 | } else { | |
419 | redisAssertWithInfo(c,sortval,1 != 1); | |
420 | } | |
421 | } | |
422 | ||
423 | /* when the object was retrieved using lookupKeyByPattern, | |
424 | * its refcount needs to be decreased. */ | |
425 | if (sortby) { | |
426 | decrRefCount(byval); | |
427 | } | |
428 | } | |
429 | } | |
430 | ||
431 | if (dontsort == 0) { | |
432 | server.sort_desc = desc; | |
433 | server.sort_alpha = alpha; | |
434 | server.sort_bypattern = sortby ? 1 : 0; | |
435 | if (sortby && (start != 0 || end != vectorlen-1)) | |
436 | pqsort(vector,vectorlen,sizeof(redisSortObject),sortCompare, start,end); | |
437 | else | |
438 | qsort(vector,vectorlen,sizeof(redisSortObject),sortCompare); | |
439 | } | |
440 | ||
441 | /* Send command output to the output buffer, performing the specified | |
442 | * GET/DEL/INCR/DECR operations if any. */ | |
443 | outputlen = getop ? getop*(end-start+1) : end-start+1; | |
444 | if (int_convertion_error) { | |
445 | addReplyError(c,"One or more scores can't be converted into double"); | |
446 | } else if (storekey == NULL) { | |
447 | /* STORE option not specified, sent the sorting result to client */ | |
448 | addReplyMultiBulkLen(c,outputlen); | |
449 | for (j = start; j <= end; j++) { | |
450 | listNode *ln; | |
451 | listIter li; | |
452 | ||
453 | if (!getop) addReplyBulk(c,vector[j].obj); | |
454 | listRewind(operations,&li); | |
455 | while((ln = listNext(&li))) { | |
456 | redisSortOperation *sop = ln->value; | |
457 | robj *val = lookupKeyByPattern(c->db,sop->pattern, | |
458 | vector[j].obj); | |
459 | ||
460 | if (sop->type == REDIS_SORT_GET) { | |
461 | if (!val) { | |
462 | addReply(c,shared.nullbulk); | |
463 | } else { | |
464 | addReplyBulk(c,val); | |
465 | decrRefCount(val); | |
466 | } | |
467 | } else { | |
468 | /* Always fails */ | |
469 | redisAssertWithInfo(c,sortval,sop->type == REDIS_SORT_GET); | |
470 | } | |
471 | } | |
472 | } | |
473 | } else { | |
474 | robj *sobj = createZiplistObject(); | |
475 | ||
476 | /* STORE option specified, set the sorting result as a List object */ | |
477 | for (j = start; j <= end; j++) { | |
478 | listNode *ln; | |
479 | listIter li; | |
480 | ||
481 | if (!getop) { | |
482 | listTypePush(sobj,vector[j].obj,REDIS_TAIL); | |
483 | } else { | |
484 | listRewind(operations,&li); | |
485 | while((ln = listNext(&li))) { | |
486 | redisSortOperation *sop = ln->value; | |
487 | robj *val = lookupKeyByPattern(c->db,sop->pattern, | |
488 | vector[j].obj); | |
489 | ||
490 | if (sop->type == REDIS_SORT_GET) { | |
491 | if (!val) val = createStringObject("",0); | |
492 | ||
493 | /* listTypePush does an incrRefCount, so we should take care | |
494 | * care of the incremented refcount caused by either | |
495 | * lookupKeyByPattern or createStringObject("",0) */ | |
496 | listTypePush(sobj,val,REDIS_TAIL); | |
497 | decrRefCount(val); | |
498 | } else { | |
499 | /* Always fails */ | |
500 | redisAssertWithInfo(c,sortval,sop->type == REDIS_SORT_GET); | |
501 | } | |
502 | } | |
503 | } | |
504 | } | |
505 | if (outputlen) { | |
506 | setKey(c->db,storekey,sobj); | |
507 | server.dirty += outputlen; | |
508 | } else if (dbDelete(c->db,storekey)) { | |
509 | signalModifiedKey(c->db,storekey); | |
510 | server.dirty++; | |
511 | } | |
512 | decrRefCount(sobj); | |
513 | addReplyLongLong(c,outputlen); | |
514 | } | |
515 | ||
516 | /* Cleanup */ | |
517 | if (sortval->type == REDIS_LIST || sortval->type == REDIS_SET) | |
518 | for (j = 0; j < vectorlen; j++) | |
519 | decrRefCount(vector[j].obj); | |
520 | decrRefCount(sortval); | |
521 | listRelease(operations); | |
522 | for (j = 0; j < vectorlen; j++) { | |
523 | if (alpha && vector[j].u.cmpobj) | |
524 | decrRefCount(vector[j].u.cmpobj); | |
525 | } | |
526 | zfree(vector); | |
527 | } | |
528 | ||
529 |