]> git.saurik.com Git - redis.git/blob - src/db.c
Add a unit test to verify that key archival works.
[redis.git] / src / db.c
1 /*
2 * Copyright (c) 2009-2012, Salvatore Sanfilippo <antirez at gmail dot com>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * * Redistributions of source code must retain the above copyright notice,
9 * this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 * * Neither the name of Redis nor the names of its contributors may be used
14 * to endorse or promote products derived from this software without
15 * specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 * POSSIBILITY OF SUCH DAMAGE.
28 */
29
30 #include "redis.h"
31
32 #include <signal.h>
33 #include <ctype.h>
34
35 void SlotToKeyAdd(robj *key);
36 void SlotToKeyDel(robj *key);
37
38 /*-----------------------------------------------------------------------------
39 * C-level DB API
40 *----------------------------------------------------------------------------*/
41
42 robj *lookupKey(redisDb *db, robj *key) {
43 dictEntry *de = dictFind(db->dict,key->ptr);
44 if (de) {
45 robj *val = dictGetVal(de);
46
47 /* Update the access time for the aging algorithm.
48 * Don't do it if we have a saving child, as this will trigger
49 * a copy on write madness. */
50 if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
51 val->lru = server.lruclock;
52 return val;
53 } else {
54 return recover(db, key);
55 }
56 }
57
58 robj *lookupKeyRead(redisDb *db, robj *key) {
59 robj *val;
60
61 expireIfNeeded(db,key);
62 val = lookupKey(db,key);
63 if (val == NULL)
64 server.stat_keyspace_misses++;
65 else
66 server.stat_keyspace_hits++;
67 return val;
68 }
69
70 robj *lookupKeyWrite(redisDb *db, robj *key) {
71 expireIfNeeded(db,key);
72 robj *val = lookupKey(db,key);
73 if (val) val->archived = 0;
74 return val;
75 }
76
77 robj *lookupKeyReadOrReply(redisClient *c, robj *key, robj *reply) {
78 robj *o = lookupKeyRead(c->db, key);
79 if (!o) addReply(c,reply);
80 return o;
81 }
82
83 robj *lookupKeyWriteOrReply(redisClient *c, robj *key, robj *reply) {
84 robj *o = lookupKeyWrite(c->db, key);
85 if (!o) addReply(c,reply);
86 return o;
87 }
88
89 /* Add the key to the DB. It's up to the caller to increment the reference
90 * counte of the value if needed.
91 *
92 * The program is aborted if the key already exists. */
93 void dbAdd(redisDb *db, robj *key, robj *val) {
94 sds copy = sdsdup(key->ptr);
95 int retval = dictAdd(db->dict, copy, val);
96
97 redisAssertWithInfo(NULL,key,retval == REDIS_OK);
98 }
99
100 /* Overwrite an existing key with a new value. Incrementing the reference
101 * count of the new value is up to the caller.
102 * This function does not modify the expire time of the existing key.
103 *
104 * The program is aborted if the key was not already present. */
105 void dbOverwrite(redisDb *db, robj *key, robj *val) {
106 struct dictEntry *de = dictFind(db->dict,key->ptr);
107
108 redisAssertWithInfo(NULL,key,de != NULL);
109 dictReplace(db->dict, key->ptr, val);
110 }
111
112 /* High level Set operation. This function can be used in order to set
113 * a key, whatever it was existing or not, to a new object.
114 *
115 * 1) The ref count of the value object is incremented.
116 * 2) clients WATCHing for the destination key notified.
117 * 3) The expire time of the key is reset (the key is made persistent). */
118 void setKey(redisDb *db, robj *key, robj *val) {
119 if (lookupKeyWrite(db,key) == NULL) {
120 dbAdd(db,key,val);
121 } else {
122 dbOverwrite(db,key,val);
123 }
124 incrRefCount(val);
125 removeExpire(db,key);
126 signalModifiedKey(db,key);
127 }
128
129 int dbExists(redisDb *db, robj *key) {
130 if (dictFind(db->dict,key->ptr) != NULL)
131 return 1;
132 if (recover(db, key) != NULL)
133 return 1;
134 return 0;
135 }
136
137 /* Return a random key, in form of a Redis object.
138 * If there are no keys, NULL is returned.
139 *
140 * The function makes sure to return keys not already expired. */
141 robj *dbRandomKey(redisDb *db) {
142 struct dictEntry *de;
143
144 while(1) {
145 sds key;
146 robj *keyobj;
147
148 de = dictGetRandomKey(db->dict);
149 if (de == NULL) return NULL;
150
151 key = dictGetKey(de);
152 keyobj = createStringObject(key,sdslen(key));
153 if (dictFind(db->expires,key)) {
154 if (expireIfNeeded(db,keyobj)) {
155 decrRefCount(keyobj);
156 continue; /* search for another key. This expired. */
157 }
158 }
159 return keyobj;
160 }
161 }
162
163 /* Delete a key, value, and associated expiration entry if any, from the DB */
164 int dbDelete(redisDb *db, robj *key) {
165 /* Deleting an entry from the expires dict will not free the sds of
166 * the key, because it is shared with the main dictionary. */
167 if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
168 if (dictDelete(db->dict,key->ptr) == DICT_OK) {
169 return 1;
170 } else {
171 return 0;
172 }
173 }
174
175 long long emptyDb() {
176 int j;
177 long long removed = 0;
178
179 for (j = 0; j < server.dbnum; j++) {
180 removed += dictSize(server.db[j].dict);
181 dictEmpty(server.db[j].dict);
182 dictEmpty(server.db[j].expires);
183 }
184 return removed;
185 }
186
187 int selectDb(redisClient *c, int id) {
188 if (id < 0 || id >= server.dbnum)
189 return REDIS_ERR;
190 c->db = &server.db[id];
191 return REDIS_OK;
192 }
193
194 /*-----------------------------------------------------------------------------
195 * Hooks for key space changes.
196 *
197 * Every time a key in the database is modified the function
198 * signalModifiedKey() is called.
199 *
200 * Every time a DB is flushed the function signalFlushDb() is called.
201 *----------------------------------------------------------------------------*/
202
203 void signalModifiedKey(redisDb *db, robj *key) {
204 touchWatchedKey(db,key);
205 }
206
207 void signalFlushedDb(int dbid) {
208 touchWatchedKeysOnFlush(dbid);
209 }
210
211 /*-----------------------------------------------------------------------------
212 * Type agnostic commands operating on the key space
213 *----------------------------------------------------------------------------*/
214
215 void flushdbCommand(redisClient *c) {
216 server.dirty += dictSize(c->db->dict);
217 signalFlushedDb(c->db->id);
218 dictEmpty(c->db->dict);
219 dictEmpty(c->db->expires);
220 addReply(c,shared.ok);
221 }
222
223 void flushallCommand(redisClient *c) {
224 signalFlushedDb(-1);
225 server.dirty += emptyDb();
226 addReply(c,shared.ok);
227 if (server.rdb_child_pid != -1) {
228 kill(server.rdb_child_pid,SIGKILL);
229 rdbRemoveTempFile(server.rdb_child_pid);
230 }
231 if (server.saveparamslen > 0) {
232 /* Normally rdbSave() will reset dirty, but we don't want this here
233 * as otherwise FLUSHALL will not be replicated nor put into the AOF. */
234 int saved_dirty = server.dirty;
235 rdbSave(server.rdb_filename);
236 server.dirty = saved_dirty;
237 }
238 server.dirty++;
239 }
240
241 void delCommand(redisClient *c) {
242 int deleted = 0, j;
243
244 for (j = 1; j < c->argc; j++) {
245 if (dbDelete(c->db,c->argv[j])) {
246 signalModifiedKey(c->db,c->argv[j]);
247 server.dirty++;
248 deleted++;
249 }
250 }
251 addReplyLongLong(c,deleted);
252 }
253
254 void existsCommand(redisClient *c) {
255 expireIfNeeded(c->db,c->argv[1]);
256 if (dbExists(c->db,c->argv[1])) {
257 addReply(c, shared.cone);
258 } else {
259 addReply(c, shared.czero);
260 }
261 }
262
263 void selectCommand(redisClient *c) {
264 long id;
265
266 if (getLongFromObjectOrReply(c, c->argv[1], &id,
267 "invalid DB index") != REDIS_OK)
268 return;
269
270 if (selectDb(c,id) == REDIS_ERR) {
271 addReplyError(c,"invalid DB index");
272 } else {
273 addReply(c,shared.ok);
274 }
275 }
276
277 void randomkeyCommand(redisClient *c) {
278 robj *key;
279
280 if ((key = dbRandomKey(c->db)) == NULL) {
281 addReply(c,shared.nullbulk);
282 return;
283 }
284
285 addReplyBulk(c,key);
286 decrRefCount(key);
287 }
288
289 void keysCommand(redisClient *c) {
290 dictIterator *di;
291 dictEntry *de;
292 sds pattern = c->argv[1]->ptr;
293 int plen = sdslen(pattern), allkeys;
294 unsigned long numkeys = 0;
295 void *replylen = addDeferredMultiBulkLength(c);
296
297 di = dictGetSafeIterator(c->db->dict);
298 allkeys = (pattern[0] == '*' && pattern[1] == '\0');
299 while((de = dictNext(di)) != NULL) {
300 sds key = dictGetKey(de);
301 robj *keyobj;
302
303 if (allkeys || stringmatchlen(pattern,plen,key,sdslen(key),0)) {
304 keyobj = createStringObject(key,sdslen(key));
305 if (expireIfNeeded(c->db,keyobj) == 0) {
306 addReplyBulk(c,keyobj);
307 numkeys++;
308 }
309 decrRefCount(keyobj);
310 }
311 }
312 dictReleaseIterator(di);
313 setDeferredMultiBulkLength(c,replylen,numkeys);
314 }
315
316 void dbsizeCommand(redisClient *c) {
317 addReplyLongLong(c,dictSize(c->db->dict));
318 }
319
320 void lastsaveCommand(redisClient *c) {
321 addReplyLongLong(c,server.lastsave);
322 }
323
324 void typeCommand(redisClient *c) {
325 robj *o;
326 char *type;
327
328 o = lookupKeyRead(c->db,c->argv[1]);
329 if (o == NULL) {
330 type = "none";
331 } else {
332 switch(o->type) {
333 case REDIS_STRING: type = "string"; break;
334 case REDIS_LIST: type = "list"; break;
335 case REDIS_SET: type = "set"; break;
336 case REDIS_ZSET: type = "zset"; break;
337 case REDIS_HASH: type = "hash"; break;
338 default: type = "unknown"; break;
339 }
340 }
341 addReplyStatus(c,type);
342 }
343
344 void shutdownCommand(redisClient *c) {
345 int flags = 0;
346
347 if (c->argc > 2) {
348 addReply(c,shared.syntaxerr);
349 return;
350 } else if (c->argc == 2) {
351 if (!strcasecmp(c->argv[1]->ptr,"nosave")) {
352 flags |= REDIS_SHUTDOWN_NOSAVE;
353 } else if (!strcasecmp(c->argv[1]->ptr,"save")) {
354 flags |= REDIS_SHUTDOWN_SAVE;
355 } else {
356 addReply(c,shared.syntaxerr);
357 return;
358 }
359 }
360 if (prepareForShutdown(flags) == REDIS_OK) exit(0);
361 addReplyError(c,"Errors trying to SHUTDOWN. Check logs.");
362 }
363
364 void renameGenericCommand(redisClient *c, int nx) {
365 robj *o;
366 long long expire;
367
368 /* To use the same key as src and dst is probably an error */
369 if (sdscmp(c->argv[1]->ptr,c->argv[2]->ptr) == 0) {
370 addReply(c,shared.sameobjecterr);
371 return;
372 }
373
374 if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.nokeyerr)) == NULL)
375 return;
376
377 incrRefCount(o);
378 expire = getExpire(c->db,c->argv[1]);
379 if (lookupKeyWrite(c->db,c->argv[2]) != NULL) {
380 if (nx) {
381 decrRefCount(o);
382 addReply(c,shared.czero);
383 return;
384 }
385 /* Overwrite: delete the old key before creating the new one with the same name. */
386 dbDelete(c->db,c->argv[2]);
387 }
388 dbAdd(c->db,c->argv[2],o);
389 if (expire != -1) setExpire(c->db,c->argv[2],expire);
390 dbDelete(c->db,c->argv[1]);
391 signalModifiedKey(c->db,c->argv[1]);
392 signalModifiedKey(c->db,c->argv[2]);
393 server.dirty++;
394 addReply(c,nx ? shared.cone : shared.ok);
395 }
396
397 void renameCommand(redisClient *c) {
398 renameGenericCommand(c,0);
399 }
400
401 void renamenxCommand(redisClient *c) {
402 renameGenericCommand(c,1);
403 }
404
405 void moveCommand(redisClient *c) {
406 robj *o;
407 redisDb *src, *dst;
408 int srcid;
409
410 /* Obtain source and target DB pointers */
411 src = c->db;
412 srcid = c->db->id;
413 if (selectDb(c,atoi(c->argv[2]->ptr)) == REDIS_ERR) {
414 addReply(c,shared.outofrangeerr);
415 return;
416 }
417 dst = c->db;
418 selectDb(c,srcid); /* Back to the source DB */
419
420 /* If the user is moving using as target the same
421 * DB as the source DB it is probably an error. */
422 if (src == dst) {
423 addReply(c,shared.sameobjecterr);
424 return;
425 }
426
427 /* Check if the element exists and get a reference */
428 o = lookupKeyWrite(c->db,c->argv[1]);
429 if (!o) {
430 addReply(c,shared.czero);
431 return;
432 }
433
434 /* Return zero if the key already exists in the target DB */
435 if (lookupKeyWrite(dst,c->argv[1]) != NULL) {
436 addReply(c,shared.czero);
437 return;
438 }
439 dbAdd(dst,c->argv[1],o);
440 incrRefCount(o);
441
442 /* OK! key moved, free the entry in the source DB */
443 dbDelete(src,c->argv[1]);
444 server.dirty++;
445 addReply(c,shared.cone);
446 }
447
448 /*-----------------------------------------------------------------------------
449 * Expires API
450 *----------------------------------------------------------------------------*/
451
452 int removeExpire(redisDb *db, robj *key) {
453 /* An expire may only be removed if there is a corresponding entry in the
454 * main dict. Otherwise, the key will never be freed. */
455 redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
456 return dictDelete(db->expires,key->ptr) == DICT_OK;
457 }
458
459 void setExpire(redisDb *db, robj *key, long long when) {
460 dictEntry *kde, *de;
461
462 /* Reuse the sds from the main dict in the expire dict */
463 kde = dictFind(db->dict,key->ptr);
464 redisAssertWithInfo(NULL,key,kde != NULL);
465 de = dictReplaceRaw(db->expires,dictGetKey(kde));
466 dictSetSignedIntegerVal(de,when);
467 }
468
469 /* Return the expire time of the specified key, or -1 if no expire
470 * is associated with this key (i.e. the key is non volatile) */
471 long long getExpire(redisDb *db, robj *key) {
472 dictEntry *de;
473
474 /* No expire? return ASAP */
475 if (dictSize(db->expires) == 0 ||
476 (de = dictFind(db->expires,key->ptr)) == NULL) return -1;
477
478 /* The entry was found in the expire dict, this means it should also
479 * be present in the main dict (safety check). */
480 redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
481 return dictGetSignedIntegerVal(de);
482 }
483
484 /* Propagate expires into slaves and the AOF file.
485 * When a key expires in the master, a DEL operation for this key is sent
486 * to all the slaves and the AOF file if enabled.
487 *
488 * This way the key expiry is centralized in one place, and since both
489 * AOF and the master->slave link guarantee operation ordering, everything
490 * will be consistent even if we allow write operations against expiring
491 * keys. */
492 void propagateExpire(redisDb *db, robj *key) {
493 robj *argv[2];
494
495 argv[0] = shared.del;
496 argv[1] = key;
497 incrRefCount(argv[0]);
498 incrRefCount(argv[1]);
499
500 if (server.aof_state != REDIS_AOF_OFF)
501 feedAppendOnlyFile(server.delCommand,db->id,argv,2);
502 if (listLength(server.slaves))
503 replicationFeedSlaves(server.slaves,db->id,argv,2);
504
505 decrRefCount(argv[0]);
506 decrRefCount(argv[1]);
507 }
508
509 int expireIfNeeded(redisDb *db, robj *key) {
510 long long when = getExpire(db,key);
511
512 if (when < 0) return 0; /* No expire for this key */
513
514 /* Don't expire anything while loading. It will be done later. */
515 if (server.loading) return 0;
516
517 /* If we are running in the context of a slave, return ASAP:
518 * the slave key expiration is controlled by the master that will
519 * send us synthesized DEL operations for expired keys.
520 *
521 * Still we try to return the right information to the caller,
522 * that is, 0 if we think the key should be still valid, 1 if
523 * we think the key is expired at this time. */
524 if (server.masterhost != NULL) {
525 return mstime() > when;
526 }
527
528 /* Return when this key has not expired */
529 if (mstime() <= when) return 0;
530
531 /* Delete the key */
532 server.stat_expiredkeys++;
533 propagateExpire(db,key);
534 return dbDelete(db,key);
535 }
536
537 /*-----------------------------------------------------------------------------
538 * Expires Commands
539 *----------------------------------------------------------------------------*/
540
541 /* This is the generic command implementation for EXPIRE, PEXPIRE, EXPIREAT
542 * and PEXPIREAT. Because the commad second argument may be relative or absolute
543 * the "basetime" argument is used to signal what the base time is (either 0
544 * for *AT variants of the command, or the current time for relative expires).
545 *
546 * unit is either UNIT_SECONDS or UNIT_MILLISECONDS, and is only used for
547 * the argv[2] parameter. The basetime is always specified in milliesconds. */
548 void expireGenericCommand(redisClient *c, long long basetime, int unit) {
549 dictEntry *de;
550 robj *key = c->argv[1], *param = c->argv[2];
551 long long when; /* unix time in milliseconds when the key will expire. */
552
553 if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
554 return;
555
556 if (unit == UNIT_SECONDS) when *= 1000;
557 when += basetime;
558
559 de = dictFind(c->db->dict,key->ptr);
560 if (de == NULL) {
561 addReply(c,shared.czero);
562 return;
563 }
564 /* EXPIRE with negative TTL, or EXPIREAT with a timestamp into the past
565 * should never be executed as a DEL when load the AOF or in the context
566 * of a slave instance.
567 *
568 * Instead we take the other branch of the IF statement setting an expire
569 * (possibly in the past) and wait for an explicit DEL from the master. */
570 if (when <= mstime() && !server.loading && !server.masterhost) {
571 robj *aux;
572
573 redisAssertWithInfo(c,key,dbDelete(c->db,key));
574 server.dirty++;
575
576 /* Replicate/AOF this as an explicit DEL. */
577 aux = createStringObject("DEL",3);
578 rewriteClientCommandVector(c,2,aux,key);
579 decrRefCount(aux);
580 signalModifiedKey(c->db,key);
581 addReply(c, shared.cone);
582 return;
583 } else {
584 setExpire(c->db,key,when);
585 addReply(c,shared.cone);
586 signalModifiedKey(c->db,key);
587 server.dirty++;
588 return;
589 }
590 }
591
592 void expireCommand(redisClient *c) {
593 expireGenericCommand(c,mstime(),UNIT_SECONDS);
594 }
595
596 void expireatCommand(redisClient *c) {
597 expireGenericCommand(c,0,UNIT_SECONDS);
598 }
599
600 void pexpireCommand(redisClient *c) {
601 expireGenericCommand(c,mstime(),UNIT_MILLISECONDS);
602 }
603
604 void pexpireatCommand(redisClient *c) {
605 expireGenericCommand(c,0,UNIT_MILLISECONDS);
606 }
607
608 void ttlGenericCommand(redisClient *c, int output_ms) {
609 long long expire, ttl = -1;
610
611 expire = getExpire(c->db,c->argv[1]);
612 if (expire != -1) {
613 ttl = expire-mstime();
614 if (ttl < 0) ttl = -1;
615 }
616 if (ttl == -1) {
617 addReplyLongLong(c,-1);
618 } else {
619 addReplyLongLong(c,output_ms ? ttl : ((ttl+500)/1000));
620 }
621 }
622
623 void ttlCommand(redisClient *c) {
624 ttlGenericCommand(c, 0);
625 }
626
627 void pttlCommand(redisClient *c) {
628 ttlGenericCommand(c, 1);
629 }
630
631 void persistCommand(redisClient *c) {
632 dictEntry *de;
633
634 de = dictFind(c->db->dict,c->argv[1]->ptr);
635 if (de == NULL) {
636 addReply(c,shared.czero);
637 } else {
638 if (removeExpire(c->db,c->argv[1])) {
639 addReply(c,shared.cone);
640 server.dirty++;
641 } else {
642 addReply(c,shared.czero);
643 }
644 }
645 }
646
647 /* -----------------------------------------------------------------------------
648 * API to get key arguments from commands
649 * ---------------------------------------------------------------------------*/
650
651 int *getKeysUsingCommandTable(struct redisCommand *cmd,robj **argv, int argc, int *numkeys) {
652 int j, i = 0, last, *keys;
653 REDIS_NOTUSED(argv);
654
655 if (cmd->firstkey == 0) {
656 *numkeys = 0;
657 return NULL;
658 }
659 last = cmd->lastkey;
660 if (last < 0) last = argc+last;
661 keys = zmalloc(sizeof(int)*((last - cmd->firstkey)+1));
662 for (j = cmd->firstkey; j <= last; j += cmd->keystep) {
663 redisAssert(j < argc);
664 keys[i++] = j;
665 }
666 *numkeys = i;
667 return keys;
668 }
669
670 int *getKeysFromCommand(struct redisCommand *cmd,robj **argv, int argc, int *numkeys, int flags) {
671 if (cmd->getkeys_proc) {
672 return cmd->getkeys_proc(cmd,argv,argc,numkeys,flags);
673 } else {
674 return getKeysUsingCommandTable(cmd,argv,argc,numkeys);
675 }
676 }
677
678 void getKeysFreeResult(int *result) {
679 zfree(result);
680 }
681
682 int *noPreloadGetKeys(struct redisCommand *cmd,robj **argv, int argc, int *numkeys, int flags) {
683 if (flags & REDIS_GETKEYS_PRELOAD) {
684 *numkeys = 0;
685 return NULL;
686 } else {
687 return getKeysUsingCommandTable(cmd,argv,argc,numkeys);
688 }
689 }
690
691 int *renameGetKeys(struct redisCommand *cmd,robj **argv, int argc, int *numkeys, int flags) {
692 if (flags & REDIS_GETKEYS_PRELOAD) {
693 int *keys = zmalloc(sizeof(int));
694 *numkeys = 1;
695 keys[0] = 1;
696 return keys;
697 } else {
698 return getKeysUsingCommandTable(cmd,argv,argc,numkeys);
699 }
700 }
701
702 int *zunionInterGetKeys(struct redisCommand *cmd,robj **argv, int argc, int *numkeys, int flags) {
703 int i, num, *keys;
704 REDIS_NOTUSED(cmd);
705 REDIS_NOTUSED(flags);
706
707 num = atoi(argv[2]->ptr);
708 /* Sanity check. Don't return any key if the command is going to
709 * reply with syntax error. */
710 if (num > (argc-3)) {
711 *numkeys = 0;
712 return NULL;
713 }
714 keys = zmalloc(sizeof(int)*num);
715 for (i = 0; i < num; i++) keys[i] = 3+i;
716 *numkeys = num;
717 return keys;
718 }