Merge branch 'develop'

This commit is contained in:
siddontang 2014-07-21 10:50:30 +08:00
commit 0011bd07f1
15 changed files with 847 additions and 95 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
build build
*.pyc *.pyc
.DS_Store .DS_Store
nohup.out

11
Makefile Normal file
View File

@ -0,0 +1,11 @@
all: build
build:
go install ./...
clean:
go clean -i ./...
test:
go test ./...
go test -race ./...

View File

@ -9,7 +9,7 @@ Ledisdb is a high performance NoSQL like Redis based on LevelDB written by go. I
+ Supports expiration and ttl. + Supports expiration and ttl.
+ Redis clients, like redis-cli, are supported directly. + Redis clients, like redis-cli, are supported directly.
+ Multi client API supports, including Golang, Python, Lua(Openresty). + Multi client API supports, including Golang, Python, Lua(Openresty).
+ Easily to embed in Golang application. + Easy to embed in Golang application.
+ Replication to guarantee data safe. + Replication to guarantee data safe.
+ Supplies tools to load, dump, repair database. + Supplies tools to load, dump, repair database.
@ -25,13 +25,15 @@ Ledisdb is a high performance NoSQL like Redis based on LevelDB written by go. I
+ Install leveldb and snappy, if you have installed, skip. + Install leveldb and snappy, if you have installed, skip.
I supply a simple shell to install leveldb and snappy, you can use: LedisDB supplies a simple shell to install leveldb and snappy:
sh build_leveldb.sh sh build_leveldb.sh
It will default install leveldb at /usr/local/leveldb and snappy at /usr/local/snappy It will default install leveldb at /usr/local/leveldb and snappy at /usr/local/snappy.
+ Change LEVELDB_DIR and SNAPPY_DIR to real install path in dev.sh. LedisDB use the modified LevelDB for better performance, see [here](https://github.com/siddontang/ledisdb/wiki/leveldb-source-modification).
+ Set LEVELDB_DIR and SNAPPY_DIR to the actual install path in dev.sh.
+ Then: + Then:
@ -40,7 +42,7 @@ Ledisdb is a high performance NoSQL like Redis based on LevelDB written by go. I
go install ./... go install ./...
## Run ## Server Example
./ledis-server -config=/etc/ledis.json ./ledis-server -config=/etc/ledis.json
@ -52,7 +54,7 @@ Ledisdb is a high performance NoSQL like Redis based on LevelDB written by go. I
ledis 127.0.0.1:6380> get a ledis 127.0.0.1:6380> get a
"1" "1"
## Lib ## Package Example
import "github.com/siddontang/ledisdb/ledis" import "github.com/siddontang/ledisdb/ledis"
l, _ := ledis.Open(cfg) l, _ := ledis.Open(cfg)
@ -63,9 +65,9 @@ Ledisdb is a high performance NoSQL like Redis based on LevelDB written by go. I
db.Get(key) db.Get(key)
## Replication ## Replication Example
set slaveof in config or dynamiclly Set slaveof in config or dynamiclly
ledis-cli -p 6381 ledis-cli -p 6381
@ -78,15 +80,16 @@ See benchmark.md for more.
## Todo ## Todo
+ Admin See [Issues todo](https://github.com/siddontang/ledisdb/issues?labels=todo&page=1&state=open)
## GoDoc
[![GoDoc](https://godoc.org/github.com/siddontang/ledisdb?status.png)](https://godoc.org/github.com/siddontang/ledisdb) ## Links
## Commands + [Official Website](http://ledisdb.com)
+ [Author's Chinese Blog](http://blog.csdn.net/siddontang/article/category/2264003)
+ [GoDoc](https://godoc.org/github.com/siddontang/ledisdb)
+ [Server Commands](https://github.com/siddontang/ledisdb/wiki/Commands)
Some server commands explaintions are [here](https://github.com/siddontang/ledisdb/wiki/Commands), others will add continuate.
## Thanks ## Thanks
@ -94,6 +97,13 @@ Gmail: cenqichao@gmail.com
Gmail: chendahui007@gmail.com Gmail: chendahui007@gmail.com
Gmail: cppgohan@gmail.com
Gmail: tiaotiaoyly@gmail.com
Gmail: wyk4true@gmail.com
## Feedback ## Feedback
Gmail: siddontang@gmail.com Gmail: siddontang@gmail.com

View File

@ -1,7 +1,7 @@
from __future__ import with_statement from __future__ import with_statement
import datetime import datetime
import time as mod_time import time as mod_time
from ledis._compat import (b, izip, imap, iteritems, iterkeys, itervalues, from ledis._compat import (b, izip, imap, iteritems,
basestring, long, nativestr, urlparse, bytes) basestring, long, nativestr, urlparse, bytes)
from ledis.connection import ConnectionPool, UnixDomainSocketConnection from ledis.connection import ConnectionPool, UnixDomainSocketConnection
from ledis.exceptions import ( from ledis.exceptions import (
@ -53,9 +53,8 @@ def zset_score_pairs(response, **options):
""" """
if not response or not options['withscores']: if not response or not options['withscores']:
return response return response
score_cast_func = options.get('score_cast_func', int)
it = iter(response) it = iter(response)
return list(izip(it, imap(score_cast_func, it))) return list(izip(it, imap(int, it)))
def int_or_none(response): def int_or_none(response):
@ -77,13 +76,14 @@ class Ledis(object):
RESPONSE_CALLBACKS = dict_merge( RESPONSE_CALLBACKS = dict_merge(
string_keys_to_dict( string_keys_to_dict(
'EXISTS EXPIRE EXPIREAT HEXISTS HMSET SETNX ' 'EXISTS EXPIRE EXPIREAT HEXISTS HMSET SETNX '
'PERSIST HPERSIST LPERSIST ZPERSIST', 'PERSIST HPERSIST LPERSIST ZPERSIST BEXPIRE '
'BEXPIREAT BPERSIST BDELETE',
bool bool
), ),
string_keys_to_dict( string_keys_to_dict(
'DECRBY DEL HDEL HLEN INCRBY LLEN ' 'DECRBY DEL HDEL HLEN INCRBY LLEN '
'ZADD ZCARD ZREM ZREMRANGEBYRANK ZREMRANGEBYSCORE' 'ZADD ZCARD ZREM ZREMRANGEBYRANK ZREMRANGEBYSCORE'
'LMCLEAR HMCLEAR ZMCLEAR', 'LMCLEAR HMCLEAR ZMCLEAR BCOUNT BGETBIT BSETBIT BOPT BMSETBIT',
int int
), ),
string_keys_to_dict( string_keys_to_dict(
@ -102,7 +102,9 @@ class Ledis(object):
{ {
'HGETALL': lambda r: r and pairs_to_dict(r) or {}, 'HGETALL': lambda r: r and pairs_to_dict(r) or {},
'PING': lambda r: nativestr(r) == 'PONG', 'PING': lambda r: nativestr(r) == 'PONG',
'SET': lambda r: r and nativestr(r) == 'OK', } 'SET': lambda r: r and nativestr(r) == 'OK',
},
) )
@classmethod @classmethod
@ -187,7 +189,6 @@ class Ledis(object):
return self.response_callbacks[command_name](response, **options) return self.response_callbacks[command_name](response, **options)
return response return response
#### SERVER INFORMATION #### #### SERVER INFORMATION ####
def echo(self, value): def echo(self, value):
"Echo the string back from the server" "Echo the string back from the server"
@ -205,7 +206,6 @@ class Ledis(object):
db = 0 db = 0
return self.execute_command('SELECT', db) return self.execute_command('SELECT', db)
#### BASIC KEY COMMANDS #### #### BASIC KEY COMMANDS ####
def decr(self, name, amount=1): def decr(self, name, amount=1):
""" """
@ -327,7 +327,6 @@ class Ledis(object):
"Removes an expiration on name" "Removes an expiration on name"
return self.execute_command('PERSIST', name) return self.execute_command('PERSIST', name)
#### LIST COMMANDS #### #### LIST COMMANDS ####
def lindex(self, name, index): def lindex(self, name, index):
""" """
@ -454,17 +453,15 @@ class Ledis(object):
``withscores`` indicates to return the scores along with the values. ``withscores`` indicates to return the scores along with the values.
The return type is a list of (value, score) pairs The return type is a list of (value, score) pairs
``score_cast_func`` a callable used to cast the score return value
""" """
if desc: if desc:
return self.zrevrange(name, start, end, withscores, return self.zrevrange(name, start, end, withscores)
score_cast_func)
pieces = ['ZRANGE', name, start, end] pieces = ['ZRANGE', name, start, end]
if withscores: if withscores:
pieces.append('withscores') pieces.append('withscores')
options = { options = {
'withscores': withscores, 'score_cast_func': int} 'withscores': withscores}
return self.execute_command(*pieces, **options) return self.execute_command(*pieces, **options)
def zrangebyscore(self, name, min, max, start=None, num=None, def zrangebyscore(self, name, min, max, start=None, num=None,
@ -478,8 +475,6 @@ class Ledis(object):
``withscores`` indicates to return the scores along with the values. ``withscores`` indicates to return the scores along with the values.
The return type is a list of (value, score) pairs The return type is a list of (value, score) pairs
`score_cast_func`` a callable used to cast the score return value
""" """
if (start is not None and num is None) or \ if (start is not None and num is None) or \
(num is not None and start is None): (num is not None and start is None):
@ -490,7 +485,7 @@ class Ledis(object):
if withscores: if withscores:
pieces.append('withscores') pieces.append('withscores')
options = { options = {
'withscores': withscores, 'score_cast_func': int} 'withscores': withscores}
return self.execute_command(*pieces, **options) return self.execute_command(*pieces, **options)
def zrank(self, name, value): def zrank(self, name, value):
@ -529,14 +524,11 @@ class Ledis(object):
``withscores`` indicates to return the scores along with the values ``withscores`` indicates to return the scores along with the values
The return type is a list of (value, score) pairs The return type is a list of (value, score) pairs
``score_cast_func`` a callable used to cast the score return value
""" """
pieces = ['ZREVRANGE', name, start, num] pieces = ['ZREVRANGE', name, start, num]
if withscores: if withscores:
pieces.append('withscores') pieces.append('withscores')
options = { options = {'withscores': withscores}
'withscores': withscores, 'score_cast_func': int}
return self.execute_command(*pieces, **options) return self.execute_command(*pieces, **options)
def zrevrangebyscore(self, name, min, max, start=None, num=None, def zrevrangebyscore(self, name, min, max, start=None, num=None,
@ -550,8 +542,6 @@ class Ledis(object):
``withscores`` indicates to return the scores along with the values. ``withscores`` indicates to return the scores along with the values.
The return type is a list of (value, score) pairs The return type is a list of (value, score) pairs
``score_cast_func`` a callable used to cast the score return value
""" """
if (start is not None and num is None) or \ if (start is not None and num is None) or \
(num is not None and start is None): (num is not None and start is None):
@ -561,8 +551,7 @@ class Ledis(object):
pieces.extend(['LIMIT', start, num]) pieces.extend(['LIMIT', start, num])
if withscores: if withscores:
pieces.append('withscores') pieces.append('withscores')
options = { options = {'withscores': withscores}
'withscores': withscores, 'score_cast_func': int}
return self.execute_command(*pieces, **options) return self.execute_command(*pieces, **options)
def zrevrank(self, name, value): def zrevrank(self, name, value):
@ -703,3 +692,79 @@ class Ledis(object):
"Removes an expiration on name" "Removes an expiration on name"
return self.execute_command('HPERSIST', name) return self.execute_command('HPERSIST', name)
### BIT COMMANDS
def bget(self, name):
""
return self.execute_command("BGET", name)
def bdelete(self, name):
""
return self.execute_command("BDELETE", name)
def bsetbit(self, name, offset, value):
""
value = value and 1 or 0
return self.execute_command("BSETBIT", name, offset, value)
def bgetbit(self, name, offset):
""
return self.execute_command("BGETBIT", name, offset)
def bmsetbit(self, name, *args):
"""
Set any number of offset, value pairs to the key ``name``. Pairs can be
specified in the following way:
offset1, value1, offset2, value2, ...
"""
pieces = []
if args:
if len(args) % 2 != 0:
raise LedisError("BMSETBIT requires an equal number of "
"offset and value")
pieces.extend(args)
return self.execute_command("BMSETBIT", name, *pieces)
def bcount(self, key, start=None, end=None):
""
params = [key]
if start is not None and end is not None:
params.append(start)
params.append(end)
elif (start is not None and end is None) or \
(start is None and end is not None):
raise LedisError("Both start and end must be specified")
return self.execute_command("BCOUNT", *params)
def bopt(self, operation, dest, *keys):
"""
Perform a bitwise operation using ``operation`` between ``keys`` and
store the result in ``dest``.
``operation`` is one of `and`, `or`, `xor`, `not`.
"""
return self.execute_command('BOPT', operation, dest, *keys)
def bexpire(self, name, time):
"Set timeout on key ``name`` with ``time``"
if isinstance(time, datetime.timedelta):
time = time.seconds + time.days * 24 * 3600
return self.execute_command('BEXPIRE', name, time)
def bexpireat(self, name, when):
"""
Set an expire flag on key name for time seconds. time can be represented by
an integer or a Python timedelta object.
"""
if isinstance(when, datetime.datetime):
when = int(mod_time.mktime(when.timetuple()))
return self.execute_command('BEXPIREAT', name, when)
def bttl(self, name):
"Returns the number of seconds until the key name will expire"
return self.execute_command('BTTL', name)
def bpersist(self, name):
"Removes an expiration on name"
return self.execute_command('BPERSIST', name)

View File

@ -0,0 +1,118 @@
# coding: utf-8
# Test Cases for bit commands
import unittest
import sys
import datetime, time
sys.path.append('..')
import ledis
from ledis._compat import b
from ledis import ResponseError
l = ledis.Ledis(port=6380)
def current_time():
return datetime.datetime.now()
class TestCmdBit(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
l.bdelete('a')
l.bdelete('non_exists_key')
def test_bget(self):
"bget is the same as get in K/V commands"
l.bmsetbit('a', 0, 1, 1, 1, 2, 1, 3, 1, 4, 1, 5, 1, 6, 1, 7, 0)
assert l.bget('a') == b('\x7f')
def test_bdelete(self):
l.bsetbit('a', 0, 1)
assert l.bdelete('a')
assert not l.bdelete('non_exists_key')
def test_get_set_bit(self):
assert not l.bgetbit('a', 5)
assert not l.bsetbit('a', 5, True)
assert l.bgetbit('a', 5)
assert not l.bsetbit('a', 4, False)
assert not l.bgetbit('a', 4)
assert not l.bsetbit('a', 4, True)
assert l.bgetbit('a', 4)
assert l.bsetbit('a', 5, True)
assert l.bgetbit('a', 5)
def test_bmsetbit(self):
assert l.bmsetbit('a', 0, 1, 2, 1, 3, 1) == 3
def test_bcount(self):
l.bsetbit('a', 5, 1)
assert l.bcount('a') == 1
l.bsetbit('a', 6, 1)
assert l.bcount('a') == 2
l.bsetbit('a', 5, 0)
assert l.bcount('a') == 1
l.bmsetbit('a', 10, 1, 20, 1, 30, 1, 40, 1)
assert l.bcount('a') == 5
assert l.bcount('a', 0, 10) == 2
assert l.bcount('a', 20, 30) == 2
assert l.bcount('a', 10, 10) == 1
def test_bopt_not_empty_string(self):
l.bopt('not', 'r', 'a')
assert l.bget('r') is None
def test_bopt(self):
l.bmsetbit('a1', 10, 1, 30, 1, 50, 1, 70, 1, 90, 1)
l.bmsetbit('a2', 20, 1, 40, 1, 60, 1, 80, 1, 100, 1)
assert l.bopt('and', 'res1', 'a1', 'a2') == 101
assert l.bcount('res1') == 0
assert l.bopt('or', 'res2', 'a1', 'a2') == 101
assert l.bcount('res2') == 10
assert l.bopt('xor', 'res3', 'a1', 'a2') == 101
assert l.bcount('res3') == 10
assert l.bopt('not', 'res4', 'a1') == 91
assert l.bcount('res4') == 86
def test_bexpire(self):
assert not l.bexpire('a', 100)
l.bsetbit('a', 1, True)
assert l.bexpire('a', 100)
assert 0 < l.bttl('a') <= 100
assert l.bpersist('a')
assert l.bttl('a') == -1
def test_bexpireat_datetime(self):
expire_at = current_time() + datetime.timedelta(minutes=1)
l.bsetbit('a', 1, True)
assert l.bexpireat('a', expire_at)
assert 0 < l.bttl('a') <= 61
def test_bexpireat_unixtime(self):
expire_at = current_time() + datetime.timedelta(minutes=1)
l.bsetbit('a', 1, True)
expire_at_seconds = int(time.mktime(expire_at.timetuple()))
assert l.bexpireat('a', expire_at_seconds)
assert 0 < l.bttl('a') <= 61
def test_bexpireat_no_key(self):
expire_at = current_time() + datetime.timedelta(minutes=1)
assert not l.bexpireat('a', expire_at)
def test_bttl_and_bpersist(self):
l.bsetbit('a', 1, True)
l.bexpire('a', 100)
assert 0 < l.bttl('a') <= 100
assert l.bpersist('a')
assert l.bttl('a') == -1

View File

@ -122,7 +122,7 @@ class TestCmdHash(unittest.TestCase):
assert l.hexpireat('a', expire_at_seconds) assert l.hexpireat('a', expire_at_seconds)
assert 0 < l.httl('a') <= 61 assert 0 < l.httl('a') <= 61
def test_zexpireat_no_key(self): def test_hexpireat_no_key(self):
expire_at = current_time() + datetime.timedelta(minutes=1) expire_at = current_time() + datetime.timedelta(minutes=1)
assert not l.hexpireat('a', expire_at) assert not l.hexpireat('a', expire_at)

View File

@ -100,6 +100,19 @@ local commands = {
"zttl", "zttl",
"zpersist", "zpersist",
--[[bit]]
"bget",
"bdelete",
"bsetbit",
"bgetbit",
"bmsetbit",
"bcount",
"bopt",
"bexpire",
"bexpireat",
"bttl",
"bpersist",
--[[server]] --[[server]]
"ping", "ping",
"echo", "echo",

84
cmd/ledis-cli/const.go Normal file
View File

@ -0,0 +1,84 @@
package main
var helpCommands = [][]string{
{"DECR", "key", "KV"},
{"DECRBY", "key decrement", "KV"},
{"DEL", "key [key ...]", "KV"},
{"EXISTS", "key", "KV"},
{"GET", "key", "KV"},
{"GETSET", " key value", "KV"},
{"INCR", "key", "KV"},
{"INCRBY", "key increment", "KV"},
{"MGET", "key [key ...]", "KV"},
{"MSET", "key value [key value ...]", "KV"},
{"SET", "key value", "KV"},
{"SETNX", "key value", "KV"},
{"EXPIRE", "key seconds", "KV"},
{"EXPIREAT", "key timestamp", "KV"},
{"TTL", "PERSIST", "KV"},
{"HDEL", "key field [field ...]", "Hash"},
{"HEXISTS", "key field", "Hash"},
{"HGET", "key field", "Hash"},
{"HGETALL", "key", "Hash"},
{"HINCRBY", "key field increment", "Hash"},
{"HKEYS", "key", "Hash"},
{"HLEN", "key", "Hash"},
{"HMGET", "key field [field ...]", "Hash"},
{"HMSET", "key field value [field value ...]", "Hash"},
{"HSET", "key field value", "Hash"},
{"HVALS", "key", "Hash"},
{"HCLEAR", "key", "Hash"},
{"HMCLEAR", "key [key ...]", "Hash"},
{"HEXPIRE", "key seconds", "Hash"},
{"HEXPIREAT", "key timestamp", "Hash"},
{"HTTL", "key", "Hash"},
{"HPERSIST", "key", "Hash"},
{"LINDEX", "key index", "List"},
{"LLEN", "key", "List"},
{"LPOP", "key", "List"},
{"LPUSH", "key value [value ...]", "List"},
{"LRANGE", "key start stop", "List"},
{"RPOP", "key", "List"},
{"RPUSH", "key value [value ...]", "List"},
{"LCLEAR", "key", "List"},
{"LMCLEAR", "key [key ...]", "List"},
{"LEXPIRE", "key seconds", "List"},
{"LEXPIREAT", "key timestamp", "List"},
{"LTTL", "key", "List"},
{"LPERSIST", "key", "List"},
{"ZADD", "key score member [score member ...]", "ZSet"},
{"ZCARD", "key", "ZSet"},
{"ZCOUNT", "key min max", "ZSet"},
{"ZINCRBY", "key increment member", "ZSet"},
{"ZRANGE", "key start stop [WITHSCORES]", "ZSet"},
{"ZRANGEBYSCORE", "key min max [WITHSCORES] [LIMIT offset count]", "ZSet"},
{"ZRANK", "key member", "ZSet"},
{"ZREM", "key member [member ...]", "ZSet"},
{"ZREMRANGEBYRANK", "key start stop", "ZSet"},
{"ZREMRANGEBYSCORE", "key min max", "ZSet"},
{"ZREVRANGEBYSCORE", "key max min [WITHSCORES][LIMIT offset count]", "ZSet"},
{"ZREVRANK", "key member", "ZSet"},
{"ZSCORE", "key member", "ZSet"},
{"ZCLEAR", "key", "ZSet"},
{"ZMCLEAR", "key [key ...]", "ZSet"},
{"ZEXPIRE", "key seconds", "ZSet"},
{"ZEXPIREAT", "key timestamp", "ZSet"},
{"ZTTL", "key", "ZSet"},
{"ZPERSIST", "key", "ZSet"},
{"BGET", "key", "Bitmap"},
{"BGETBIT", "key offset", "Bitmap"},
{"BSETBIT", "key offset value", "Bitmap"},
{"BMSETBIT", "key offset value [offset value ...]", "Bitmap"},
{"BOPT", "operation destkey key [key ...]", "Bitmap"},
{"BCOUNT", "key [start end]", "Bitmap"},
{"BEXPIRE", "key seconds", "Bitmap"},
{"BEXPIREAT", "key timestamp", "Bitmap"},
{"BTTL", "key", "Bitmap"},
{"BPERSIST", "key", "Bitmap"},
{"SLAVEOF", "host port", "Replication"},
{"FULLSYNC", "-", "Replication"},
{"SYNC", "index offset", "Replication"},
{"PING", "-", "Server"},
{"ECHO", "message", "Server"},
{"SELECT", "index", "Server"},
}

View File

@ -5,12 +5,14 @@ import (
"fmt" "fmt"
"github.com/siddontang/ledisdb/client/go/ledis" "github.com/siddontang/ledisdb/client/go/ledis"
"regexp" "regexp"
"strconv"
"strings" "strings"
) )
var ip = flag.String("h", "127.0.0.1", "ledisdb server ip (default 127.0.0.1)") var ip = flag.String("h", "127.0.0.1", "ledisdb server ip (default 127.0.0.1)")
var port = flag.Int("p", 6380, "ledisdb server port (default 6380)") var port = flag.Int("p", 6380, "ledisdb server port (default 6380)")
var socket = flag.String("s", "", "ledisdb server socket, overwrite ip and port") var socket = flag.String("s", "", "ledisdb server socket, overwrite ip and port")
var dbn = flag.Int("n", 0, "ledisdb database number(default 0)")
func main() { func main() {
flag.Parse() flag.Parse()
@ -30,8 +32,20 @@ func main() {
reg, _ := regexp.Compile(`'.*?'|".*?"|\S+`) reg, _ := regexp.Compile(`'.*?'|".*?"|\S+`)
prompt := ""
for { for {
cmd, err := line(fmt.Sprintf("%s> ", cfg.Addr)) if *dbn >= 16 {
fmt.Printf("ERR invalid db index %d. Auto switch back db 0.\n", *dbn)
*dbn = 0
continue
} else if *dbn > 0 && *dbn < 16 {
prompt = fmt.Sprintf("%s[%d]>", cfg.Addr, *dbn)
} else {
prompt = fmt.Sprintf("%s>", cfg.Addr)
}
cmd, err := line(prompt)
if err != nil { if err != nil {
fmt.Printf("%s\n", err.Error()) fmt.Printf("%s\n", err.Error())
return return
@ -44,17 +58,29 @@ func main() {
addHistory(cmd) addHistory(cmd)
args := make([]interface{}, len(cmds[1:])) args := make([]interface{}, len(cmds[1:]))
for i := range args { for i := range args {
args[i] = strings.Trim(string(cmds[1+i]), "\"'") args[i] = strings.Trim(string(cmds[1+i]), "\"'")
} }
r, err := c.Do(cmds[0], args...)
if err != nil { cmd := cmds[0]
fmt.Printf("%s", err.Error()) if strings.ToLower(cmd) == "help" || cmd == "?" {
printHelp(cmds)
} else { } else {
printReply(cmd, r) r, err := c.Do(cmds[0], args...)
if err != nil {
fmt.Printf("%s", err.Error())
} else if nb, _ := strconv.Atoi(cmds[1]); strings.ToLower(cmds[0]) == "select" && nb < 16 {
*dbn = nb
printReply(cmd, r)
} else {
printReply(cmd, r)
}
fmt.Printf("\n")
} }
fmt.Printf("\n")
} }
} }
} }
@ -66,7 +92,7 @@ func printReply(cmd string, reply interface{}) {
case string: case string:
fmt.Printf("%s", reply) fmt.Printf("%s", reply)
case []byte: case []byte:
fmt.Printf("%s", string(reply)) fmt.Printf("%q", reply)
case nil: case nil:
fmt.Printf("(nil)") fmt.Printf("(nil)")
case ledis.Error: case ledis.Error:
@ -87,3 +113,34 @@ func printReply(cmd string, reply interface{}) {
fmt.Printf("invalid ledis reply") fmt.Printf("invalid ledis reply")
} }
} }
func printGenericHelp() {
msg :=
`ledis-cli
Type: "help <command>" for help on <command>
`
fmt.Println(msg)
}
func printCommandHelp(arr []string) {
fmt.Println()
fmt.Printf("\t%s %s \n", arr[0], arr[1])
fmt.Printf("\tGroup: %s \n", arr[2])
fmt.Println()
}
func printHelp(cmds []string) {
args := cmds[1:]
if len(args) == 0 {
printGenericHelp()
} else if len(args) > 1 {
fmt.Println()
} else {
cmd := strings.ToUpper(args[0])
for i := 0; i < len(helpCommands); i++ {
if helpCommands[i][0] == cmd {
printCommandHelp(helpCommands[i])
}
}
}
}

View File

@ -3,16 +3,13 @@ package main
import ( import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt"
"github.com/siddontang/ledisdb/ledis" "github.com/siddontang/ledisdb/ledis"
"github.com/siddontang/ledisdb/server"
"io/ioutil" "io/ioutil"
"path"
) )
var configPath = flag.String("config", "/etc/ledis.json", "ledisdb config file") var configPath = flag.String("config", "/etc/ledis.json", "ledisdb config file")
var dumpPath = flag.String("dump_file", "", "ledisdb dump file") var dumpPath = flag.String("dump_file", "", "ledisdb dump file")
var masterAddr = flag.String("master_addr", "",
"master addr to set where dump file comes from, if not set correctly, next slaveof may cause fullsync")
func main() { func main() {
flag.Parse() flag.Parse()
@ -74,13 +71,11 @@ func loadDump(cfg *ledis.Config, ldb *ledis.Ledis) error {
return err return err
} }
info := new(server.MasterInfo) //master enable binlog, here output this like mysql
if head.LogFileIndex != 0 && head.LogPos != 0 {
format := "MASTER_LOG_FILE='binlog.%07d', MASTER_LOG_POS=%d;\n"
fmt.Printf(format, head.LogFileIndex, head.LogPos)
}
info.Addr = *masterAddr return nil
info.LogFileIndex = head.LogFileIndex
info.LogPos = head.LogPos
infoFile := path.Join(cfg.DataDir, "master.info")
return info.Save(infoFile)
} }

View File

@ -70,7 +70,7 @@ Table of Contents
- [ZREMRANGEBYRANK key start stop](#zremrangebyrank-key-start-stop) - [ZREMRANGEBYRANK key start stop](#zremrangebyrank-key-start-stop)
- [ZREMRANGEBYSCORE key min max](#zremrangebyscore-key-min-max) - [ZREMRANGEBYSCORE key min max](#zremrangebyscore-key-min-max)
- [ZREVRANGE key start stop [WITHSCORES]](#zrevrange-key-start-stop-withscores) - [ZREVRANGE key start stop [WITHSCORES]](#zrevrange-key-start-stop-withscores)
- [ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]](#zrevrangebyscore--key-max-min-withscores-limit-offset-count) - [ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]](#zrevrangebyscore-key-max-min-withscores-limit-offset-count)
- [ZREVRANK key member](#zrevrank-key-member) - [ZREVRANK key member](#zrevrank-key-member)
- [ZSCORE key member](#zscore-key-member) - [ZSCORE key member](#zscore-key-member)
- [ZCLEAR key](#zclear-key) - [ZCLEAR key](#zclear-key)
@ -79,6 +79,19 @@ Table of Contents
- [ZEXPIREAT key timestamp](#zexpireat-key-timestamp) - [ZEXPIREAT key timestamp](#zexpireat-key-timestamp)
- [ZTTL key](#zttl-key) - [ZTTL key](#zttl-key)
- [ZPERSIST key](#zpersist-key) - [ZPERSIST key](#zpersist-key)
- [Bitmap](#bitmap)
- [BGET key](#bget-key)
- [BGETBIT key offset](#bgetbit-key-offset)
- [BSETBIT key offset value](#bsetbit-key-offset-value)
- [BMSETBIT key offset value[offset value ...]](#bmsetbit-key-offset-value-offset-value-)
- [BOPT operation destkey key [key ...]](#bopt-operation-destkey-key-key-)
- [BCOUNT key [start, end]](#bcount-key-start-end)
- [BEXPIRE key seconds](#bexpire-key-seconds)
- [BEXPIREAT key timestamp](#bexpireat-key-timestamp)
- [BTTL key](#bttl-key)
- [BPERSIST key](#bpersist-key)
- [Replication](#replication) - [Replication](#replication)
- [SLAVEOF host port](#slaveof-host-port) - [SLAVEOF host port](#slaveof-host-port)
- [FULLSYNC](#fullsync) - [FULLSYNC](#fullsync)
@ -1138,7 +1151,7 @@ ledis> ZRANGE myset 0 -1 WITHSCORES
6) "2" 6) "2"
7) "three" 7) "three"
8) "3" 8) "3"
ledis> zcard myset ledis> ZCARD myset
(integer) 4 (integer) 4
``` ```
@ -1168,9 +1181,9 @@ ledis> ZRANGE myset 0 -1 WITHSCORES
6) "2" 6) "2"
7) "three" 7) "three"
8) "3" 8) "3"
ledis> zcount myset -inf +inf ledis> ZCOUNT myset -inf +inf
(integer) 4 (integer) 4
ledis> zcount myset (1 3 ledis> ZCOUNT myset (1 3
(integer) 2 (integer) 2
``` ```
@ -1444,13 +1457,13 @@ Use ZRANK to get the rank of an element with the scores ordered from low to high
**Examples** **Examples**
``` ```
127.0.0.1:6380> zadd myset 1 one ledis> ZADD myset 1 one
(integer) 1 (integer) 1
127.0.0.1:6380> zadd myset 2 two ledis> ZADD myset 2 two
(integer) 1 (integer) 1
127.0.0.1:6380> zrevrank myset one ledis> ZREVRANK myset one
(integer) 1 (integer) 1
127.0.0.1:6380> zrevrank myset three ledis> ZREVRANK myset three
(nil) (nil)
``` ```
@ -1581,13 +1594,13 @@ int64: TTL in seconds
**Examples** **Examples**
``` ```
ledis> zadd myset 1 'one' ledis> ZADD myset 1 'one'
(integer) 1 (integer) 1
ledis> zexpire myset 100 ledis> ZEXPIRE myset 100
(integer) 1 (integer) 1
ledis> zttl myset ledis> ZTTL myset
(integer) 97 (integer) 97
ledis> zttl myset2 ledis> ZTTL myset2
(integer) -1 (integer) -1
``` ```
@ -1617,6 +1630,166 @@ ledis> ZTTL mset
``` ```
## Bitmap
### BGET key
Returns the whole binary data stored at `key`.
**Return value**
bulk: the raw value of key, or nil when key does not exist.
**Examples**
```
ledis> BMSETBIT flag 0 1 5 1 6 1
(integer) 3
ledis> BGET flag
a
```
### BGETBIT key offset
Returns the bit value at `offset` in the string value stored at `key`.
When *offset* beyond the data length, ot the target data is not exist, the bit value will be 0 always.
**Return value**
int64 : the bit value stored at offset.
**Examples**
```
ledis> BSETBIT flag 1024 1
(integer) 0
ledis> BGETBIT flag 0
(integer) 0
ledis> BGETBIT flag 1024
(integer) 1
ledis> BGETBIT flag 65535
(integer) 0
```
### BSETBIT key offset value
Sets or clear the bit at `offset` in the binary data sotred at `key`.
The bit is either set or cleared depending on `value`, which can be either `0` or `1`.
The *offset* argument is required to be qual to 0, and smaller than
2^23 (this means bitmap limits to 8MB).
**Return value**
int64 : the original bit value stored at offset.
**Examples**
```
ledis> BSETBIT flag 0 1
(integer) 0
ledis> BSETBIT flag 0 0
(integer) 1
ledis> BGETBIT flag 0 99
ERR invalid command param
```
### BMSETBIT key offset value [offset value ...]
Sets the given *offset* to their respective values.
**Return value**
int64 : The number of input *offset*
**Examples**
```
ledis> BMSETBIT flag 0 1 1 1 2 0 3 1
(integer) 4
ledis> BCOUNT flag
(integer) 3
```
### BOPT operation destkey key [key ...]
Perform a bitwise operation between multiple keys (containing string values) and store the result in the destination key.
**Return value**
Int64:
The size of the string stored in the destination key, that is equal to the size of the longest input string.
**Examples**
```
ledis> BMSETBIT a 0 1 2 1
(integer) 2
ledis> BMSETBIT b 1 1
(integer) 1
ledis> BOPT AND res a b
(integer) 3
ledis> BCOUNT res
(integer) 0
ledis> BOPT OR res2 a b
(integer) 3
ledis> BCOUNT res2
(integer) 3
ledis> BOPT XOR res3 a b
(integer) 3
ledis> BCOUNT res3
(integer) 3
```
### BCOUNT key [start end]
Count the number of set bits in a bitmap.
**Return value**
int64 : The number of bits set to 1.
**Examples**
```
ledis> BMSETBIT flag 0 1 5 1 6 1
(integer) 3
ledis> BGET flag
a
ledis> BCOUNT flag
(integer) 3
ledis> BCOUNT flag 0 0s
(integer) 1
ledis> BCOUNT flag 0 4
(integer) 1
ledis> BCOUNT flag 0 5
(integer) 2
ledis> BCOUNT flag 5 6
(integer) 2
```
### BEXPIRE key seconds
(refer to [EXPIRE](#expire-key-seconds) api for other types)
### BEXPIREAT key timestamp
(refer to [EXPIREAT](#expireat-key-timestamp) api for other types)
### BTTL key
(refer to [TTL](#ttl-key) api for other types)
### BPERSIST key
(refer to [PERSIST](#persist-key) api for other types)
## Replication ## Replication
### SLAVEOF host port ### SLAVEOF host port

View File

@ -371,6 +371,65 @@ func (db *DB) bExpireAt(key []byte, when int64) (int64, error) {
return 1, nil return 1, nil
} }
func (db *DB) bCountByte(val byte, soff uint32, eoff uint32) int32 {
if soff > eoff {
soff, eoff = eoff, soff
}
mask := uint8(0)
if soff > 0 {
mask |= fillBits[soff-1]
}
if eoff < 7 {
mask |= (fillBits[7] ^ fillBits[eoff])
}
mask = fillBits[7] ^ mask
return bitsInByte[val&mask]
}
func (db *DB) bCountSeg(key []byte, seq uint32, soff uint32, eoff uint32) (cnt int32, err error) {
if soff >= segBitSize || soff < 0 ||
eoff >= segBitSize || eoff < 0 {
return
}
var segment []byte
if _, segment, err = db.bGetSegment(key, seq); err != nil {
return
}
if segment == nil {
return
}
if soff > eoff {
soff, eoff = eoff, soff
}
headIdx := int(soff >> 3)
endIdx := int(eoff >> 3)
sByteOff := soff - ((soff >> 3) << 3)
eByteOff := eoff - ((eoff >> 3) << 3)
if headIdx == endIdx {
cnt = db.bCountByte(segment[headIdx], sByteOff, eByteOff)
} else {
cnt = db.bCountByte(segment[headIdx], sByteOff, 7) +
db.bCountByte(segment[endIdx], 0, eByteOff)
}
// sum up following bytes
for idx, end := headIdx+1, endIdx-1; idx <= end; idx += 1 {
cnt += bitsInByte[segment[idx]]
if idx == end {
break
}
}
return
}
func (db *DB) BGet(key []byte) (data []byte, err error) { func (db *DB) BGet(key []byte) (data []byte, err error) {
if err = checkKeySize(key); err != nil { if err = checkKeySize(key); err != nil {
return return
@ -561,25 +620,53 @@ func (db *DB) BGetBit(key []byte, offset int32) (uint8, error) {
// } // }
func (db *DB) BCount(key []byte, start int32, end int32) (cnt int32, err error) { func (db *DB) BCount(key []byte, start int32, end int32) (cnt int32, err error) {
var sseq uint32 var sseq, soff uint32
if sseq, _, err = db.bParseOffset(key, start); err != nil { if sseq, soff, err = db.bParseOffset(key, start); err != nil {
return return
} }
var eseq uint32 var eseq, eoff uint32
if eseq, _, err = db.bParseOffset(key, end); err != nil { if eseq, eoff, err = db.bParseOffset(key, end); err != nil {
return return
} }
if sseq > eseq || (sseq == eseq && soff > eoff) {
sseq, eseq = eseq, sseq
soff, eoff = eoff, soff
}
var segCnt int32
if eseq == sseq {
if segCnt, err = db.bCountSeg(key, sseq, soff, eoff); err != nil {
return 0, err
}
cnt = segCnt
} else {
if segCnt, err = db.bCountSeg(key, sseq, soff, segBitSize-1); err != nil {
return 0, err
} else {
cnt += segCnt
}
if segCnt, err = db.bCountSeg(key, eseq, 0, eoff); err != nil {
return 0, err
} else {
cnt += segCnt
}
}
// middle segs
var segment []byte var segment []byte
skey := db.bEncodeBinKey(key, sseq) skey := db.bEncodeBinKey(key, sseq)
ekey := db.bEncodeBinKey(key, eseq) ekey := db.bEncodeBinKey(key, eseq)
it := db.db.RangeIterator(skey, ekey, leveldb.RangeClose) it := db.db.RangeIterator(skey, ekey, leveldb.RangeOpen)
for ; it.Valid(); it.Next() { for ; it.Valid(); it.Next() {
segment = it.Value() segment = it.Value()
for _, bit := range segment { for _, bt := range segment {
cnt += bitsInByte[bit] cnt += bitsInByte[bt]
} }
} }
it.Close() it.Close()
@ -604,7 +691,7 @@ func (db *DB) BTail(key []byte) (int32, error) {
func (db *DB) BOperation(op uint8, dstkey []byte, srckeys ...[]byte) (blen int32, err error) { func (db *DB) BOperation(op uint8, dstkey []byte, srckeys ...[]byte) (blen int32, err error) {
// blen - // blen -
// the size of the string stored in the destination key, // the total bit size of data stored in destination key,
// that is equal to the size of the longest input string. // that is equal to the size of the longest input string.
var exeOp func([]byte, []byte, *[]byte) var exeOp func([]byte, []byte, *[]byte)
@ -759,7 +846,8 @@ func (db *DB) BOperation(op uint8, dstkey []byte, srckeys ...[]byte) (blen int32
err = t.Commit() err = t.Commit()
if err == nil { if err == nil {
blen = int32(maxDstSeq<<segBitWidth | maxDstOff) // blen = int32(db.bCapByteSize(maxDstOff, maxDstOff))
blen = int32(maxDstSeq<<segBitWidth | maxDstOff + 1)
} }
return return

View File

@ -36,6 +36,7 @@ func newBytes(bitLen int32) []byte {
func TestBinary(t *testing.T) { func TestBinary(t *testing.T) {
testSimple(t) testSimple(t)
testSimpleII(t) testSimpleII(t)
testCount(t)
testOpAndOr(t) testOpAndOr(t)
testOpXor(t) testOpXor(t)
testOpNot(t) testOpNot(t)
@ -130,6 +131,102 @@ func testSimpleII(t *testing.T) {
} }
} }
func testCount(t *testing.T) {
db := getTestDB()
db.FlushAll()
key := []byte("test_bin_count")
if ori, _ := db.BSetBit(key, 0, 1); ori != 0 {
t.Error(ori)
}
if ori, _ := db.BSetBit(key, 10, 1); ori != 0 {
t.Error(ori)
}
if ori, _ := db.BSetBit(key, 262140, 1); ori != 0 {
t.Error(ori)
}
// count
if sum, _ := db.BCount(key, 0, -1); sum != 3 {
t.Error(sum)
}
if sum, _ := db.BCount(key, 0, 9); sum != 1 {
t.Error(sum)
}
if sum, _ := db.BCount(key, 0, 10); sum != 2 {
t.Error(sum)
}
if sum, _ := db.BCount(key, 0, 11); sum != 2 {
t.Error(sum)
}
if sum, _ := db.BCount(key, 0, 262139); sum != 2 {
t.Error(sum)
}
if sum, _ := db.BCount(key, 0, 262140); sum != 3 {
t.Error(sum)
}
if sum, _ := db.BCount(key, 0, 262141); sum != 3 {
t.Error(sum)
}
if sum, _ := db.BCount(key, 10, 262140); sum != 2 {
t.Error(sum)
}
if sum, _ := db.BCount(key, 11, 262140); sum != 1 {
t.Error(sum)
}
if sum, _ := db.BCount(key, 11, 262139); sum != 0 {
t.Error(sum)
}
key = []byte("test_bin_count_2")
db.BSetBit(key, 1, 1)
db.BSetBit(key, 2, 1)
db.BSetBit(key, 4, 1)
db.BSetBit(key, 6, 1)
if sum, _ := db.BCount(key, 0, -1); sum != 4 {
t.Error(sum)
}
if sum, _ := db.BCount(key, 1, 1); sum != 1 {
t.Error(sum)
}
if sum, _ := db.BCount(key, 0, 7); sum != 4 {
t.Error(sum)
}
if ori, _ := db.BSetBit(key, 8, 1); ori != 0 {
t.Error(ori)
}
if ori, _ := db.BSetBit(key, 11, 1); ori != 0 {
t.Error(ori)
}
if sum, _ := db.BCount(key, 0, -1); sum != 6 {
t.Error(sum)
}
if sum, _ := db.BCount(key, 0, 16); sum != 6 {
t.Error(sum)
}
}
func testOpAndOr(t *testing.T) { func testOpAndOr(t *testing.T) {
db := getTestDB() db := getTestDB()
db.FlushAll() db.FlushAll()
@ -256,6 +353,30 @@ func testOpAndOr(t *testing.T) {
} }
func testOpAnd(t *testing.T) {
db := getTestDB()
db.FlushAll()
dstKey := []byte("test_bin_or")
k0 := []byte("op_or_0")
k1 := []byte("op_or_01")
srcKeys := [][]byte{k0, k1}
db.BSetBit(k0, 0, 1)
db.BSetBit(k0, 2, 1)
db.BSetBit(k1, 1, 1)
if blen, _ := db.BOperation(OPand, dstKey, srcKeys...); blen != 3 {
t.Fatal(blen)
}
if cnt, _ := db.BCount(dstKey, 0, -1); cnt != 1 {
t.Fatal(1)
}
}
func testOpXor(t *testing.T) { func testOpXor(t *testing.T) {
db := getTestDB() db := getTestDB()
db.FlushAll() db.FlushAll()
@ -330,6 +451,22 @@ func testOpNot(t *testing.T) {
if cmpBytes(data, stdData) { if cmpBytes(data, stdData) {
t.Fatal(false) t.Fatal(false)
} }
k1 := []byte("op_not_2")
srcKeys = [][]byte{k1}
db.BSetBit(k1, 0, 1)
db.BSetBit(k1, 2, 1)
db.BSetBit(k1, 4, 1)
db.BSetBit(k1, 6, 1)
if blen, _ := db.BOperation(OPnot, dstKey, srcKeys...); blen != 7 {
t.Fatal(blen)
}
if cnt, _ := db.BCount(dstKey, 0, -1); cnt != 3 {
t.Fatal(cnt)
}
} }
func testMSetBit(t *testing.T) { func testMSetBit(t *testing.T) {

View File

@ -79,7 +79,7 @@ func StrInt8(v []byte, err error) (int8, error) {
} else if v == nil { } else if v == nil {
return 0, nil return 0, nil
} else { } else {
res, err := strconv.ParseInt(String(v), 10, 32) res, err := strconv.ParseInt(String(v), 10, 8)
return int8(res), err return int8(res), err
} }
} }

View File

@ -5,14 +5,14 @@ import (
"testing" "testing"
) )
func TestBin(t *testing.T) { func TestBit(t *testing.T) {
testBinGetSet(t) testBitGetSet(t)
testBinMset(t) testBitMset(t)
testBinCount(t) testBitCount(t)
testBinOpt(t) testBitOpt(t)
} }
func testBinGetSet(t *testing.T) { func testBitGetSet(t *testing.T) {
c := getTestConn() c := getTestConn()
defer c.Close() defer c.Close()
@ -66,7 +66,7 @@ func testBinGetSet(t *testing.T) {
} }
} }
func testBinMset(t *testing.T) { func testBitMset(t *testing.T) {
c := getTestConn() c := getTestConn()
defer c.Close() defer c.Close()
@ -103,7 +103,7 @@ func testBinMset(t *testing.T) {
} }
} }
func testBinCount(t *testing.T) { func testBitCount(t *testing.T) {
c := getTestConn() c := getTestConn()
defer c.Close() defer c.Close()
@ -121,7 +121,7 @@ func testBinCount(t *testing.T) {
} }
} }
func testBinOpt(t *testing.T) { func testBitOpt(t *testing.T) {
c := getTestConn() c := getTestConn()
defer c.Close() defer c.Close()
@ -163,7 +163,7 @@ func testBinOpt(t *testing.T) {
if blen, err := ledis.Int( if blen, err := ledis.Int(
c.Do("bopt", "and", dstk, k0, k1)); err != nil { c.Do("bopt", "and", dstk, k0, k1)); err != nil {
t.Fatal(err) t.Fatal(err)
} else if blen != 100 { } else if blen != 101 {
t.Fatal(blen) t.Fatal(blen)
} }
@ -183,7 +183,7 @@ func testBinOpt(t *testing.T) {
if blen, err := ledis.Int( if blen, err := ledis.Int(
c.Do("bopt", "or", dstk, k0, k1)); err != nil { c.Do("bopt", "or", dstk, k0, k1)); err != nil {
t.Fatal(err) t.Fatal(err)
} else if blen != 100 { } else if blen != 101 {
t.Fatal(blen) t.Fatal(blen)
} }
@ -203,7 +203,7 @@ func testBinOpt(t *testing.T) {
if blen, err := ledis.Int( if blen, err := ledis.Int(
c.Do("bopt", "xor", dstk, k0, k1)); err != nil { c.Do("bopt", "xor", dstk, k0, k1)); err != nil {
t.Fatal(err) t.Fatal(err)
} else if blen != 100 { } else if blen != 101 {
t.Fatal(blen) t.Fatal(blen)
} }