Skip to content

Commit a423a48

Browse files
authored
Merge pull request #47 from scalableminds/multi-delete-multi-put
Add DeleteAllByPrefix and PutMultipleVersions API endpoints
2 parents 67d3a92 + b1fe6b9 commit a423a48

File tree

7 files changed

+75
-4
lines changed

7 files changed

+75
-4
lines changed

Changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## Added
4+
- New API endpoints `DeleteAllByPrefix` and `PutMultipleVersions`. [#47](https://github.com/scalableminds/fossildb/pull/47)
5+
36
## Breaking Changes
47

58
- The `GetMultipleKeys` call now takes a `startAfterKey` instead of a `key` for pagination. The returned list will only start *after* this key. [#38](https://github.com/scalableminds/fossildb/pull/38)

build.sbt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ version := getVersionFromGit
2424
scalaVersion := "2.13.12"
2525

2626
libraryDependencies ++= Seq(
27-
"ch.qos.logback" % "logback-classic" % "1.4.7",
27+
"ch.qos.logback" % "logback-classic" % "1.5.6",
2828
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.5",
29-
"org.scalatest" % "scalatest_2.13" % "3.2.15" % "test",
29+
"org.scalatest" % "scalatest_2.13" % "3.2.19" % "test",
3030
"io.grpc" % "grpc-netty" % scalapb.compiler.Version.grpcJavaVersion,
3131
"io.grpc" % "grpc-services" % scalapb.compiler.Version.grpcJavaVersion,
3232
"com.thesamet.scalapb" %% "scalapb-runtime-grpc" % scalapb.compiler.Version.scalapbVersion,

src/main/protobuf/fossildbapi.proto

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ message PutReply {
3434
optional string errorMessage = 2;
3535
}
3636

37+
message PutMultipleVersionsRequest {
38+
required string collection = 1;
39+
required string key = 2;
40+
repeated uint64 versions = 3;
41+
repeated bytes values = 4;
42+
}
43+
44+
message PutMultipleVersionsReply {
45+
required bool success = 1;
46+
optional string errorMessage = 2;
47+
}
48+
3749
message DeleteRequest {
3850
required string collection = 1;
3951
required string key = 2;
@@ -45,6 +57,16 @@ message DeleteReply {
4557
optional string errorMessage = 2;
4658
}
4759

60+
message DeleteAllByPrefixRequest {
61+
required string collection = 1;
62+
required string prefix = 2;
63+
}
64+
65+
message DeleteAllByPrefixReply {
66+
required bool success = 1;
67+
optional string errorMessage = 2;
68+
}
69+
4870
message GetMultipleVersionsRequest {
4971
required string collection = 1;
5072
required string key = 2;
@@ -154,12 +176,14 @@ service FossilDB {
154176
rpc GetMultipleVersions (GetMultipleVersionsRequest) returns (GetMultipleVersionsReply) {}
155177
rpc GetMultipleKeys (GetMultipleKeysRequest) returns (GetMultipleKeysReply) {}
156178
rpc Put (PutRequest) returns (PutReply) {}
179+
rpc PutMultipleVersions (PutMultipleVersionsRequest) returns (PutMultipleVersionsReply) {}
157180
rpc Delete (DeleteRequest) returns (DeleteReply) {}
158181
rpc DeleteMultipleVersions (DeleteMultipleVersionsRequest) returns (DeleteMultipleVersionsReply) {}
182+
rpc DeleteAllByPrefix (DeleteAllByPrefixRequest) returns (DeleteAllByPrefixReply) {}
159183
rpc ListKeys (ListKeysRequest) returns (ListKeysReply) {}
160184
rpc ListVersions (ListVersionsRequest) returns (ListVersionsReply) {}
161185
rpc Backup (BackupRequest) returns (BackupReply) {}
162186
rpc RestoreFromBackup (RestoreFromBackupRequest) returns (RestoreFromBackupReply) {}
163187
rpc CompactAllData (CompactAllDataRequest) returns (CompactAllDataReply) {}
164188
rpc ExportDB (ExportDBRequest) returns (ExportDBReply) {}
165-
}
189+
}

src/main/scala/com/scalableminds/fossildb/FossilDBGrpcImpl.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ class FossilDBGrpcImpl(storeManager: StoreManager)
3636
PutReply(success = true)
3737
} { errorMsg => PutReply(success = false, errorMsg) }
3838

39+
override def putMultipleVersions(req: PutMultipleVersionsRequest): Future[PutMultipleVersionsReply] = withExceptionHandler(req) {
40+
val store = storeManager.getStore(req.collection)
41+
require(req.versions.length == req.values.length, s"Must supply as many versions as values, got ${req.versions.length} versions vs ${req.values.length} values.")
42+
require(req.versions.forall(_ >= 0), "Version numbers must be non-negative")
43+
req.versions.zip(req.values).foreach { case (version, value) =>
44+
store.put(req.key, version, value.toByteArray)
45+
}
46+
PutMultipleVersionsReply(success = true)
47+
} { errorMsg => PutMultipleVersionsReply(success = false, errorMsg)}
48+
3949
override def delete(req: DeleteRequest): Future[DeleteReply] = withExceptionHandler(req) {
4050
val store = storeManager.getStore(req.collection)
4151
store.delete(req.key, req.version)
@@ -60,6 +70,12 @@ class FossilDBGrpcImpl(storeManager: StoreManager)
6070
DeleteMultipleVersionsReply(success = true)
6171
} { errorMsg => DeleteMultipleVersionsReply(success = false, errorMsg) }
6272

73+
override def deleteAllByPrefix(req: DeleteAllByPrefixRequest): Future[DeleteAllByPrefixReply] = withExceptionHandler(req) {
74+
val store = storeManager.getStore(req.collection)
75+
store.withRawRocksIterator{rocksIt => store.deleteAllByPrefix(rocksIt, req.prefix)}
76+
DeleteAllByPrefixReply(success = true)
77+
} { errorMsg => DeleteAllByPrefixReply(success = false, errorMsg)}
78+
6379
override def listKeys(req: ListKeysRequest): Future[ListKeysReply] = withExceptionHandler(req) {
6480
val store = storeManager.getStore(req.collection)
6581
val keys = store.withRawRocksIterator{rocksIt => store.listKeys(rocksIt, req.limit, req.startAfterKey)}

src/main/scala/com/scalableminds/fossildb/db/RocksDBStore.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ class RocksDBManager(dataDir: Path, columnFamilies: List[String], optionsFilePat
3636
}
3737
options.setCreateIfMissing(true).setCreateMissingColumnFamilies(true)
3838
val defaultColumnFamilyOptions: ColumnFamilyOptions = cfListRef.find(_.getName sameElements RocksDB.DEFAULT_COLUMN_FAMILY).map(_.getOptions).getOrElse(columnOptions)
39-
println(defaultColumnFamilyOptions)
4039
val newColumnFamilyDescriptors = (columnFamilies.map(_.getBytes) :+ RocksDB.DEFAULT_COLUMN_FAMILY).diff(cfListRef.toList.map(_.getName)).map(new ColumnFamilyDescriptor(_, defaultColumnFamilyOptions))
4140
val columnFamilyDescriptors = cfListRef.toList ::: newColumnFamilyDescriptors
4241
logger.info("Opening RocksDB at " + dataDir.toAbsolutePath)

src/main/scala/com/scalableminds/fossildb/db/VersionedKeyValueStore.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ class VersionedKeyValueStore(underlying: RocksDBStore) {
180180
deleteIter(versionsIterator)
181181
}
182182

183+
def deleteAllByPrefix(rocksIt: RocksIterator, prefix: String): Unit = {
184+
RocksDBStore.scanKeysOnly(rocksIt, prefix, Some(prefix)).foreach(underlying.delete)
185+
}
186+
183187
def put(key: String, version: Long, value: Array[Byte]): Unit = {
184188
requireValidKey(key)
185189
underlying.put(VersionedKey(key, version).toString, value)

src/test/scala/com/scalableminds/fossildb/FossilDBSuite.scala

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ class FossilDBSuite extends AnyFlatSpec with BeforeAndAfterEach with TestHelpers
6969
assert(testData2 == reply.value)
7070
}
7171

72+
"PutMultipleVersions" should "overwrite old values, leave others untouched" in {
73+
client.put(PutRequest(collectionA, aKey, Some(0), testData1))
74+
client.put(PutRequest(collectionA, aKey, Some(2), testData1))
75+
client.putMultipleVersions(PutMultipleVersionsRequest(collectionA, aKey, Seq(1,2,3), Seq(testData2, testData3, testData3)))
76+
val reply = client.getMultipleVersions(GetMultipleVersionsRequest(collectionA, aKey))
77+
assert(reply.values.length == 4)
78+
assert(reply.versions == Seq(3,2,1,0))
79+
assert(reply.values == Seq(testData3, testData3, testData2, testData1))
80+
}
81+
7282
it should "fail on non-existent collection" in {
7383
val reply = client.put(PutRequest("nonExistentCollection", aKey, Some(0), testData1))
7484
assert(!reply.success)
@@ -134,6 +144,21 @@ class FossilDBSuite extends AnyFlatSpec with BeforeAndAfterEach with TestHelpers
134144
assert(testData1 == reply.value)
135145
}
136146

147+
"DeleteAllByPrefix" should "delete all versions of all values matching this prefix" in {
148+
client.put(PutRequest(collectionA, "prefixedA", Some(0), testData1))
149+
client.put(PutRequest(collectionA, "prefixedA", Some(1), testData1))
150+
client.put(PutRequest(collectionA, "prefixedB", Some(0), testData2))
151+
client.put(PutRequest(collectionA, "prefixedC", Some(0), testData2))
152+
client.put(PutRequest(collectionA, "differentKey", Some(0), testData2))
153+
client.put(PutRequest(collectionA, "differentKey", Some(1), testData2))
154+
client.put(PutRequest(collectionA, "yetDifferentKey", Some(0), testData2))
155+
client.deleteAllByPrefix(DeleteAllByPrefixRequest(collectionA, "prefixed"))
156+
val reply = client.listKeys(ListKeysRequest(collectionA))
157+
assert(reply.keys.length == 2)
158+
assert(reply.keys.contains("differentKey"))
159+
assert(reply.keys.contains("yetDifferentKey"))
160+
}
161+
137162
"ListKeys" should "list all keys of a collection" in {
138163
client.put(PutRequest(collectionA, aKey, Some(0), testData1))
139164
client.put(PutRequest(collectionA, aKey, Some(1), testData2))

0 commit comments

Comments
 (0)