diff --git a/core/src/main/java/dev/keva/core/command/impl/string/Decr.java b/core/src/main/java/dev/keva/core/command/impl/string/Decr.java new file mode 100644 index 00000000..3b6aa3b1 --- /dev/null +++ b/core/src/main/java/dev/keva/core/command/impl/string/Decr.java @@ -0,0 +1,38 @@ +package dev.keva.core.command.impl.string; + +import dev.keva.core.command.annotation.CommandImpl; +import dev.keva.core.command.annotation.Execute; +import dev.keva.core.command.annotation.ParamLength; +import dev.keva.core.exception.CommandException; +import dev.keva.ioc.annotation.Autowired; +import dev.keva.ioc.annotation.Component; +import dev.keva.protocol.resp.reply.IntegerReply; +import dev.keva.store.KevaDatabase; + +import java.nio.charset.StandardCharsets; + +import static dev.keva.core.command.annotation.ParamLength.Type.EXACT; + + +@Component +@CommandImpl("decr") +@ParamLength(type = EXACT, value = 1) +public class Decr { + private final KevaDatabase database; + + @Autowired + public Decr(KevaDatabase database) { + this.database = database; + } + + @Execute + public IntegerReply execute(byte[] key) { + byte[] newVal; + try { + newVal = database.decrby(key, 1L); + } catch (NumberFormatException ex) { + throw new CommandException("value is not an integer or out of range"); + } + return new IntegerReply(Long.parseLong(new String(newVal, StandardCharsets.UTF_8))); + } +} diff --git a/core/src/main/java/dev/keva/core/command/impl/string/Decrby.java b/core/src/main/java/dev/keva/core/command/impl/string/Decrby.java new file mode 100644 index 00000000..1bde454e --- /dev/null +++ b/core/src/main/java/dev/keva/core/command/impl/string/Decrby.java @@ -0,0 +1,39 @@ +package dev.keva.core.command.impl.string; + +import dev.keva.core.command.annotation.CommandImpl; +import dev.keva.core.command.annotation.Execute; +import dev.keva.core.command.annotation.ParamLength; +import dev.keva.core.exception.CommandException; +import dev.keva.ioc.annotation.Autowired; +import dev.keva.ioc.annotation.Component; +import dev.keva.protocol.resp.reply.IntegerReply; +import dev.keva.store.KevaDatabase; +import lombok.var; + +import java.nio.charset.StandardCharsets; + +import static dev.keva.core.command.annotation.ParamLength.Type.EXACT; + +@Component +@CommandImpl("decrby") +@ParamLength(type = EXACT, value = 2) +public class Decrby { + private final KevaDatabase database; + + @Autowired + public Decrby(KevaDatabase database) { + this.database = database; + } + + @Execute + public IntegerReply execute(byte[] key, byte[] decrBy) { + var amount = Long.parseLong(new String(decrBy, StandardCharsets.UTF_8)); + byte[] newVal; + try { + newVal = database.decrby(key, amount); + } catch (NumberFormatException ex) { + throw new CommandException("value is not an integer or out of range"); + } + return new IntegerReply(Long.parseLong(new String(newVal, StandardCharsets.UTF_8))); + } +} diff --git a/core/src/main/java/dev/keva/core/command/impl/string/GetRange.java b/core/src/main/java/dev/keva/core/command/impl/string/GetRange.java new file mode 100644 index 00000000..57ef0bd0 --- /dev/null +++ b/core/src/main/java/dev/keva/core/command/impl/string/GetRange.java @@ -0,0 +1,31 @@ +package dev.keva.core.command.impl.string; + +import dev.keva.core.command.annotation.CommandImpl; +import dev.keva.core.command.annotation.Execute; +import dev.keva.core.command.annotation.ParamLength; +import dev.keva.core.command.impl.key.manager.ExpirationManager; +import dev.keva.ioc.annotation.Autowired; +import dev.keva.ioc.annotation.Component; +import dev.keva.protocol.resp.reply.BulkReply; +import dev.keva.store.KevaDatabase; + +import static dev.keva.core.command.annotation.ParamLength.Type.EXACT; + +@Component +@CommandImpl("getrange") +@ParamLength(type = EXACT, value = 3) +public class GetRange { + private final KevaDatabase database; + private final ExpirationManager expirationManager; + + @Autowired + public GetRange(KevaDatabase database, ExpirationManager expirationManager) { + this.database = database; + this.expirationManager = expirationManager; + } + + @Execute + public BulkReply execute(byte[] key, byte[] start, byte[] end) { + return new BulkReply(database.getrange(key, start, end)); + } +} diff --git a/core/src/main/java/dev/keva/core/command/impl/key/Incr.java b/core/src/main/java/dev/keva/core/command/impl/string/Incr.java similarity index 96% rename from core/src/main/java/dev/keva/core/command/impl/key/Incr.java rename to core/src/main/java/dev/keva/core/command/impl/string/Incr.java index bc37d037..1227c91b 100644 --- a/core/src/main/java/dev/keva/core/command/impl/key/Incr.java +++ b/core/src/main/java/dev/keva/core/command/impl/string/Incr.java @@ -1,4 +1,4 @@ -package dev.keva.core.command.impl.key; +package dev.keva.core.command.impl.string; import dev.keva.core.command.annotation.CommandImpl; import dev.keva.core.command.annotation.Execute; diff --git a/core/src/main/java/dev/keva/core/command/impl/string/IncrByFloat.java b/core/src/main/java/dev/keva/core/command/impl/string/IncrByFloat.java new file mode 100644 index 00000000..ab60ff7c --- /dev/null +++ b/core/src/main/java/dev/keva/core/command/impl/string/IncrByFloat.java @@ -0,0 +1,42 @@ +package dev.keva.core.command.impl.string; + +import dev.keva.core.command.annotation.CommandImpl; +import dev.keva.core.command.annotation.Execute; +import dev.keva.core.command.annotation.Mutate; +import dev.keva.core.command.annotation.ParamLength; +import dev.keva.core.exception.CommandException; +import dev.keva.ioc.annotation.Autowired; +import dev.keva.ioc.annotation.Component; +import dev.keva.protocol.resp.reply.BulkReply; +import dev.keva.protocol.resp.reply.IntegerReply; +import dev.keva.store.KevaDatabase; +import lombok.var; + +import java.nio.charset.StandardCharsets; + +import static dev.keva.core.command.annotation.ParamLength.Type.EXACT; + +@Component +@CommandImpl("incrbyfloat") +@ParamLength(type = EXACT, value = 2) +@Mutate +public class IncrByFloat { + private final KevaDatabase database; + + @Autowired + public IncrByFloat(KevaDatabase database) { + this.database = database; + } + + @Execute + public BulkReply execute(byte[] key, byte[] incr) { + byte[] newVal; + try { + double amount = Double.parseDouble(new String(incr, StandardCharsets.UTF_8)); + newVal = database.incrbyfloat(key, amount); + } catch (NumberFormatException ex) { + throw new CommandException("Value is not a valid float"); + } + return new BulkReply(newVal); + } +} diff --git a/core/src/main/java/dev/keva/core/command/impl/key/Incrby.java b/core/src/main/java/dev/keva/core/command/impl/string/Incrby.java similarity index 96% rename from core/src/main/java/dev/keva/core/command/impl/key/Incrby.java rename to core/src/main/java/dev/keva/core/command/impl/string/Incrby.java index 9984e5a3..c085cac8 100644 --- a/core/src/main/java/dev/keva/core/command/impl/key/Incrby.java +++ b/core/src/main/java/dev/keva/core/command/impl/string/Incrby.java @@ -1,4 +1,4 @@ -package dev.keva.core.command.impl.key; +package dev.keva.core.command.impl.string; import dev.keva.core.command.annotation.CommandImpl; import dev.keva.core.command.annotation.Execute; diff --git a/core/src/main/java/dev/keva/core/command/impl/string/MSet.java b/core/src/main/java/dev/keva/core/command/impl/string/MSet.java new file mode 100644 index 00000000..dad69a61 --- /dev/null +++ b/core/src/main/java/dev/keva/core/command/impl/string/MSet.java @@ -0,0 +1,40 @@ +package dev.keva.core.command.impl.string; + +import dev.keva.core.command.annotation.CommandImpl; +import dev.keva.core.command.annotation.Execute; +import dev.keva.core.command.annotation.ParamLength; +import dev.keva.core.command.impl.key.manager.ExpirationManager; +import dev.keva.core.exception.CommandException; +import dev.keva.ioc.annotation.Autowired; +import dev.keva.ioc.annotation.Component; +import dev.keva.protocol.resp.reply.BulkReply; +import dev.keva.protocol.resp.reply.MultiBulkReply; +import dev.keva.protocol.resp.reply.StatusReply; +import dev.keva.store.KevaDatabase; + +import java.nio.charset.StandardCharsets; + +import static dev.keva.core.command.annotation.ParamLength.Type.AT_LEAST; + +@Component +@CommandImpl("mset") +@ParamLength(type = AT_LEAST, value = 2) +public class MSet { + private final KevaDatabase database; + private final ExpirationManager expirationManager; + + @Autowired + public MSet(KevaDatabase database, ExpirationManager expirationManager) { + this.database = database; + this.expirationManager = expirationManager; + } + + @Execute + public StatusReply execute(byte[]... keys) { + if (keys.length % 2 != 0) { + throw new CommandException("Wrong number of arguments for MSET"); + } + database.mset(keys); + return StatusReply.OK; + } +} diff --git a/core/src/test/java/dev/keva/core/server/AbstractServerTest.java b/core/src/test/java/dev/keva/core/server/AbstractServerTest.java index c0f71cfc..151229fd 100644 --- a/core/src/test/java/dev/keva/core/server/AbstractServerTest.java +++ b/core/src/test/java/dev/keva/core/server/AbstractServerTest.java @@ -941,6 +941,38 @@ void dumpAndRestore() { } } + @Test + void decr() { + try { + String set1 = jedis.set("mykey", "10"); + assertEquals(set1, "OK"); + Long decr1 = jedis.decr("mykey"); + assertEquals(decr1, 9); + String set2 = jedis.set("errKey", "foobar"); + assertThrows(JedisDataException.class, () -> jedis.decr("errKey")); + + } catch (Exception e) { + fail(e); + } + } + + @Test + void decrBy() { + try { + String set1 = jedis.set("mykey", "10"); + assertEquals(set1, "OK"); + Long decrby1 = jedis.decrBy("mykey", 5); + assertEquals(decrby1, 5); + Long decrby2 = jedis.decrBy("mykey", 10); + assertEquals(decrby2, -5); + String set2 = jedis.set("mykey2", "abc123"); + + assertThrows(JedisDataException.class, () -> jedis.decrBy("mykey2", 10)); + } catch (Exception e) { + fail(e); + } + } + @Test void type() { try { @@ -958,4 +990,60 @@ void type() { fail(e); } } + + @Test + void getRange() { + try { + String set1 = jedis.set("mykey", "This is a string"); + String getrange1 = jedis.getrange("mykey", 0, 3); + assertEquals("This", getrange1); + String getrange2 = jedis.getrange("mykey", -3, -1); + assertEquals("ing", getrange2); + String getrange3 = jedis.getrange("mykey", 0, -1); + assertEquals("This is a string", getrange3); + String getrange4 = jedis.getrange("mykey", 10, 100); + assertEquals("string", getrange4); + String getrange5 = jedis.getrange("mykey", 10, 5); + assertEquals("", getrange5); + String getrange6 = jedis.getrange("mykey", -10, 10); + assertEquals("s a s", getrange6); + } catch (Exception e) { + fail(e); + } + } + + @Test + void incrByFloat() { + try { + String set1 = jedis.set("mykey", "10.50"); + assertEquals(set1, "OK"); + Double incrbyfloat1 = jedis.incrByFloat("mykey", 0.1); + assertEquals(incrbyfloat1, 10.6); + Double incrbyfloat2 = jedis.incrByFloat("mykey", -5); + assertEquals(incrbyfloat2, 5.6); + String set2 = jedis.set("mykey", "5.0e3"); + assertEquals(set2, "OK"); + Double incrbyfloat3 = jedis.incrByFloat("mykey", 2.0e2); + assertEquals(incrbyfloat3, 5200); + String set3 = jedis.set("mykey3", "abc"); + assertEquals(set3, "OK"); + assertThrows(JedisDataException.class, () -> jedis.incrByFloat("mykey3", 123)); + } catch (Exception e) { + fail(e); + } + } + + @Test + void mset() { + try { + String mset1 = jedis.mset("key1", "Hello", "key2", "World"); + assertEquals("OK", mset1); + String get1 = jedis.get("key1"); + assertEquals("Hello", get1); + String get2 = jedis.get("key2"); + assertEquals("World", get2); + } catch (Exception e) { + fail(e); + } + } } diff --git a/docs/src/guide/overview/commands.md b/docs/src/guide/overview/commands.md index 201a141d..5a2b8aa4 100644 --- a/docs/src/guide/overview/commands.md +++ b/docs/src/guide/overview/commands.md @@ -51,6 +51,11 @@ Implemented commands: - MGET - STRLEN - SETRANGE +- DECR +- DECRBY +- GETRANGE +- MSET +- INCRBYFLOAT diff --git a/store/src/main/java/dev/keva/store/KevaDatabase.java b/store/src/main/java/dev/keva/store/KevaDatabase.java index d18bb0bd..3f15a5c1 100644 --- a/store/src/main/java/dev/keva/store/KevaDatabase.java +++ b/store/src/main/java/dev/keva/store/KevaDatabase.java @@ -77,4 +77,13 @@ public interface KevaDatabase { Double zincrby(byte[] key, Double score, BytesKey e, int flags); Double zscore(byte[] key, byte[] member); + + byte[] decrby(byte[] key, long amount); + + byte[] getrange(byte[] key, byte[] start, byte[] end); + + byte[] incrbyfloat(byte[] key, double amount); + + void mset(byte[]... key); + } diff --git a/store/src/main/java/dev/keva/store/impl/OffHeapDatabaseImpl.java b/store/src/main/java/dev/keva/store/impl/OffHeapDatabaseImpl.java index 7412ffc8..10dbc9cb 100644 --- a/store/src/main/java/dev/keva/store/impl/OffHeapDatabaseImpl.java +++ b/store/src/main/java/dev/keva/store/impl/OffHeapDatabaseImpl.java @@ -10,7 +10,6 @@ import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import lombok.val; -import lombok.var; import net.openhft.chronicle.map.ChronicleMap; import net.openhft.chronicle.map.ChronicleMapBuilder; import org.apache.commons.lang3.SerializationUtils; @@ -696,7 +695,7 @@ public int strlen(byte[] key) { public int setrange(byte[] key, byte[] offset, byte[] val) { lock.lock(); try { - var offsetPosition = Integer.parseInt(new String(offset, StandardCharsets.UTF_8)); + int offsetPosition = Integer.parseInt(new String(offset, StandardCharsets.UTF_8)); byte[] oldVal = chronicleMap.get(key); int newValLength = oldVal == null ? offsetPosition + val.length : Math.max(offsetPosition + val.length, oldVal.length); byte[] newVal = new byte[newValLength]; @@ -723,7 +722,7 @@ public byte[][] mget(byte[]... keys) { byte[][] result = new byte[keys.length][]; for (int i = 0; i < keys.length; i++) { byte[] key = keys[i]; - val got = chronicleMap.get(key); + byte[] got = chronicleMap.get(key); result[i] = got; } return result; @@ -827,4 +826,78 @@ public Double zscore(byte[] key, byte[] member) { lock.unlock(); } } + + public byte[] getrange(byte[] key, byte[] start, byte[] end) { + lock.lock(); + try { + byte[] value = chronicleMap.get(key); + int startInt = Integer.parseInt(new String(start, StandardCharsets.UTF_8)); + int endInt = Integer.parseInt(new String(end, StandardCharsets.UTF_8)); + + // convert negative indexes to positive ones + if (startInt < 0 && endInt < 0 && startInt > endInt) { + return null; + } + if (startInt < 0) startInt = value.length + startInt; + if (endInt < 0) endInt = value.length + endInt; + if (startInt < 0) startInt = 0; + if (endInt < 0) endInt = 0; + if (endInt >= value.length) endInt = value.length - 1; + + byte[] result; + if (startInt > endInt || value.length == 0) { + result = new String("").getBytes(); + } else { + result = Arrays.copyOfRange(value, startInt, endInt + 1); + } + return result; + } finally { + lock.unlock(); + } + } + + @Override + public byte[] incrbyfloat(byte[] key, double amount) { + lock.lock(); + try { + return chronicleMap.compute(key, (k, oldVal) -> { + double curVal = 0L; + if (oldVal != null) { + curVal = Double.parseDouble(new String(oldVal, StandardCharsets.UTF_8)); + } + curVal = curVal + amount; + return Double.toString(curVal).getBytes(StandardCharsets.UTF_8); + }); + } finally { + lock.unlock(); + } + } + + @Override + public void mset(byte[]... keys) { + lock.lock(); + try { + for (int i = 0; i < keys.length; i += 2) { + chronicleMap.put(keys[i], keys[i + 1]); + } + } finally { + lock.unlock(); + } + } + + public byte[] decrby(byte[] key, long amount) { + lock.lock(); + try { + return chronicleMap.compute(key, (k, oldVal) -> { + long curVal = 0L; + if (oldVal != null) { + curVal = Long.parseLong(new String(oldVal, StandardCharsets.UTF_8)); + } + curVal = curVal - amount; + return Long.toString(curVal).getBytes(StandardCharsets.UTF_8); + }); + } finally { + lock.unlock(); + } + } } diff --git a/store/src/main/java/dev/keva/store/impl/OnHeapDatabaseImpl.java b/store/src/main/java/dev/keva/store/impl/OnHeapDatabaseImpl.java index e28b1c80..c7115015 100644 --- a/store/src/main/java/dev/keva/store/impl/OnHeapDatabaseImpl.java +++ b/store/src/main/java/dev/keva/store/impl/OnHeapDatabaseImpl.java @@ -7,7 +7,6 @@ import dev.keva.store.lock.SpinLock; import lombok.Getter; import lombok.val; -import lombok.var; import org.apache.commons.lang3.SerializationUtils; import java.nio.charset.StandardCharsets; @@ -650,7 +649,7 @@ public int strlen(byte[] key) { public int setrange(byte[] key, byte[] offset, byte[] val) { lock.lock(); try { - var offsetPosition = Integer.parseInt(new String(offset, StandardCharsets.UTF_8)); + int offsetPosition = Integer.parseInt(new String(offset, StandardCharsets.UTF_8)); byte[] oldVal = map.get(new BytesKey(key)).getBytes(); int newValLength = oldVal == null ? offsetPosition + val.length : Math.max(offsetPosition + val.length, oldVal.length); byte[] newVal = new byte[newValLength]; @@ -784,4 +783,78 @@ public Double zscore(byte[] key, byte[] member) { lock.unlock(); } } + + public byte[] getrange(byte[] key, byte[] start, byte[] end) { + lock.lock(); + try { + byte[] value = map.get (new BytesKey (key)).getBytes (); + int startInt = Integer.parseInt (new String (start, StandardCharsets.UTF_8)); + int endInt = Integer.parseInt (new String (end, StandardCharsets.UTF_8)); + + // convert negative indexes to positive ones + if (startInt < 0 && endInt < 0 && startInt > endInt) { + return null; + } + if (startInt < 0) startInt = value.length + startInt; + if (endInt < 0) endInt = value.length + endInt; + if (startInt < 0) startInt = 0; + if (endInt < 0) endInt = 0; + if (endInt >= value.length) endInt = value.length - 1; + + byte[] result; + if (startInt > endInt || value.length == 0) { + result = new String ("").getBytes (); + } else { + result = Arrays.copyOfRange(value, startInt, endInt + 1); + } + return result; + } finally { + lock.unlock (); + } + } + + @Override + public byte[] incrbyfloat(byte[] key, double amount) { + lock.lock(); + try { + return map.compute(new BytesKey(key), (k, oldVal) -> { + double curVal = 0L; + if (oldVal != null) { + curVal = Double.parseDouble(oldVal.toString()); + } + curVal = curVal + amount; + return new BytesValue(Double.toString(curVal).getBytes(StandardCharsets.UTF_8)); + }).getBytes(); + } finally { + lock.unlock(); + } + } + + @Override + public void mset(byte[]... keys) { + lock.lock(); + try { + for (int i = 0; i < keys.length; i += 2) { + map.put(new BytesKey(keys[i]), new BytesValue(keys[i + 1])); + } + } finally { + lock.unlock(); + } + } + + public byte[] decrby(byte[] key, long amount) { + lock.lock(); + try { + return map.compute(new BytesKey(key), (k, oldVal) -> { + long curVal = 0L; + if (oldVal != null) { + curVal = Long.parseLong(oldVal.toString()); + } + curVal = curVal - amount; + return new BytesValue(Long.toString(curVal).getBytes(StandardCharsets.UTF_8)); + }).getBytes(); + } finally { + lock.unlock(); + } + } }