diff --git a/.editorconfig b/.editorconfig index 6915df1..d394201 100644 --- a/.editorconfig +++ b/.editorconfig @@ -28,8 +28,10 @@ ktlint_standard_statement-wrapping = disabled ktlint_standard_if-else-bracing = disabled ktlint_standard_argument-list-wrapping = disabled -[**/src/test/**/*.kt] +[**/src/{test,commonTest,jvmTest,jsTest,nativeTest}/**/*.kt] # Test fixtures (long inline JSON assertions, verbose set-up) get a little more room than production code. +# The multiplatform source-set names are listed alongside src/test: kdrant-core's tests live in +# commonTest and jvmTest, and without them the tests there would be held to the production limit. max_line_length = 140 [*.{yml,yaml,json,toml}] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46ac92d..87ec186 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,33 @@ jobs: **/build/test-results/ retention-days: 7 + # The Linux build above compiles kdrant-core for the JVM, JS, Linux and Windows, and skips the Apple + # targets because no Linux host can build them. Without this job an iOS or macOS break would first be + # seen at release time, on the one runner that does compile them. + apple-targets: + name: Apple targets (kdrant-core) + runs-on: macos-latest + steps: + - uses: actions/checkout@v7 + + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + java-version: "17" + distribution: temurin + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Compile and test the Apple targets + run: > + ./gradlew + :kdrant-core:compileKotlinIosArm64 + :kdrant-core:compileKotlinIosX64 + :kdrant-core:iosSimulatorArm64Test + :kdrant-core:macosArm64Test + --no-daemon --stacktrace + qdrant-compat: name: Integration (${{ matrix.qdrant }}) runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index caf44dc..06a0437 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,9 @@ permissions: jobs: publish: - runs-on: ubuntu-latest + # macOS, because kdrant-core is multiplatform and only a macOS host can compile the Apple targets. + # A Linux runner would silently skip them and publish a release missing its iOS and macOS klibs. + runs-on: macos-latest steps: - uses: actions/checkout@v7 @@ -36,6 +38,7 @@ jobs: with: subject-path: | kdrant-core/build/libs/*.jar + kdrant-transport-grpc/build/libs/*.jar kdrant-transport-rest/build/libs/*.jar kdrant-spring-boot-starter/build/libs/*.jar kdrant-spring-ai/build/libs/*.jar @@ -64,18 +67,22 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VERSION="${GITHUB_REF_NAME#v}" - for module in kdrant-core kdrant-transport-rest kdrant-spring-boot-starter \ - kdrant-spring-ai kdrant-langchain4j kdrant-micrometer; do - jar="$module/build/libs/$module-$VERSION.jar" + # kdrant-core is multiplatform, so its JVM jar is published as kdrant-core-jvm and the + # kdrant-core coordinate carries only Gradle metadata. The record names the artifact that + # exists; the digest has to match a file, not a coordinate. + for artifact in kdrant-core-jvm kdrant-transport-rest kdrant-transport-grpc \ + kdrant-spring-boot-starter kdrant-spring-ai kdrant-langchain4j kdrant-micrometer; do + module="${artifact%-jvm}" + jar="$module/build/libs/$artifact-$VERSION.jar" [ -f "$jar" ] || { echo "::error::$jar not found"; exit 1; } - digest="sha256:$(sha256sum "$jar" | cut -d' ' -f1)" - echo "recording $module $VERSION $digest" + digest="sha256:$(shasum -a 256 "$jar" | cut -d' ' -f1)" + echo "recording $artifact $VERSION $digest" gh api -X POST "/orgs/${GITHUB_REPOSITORY_OWNER}/artifacts/metadata/storage-record" \ - -f name="$module" \ + -f name="$artifact" \ -f version="$VERSION" \ -f digest="$digest" \ -f registry_url="https://repo1.maven.org/maven2/" \ - -f artifact_url="https://repo1.maven.org/maven2/io/github/nacode-studios/$module/$VERSION/$module-$VERSION.jar" \ + -f artifact_url="https://repo1.maven.org/maven2/io/github/nacode-studios/$artifact/$VERSION/$artifact-$VERSION.jar" \ --silent done diff --git a/CHANGELOG.md b/CHANGELOG.md index 2adc489..5e54a2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,9 +58,34 @@ All notable changes to this project are documented in this file. The format is b - Both engines are held to one **shared client contract** (`kdrant-testkit`), which runs the same 30 behavioural tests against a real Qdrant over each protocol. The REST tests that came before it asserted HTTP bodies, which a gRPC engine cannot satisfy by construction. +- **`kdrant-core` is a Kotlin Multiplatform library** (M25). It builds for the JVM and for eight + Kotlin/Native targets: `iosArm64`, `iosSimulatorArm64`, `iosX64`, `macosArm64`, `macosX64`, + `linuxArm64`, `linuxX64` and `mingwX64`. Kotlin/JS is deliberately not among them: there is no JS + engine, so the target would ship models with nothing to send them over, and its test tooling is the + only npm dependency graph this repository would have. The models, DSLs, error hierarchy and client + logic were already free of the JVM — that is what the transport seam was for — so the migration moved + sources into `commonMain` and changed one declaration. The engines stay JVM-only, because Ktor CIO and + grpc-java are. +- A `commonTest` suite that runs on every target, covering the places a platform could actually + differ: the hand-written serializers, the uint64 point id, integer payload values above 2^53, and the + config's validation. ### Changed +- **`kdrant-core`'s artifact layout changed with the multiplatform move.** The `kdrant-core` coordinate + now carries Gradle module metadata and the JVM classes live in `kdrant-core-jvm`. A Gradle build + resolves the right variant from the same coordinate and needs no change; a Maven build names the + artifact directly and must move to `kdrant-core-jvm`, which the BOM now constrains as well. The JVM + public API is unchanged, byte for byte — `apiCheck` reports no diff. +- The default dispatcher is platform-dependent, and is the one declaration the migration had to split. + It stays `Dispatchers.IO` on the JVM. On Kotlin/Native it is `Dispatchers.Default`, because the + coroutines library still keeps its native IO dispatcher internal. Passing your own dispatcher works + as before, everywhere. +- Releases are built on macOS. Only a macOS host can compile the Apple targets, so a Linux runner would + publish a release quietly missing its iOS and macOS klibs. +- `kdrant-core`'s `-javadoc.jar` holds Dokka's HTML output rather than Javadoc HTML: the Dokka Javadoc + generator refuses a multiplatform project. Maven Central requires the jar to exist rather than to be + Javadoc, and HTML is what a Kotlin reader wants. - Eleven `QdrantTransport` operations have no gRPC equivalent, because the seam was shaped by Qdrant's REST API and Qdrant serves these over HTTP only: `telemetry`, `metrics`, `listIssues`, `clearIssues`, `recoverSnapshot`, snapshot download and upload, and the five shard-scope snapshot operations. On the diff --git a/README.md b/README.md index 8ae2cdd..0bac4f9 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,13 @@ dependencies { } ``` -`kdrant-transport-rest` brings in `kdrant-core` transitively; it is the only dependency you add. +`kdrant-transport-rest` brings in `kdrant-core` transitively; it is the only dependency you add. To use +the gRPC engine instead, depend on `kdrant-transport-grpc` and build the client with `KdrantGrpc(host)`; +nothing else in this README changes, because the API above the wire is the same API. + +`kdrant-core` is a Kotlin Multiplatform library and publishes one artifact per target. A Gradle build +resolves the right one from the `kdrant-core` coordinate and needs no change. A Maven build names the +artifact directly and wants `kdrant-core-jvm`. The engines and adapters are JVM-only and are unaffected. You also need a running Qdrant. For local development: @@ -282,6 +288,13 @@ Three modules keep protocol concerns out of the public API: | `kdrant-transport-rest` | The default REST engine (Ktor CIO) implementing `QdrantTransport`, plus the `Kdrant(...)` factory. | | `kdrant-transport-grpc` | The opt-in gRPC engine over Qdrant's own protobuf services, plus the `KdrantGrpc(...)` factory. Depend on it only if you want it. | +`kdrant-core` builds for the JVM and for eight Kotlin/Native targets: iOS, macOS, Linux and Windows. +That is what the transport seam bought — the models, DSLs and client logic never knew about a wire, so +they compile anywhere Kotlin does. The engines stay JVM-only, because Ktor CIO and grpc-java are, so a +multiplatform consumer today shares its models and its query building and supplies its own transport. +Kotlin/JS is left out for that reason rather than an accident of effort: with no JS engine the target +would ship a DSL with nothing to send. + The DSLs and client logic live in `kdrant-core` and are independent of the protocol; only an engine module knows about a wire. Both engines are held to the same behavioural test suite, so the choice is a footprint and throughput decision rather than a feature one — with the exception of the operations @@ -305,8 +318,10 @@ from `1.1.0` is a recompile rather than a jar swap — see [STABILITY.md](STABILITY.md#what-may-still-change-in-a-minor); the [CHANGELOG](CHANGELOG.md) has the version-by-version detail. -**Merged, unreleased.** The opt-in gRPC engine, `kdrant-transport-grpc`. It is in `main` and has not -shipped; see the [CHANGELOG](CHANGELOG.md#unreleased) for what it changes. +**Merged, unreleased.** The opt-in gRPC engine, `kdrant-transport-grpc`, and the move of `kdrant-core` +to Kotlin Multiplatform. Both are in `main` and neither has shipped; see the +[CHANGELOG](CHANGELOG.md#unreleased) for what they change and what the multiplatform move does to +`kdrant-core`'s artifact coordinates. The plan lives on the [Kdrant board](https://github.com/orgs/NaCode-Studios/projects/4) — one item per milestone, each with its exit criterion — and every tier is a [milestone](https://github.com/NaCode-Studios/Kdrant/milestones) in this repository. See diff --git a/STABILITY.md b/STABILITY.md index e8bfc8a..3a8b4fa 100644 --- a/STABILITY.md +++ b/STABILITY.md @@ -38,6 +38,12 @@ so the compiler tells you what a new release added; if you need a stub in a test third-party wire engine is a supported use of `QdrantTransport`, but it is a use that recompiles against each minor. +**A multiplatform artifact is named per target.** `kdrant-core` publishes Gradle module metadata under +its own coordinate and the JVM classes under `kdrant-core-jvm`. Gradle reads the metadata and resolves +the variant; Maven does not, and a Maven build naming `kdrant-core` gets no classes. That is a +coordinate change rather than an API change — the JVM public API is unchanged — but it is the kind that +belongs in a major, which is where it landed. + **Adding a field to a public `data class` changes its generated `copy` and `componentN`.** New response fields arrive as Qdrant returns more, and while the constructor keeps its defaults and source keeps compiling, code that called `copy()` against an older jar needs recompiling. Kdrant does not add fields diff --git a/build.gradle.kts b/build.gradle.kts index 1058fb2..a42f904 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.dokka) alias(libs.plugins.dokka.javadoc) apply false @@ -73,12 +74,27 @@ configure( } // Treat every Kotlin compiler warning — deprecations included — as a build error, so these - // modules stay warning-clean across dependency and toolchain upgrades. - tasks.withType().configureEach { + // modules stay warning-clean across dependency and toolchain upgrades. Every compilation task + // rather than only the JVM one: kdrant-core also compiles for JS and for nine native targets, and + // a warning that only appears there would otherwise never fail a build. + tasks.withType>().configureEach { compilerOptions { allWarningsAsErrors.set(true) } } + + // Detekt reads declared source directories rather than the Kotlin source sets, and its defaults + // name src/main and src/test, which a multiplatform module does not have. + extensions.configure { + source.setFrom( + files( + "src/main/kotlin", "src/test/kotlin", + "src/commonMain/kotlin", "src/commonTest/kotlin", + "src/jvmMain/kotlin", "src/jvmTest/kotlin", + "src/jsMain/kotlin", "src/nativeMain/kotlin", + ).filter { it.exists() }, + ) + } } // Aggregate the documented modules into one multi-module HTML API site (published to GitHub Pages). diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index acfa027..58a3ff0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,6 +47,7 @@ testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version testcontainers-qdrant = { module = "org.testcontainers:testcontainers-qdrant", version.ref = "testcontainers" } kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } spring-boot-autoconfigure = { module = "org.springframework.boot:spring-boot-autoconfigure", version.ref = "springBoot" } spring-boot-configuration-processor = { module = "org.springframework.boot:spring-boot-configuration-processor", version.ref = "springBoot" } @@ -75,6 +76,7 @@ mockk = { module = "io.mockk:mockk", version.ref = "mockk" } [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.18.1" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } diff --git a/kdrant-bom/build.gradle.kts b/kdrant-bom/build.gradle.kts index a9ea80c..fb7525a 100644 --- a/kdrant-bom/build.gradle.kts +++ b/kdrant-bom/build.gradle.kts @@ -8,6 +8,10 @@ plugins { dependencies { constraints { api("io.github.nacode-studios:kdrant-core:${project.version}") + // kdrant-core is multiplatform: a Gradle consumer resolves the right variant from the + // kdrant-core coordinate, but a Maven consumer names the JVM artifact directly and would + // otherwise get no constraint from this platform. + api("io.github.nacode-studios:kdrant-core-jvm:${project.version}") api("io.github.nacode-studios:kdrant-transport-rest:${project.version}") api("io.github.nacode-studios:kdrant-transport-grpc:${project.version}") api("io.github.nacode-studios:kdrant-spring-boot-starter:${project.version}") diff --git a/kdrant-core/build.gradle.kts b/kdrant-core/build.gradle.kts index ca5f809..f29f059 100644 --- a/kdrant-core/build.gradle.kts +++ b/kdrant-core/build.gradle.kts @@ -1,30 +1,67 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.dokka) - alias(libs.plugins.dokka.javadoc) + // No dokka-javadoc here: its generator refuses a multiplatform project outright. Maven Central + // requires a -javadoc.jar to exist rather than to be Javadoc, so the publishing plugin fills it + // with Dokka's HTML output, which is what a Kotlin reader wants anyway. alias(libs.plugins.maven.publish) } kotlin { - jvmToolchain(17) explicitApi() -} -dependencies { - api(libs.kotlinx.coroutines.core) - api(libs.kotlinx.serialization.json) + // The JVM stays the reference target: it is what every engine and adapter in this repository + // compiles against, and `kdrant-core-jvm` is the artifact those modules resolve. + jvm { + compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } + } + + // Everything the two dependencies support and someone might plausibly run a Qdrant client on. + // watchOS and tvOS are left out: they cost one line each and neither has a use case that would + // justify carrying the klibs, so they wait for someone to ask. + // + // Kotlin/JS is left out for a stronger reason. There is no JS engine — Ktor CIO and grpc-java are + // both JVM-only — so kdrant-core on JS would be models and a DSL with nothing to send them over. + // The target is not free: its Karma and webpack test tooling is the only npm dependency graph this + // repository has, and it arrived carrying a high-severity advisory in a package no published + // artifact contains. One line brings it back the day someone writes a JS engine. + iosArm64() + iosSimulatorArm64() + iosX64() + macosArm64() + macosX64() + linuxArm64() + linuxX64() + mingwX64() + + sourceSets { + commonMain.dependencies { + api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.serialization.json) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + implementation(libs.kotlinx.coroutines.test) + } + jvmTest.dependencies { + implementation(project.dependencies.platform(libs.junit.bom)) + implementation(libs.junit.jupiter) + runtimeOnly(libs.junit.platform.launcher) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.kotest.property) + } + } +} - testImplementation(platform(libs.junit.bom)) - testImplementation(libs.junit.jupiter) - testRuntimeOnly(libs.junit.platform.launcher) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.kotest.property) +java { + toolchain { languageVersion.set(JavaLanguageVersion.of(17)) } } -tasks.test { +tasks.named("jvmTest") { useJUnitPlatform() testLogging { events("passed", "skipped", "failed") diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/Catching.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/Catching.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/Catching.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/Catching.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/CollectionExtensions.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/CollectionExtensions.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/CollectionExtensions.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/CollectionExtensions.kt diff --git a/kdrant-core/src/commonMain/kotlin/dev/kdrant/DefaultDispatcher.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/DefaultDispatcher.kt new file mode 100644 index 0000000..4732c2e --- /dev/null +++ b/kdrant-core/src/commonMain/kotlin/dev/kdrant/DefaultDispatcher.kt @@ -0,0 +1,16 @@ +package dev.kdrant + +import kotlinx.coroutines.CoroutineDispatcher + +/** + * The dispatcher a client runs on when the caller does not choose one. + * + * This is the only platform-dependent declaration in the core, and the reason it has to be one is that + * only the JVM publishes an IO dispatcher. That is the right default where it exists: a Qdrant client + * blocks on a socket rather than on a CPU, so its work belongs on the elastic pool and not on the one + * sized to the number of cores. + * + * On Kotlin/Native it is `Dispatchers.Default`, because the IO dispatcher that exists there is still + * internal to the coroutines library. + */ +internal expect val ioDispatcher: CoroutineDispatcher diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/KdrantConfig.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/KdrantConfig.kt similarity index 95% rename from kdrant-core/src/main/kotlin/dev/kdrant/KdrantConfig.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/KdrantConfig.kt index 6d90346..d339c48 100644 --- a/kdrant-core/src/main/kotlin/dev/kdrant/KdrantConfig.kt +++ b/kdrant-core/src/commonMain/kotlin/dev/kdrant/KdrantConfig.kt @@ -1,7 +1,6 @@ package dev.kdrant import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -26,7 +25,8 @@ public annotation class KdrantDsl * @property retryBaseDelay base delay before the first retry; each subsequent retry backs off * exponentially, capped at [retryMaxDelay]. The server's `Retry-After` is honored when present. * @property retryMaxDelay upper bound on a single retry delay. - * @property dispatcher dispatcher the client runs on; injectable for tests. + * @property dispatcher dispatcher the client runs on; injectable for tests. The default is + * platform-dependent, see [ioDispatcher]. */ public class KdrantConfig( public val host: String, @@ -39,7 +39,7 @@ public class KdrantConfig( public val maxRetries: Int = 3, public val retryBaseDelay: Duration = 500.milliseconds, public val retryMaxDelay: Duration = 5.seconds, - public val dispatcher: CoroutineDispatcher = Dispatchers.IO, + public val dispatcher: CoroutineDispatcher = ioDispatcher, ) { init { require(port in 1..65535) { "port must be in 1..65535, was $port" } @@ -94,8 +94,8 @@ public class KdrantConfigBuilder internal constructor( /** Upper bound on a single retry delay. */ public var retryMaxDelay: Duration = 5.seconds - /** Dispatcher used for client work. Injectable for tests; defaults to [Dispatchers.IO]. */ - public var dispatcher: CoroutineDispatcher = Dispatchers.IO + /** Dispatcher used for client work. Injectable for tests; defaults to [ioDispatcher]. */ + public var dispatcher: CoroutineDispatcher = ioDispatcher internal fun build(): KdrantConfig = KdrantConfig( diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/KdrantException.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/KdrantException.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/KdrantException.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/KdrantException.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/PayloadAccess.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/PayloadAccess.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/PayloadAccess.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/PayloadAccess.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/QdrantClient.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/QdrantClient.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/QdrantClient.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/QdrantClient.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/dsl/BatchUpdateBuilder.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/BatchUpdateBuilder.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/dsl/BatchUpdateBuilder.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/BatchUpdateBuilder.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/dsl/CreateCollectionBuilder.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/CreateCollectionBuilder.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/dsl/CreateCollectionBuilder.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/CreateCollectionBuilder.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/dsl/FilterBuilder.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/FilterBuilder.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/dsl/FilterBuilder.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/FilterBuilder.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/dsl/PayloadDsl.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/PayloadDsl.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/dsl/PayloadDsl.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/PayloadDsl.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/dsl/ScrollBuilder.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/ScrollBuilder.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/dsl/ScrollBuilder.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/ScrollBuilder.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/dsl/SearchBuilder.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/SearchBuilder.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/dsl/SearchBuilder.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/SearchBuilder.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/dsl/SearchMatrixBuilder.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/SearchMatrixBuilder.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/dsl/SearchMatrixBuilder.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/SearchMatrixBuilder.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/dsl/UpdateAliasesBuilder.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/UpdateAliasesBuilder.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/dsl/UpdateAliasesBuilder.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/UpdateAliasesBuilder.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/dsl/UpsertBuilder.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/UpsertBuilder.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/dsl/UpsertBuilder.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/dsl/UpsertBuilder.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/internal/DefaultQdrantClient.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/internal/DefaultQdrantClient.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/internal/DefaultQdrantClient.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/internal/DefaultQdrantClient.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/internal/InternalKdrantApi.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/internal/InternalKdrantApi.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/internal/InternalKdrantApi.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/internal/InternalKdrantApi.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/internal/KdrantJson.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/internal/KdrantJson.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/internal/KdrantJson.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/internal/KdrantJson.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/Aliases.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Aliases.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/Aliases.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Aliases.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/Cluster.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Cluster.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/Cluster.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Cluster.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/CollectionDescription.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/CollectionDescription.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/CollectionDescription.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/CollectionDescription.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/CollectionInfo.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/CollectionInfo.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/CollectionInfo.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/CollectionInfo.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/Condition.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Condition.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/Condition.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Condition.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/CreateCollectionRequest.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/CreateCollectionRequest.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/CreateCollectionRequest.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/CreateCollectionRequest.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/DeleteSelector.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/DeleteSelector.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/DeleteSelector.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/DeleteSelector.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/Distance.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Distance.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/Distance.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Distance.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/Expression.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Expression.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/Expression.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Expression.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/Facet.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Facet.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/Facet.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Facet.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/FieldMatcher.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/FieldMatcher.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/FieldMatcher.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/FieldMatcher.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/Filter.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Filter.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/Filter.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Filter.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/GeoPoint.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/GeoPoint.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/GeoPoint.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/GeoPoint.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/Groups.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Groups.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/Groups.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Groups.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/HnswConfig.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/HnswConfig.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/HnswConfig.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/HnswConfig.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/OptimizersConfig.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/OptimizersConfig.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/OptimizersConfig.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/OptimizersConfig.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/Payload.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Payload.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/Payload.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Payload.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/PayloadSchemaType.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/PayloadSchemaType.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/PayloadSchemaType.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/PayloadSchemaType.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/PointId.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/PointId.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/PointId.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/PointId.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/PointStruct.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/PointStruct.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/PointStruct.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/PointStruct.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/PointVectors.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/PointVectors.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/PointVectors.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/PointVectors.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/PointsUpdateOperation.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/PointsUpdateOperation.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/PointsUpdateOperation.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/PointsUpdateOperation.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/QuantizationConfig.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/QuantizationConfig.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/QuantizationConfig.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/QuantizationConfig.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/QueryInterface.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/QueryInterface.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/QueryInterface.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/QueryInterface.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/Record.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Record.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/Record.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Record.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/ScoredPoint.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/ScoredPoint.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/ScoredPoint.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/ScoredPoint.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/ScrollRequest.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/ScrollRequest.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/ScrollRequest.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/ScrollRequest.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/SearchMatrix.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/SearchMatrix.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/SearchMatrix.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/SearchMatrix.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/SearchRequest.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/SearchRequest.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/SearchRequest.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/SearchRequest.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/Snapshots.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Snapshots.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/Snapshots.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/Snapshots.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/SparseVectorParams.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/SparseVectorParams.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/SparseVectorParams.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/SparseVectorParams.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/UpdateCollectionRequest.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/UpdateCollectionRequest.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/UpdateCollectionRequest.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/UpdateCollectionRequest.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/VectorData.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/VectorData.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/VectorData.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/VectorData.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/VectorDatatype.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/VectorDatatype.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/VectorDatatype.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/VectorDatatype.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/VectorParams.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/VectorParams.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/VectorParams.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/VectorParams.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/VectorsConfig.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/VectorsConfig.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/VectorsConfig.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/VectorsConfig.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/model/WithPayload.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/model/WithPayload.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/model/WithPayload.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/model/WithPayload.kt diff --git a/kdrant-core/src/main/kotlin/dev/kdrant/transport/QdrantTransport.kt b/kdrant-core/src/commonMain/kotlin/dev/kdrant/transport/QdrantTransport.kt similarity index 100% rename from kdrant-core/src/main/kotlin/dev/kdrant/transport/QdrantTransport.kt rename to kdrant-core/src/commonMain/kotlin/dev/kdrant/transport/QdrantTransport.kt diff --git a/kdrant-core/src/commonTest/kotlin/dev/kdrant/CommonCoreTest.kt b/kdrant-core/src/commonTest/kotlin/dev/kdrant/CommonCoreTest.kt new file mode 100644 index 0000000..16699fd --- /dev/null +++ b/kdrant-core/src/commonTest/kotlin/dev/kdrant/CommonCoreTest.kt @@ -0,0 +1,151 @@ +package dev.kdrant + +import dev.kdrant.dsl.filter +import dev.kdrant.dsl.payloadOf +import dev.kdrant.model.Condition +import dev.kdrant.model.Distance +import dev.kdrant.model.FieldMatcher +import dev.kdrant.model.Filter +import dev.kdrant.model.PointId +import dev.kdrant.model.VectorData +import dev.kdrant.model.VectorParams +import dev.kdrant.model.VectorsConfig +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +/** + * The core's behaviour, run on every target it now builds for. + * + * The bulk of the test suite stays on the JVM: it is written against JUnit 5 and kotest-property, both + * of which are JVM-only, and rewriting eighty tests to move them would be a large mechanical change + * with nothing to show for it — the same assertions, running in more places, testing the same + * platform-independent code. + * + * What is here instead is what would actually differ per platform if anything did: number handling on + * the way to and from JSON, which is where a JS `Number` and a JVM `Long` disagree; the serializers + * that are hand-written rather than generated; and the two places the core touches coroutines. A + * failure here is a platform difference, not a logic error, which is what makes the file worth its + * duplication. + */ +class CommonCoreTest { + + @Test + fun `a point id above Long MAX survives the JSON round trip on every platform`() { + // The reason this is not a JVM-only test: JS numbers are doubles, so an id near 2^64 is exactly + // where a platform would quietly lose precision if the serializer went through a number type + // rather than through the literal. + val id: PointId = PointId.num(ULong.MAX_VALUE) + + val json = kdrantJson.encodeToString(PointId.serializer(), id) + + assertEquals("18446744073709551615", json) + assertEquals(id, kdrantJson.decodeFromString(PointId.serializer(), json)) + } + + @Test + fun `a uuid id stays a quoted string so it is never read back as a number`() { + val id: PointId = PointId.uuid("550e8400-e29b-41d4-a716-446655440000") + + val json = kdrantJson.encodeToString(PointId.serializer(), id) + + assertTrue(json.startsWith("\""), json) + assertEquals(id, kdrantJson.decodeFromString(PointId.serializer(), json)) + } + + @Test + fun `a negative numeric id is refused because Qdrant ids are unsigned`() { + assertFailsWith { PointId.num(-1L) } + } + + @Test + fun `the filter DSL builds the clauses it was given`() { + val built = filter { + must { "lang" eq "it"; "year" gte 2020 } + mustNot { isEmpty("title") } + } + + assertEquals(2, built.must?.size) + assertEquals(Condition.Field("lang", FieldMatcher.Match(JsonPrimitive("it"))), built.must?.first()) + assertEquals(listOf(Condition.IsEmpty("title")), built.mustNot) + } + + @Test + fun `a filter serializes to the shape Qdrant reads`() { + val json = kdrantJson.encodeToString(Filter.serializer(), filter { must { "lang" eq "it" } }) + + assertEquals("""{"must":[{"key":"lang","match":{"value":"it"}}]}""", json) + } + + @Test + fun `a named vectors config serializes as a map and a single one as a bare object`() { + val single = kdrantJson.encodeToString( + VectorsConfig.serializer(), + VectorsConfig.Single(VectorParams(size = 4, distance = Distance.COSINE)), + ) + val named = kdrantJson.encodeToString( + VectorsConfig.serializer(), + VectorsConfig.Named(mapOf("text" to VectorParams(size = 4, distance = Distance.DOT))), + ) + + assertEquals("""{"size":4,"distance":"Cosine"}""", single) + assertEquals("""{"text":{"size":4,"distance":"Dot"}}""", named) + } + + @Test + fun `a dense vector and its FloatArray fast path serialize identically`() { + val boxed = kdrantJson.encodeToString(VectorData.serializer(), VectorData.Dense(listOf(0.5f, 0.25f))) + val array = kdrantJson.encodeToString(VectorData.serializer(), VectorData.DenseArray(floatArrayOf(0.5f, 0.25f))) + + assertEquals("[0.5,0.25]", boxed) + assertEquals(boxed, array) + } + + @Test + fun `a payload keeps an integer an integer rather than rounding it through a double`() { + val payload = payloadOf("year" to 2024, "big" to 9_007_199_254_740_993L) + + val json = kdrantJson.encodeToString(JsonObject.serializer(), payload) + + // 2^53 + 1 is the first integer a double cannot represent; it is here because JS would be the + // platform to lose it. + assertEquals("""{"year":2024,"big":9007199254740993}""", json) + } + + @Test + fun `a config with an api key over plaintext is refused`() { + val error = assertFailsWith { + kdrantConfig("localhost", 6333) { apiKey = "secret" } + } + + assertTrue(error.message!!.contains("useTls"), error.message) + } + + @Test + fun `a port outside the range is refused`() { + assertFailsWith { kdrantConfig("localhost", 0) } + assertFailsWith { kdrantConfig("localhost", 70_000) } + } + + @Test + fun `the config never prints the api key`() { + val rendered = kdrantConfig("localhost", 6333) { apiKey = "secret"; useTls = true }.toString() + + assertTrue(rendered.contains("apiKey=***"), rendered) + assertTrue(!rendered.contains("secret"), rendered) + } + + @Test + fun `catching captures a failure and re-throws a cancellation`() = runTest { + assertTrue(catching { error("boom") }.isFailure) + + assertFailsWith { + catching { throw CancellationException("cancelled") } + } + } +} diff --git a/kdrant-core/src/jvmMain/kotlin/dev/kdrant/DefaultDispatcher.jvm.kt b/kdrant-core/src/jvmMain/kotlin/dev/kdrant/DefaultDispatcher.jvm.kt new file mode 100644 index 0000000..e9794f3 --- /dev/null +++ b/kdrant-core/src/jvmMain/kotlin/dev/kdrant/DefaultDispatcher.jvm.kt @@ -0,0 +1,6 @@ +package dev.kdrant + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +internal actual val ioDispatcher: CoroutineDispatcher get() = Dispatchers.IO diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/CatchingTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/CatchingTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/CatchingTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/CatchingTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/JsonAssertions.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/JsonAssertions.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/JsonAssertions.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/JsonAssertions.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/KdrantConfigTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/KdrantConfigTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/KdrantConfigTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/KdrantConfigTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/PayloadAccessTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/PayloadAccessTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/PayloadAccessTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/PayloadAccessTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/dsl/CreateCollectionBuilderTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/dsl/CreateCollectionBuilderTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/dsl/CreateCollectionBuilderTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/dsl/CreateCollectionBuilderTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/dsl/FilterBuilderTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/dsl/FilterBuilderTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/dsl/FilterBuilderTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/dsl/FilterBuilderTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/dsl/FormulaMmrTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/dsl/FormulaMmrTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/dsl/FormulaMmrTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/dsl/FormulaMmrTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/dsl/M19BuildersTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/dsl/M19BuildersTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/dsl/M19BuildersTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/dsl/M19BuildersTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/dsl/ScrollBuilderTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/dsl/ScrollBuilderTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/dsl/ScrollBuilderTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/dsl/ScrollBuilderTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/dsl/SearchBuilderTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/dsl/SearchBuilderTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/dsl/SearchBuilderTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/dsl/SearchBuilderTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/dsl/UpsertBuilderTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/dsl/UpsertBuilderTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/dsl/UpsertBuilderTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/dsl/UpsertBuilderTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/model/CollectionInfoDeserializationTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/CollectionInfoDeserializationTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/model/CollectionInfoDeserializationTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/CollectionInfoDeserializationTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/model/CreateCollectionSerializationTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/CreateCollectionSerializationTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/model/CreateCollectionSerializationTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/CreateCollectionSerializationTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/model/FloatArrayVectorTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/FloatArrayVectorTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/model/FloatArrayVectorTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/FloatArrayVectorTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/model/ModelRoundTripPropertyTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/ModelRoundTripPropertyTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/model/ModelRoundTripPropertyTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/ModelRoundTripPropertyTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/model/PointIdSerializationTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/PointIdSerializationTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/model/PointIdSerializationTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/PointIdSerializationTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/model/ResultDeserializationTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/ResultDeserializationTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/model/ResultDeserializationTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/ResultDeserializationTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/model/VectorDataSerializationTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/VectorDataSerializationTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/model/VectorDataSerializationTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/VectorDataSerializationTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/model/VectorsConfigSerializationTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/VectorsConfigSerializationTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/model/VectorsConfigSerializationTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/VectorsConfigSerializationTest.kt diff --git a/kdrant-core/src/test/kotlin/dev/kdrant/model/WithPayloadSerializationTest.kt b/kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/WithPayloadSerializationTest.kt similarity index 100% rename from kdrant-core/src/test/kotlin/dev/kdrant/model/WithPayloadSerializationTest.kt rename to kdrant-core/src/jvmTest/kotlin/dev/kdrant/model/WithPayloadSerializationTest.kt diff --git a/kdrant-core/src/nativeMain/kotlin/dev/kdrant/DefaultDispatcher.native.kt b/kdrant-core/src/nativeMain/kotlin/dev/kdrant/DefaultDispatcher.native.kt new file mode 100644 index 0000000..1752630 --- /dev/null +++ b/kdrant-core/src/nativeMain/kotlin/dev/kdrant/DefaultDispatcher.native.kt @@ -0,0 +1,11 @@ +package dev.kdrant + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +/** + * Kotlin/Native declares an IO dispatcher but keeps it internal to the coroutines library, so this is + * `Default` until it is published. A native application doing enough blocking to notice should pass its + * own dispatcher rather than wait for that. + */ +internal actual val ioDispatcher: CoroutineDispatcher get() = Dispatchers.Default