diff --git a/README.adoc b/README.adoc index 00d93482..d951865b 100644 --- a/README.adoc +++ b/README.adoc @@ -21,12 +21,10 @@ in your Antora playbook. If you are not sure the best way to update the docs, please raise a JIRA ticket with your suggestion, putting in as much information as possible. -TODO: Add in link to new JIRA. - == Notes for Docs Maintainers -The philosophy behind the Information Architecture can be found in the https://docs.couchbase.com/scala-sdk/1.8/project-docs/metadoc-about-these-sdk-docs.html[meta doc]. +The philosophy behind the Information Architecture can be found in the https://docs.couchbase.com/scala-sdk/3.9/project-docs/metadoc-about-these-sdk-docs.html[meta doc]. === Directory Structure diff --git a/build.gradle b/build.gradle index 02c8ac76..20609c39 100644 --- a/build.gradle +++ b/build.gradle @@ -23,10 +23,10 @@ repositories { sourceSets { main { scala { - srcDirs = ['modules/hello-world/examples', - 'modules/howtos/examples', - 'modules/ref/examples', - 'modules/ROOT/examples'] + // Include the actual location of the Scala example sources that ship with the documentation. + // The previous paths pointed to now-non-existent directories, so Gradle found no source files + // and therefore appeared to "do nothing" when you ran the build. + srcDirs = ['modules/devguide/examples/scala'] } } } diff --git a/modules/ROOT/nav.adoc b/modules/ROOT/nav.adoc index b2aa3821..bccd6ae8 100644 --- a/modules/ROOT/nav.adoc +++ b/modules/ROOT/nav.adoc @@ -56,7 +56,7 @@ ** xref:project-docs:sdk-release-notes.adoc[Release Notes] *** https://docs-archive.couchbase.com/home/index.html[Older Versions Archive^] ** xref:project-docs:compatibility.adoc[] -*** xref:project-docs:migrating-sdk-code-to-3.n.adoc[] +*** xref:project-docs:migrating-to-scala-3.adoc[] // *** xref:project-docs:distributed-acid-transactions-migration-guide.adoc[] *** xref:project-docs:third-party-integrations.adoc[] ** xref:project-docs:sdk-full-installation.adoc[] diff --git a/modules/concept-docs/pages/data-durability-acid-transactions.adoc b/modules/concept-docs/pages/data-durability-acid-transactions.adoc index 60a3c491..48cca97f 100644 --- a/modules/concept-docs/pages/data-durability-acid-transactions.adoc +++ b/modules/concept-docs/pages/data-durability-acid-transactions.adoc @@ -155,8 +155,6 @@ A practical look at this -- using the `durability` parameter with mutating opera -This durability is enforced by the cluster, - // *TODO* Durability section // Worth putting in? @@ -181,6 +179,7 @@ The method can either be one to return the first result returned -- active or re which is useful if a node is timing out; or to return _all_ of the results, for client code to handle any inconsistency, or to build a consensus answer. +NOTE: Reading from replicas is not supported in the Scala 3 version of the SDK. Please see xref:project-docs:migrating-to-scala-3.adoc[Migrating to Scala 3] for guidance on these and other changes. === Preferred Server Group Replica Reads @@ -188,7 +187,7 @@ IMPORTANT: Preferred Server Group Replica Reads are only accessible with the {na xref:server:learn:clusters-and-availability/groups.adoc#understanding-server-group-awareness[Server Groups] can be used to define subsets of nodes within a Couchbase cluster, which contain a complete set of vbuckets (active or replica). -As well as high availability use cases, Servre Groups can also be used to keep much traffic within the same cloud Availability Zone. +As well as high availability use cases, Server Groups can also be used to keep much traffic within the same cloud Availability Zone. For Capella users with high data volumes, egress charges for reads from other Availability Zones (AZ) in AWS can be a significant cost. The {name-sdk}, when making read replica requests, can make a request to a preferred Server Group -- diff --git a/modules/devguide/examples/scala/ErrorHandling.scala b/modules/devguide/examples/scala/ErrorHandling.scala index 985ebd2d..16426ba9 100644 --- a/modules/devguide/examples/scala/ErrorHandling.scala +++ b/modules/devguide/examples/scala/ErrorHandling.scala @@ -16,12 +16,11 @@ // #tag::imports[] import java.util.concurrent.TimeUnit - import com.couchbase.client.core.error._ import com.couchbase.client.scala._ import com.couchbase.client.scala.durability._ import com.couchbase.client.scala.json._ -import com.couchbase.client.scala.kv.MutationResult +import com.couchbase.client.scala.kv.{InsertOptions, MutationResult, ReplaceOptions} import scala.concurrent.duration._ import scala.util.{Failure, Success, Try} @@ -86,7 +85,7 @@ object ErrorHandling { // #tag::cas[] def doOperation(guard: Int = 3): Try[MutationResult] = { collection.get("doc") - .flatMap(doc => collection.replace(doc.id, newJson, cas = doc.cas)) match { + .flatMap(doc => collection.replace(doc.id, newJson, ReplaceOptions().cas(doc.cas))) match { case Success(value) => Success(value) @@ -107,7 +106,7 @@ object ErrorHandling { // #tag::insert[] def doInsert(docId: String, json: JsonObject, guard: Int = InitialGuard): Try[String] = { - val result = collection.insert(docId, json, durability = Durability.Majority) + val result = collection.insert(docId, json, InsertOptions().durability(Durability.Majority)) result match { @@ -143,7 +142,7 @@ object ErrorHandling { json: JsonObject, guard: Int = InitialGuard, delay: Duration = Duration(10, TimeUnit.MILLISECONDS)): Try[String] = { - val result = collection.insert(docId, json, durability = Durability.Majority) + val result = collection.insert(docId, json, InsertOptions().durability(Durability.Majority)) result match { diff --git a/modules/devguide/examples/scala/Queries.scala b/modules/devguide/examples/scala/Queries.scala index 10ab8a4d..d5872a9f 100644 --- a/modules/devguide/examples/scala/Queries.scala +++ b/modules/devguide/examples/scala/Queries.scala @@ -180,8 +180,8 @@ object Queries { // ReactiveQueryResult contains a rows: Flux[QueryRow] .flatMapMany(result => result.rowsAs[JsonObject]) -// Just for example, block on the rows. This is not best practice and apps -// should generally not block. + // Just for example, block on the rows. This is not best practice and apps + // should generally not block. val allRows: Seq[JsonObject] = rows .doOnNext(row => println(row)) .doOnError(err => println(s"Error: $err")) diff --git a/modules/devguide/examples/scala/SubDocument.scala b/modules/devguide/examples/scala/SubDocument.scala index 8bea66c1..021dd59f 100644 --- a/modules/devguide/examples/scala/SubDocument.scala +++ b/modules/devguide/examples/scala/SubDocument.scala @@ -321,7 +321,7 @@ def cas() { val result = collection.get("player432") .flatMap(doc => collection.mutateIn("player432", Array( decrement("gold", 150) - ), cas = doc.cas)) + ), MutateInOptions().cas(doc.cas))) // #end::cas[] } @@ -329,7 +329,7 @@ val result = collection.get("player432") // #tag::durability[] val result = collection.mutateIn("key", Array( insert("name", "andy") - ), durability = Durability.ClientVerified(ReplicateTo.One, PersistTo.One)) + ), MutateInOptions().durability(Durability.ClientVerified(ReplicateTo.One, PersistTo.One))) // #end::durability[] } @@ -337,7 +337,7 @@ val result = collection.get("player432") // #tag::sync-durability[] val result = collection.mutateIn("key", Array( insert("name", "andy") - ), durability = Durability.Majority) + ), MutateInOptions().durability(Durability.Majority)) // #end::sync-durability[] } diff --git a/modules/devguide/examples/scala/Transcoding.scala b/modules/devguide/examples/scala/Transcoding.scala index 6d34638d..12256198 100644 --- a/modules/devguide/examples/scala/Transcoding.scala +++ b/modules/devguide/examples/scala/Transcoding.scala @@ -1,291 +1,291 @@ -// /* -// * Copyright (c) 2020 Couchbase, Inc. -// * -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// */ - -// import java.nio.charset.StandardCharsets - -// import com.couchbase.client.core.msg.kv.CodecFlags.CommonFlags -// import com.couchbase.client.scala.Cluster -// import com.couchbase.client.scala.codec._ -// import com.couchbase.client.scala.kv.{GetOptions, UpsertOptions} -// import org.msgpack.core.MessagePack -// import org.scalatest.funsuite.AnyFunSuite - -// import scala.reflect.runtime.universe -// import scala.util.{Failure, Success, Try} -// import scala.reflect.runtime.universe._ - -// // #tag::ujson[] -// case class MyUser(name: String, age: Int) - -// object MyUser { -// implicit object UserSerializer extends JsonSerializer[MyUser] { -// override def serialize(content: MyUser): Try[Array[Byte]] = { -// // It's also possible for uPickle to serialize and deserialize -// // case classes directly to/from JSON, but for the purposes of -// // demonstration we will generate the JSON manually. -// val json = ujson.Obj("name" -> content.name, "age" -> content.age) -// Success(ujson.transform(json, ujson.BytesRenderer()).toBytes) -// } -// } - -// implicit object UserDeserializer extends JsonDeserializer[MyUser] { -// override def deserialize(bytes: Array[Byte]): Try[MyUser] = { -// Try({ -// val json = upickle.default.read[ujson.Value](bytes) -// MyUser(json("name").str, json("age").num.toInt) -// }) -// } -// } -// } -// // #end::ujson[] - -// // #tag::overloaded1[] -// case class MyUser2(name: String, age: Int) - -// object MyUser2 { -// // First serializer uses uPickle -// implicit object UserSerializer1 extends JsonSerializer[MyUser2] { -// override def serialize(content: MyUser2): Try[Array[Byte]] = { -// val json = ujson.Obj("name" -> content.name, "age" -> content.age) -// Success(ujson.transform(json, ujson.BytesRenderer()).toBytes) -// } -// } - -// // Second serializer writes the JSON manually -// implicit object UserSerializer2 extends JsonSerializer[MyUser2] { -// override def serialize(content: MyUser2): Try[Array[Byte]] = { -// val sb = new StringBuilder -// sb.append("""{"name":""") -// sb.append(content.name) -// sb.append("""","age":""") -// sb.append(content.age) -// sb.append("}") -// Success(sb.toString.getBytes(StandardCharsets.UTF_8)) -// } -// } -// } -// // #end::overloaded1[] - -// // #tag::msgpack-serializer[] -// object MsgPack { -// implicit object MsgPackSerializer extends JsonSerializer[MyUser] { -// override def serialize(content: MyUser): Try[Array[Byte]] = { -// Try({ -// // MessagePack can automatically generate equivalent code, -// // but for demonstration purposes we will do it manually -// val packer = MessagePack.newDefaultBufferPacker() -// packer.packString(content.name) -// packer.packInt(content.age) -// packer.close() -// packer.toByteArray -// }) -// } -// } - -// implicit object MsgPackDeserializer extends JsonDeserializer[MyUser] { -// override def deserialize(bytes: Array[Byte]): Try[MyUser] = { -// Try({ -// val unpacker = MessagePack.newDefaultUnpacker(bytes) -// MyUser(unpacker.unpackString(), unpacker.unpackInt()) -// }) -// } -// } -// } -// // #end::msgpack-serializer[] - -// // #tag::msgpack-transcoder[] -// class BinaryTranscoder extends TranscoderWithSerializer { -// def encode[A](value: A, serializer: JsonSerializer[A]): Try[EncodedValue] = { -// serializer -// .serialize(value) -// .map(bytes => EncodedValue(bytes, DocumentFlags.Binary)) -// } - -// def decode[A]( -// value: Array[Byte], -// flags: Int, -// serializer: JsonDeserializer[A] -// )(implicit tag: WeakTypeTag[A]): Try[A] = { -// serializer.deserialize(value) -// } -// } -// // #end::msgpack-transcoder[] - -// class Transcoding extends AnyFunSuite { - -// val cluster = Cluster.connect("localhost", "Administrator", "password").get -// val collection = cluster.bucket("default").defaultCollection - -// test("ujson") { -// // #tag::ujson-used[] -// val user = MyUser("John Smith", 27) - -// // The compiler will find our UserSerializer for this -// collection.upsert("john-smith", user) match { - -// case Success(_) => -// collection -// .get("john-smith") - -// // ... and our UserDeserializer for this -// .flatMap(fetched => fetched.contentAs[MyUser]) match { - -// case Success(fetchedUser) => -// assert(fetchedUser == user) - -// case Failure(err) => fail(s"Failed to get doc: $err") -// } - -// case Failure(err) => fail(s"Failed to upsert doc: $err") -// } -// // #end::ujson-used[] -// } - -// test("overloaded serializers") { -// // #tag::overloaded2[] -// val user = MyUser2("John Smith", 27) - -// // This import will cause the compiler to prefer UserSerializer2 -// import MyUser2.UserSerializer2 -// collection.upsert("john-smith", user).get - -// // But the application can override this -// collection.upsert("john-smith", user)(MyUser2.UserSerializer1).get -// // #end::overloaded2[] -// } - -// test("messagepack") { -// // #tag::msgpack-encode[] -// val user = MyUser("John Smith", 27) - -// // Make sure the MessagePack serializers are used -// import MsgPack._ - -// val transcoder = new BinaryTranscoder - -// // The compiler will find and use our MsgPackSerializer here -// collection.upsert( -// "john-smith", -// user, -// UpsertOptions().transcoder(transcoder) -// ) match { -// case Success(_) => - -// collection -// .get("john-smith", GetOptions().transcoder(transcoder)) - -// // ... and our MsgPackDeserializer here -// .flatMap(result => result.contentAs[MyUser]) match { - -// case Success(fetched) => -// assert(fetched == user) - -// case Failure(err) => fail(s"Failed to get or convert doc: $err") -// } - -// case Failure(err) => fail(s"Failed to upsert doc: $err") -// } -// // #end::msgpack-encode[] -// } - -// test("string") { -// // #tag::string[] -// collection.upsert( -// "doc-id", -// "hello world", -// UpsertOptions().transcoder(RawStringTranscoder.Instance) -// ) match { - -// case Success(_) => -// collection -// .get( -// "doc-id", -// GetOptions().transcoder(RawStringTranscoder.Instance) -// ) -// .flatMap(result => result.contentAs[String]) match { - -// case Success(fetched) => -// assert(fetched == "hello world") - -// case Failure(err) => fail(s"Failed to get or convert doc: $err") -// } - -// case Failure(err) => fail(s"Failed to upsert doc: $err") -// } -// // #end::string[] -// } - -// test("binary") { -// // #tag::binary[] -// val content: Array[Byte] = "hello world".getBytes(StandardCharsets.UTF_8) - -// collection.upsert( -// "doc-id", -// content, -// UpsertOptions().transcoder(RawBinaryTranscoder.Instance) -// ) match { -// case Success(_) => -// collection -// .get( -// "doc-id", -// GetOptions().transcoder(RawBinaryTranscoder.Instance) -// ) -// .flatMap(result => result.contentAs[Array[Byte]]) match { -// case Success(fetched) => -// assert(fetched(0) == 'h') -// assert(fetched(1) == 'e') -// assert(fetched(2) == 'l') -// // ... - -// case Failure(err) => fail(s"Failed to get or convert doc: $err") -// } - -// case Failure(err) => fail(s"Failed to upsert doc: $err") -// } -// // #end::binary[] -// } - -// test("json") { -// // #tag::json[] - -// val json = ujson.Obj("name" -> "John Smith", "age" -> 27) -// val bytes: Array[Byte] = ujson.transform(json, ujson.BytesRenderer()).toBytes - -// collection.upsert( -// "doc-id", -// bytes, -// UpsertOptions().transcoder(RawJsonTranscoder.Instance) -// ) match { -// case Success(_) => -// collection -// .get( -// "doc-id", -// GetOptions().transcoder(RawJsonTranscoder.Instance) -// ) -// .flatMap(result => result.contentAs[Array[Byte]]) match { -// case Success(fetched) => -// val jsonFetched = upickle.default.read[ujson.Value](fetched) -// assert(jsonFetched("name").str == "John Smith") -// assert(jsonFetched("age").num == 27) - -// case Failure(err) => fail(s"Failed to get or convert doc: $err") -// } - -// case Failure(err) => fail(s"Failed to upsert doc: $err") -// } -// // #end::json[] -// } -// } \ No newline at end of file + /* + * Copyright (c) 2020 Couchbase, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + import java.nio.charset.StandardCharsets + import com.couchbase.client.core.msg.kv.CodecFlags.CommonFlags + import com.couchbase.client.scala.Cluster + import com.couchbase.client.scala.codec._ + import com.couchbase.client.scala.kv.{GetOptions, UpsertOptions} + import org.msgpack.core.MessagePack + import org.scalatest.funsuite.AnyFunSuite + + import scala.reflect.ClassTag + import scala.reflect.runtime.universe + import scala.util.{Failure, Success, Try} + import scala.reflect.runtime.universe._ + + // #tag::ujson[] + case class MyUser(name: String, age: Int) + + object MyUser { + implicit object UserSerializer extends JsonSerializer[MyUser] { + override def serialize(content: MyUser): Try[Array[Byte]] = { + // It's also possible for uPickle to serialize and deserialize + // case classes directly to/from JSON, but for the purposes of + // demonstration we will generate the JSON manually. + val json = ujson.Obj("name" -> content.name, "age" -> content.age) + Success(ujson.transform(json, ujson.BytesRenderer()).toByteArray) + } + } + + implicit object UserDeserializer extends JsonDeserializer[MyUser] { + override def deserialize(bytes: Array[Byte]): Try[MyUser] = { + Try({ + val json = upickle.default.read[ujson.Value](bytes) + MyUser(json("name").str, json("age").num.toInt) + }) + } + } + } + // #end::ujson[] + + // #tag::overloaded1[] + case class MyUser2(name: String, age: Int) + + object MyUser2 { + // First serializer uses uPickle + implicit object UserSerializer1 extends JsonSerializer[MyUser2] { + override def serialize(content: MyUser2): Try[Array[Byte]] = { + val json = ujson.Obj("name" -> content.name, "age" -> content.age) + Success(ujson.transform(json, ujson.BytesRenderer()).toByteArray) + } + } + + // Second serializer writes the JSON manually + implicit object UserSerializer2 extends JsonSerializer[MyUser2] { + override def serialize(content: MyUser2): Try[Array[Byte]] = { + val sb = new StringBuilder + sb.append("""{"name":""") + sb.append(content.name) + sb.append("""","age":""") + sb.append(content.age) + sb.append("}") + Success(sb.toString.getBytes(StandardCharsets.UTF_8)) + } + } + } + // #end::overloaded1[] + + // #tag::msgpack-serializer[] + object MsgPack { + implicit object MsgPackSerializer extends JsonSerializer[MyUser] { + override def serialize(content: MyUser): Try[Array[Byte]] = { + Try({ + // MessagePack can automatically generate equivalent code, + // but for demonstration purposes we will do it manually + val packer = MessagePack.newDefaultBufferPacker() + packer.packString(content.name) + packer.packInt(content.age) + packer.close() + packer.toByteArray + }) + } + } + + implicit object MsgPackDeserializer extends JsonDeserializer[MyUser] { + override def deserialize(bytes: Array[Byte]): Try[MyUser] = { + Try({ + val unpacker = MessagePack.newDefaultUnpacker(bytes) + MyUser(unpacker.unpackString(), unpacker.unpackInt()) + }) + } + } + } + // #end::msgpack-serializer[] + + // #tag::msgpack-transcoder[] + class BinaryTranscoder extends TranscoderWithSerializer { + def encode[A](value: A, serializer: JsonSerializer[A]): Try[EncodedValue] = { + serializer + .serialize(value) + .map(bytes => EncodedValue(bytes, DocumentFlags.Binary)) + } + + def decode[A]( + value: Array[Byte], + flags: Int, + serializer: JsonDeserializer[A] + )(implicit tag: ClassTag[A]): Try[A] = { + serializer.deserialize(value) + } + } + // #end::msgpack-transcoder[] + + class Transcoding extends AnyFunSuite { + + val cluster = Cluster.connect("localhost", "Administrator", "password").get + val collection = cluster.bucket("default").defaultCollection + + test("ujson") { + // #tag::ujson-used[] + val user = MyUser("John Smith", 27) + + // The compiler will find our UserSerializer for this + collection.upsert("john-smith", user) match { + + case Success(_) => + collection + .get("john-smith") + + // ... and our UserDeserializer for this + .flatMap(fetched => fetched.contentAs[MyUser]) match { + + case Success(fetchedUser) => + assert(fetchedUser == user) + + case Failure(err) => fail(s"Failed to get doc: $err") + } + + case Failure(err) => fail(s"Failed to upsert doc: $err") + } + // #end::ujson-used[] + } + + test("overloaded serializers") { + // #tag::overloaded2[] + val user = MyUser2("John Smith", 27) + + // This import will cause the compiler to prefer UserSerializer2 + import MyUser2.UserSerializer2 + collection.upsert("john-smith", user).get + + // But the application can override this + collection.upsert("john-smith", user)(MyUser2.UserSerializer1).get + // #end::overloaded2[] + } + + test("messagepack") { + // #tag::msgpack-encode[] + val user = MyUser("John Smith", 27) + + // Make sure the MessagePack serializers are used + import MsgPack._ + + val transcoder = new BinaryTranscoder + + // The compiler will find and use our MsgPackSerializer here + collection.upsert( + "john-smith", + user, + UpsertOptions().transcoder(transcoder) + ) match { + case Success(_) => + + collection + .get("john-smith", GetOptions().transcoder(transcoder)) + + // ... and our MsgPackDeserializer here + .flatMap(result => result.contentAs[MyUser]) match { + + case Success(fetched) => + assert(fetched == user) + + case Failure(err) => fail(s"Failed to get or convert doc: $err") + } + + case Failure(err) => fail(s"Failed to upsert doc: $err") + } + // #end::msgpack-encode[] + } + + test("string") { + // #tag::string[] + collection.upsert( + "doc-id", + "hello world", + UpsertOptions().transcoder(RawStringTranscoder.Instance) + ) match { + + case Success(_) => + collection + .get( + "doc-id", + GetOptions().transcoder(RawStringTranscoder.Instance) + ) + .flatMap(result => result.contentAs[String]) match { + + case Success(fetched) => + assert(fetched == "hello world") + + case Failure(err) => fail(s"Failed to get or convert doc: $err") + } + + case Failure(err) => fail(s"Failed to upsert doc: $err") + } + // #end::string[] + } + + test("binary") { + // #tag::binary[] + val content: Array[Byte] = "hello world".getBytes(StandardCharsets.UTF_8) + + collection.upsert( + "doc-id", + content, + UpsertOptions().transcoder(RawBinaryTranscoder.Instance) + ) match { + case Success(_) => + collection + .get( + "doc-id", + GetOptions().transcoder(RawBinaryTranscoder.Instance) + ) + .flatMap(result => result.contentAs[Array[Byte]]) match { + case Success(fetched) => + assert(fetched(0) == 'h') + assert(fetched(1) == 'e') + assert(fetched(2) == 'l') + // ... + + case Failure(err) => fail(s"Failed to get or convert doc: $err") + } + + case Failure(err) => fail(s"Failed to upsert doc: $err") + } + // #end::binary[] + } + + test("json") { + // #tag::json[] + + val json = ujson.Obj("name" -> "John Smith", "age" -> 27) + val bytes: Array[Byte] = ujson.transform(json, ujson.BytesRenderer()).toByteArray + + collection.upsert( + "doc-id", + bytes, + UpsertOptions().transcoder(RawJsonTranscoder.Instance) + ) match { + case Success(_) => + collection + .get( + "doc-id", + GetOptions().transcoder(RawJsonTranscoder.Instance) + ) + .flatMap(result => result.contentAs[Array[Byte]]) match { + case Success(fetched) => + val jsonFetched = upickle.default.read[ujson.Value](fetched) + assert(jsonFetched("name").str == "John Smith") + assert(jsonFetched("age").num == 27) + + case Failure(err) => fail(s"Failed to get or convert doc: $err") + } + + case Failure(err) => fail(s"Failed to upsert doc: $err") + } + // #end::json[] + } + } \ No newline at end of file diff --git a/modules/devguide/examples/scala/pom.xml b/modules/devguide/examples/scala/pom.xml index 7bacadde..b271eaaf 100644 --- a/modules/devguide/examples/scala/pom.xml +++ b/modules/devguide/examples/scala/pom.xml @@ -2,6 +2,6 @@ com.couchbase.client scala-client_2.13 - 1.8.3 + 3.9.0 diff --git a/modules/hello-world/pages/overview.adoc b/modules/hello-world/pages/overview.adoc index bb37f8e7..fdcdea97 100644 --- a/modules/hello-world/pages/overview.adoc +++ b/modules/hello-world/pages/overview.adoc @@ -76,6 +76,11 @@ Install the SDK, and explore in the way that works best for you. libraryDependencies += "com.couchbase.client" %% "scala-client" % "{sdk_current_version}" ---- +The Scala SDK is provided with builds for Scala 2.12, 2.13, and 3. +`%%` takes care of selecting the right version in Scala Build Tool. +If you are using another build tool such as Maven or Gradle, then specify `scala-client_2.12`, `scala-client_2.13`, or `scala-client_3`, as appropriate. +The Scala 3 build can be used from applications compiled with Scala 3.3 through 3.7 inclusive, and even Scala 2.13. +It is the recommended build for all users, except those on 2.12. The links below will take you where you want to go -- as will the navigation on the left-hand side of this page. But if you don't know exactly where you need to go, try one of the following: diff --git a/modules/howtos/pages/analytics-using-sdk.adoc b/modules/howtos/pages/analytics-using-sdk.adoc index c2682058..386a1a84 100644 --- a/modules/howtos/pages/analytics-using-sdk.adoc +++ b/modules/howtos/pages/analytics-using-sdk.adoc @@ -18,6 +18,7 @@ This is the analytic counterpart to our xref:sqlpp-queries-with-sdk.adoc[operati The analytics service is available in xref:cloud:clusters:analytics-service/analytics-service.adoc[Capella operational] or the Enterprise Edition of self-managed Couchbase Server. +NOTE: The Scala 3 version of the SDK does not carry forward support for analytics. Please see xref:project-docs:migrating-to-scala-3.adoc[Migrating to Scala 3] for guidance on these and other changes. == Getting Started diff --git a/modules/howtos/pages/concurrent-async-apis.adoc b/modules/howtos/pages/concurrent-async-apis.adoc index 2b96a02e..a2f9baa2 100644 --- a/modules/howtos/pages/concurrent-async-apis.adoc +++ b/modules/howtos/pages/concurrent-async-apis.adoc @@ -16,6 +16,7 @@ The Scala SDK provides three APIs, which can be freely mixed: * An asynchronous one, that returns `Future`. * A reactive one, that returns reactive primitives from the https://projectreactor.io/[Project Reactor] library, e.g. `Mono` and `Flux`. +NOTE: The Scala 3 version of the SDK does not carry forward support for the reactive API, since it depends on an external library that is end-of-life. Please see xref:project-docs:migrating-to-scala-3.adoc[Migrating to Scala 3] for guidance on these and other changes. == Using the Blocking API diff --git a/modules/howtos/pages/json.adoc b/modules/howtos/pages/json.adoc index 1d448b53..3e4b38d8 100644 --- a/modules/howtos/pages/json.adoc +++ b/modules/howtos/pages/json.adoc @@ -22,6 +22,7 @@ There's a wide range of great, popular JSON libaries for the JVM, and we've supp That is, you can use types from the https://circe.github.io/circe/[Circe library], and many others, when doing any operation. And if we're missing support for your favourite, then please let us know on the http://forums.couchbase.com/[forums]. +NOTE: The Scala 3 version of the SDK does not carry forward direct support for the external JSON libraries, though it remains easy to integrate with them. Please see xref:project-docs:migrating-to-scala-3.adoc[Migrating to Scala 3] for guidance on these and other changes. == Optional Dependencies @@ -196,6 +197,9 @@ Note that `JsonObjectSafe`, though presenting a more functional interface, is st It can be very useful to deal directly with Scala case classes, that is to send and retrieve them directly rather than via some interim type, and the Scala SDK includes built-in support for this. +NOTE: The Scala 3 version of the SDK does not carry forward direct support for case classes, though it remains easy to use them directly via libraries such as `Jsoniter` and `Circe`. +Please contact us if this is functionality that is useful to you. + It's necessary to write a small amount of boilerplate code first. If you try and insert a case class directly, you'll get an error. E.g. this won't work: diff --git a/modules/howtos/pages/kv-operations.adoc b/modules/howtos/pages/kv-operations.adoc index 952d3d19..c9744a5c 100644 --- a/modules/howtos/pages/kv-operations.adoc +++ b/modules/howtos/pages/kv-operations.adoc @@ -65,6 +65,8 @@ You can also directly encode and decode Scala case classes to and from the SDK. To make things easy and to help get you started, the Scala SDK also bundles a home-grown small JSON library, which you are free to use instead of or alongside any of the other supported JSON libraries. The philosophy behind this library is to provide a very easy-to-use API and the fastest JSON implementation possible. +NOTE: The Scala 3 version of the SDK removes direct support for external JSON libraries and direct handling of case classes, though it remains easy to integrate both. Please see xref:project-docs:migrating-to-scala-3.adoc[Migrating to Scala 3] for guidance on these and other changes. + === Using JsonObject and JsonArray Using the built-in JSON library here's how to create some simple JSON: @@ -124,7 +126,7 @@ include::devguide:example$scala/KvOperations.scala[indent=0,tag=upsert] [NOTE] ===== All the examples here use the Scala SDK's simplest API, which blocks until the operation is performed. There's also an asynchronous API that is based around Scala `Future`, and a -reactive API. See xref:concurrent-async-apis[Choosing an API] for more details. +reactive API. See xref:concurrent-async-apis#choosing-an-api[Choosing an API] for more details. ===== == Handling Single Errors diff --git a/modules/howtos/pages/sqlpp-queries-with-sdk.adoc b/modules/howtos/pages/sqlpp-queries-with-sdk.adoc index 36f24e4d..8eabec5e 100644 --- a/modules/howtos/pages/sqlpp-queries-with-sdk.adoc +++ b/modules/howtos/pages/sqlpp-queries-with-sdk.adoc @@ -134,6 +134,7 @@ Here's how to perform a query and stream the results using the reactive API: include::devguide:example$scala/Queries.scala[tag=reactive,indent=0] ---- +NOTE: The Scala 3 version of the SDK does not carry forward support for the reactive API, since it depends on an external library that is end-of-life. Please see xref:project-docs:migrating-to-scala-3.adoc[Migrating to Scala 3] for guidance on these and other changes. == Querying at Scope Level diff --git a/modules/howtos/pages/subdocument-operations.adoc b/modules/howtos/pages/subdocument-operations.adoc index a35bbf75..329f8e78 100644 --- a/modules/howtos/pages/subdocument-operations.adoc +++ b/modules/howtos/pages/subdocument-operations.adoc @@ -112,25 +112,6 @@ Multiple operations can be combined, and this can be most neatly done with a for include::devguide:example$scala/SubDocument.scala[tag=combine] ---- - -== Choosing an API - -The Scala SDK provides three APIs for all operations. There's the simple blocking one you've already seen, then this -asynchronous variant that returns Scala `Future`: - -[source,scala] ----- -include::devguide:example$scala/SubDocument.scala[tag=get-future] ----- - -And a third that uses reactive programming primitives from https://projectreactor.io/[Project Reactor]: - -[source,scala] ----- -include::devguide:example$scala/SubDocument.scala[tag=get-reactive] ----- - - == Mutating Mutation operations modify one or more paths in the document. diff --git a/modules/project-docs/pages/compatibility.adoc b/modules/project-docs/pages/compatibility.adoc index 92202693..dc3dba16 100644 --- a/modules/project-docs/pages/compatibility.adoc +++ b/modules/project-docs/pages/compatibility.adoc @@ -131,19 +131,24 @@ It is best to upgrade either the SDK or the Couchbase version you are using. .Recommended SDK per Server Version Matrix [#table_sdk_versions] |=== -| | 1.4, 1.5 | 1.6, 1.7 | 1.8 +| | 1.4, 1.5 | 1.6, 1.7 | 1.8 | 3.9 | *Server 7.0 - 7.2* | *✔* | *✔* | *✔* +| *✔* | *Server 7.6* ① | *✔* | *✔* | *✔* +| *✔* |=== +Note that from 3.9.0 on, all Couchbase JVM SDKs have an aligned version number to make it easier to users to track changes. +So the version has jumped from 1.8.x to 3.9.x. + ① Server 7.6 is compatible with all supported (not yet End-of-Life) versions of the {name-sdk}, but for full support of the latest features you need to upgrade to a recent version of the SDK. See the <<#couchbase-new-feature-availability-matrix,Feature Availablity matrix below>> and the xref:sdk-release-notes.adoc[Release Notes page]. diff --git a/modules/project-docs/pages/migrating-to-scala-3.adoc b/modules/project-docs/pages/migrating-to-scala-3.adoc new file mode 100644 index 00000000..e026321a --- /dev/null +++ b/modules/project-docs/pages/migrating-to-scala-3.adoc @@ -0,0 +1,22 @@ += Differences between Scala 2 and 3 SDK versions +:description: The Scala 3 version of the SDK has some differences from the Scala 2 versions. +:nav-title: Migrating to Scala SDK 3.x API +:page-aliases: migrating-sdk-code-to-3.n.adoc,howtos:view-queries-with-sdk.adoc,concept-docs:understanding-views.adoc,howtos:working-with-collections.adoc + +[abstract] +{description} +But we expect the majority of Scala 2 applications will be able to migrate to Scala 3 and the Scala 3 version of the SDK with minimal or even no differences. + + +If any of the following changes impact you as either an existing Scala 2 SDK user or an interested Scala 3 user, then please let us know. +For some of these migrations we can offer alternative approaches, or discuss re-adding desired functionality. + +The Scala 3 version of the SDK: + +* Does not carry forward support for the Reactor API, as the Scala Reactor Extensions library that it depends on is long end-of-life. +The `Future` API offers most of the same functionality while being considerably easier to use, and more Scala-centric. +It is also easily adapted to other Scala libraries such as Cats Effect and ZIO. +* Does not carry forward the built-in support for JSON libraries (`Circe`, `json4s`, etc.), as this has increasingly become problematic to maintain as those libraries migrate to new majors. +* Does not carry forward support for views, analytics, replica reads, management operations, and datastructures. +* There are two overloads for most API methods, one that takes an options block, and one that tries to be a convenience method offering a few popular settings. +The latter is removed in the Scala 3 version, significantly simplifying the API. diff --git a/modules/project-docs/pages/sdk-release-notes.adoc b/modules/project-docs/pages/sdk-release-notes.adoc index a34738dd..86235be8 100644 --- a/modules/project-docs/pages/sdk-release-notes.adoc +++ b/modules/project-docs/pages/sdk-release-notes.adoc @@ -24,7 +24,6 @@ include::hello-world:start-using-sdk.adoc[tag=quick-install] include::{version-common}@sdk:pages:partial$signed.adoc[tag=signed] - // reminder - add spoiler tag to wrappers for cxx notes? [#latest-release] @@ -33,10 +32,20 @@ include::{version-common}@sdk:pages:partial$signed.adoc[tag=signed] Version 3.9 of the Scala SDK implements the 3.8 xref:compatibility.adoc#api-version[SDK API]. See the xref:compatibility.adoc#couchbase-feature-availability-matrix[compatibility pages] for more information on feature compatibility with different versions of Couchbase Server. +Note that from 3.9.0 on, all Couchbase JVM SDKs have an aligned version number to make it easier to users to track changes, and so the Scala SDK version has jumped from 1.8.x to 3.9.x. + +While this is technically an increase in the major, the Scala 2 versions of the SDK have only one very small breaking change made to the API, to remove a long-deprecated transactions method. All other functionality works unchanged and users can upgrade existing applications without issue. + +The major new feature in the 3.9 series is the addition of a new version of the SDK, built for Scala 3. +The Scala 3 version of the SDK has some API differences, documented on xref:project-docs:migrating-to-scala-3.adoc[migrating to Scala 3]. +But we expect the majority of Scala 2 applications will be able to migrate to Scala 3 and the Scala 3 version of the SDK with no-to-minimal differences. + We always recommend using the latest version of the SDK -- it contains all of the latest security patches and support for new and upcoming features. All patch releases for each dot minor release should be API compatible, and safe to upgrade; any changes to expected behavior are noted in the release notes that follow. +Binary compatibility is not guaranteed for any Scala SDK release, and you should rebuild your application when changing the SDK. + //// # get version numbers like this src=../couchbase-jvm-clients @@ -45,9 +54,30 @@ grep ' ' $src/pom.xml //// -=== Version 3.9.0 (?? August 2025) -// Note on version jump to 3.9?? +=== Version 3.9.0 (August 2025) + +https://docs.couchbase.com/sdk-api/couchbase-scala-client-3.9.0/com/couchbase/client/scala/index.html[API Reference] | +http://docs.couchbase.com/sdk-api/couchbase-core-io-3.9.0/[Core API Reference] + +Note that the API reference is for the Scala 2.12 version of the SDK. +The Scala 3 version of the SDK has some API differences, documented on xref:project-docs:migrating-to-scala-3.adoc[migrating to Scala 3]. + +The supported and tested dependencies for this release are: + +* io.projectreactor:**reactor-core:3.6.9** +* org.reactivestreams:**reactive-streams:1.0.4** + +==== Improvements + +// TODO: for release captain! + +==== Bug Fixes + +// TODO: for release captain! + + + == Scala SDK 1.8 Releases