diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b00c3bd --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c53e10a..a9e5699 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,31 +1,27 @@ -on: [ pull_request, push ] +on: + pull_request: + push: + branches: + - master name: Test and lint jobs: - check: - name: Test and lint (without RocksDB) - runs-on: macos-latest + strategy: + matrix: + os: [ubuntu, windows, macos] + runs-on: ${{ matrix.os }}-latest + name: "[${{ matrix.os }}] Test and lint" steps: - - uses: actions/checkout@v2 - - uses: gradle/actions/setup-gradle@v3 - - uses: maxim-lobanov/setup-xcode@v1 + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: actions/setup-java@v4 with: - xcode-version: '15.3' + distribution: corretto + java-version: 21 + - run: chmod +x gradlew + - uses: gradle/actions/setup-gradle@v4 - name: Run tests run: ./gradlew check - - rocksdb-check: - name: Test and lint (with RocksDB) - runs-on: macos-latest - env: - ENABLE_ROCKSDB_NATIVE: true - steps: - - uses: actions/checkout@v2 - - uses: gradle/actions/setup-gradle@v3 - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: '15.3' - - name: Run tests - run: ./gradlew check diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..6091fcd --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,70 @@ +name: Publish release + +on: + release: + types: [ published ] + +jobs: + create-staging-repository: + runs-on: ubuntu-latest + name: "Create Sonatype Staging Repository" + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: actions/setup-java@v4 + with: + distribution: adopt + java-version: 21 + - uses: gradle/actions/setup-gradle@v4 + - run: chmod +x gradlew + - run: ./gradlew initializeSonatypeStagingRepository + env: + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_REPOSITORY_DESCRIPTION: ${{ github.event_name }}-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.ref_name }} + + publish-artifacts: + needs: [ create-staging-repository ] + strategy: + matrix: + os: [ ubuntu, windows, macos ] + runs-on: ${{ matrix.os }}-latest + name: "Publish artifacts on ${{ matrix.os }}" + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: actions/setup-java@v4 + with: + distribution: adopt + java-version: 21 + - uses: gradle/actions/setup-gradle@v4 + - run: chmod +x gradlew + - run: ./gradlew findSonatypeStagingRepository -x initializeSonatypeStagingRepository publishToSonatype + env: + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + SIGNING_PRIVATE_KEY: ${{ secrets.SIGNING_PRIVATE_KEY }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_REPOSITORY_DESCRIPTION: ${{ github.event_name }}-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.ref_name }} + + close-staging-repository: + needs: [ publish-artifacts ] + runs-on: ubuntu-latest + name: "Close Sonatype Staging Repository" + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: actions/setup-java@v4 + with: + distribution: adopt + java-version: 21 + - uses: gradle/actions/setup-gradle@v4 + - run: chmod +x gradlew + - run: ./gradlew findSonatypeStagingRepository -x initializeSonatypeStagingRepository closeAndReleaseSonatypeStagingRepository + env: + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_REPOSITORY_DESCRIPTION: ${{ github.event_name }}-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.ref_name }} \ No newline at end of file diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml new file mode 100644 index 0000000..d357ea4 --- /dev/null +++ b/.github/workflows/publish-snapshot.yml @@ -0,0 +1,28 @@ +name: Publish snapshot + +on: + push: + branches: + - master + +jobs: + publish-artifacts: + strategy: + matrix: + os: [ ubuntu, windows, macos ] + runs-on: ${{ matrix.os }}-latest + name: "Publish artifacts on ${{ matrix.os }}" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: adopt + java-version: 21 + - uses: gradle/actions/setup-gradle@v4 + - run: chmod +x gradlew + - run: ./gradlew publishToSonatype + env: + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + SIGNING_PRIVATE_KEY: ${{ secrets.SIGNING_PRIVATE_KEY }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 48621b3..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,25 +0,0 @@ -on: - push: - branches: [ master ] - release: - types: [ published ] - -name: Publish to Space - -jobs: - build: - runs-on: macos-latest - steps: - - uses: actions/checkout@v2 - - uses: gradle/actions/setup-gradle@v3 - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: '15.3' - - name: Publish - run: ./gradlew publishAllPublicationsToSpaceRepository - env: - MAVEN_SPACE_USERNAME: ${{ secrets.MAVEN_SPACE_USERNAME }} - MAVEN_SPACE_PASSWORD: ${{ secrets.MAVEN_SPACE_PASSWORD }} - SECRET_KEY: ${{ secrets.SECRET_KEY }} - SECRET_KEY_PASSWORD: ${{ secrets.SECRET_KEY_PASSWORD }} - diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f9914da --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "kotlin-leveldb"] + path = kotlin-leveldb + url = https://github.com/lamba92/kotlin-leveldb.git diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..66ca6d2 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + versions + id("io.github.gradle-nexus.publish-plugin") +} + +nexusPublishing { + // repositoryDescription is used by the nexus publish plugin as identifier + // for the repository to publish to. + val repoDesc = + System.getenv("SONATYPE_REPOSITORY_DESCRIPTION") + ?: project.properties["central.sonatype.repositoryDescription"] as? String + repoDesc?.let { repositoryDescription = it } + + repositories { + sonatype { + username = System.getenv("SONATYPE_USERNAME") + ?: project.properties["central.sonatype.username"] as? String + password = System.getenv("SONATYPE_PASSWORD") + ?: project.properties["central.sonatype.password"] as? String + } + } +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 6fc2afd..22a1ce4 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,5 +1,6 @@ plugins { `kotlin-dsl` + alias(libs.plugins.ktlint) } dependencies { @@ -9,5 +10,7 @@ dependencies { api(libs.kotlin.power.assert.plugin) api(libs.ktlint.gradle) api(libs.dokka.gradle.plugin) + api(libs.android.gradle.plugin) + api(libs.nexus.publish.plugin) implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts index a94f1c4..2d285f7 100644 --- a/buildSrc/settings.gradle.kts +++ b/buildSrc/settings.gradle.kts @@ -1,10 +1,10 @@ @file:Suppress("UnstableApiUsage") rootProject.name = "buildSrc" -enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") dependencyResolutionManagement { repositories { + google() mavenCentral() gradlePluginPortal() } diff --git a/buildSrc/src/main/kotlin/convention.gradle.kts b/buildSrc/src/main/kotlin/convention.gradle.kts deleted file mode 100644 index 39b6ed6..0000000 --- a/buildSrc/src/main/kotlin/convention.gradle.kts +++ /dev/null @@ -1,139 +0,0 @@ -import org.jetbrains.dokka.gradle.DokkaTask -import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet -import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeHostTest - -val GITHUB_REF: String? = System.getenv("GITHUB_REF") - -group = "com.github.lamba92" -version = when { - GITHUB_REF?.startsWith("refs/tags/") == true -> GITHUB_REF.substringAfter("refs/tags/") - else -> "1.0.0-SNAPSHOT" -} - -plugins { - id("org.jlleitschuh.gradle.ktlint") - id("org.jetbrains.dokka") - `maven-publish` - signing -} - -val dokkaHtml = tasks.named("dokkaHtml") - -val javadocJar by tasks.registering(Jar::class) { - dependsOn(dokkaHtml) - archiveClassifier = "javadoc" - from(dokkaHtml) -} - -plugins.withId("org.jetbrains.kotlin.jvm") { - extensions.getByName("kotlin").apply { - sourceSets.silenceOptIns() - explicitApi() - jvmToolchain(17) - val sourcesJar by tasks.registering(Jar::class) { - archiveClassifier = "sources" - from(sourceSets["main"].kotlin) - } - - publishing { - publications { - register(project.name) { - from(components["kotlin"]) - artifact(sourcesJar) - artifact(javadocJar) - } - } - } - } -} - -plugins.withId("org.jetbrains.kotlin.multiplatform") { - extensions.getByName("kotlin").apply { - sourceSets.silenceOptIns() - jvmToolchain(17) - explicitApi() - publishing { - publications.withType { - artifact(javadocJar) - } - } - } -} - -fun NamedDomainObjectContainer.silenceOptIns() = all { - languageSettings { - optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") - optIn("kotlinx.cinterop.ExperimentalForeignApi") - optIn("kotlin.io.path.ExperimentalPathApi") - } -} - -val secretKey: String? = System.getenv("SECRET_KEY") - ?: rootProject.file("secret.txt") - .takeIf { it.exists() } - ?.readText() - -val password: String? = System.getenv("SECRET_KEY_PASSWORD") - ?: rootProject.file("local.properties") - .takeIf { it.exists() } - ?.readLines() - ?.map { it.split("=") } - ?.find { it.first() == "secret.password" } - ?.get(1) - - -if (secretKey != null && password != null) { - signing { - useInMemoryPgpKeys(secretKey, password) - publishing.publications.all { - sign(this) - } - } -} - -publishing { - - repositories { - maven(rootProject.layout.buildDirectory.dir("mavenRepo")) { - name = "test" - } - maven { - name = "Space" - setUrl("https://packages.jetbrains.team/maven/p/kpm/public") - credentials { - username = System.getenv("MAVEN_SPACE_USERNAME") - password = System.getenv("MAVEN_SPACE_PASSWORD") - } - } - } - - afterEvaluate { - publications.all { - if (this !is MavenPublication) return@all - artifactId = "${rootProject.name}-$artifactId" - } - } - -} - -tasks { - check { - dependsOn(ktlintCheck) - } - - // workaround https://github.com/gradle/gradle/issues/26091 - withType { - dependsOn(withType()) - } - - withType { - environment("DB_PATH", layout.buildDirectory.file("test.db").get().asFile.absolutePath) - useJUnitPlatform() - } - withType { - environment("DB_PATH", layout.buildDirectory.file("test.db").get().asFile.absolutePath) - } -} - diff --git a/buildSrc/src/main/kotlin/kotlin-jvm-convention.gradle.kts b/buildSrc/src/main/kotlin/kotlin-jvm-convention.gradle.kts new file mode 100644 index 0000000..4d8946f --- /dev/null +++ b/buildSrc/src/main/kotlin/kotlin-jvm-convention.gradle.kts @@ -0,0 +1,43 @@ +@file:OptIn(ExperimentalPathApi::class) + +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.absolutePathString + +plugins { + kotlin("jvm") + kotlin("plugin.serialization") + id("linting-convention") + id("versions") +} + +kotlin { + sourceSets.silenceOptIns() + jvmToolchain(8) + explicitApi() +} + +fun NamedDomainObjectContainer.silenceOptIns() = all { + languageSettings { + optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") + optIn("kotlin.io.path.ExperimentalPathApi") + } +} + +tasks { + val testDbPath = layout.buildDirectory.file("test-databases").get().asPath + withType { + environment("DB_PATH", testDbPath.absolutePathString()) + useJUnitPlatform() + systemProperty("jna.debug_load", "true") + systemProperty("jna.debug_load.jna", "true") + testLogging { + exceptionFormat = TestExceptionFormat.FULL + showStandardStreams = true + showCauses = true + showExceptions = true + showStackTraces = true + } + } +} diff --git a/buildSrc/src/main/kotlin/kotlin-multiplatform-convention.gradle.kts b/buildSrc/src/main/kotlin/kotlin-multiplatform-convention.gradle.kts new file mode 100644 index 0000000..857247b --- /dev/null +++ b/buildSrc/src/main/kotlin/kotlin-multiplatform-convention.gradle.kts @@ -0,0 +1,92 @@ +@file:OptIn(ExperimentalPathApi::class) + +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.internal.os.OperatingSystem +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeHostTest +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.absolutePathString +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteRecursively + +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") + id("linting-convention") + id("versions") +} + +val currentOs: OperatingSystem = OperatingSystem.current() + +kotlin { + sourceSets.silenceOptIns() + jvmToolchain(8) + explicitApi() +} + +fun NamedDomainObjectContainer.silenceOptIns() = all { + languageSettings { + optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") + optIn("kotlinx.cinterop.ExperimentalForeignApi") + optIn("kotlin.io.path.ExperimentalPathApi") + } +} + +tasks { + + val testDbPath = layout.buildDirectory.file("test-databases").get().asPath + withType { + val namedTestDbPath = testDbPath + .resolve(name) + .createDirectories() + doFirst { namedTestDbPath.deleteRecursively() } + environment("DB_PATH", namedTestDbPath.absolutePathString()) + useJUnitPlatform() + systemProperty("jna.debug_load", "true") + systemProperty("jna.debug_load.jna", "true") + testLogging { + exceptionFormat = TestExceptionFormat.FULL + showStandardStreams = true + showCauses = true + showExceptions = true + showStackTraces = true + } + } + withType { + val namedTestDbPath = testDbPath + .resolve(name) + .createDirectories() + doFirst { namedTestDbPath.deleteRecursively() } + environment("DB_PATH", namedTestDbPath.absolutePathString()) + testLogging { + exceptionFormat = TestExceptionFormat.FULL + showStandardStreams = true + showCauses = true + showExceptions = true + showStackTraces = true + } + } + + // in CI we only want to publish the artifacts for the current OS only + // but when developing we want to publish all the possible artifacts to test them + if (isCi) { + + val linuxNames = listOf("linux", "android", "jvm", "js", "kotlin", "metadata", "wasm") + val windowsNames = listOf("mingw", "windows") + val appleNames = listOf("macos", "ios", "watchos", "tvos") + + withType { + when { + name.containsAny(linuxNames) -> onlyIf { currentOs.isLinux } + name.containsAny(windowsNames) -> onlyIf { currentOs.isWindows } + name.containsAny(appleNames) -> onlyIf { currentOs.isMacOsX } + } + } + } +} + +val isCi + get() = System.getenv("CI") == "true" + +fun String.containsAny(strings: List, ignoreCase: Boolean = true): Boolean = + strings.any { contains(it, ignoreCase) } diff --git a/buildSrc/src/main/kotlin/kotlin-multiplatform-with-android-convention.gradle.kts b/buildSrc/src/main/kotlin/kotlin-multiplatform-with-android-convention.gradle.kts new file mode 100644 index 0000000..b8e0aa8 --- /dev/null +++ b/buildSrc/src/main/kotlin/kotlin-multiplatform-with-android-convention.gradle.kts @@ -0,0 +1,42 @@ +@file:OptIn(ExperimentalPathApi::class, ExperimentalKotlinGradlePluginApi::class) + +import com.android.build.gradle.tasks.factory.AndroidUnitTest +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget +import kotlin.io.path.ExperimentalPathApi + +plugins { + id("com.android.library") + id("kotlin-multiplatform-convention") +} + +android { + namespace = "com.github.lamba92.leveldb" + compileSdk = 35 + defaultConfig { + minSdk = 21 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } +} + +kotlin { + androidTarget() + targets.withType { + publishLibraryVariants("release") + + // KT-46452 Allow to run common tests as Android Instrumentation tests + // https://youtrack.jetbrains.com/issue/KT-46452 + instrumentedTestVariant { + sourceSetTree = KotlinSourceSetTree.test + } + } +} + +tasks { + + // This project will test only instrumentation tests + withType { + onlyIf { false } + } +} diff --git a/buildSrc/src/main/kotlin/linting-convention.gradle.kts b/buildSrc/src/main/kotlin/linting-convention.gradle.kts new file mode 100644 index 0000000..19d1fdb --- /dev/null +++ b/buildSrc/src/main/kotlin/linting-convention.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("org.jlleitschuh.gradle.ktlint") +} + +tasks { + all { + if (name == "check") dependsOn(ktlintCheck) + } +} diff --git a/buildSrc/src/main/kotlin/publishing-convention.gradle.kts b/buildSrc/src/main/kotlin/publishing-convention.gradle.kts new file mode 100644 index 0000000..ce91874 --- /dev/null +++ b/buildSrc/src/main/kotlin/publishing-convention.gradle.kts @@ -0,0 +1,79 @@ +import kotlin.io.path.Path +import kotlin.io.path.readText + +plugins { + id("org.jetbrains.dokka") + id("org.jlleitschuh.gradle.ktlint") + `maven-publish` + signing +} + +val javadocJar by tasks.registering(Jar::class) { + dependsOn(tasks.dokkaGeneratePublicationHtml) + archiveClassifier = "javadoc" + from(tasks.dokkaGeneratePublicationHtml) + destinationDirectory = layout.buildDirectory.dir("artifacts") +} + +publishing { + repositories { + maven(rootProject.layout.buildDirectory.dir("mavenRepo")) { + name = "test" + } + } + publications.withType { + + // the publishing plugin is old AF and does not support lazy + // properties, so we need to set the artifactId after the + // publication is created + afterEvaluate { artifactId = "kotlinx-document-store-$artifactId" } + + artifact(javadocJar) + pom { + name = "kotlin-document-store" + description = "Kotlin Multiplatform NoSQL document storage" + url = "https://github.com/lamba92/kotlin-document-store" + licenses { + license { + name = "Apache-2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "lamba92" + name = "Lamberto Basti" + email = "basti.lamberto@gmail.com" + } + } + scm { + connection = "https://github.com/lamba92/kotlinx-document-store.git" + developerConnection = "https://github.com/lamba92/kotlinx-document-store.git" + url = "https://github.com/lamba92/kotlinx-document-store.git" + } + } + } +} + +signing { + val privateKey = + System.getenv("SIGNING_PRIVATE_KEY") + ?: project.properties["central.signing.privateKeyPath"] + ?.let { it as? String } + ?.let { Path(it).readText() } + ?: return@signing + val password = + System.getenv("SIGNING_PASSWORD") + ?: project.properties["central.signing.privateKeyPassword"] as? String + ?: return@signing + useInMemoryPgpKeys(privateKey, password) + sign(publishing.publications) +} + +tasks { + + // workaround https://github.com/gradle/gradle/issues/26091 + withType { + dependsOn(withType()) + } +} diff --git a/buildSrc/src/main/kotlin/versions.gradle.kts b/buildSrc/src/main/kotlin/versions.gradle.kts new file mode 100644 index 0000000..972e337 --- /dev/null +++ b/buildSrc/src/main/kotlin/versions.gradle.kts @@ -0,0 +1,14 @@ +group = "com.github.lamba92" + +val githubRef = + System.getenv("GITHUB_EVENT_NAME") + ?.takeIf { it == "release" } + ?.let { System.getenv("GITHUB_REF") } + ?.removePrefix("refs/tags/") + ?.removePrefix("v") + +version = + when { + githubRef != null -> githubRef + else -> "1.0-SNAPSHOT" + } \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts index ab93a96..b37b884 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,21 +1,42 @@ plugins { - convention - kotlin("multiplatform") - kotlin("plugin.serialization") + `publishing-convention` + `kotlin-multiplatform-with-android-convention` } kotlin { - jvm() js { browser() } + mingwX64() + + androidTarget() + + linuxX64() + linuxArm64() + macosArm64() macosX64() + iosArm64() iosX64() iosSimulatorArm64() + watchosArm64() + watchosX64() + watchosSimulatorArm64() + + tvosArm64() + tvosX64() + tvosSimulatorArm64() + + androidNativeX64() + androidNativeX86() + androidNativeArm64() + androidNativeArm32() + + applyDefaultHierarchyTemplate() + sourceSets { commonMain { diff --git a/core/src/commonMain/kotlin/kotlinx/document/database/DataStore.kt b/core/src/commonMain/kotlin/kotlinx/document/database/DataStore.kt deleted file mode 100644 index eaaf992..0000000 --- a/core/src/commonMain/kotlin/kotlinx/document/database/DataStore.kt +++ /dev/null @@ -1,25 +0,0 @@ -package kotlinx.document.database - -import kotlinx.serialization.Serializable -import kotlin.time.Duration - -public interface DataStore { - public val commitStrategy: CommitStrategy - - @Serializable - public sealed interface CommitStrategy { - @Serializable - public data class Periodic(val interval: Duration) : CommitStrategy - - @Serializable - public data object OnChange : CommitStrategy - } - - public suspend fun getMap(name: String): PersistentMap - - public suspend fun deleteMap(name: String) - - public suspend fun close() - - public suspend fun commit() -} diff --git a/core/src/commonMain/kotlin/kotlinx/document/store/DataStore.kt b/core/src/commonMain/kotlin/kotlinx/document/store/DataStore.kt new file mode 100644 index 0000000..61e73a3 --- /dev/null +++ b/core/src/commonMain/kotlin/kotlinx/document/store/DataStore.kt @@ -0,0 +1,51 @@ +package kotlinx.document.store + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +public interface DataStore : SuspendCloseable { + public suspend fun getMap(name: String): PersistentMap + + public suspend fun deleteMap(name: String) +} + +public abstract class AbstractDataStore : DataStore { + public class MutexLockedScope(private val mutexMap: MutableMap) { + public fun getMutex(name: String): Mutex = mutexMap.getOrPut(name) { Mutex() } + + public suspend fun lockAndRemoveMutex( + mutexName: String, + block: suspend () -> T, + ): T { + val mutex = getMutex(mutexName) + return mutex.withLock { + try { + block() + } finally { + mutexMap.remove(mutexName) + } + } + } + } + + private val mutexMap: MutableMap = mutableMapOf() + private val mutexMapLock: Mutex = Mutex() + + protected suspend fun withStoreLock(block: suspend MutexLockedScope.() -> T): T = + mutexMapLock.withLock { block(MutexLockedScope(mutexMap)) } + + override suspend fun close() { + } +} + +public fun interface SuspendCloseable { + public suspend fun close() +} + +public suspend fun R.use(block: suspend (R) -> T): T { + return try { + block(this) + } finally { + close() + } +} diff --git a/core/src/commonMain/kotlin/kotlinx/document/database/JsonCollection.kt b/core/src/commonMain/kotlin/kotlinx/document/store/JsonCollection.kt similarity index 95% rename from core/src/commonMain/kotlin/kotlinx/document/database/JsonCollection.kt rename to core/src/commonMain/kotlin/kotlinx/document/store/JsonCollection.kt index 23f3914..1865464 100644 --- a/core/src/commonMain/kotlin/kotlinx/document/database/JsonCollection.kt +++ b/core/src/commonMain/kotlin/kotlinx/document/store/JsonCollection.kt @@ -1,4 +1,4 @@ -package kotlinx.document.database +package kotlinx.document.store import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow @@ -8,10 +8,10 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.document.database.maps.Collection -import kotlinx.document.database.maps.IdGenerator -import kotlinx.document.database.maps.IndexOfIndexes -import kotlinx.document.database.maps.asIndex +import kotlinx.document.store.maps.Collection +import kotlinx.document.store.maps.IdGenerator +import kotlinx.document.store.maps.IndexOfIndexes +import kotlinx.document.store.maps.asIndex import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement @@ -42,9 +42,9 @@ public class JsonCollection( private suspend fun hasIndex(field: String) = indexMap.get(name)?.contains(field) ?: false - public fun iterateAll(fromIndex: Long = 0L): Flow = + public fun iterateAll(): Flow = collection - .entries(fromIndex) + .entries() .map { json.parseToJsonElement(it.value).jsonObject } override suspend fun createIndex(selector: String): Unit = @@ -52,8 +52,6 @@ public class JsonCollection( if (hasIndex(selector)) return val index = getIndexMap(selector) - indexMap.update(name, listOf(selector)) { it + selector } - val query = selector.split(".") iterateAll() @@ -73,6 +71,7 @@ public class JsonCollection( .collect { (fieldValue, ids) -> index.update(fieldValue, ids) { it + ids } } + indexMap.update(name, listOf(selector)) { it + selector } } public suspend fun findById(id: Long): JsonObject? { @@ -295,4 +294,4 @@ public suspend inline fun KotlinxDatabaseCollection.removeWhere( private fun Iterable>.toMap() = buildMap { this@toMap.forEach { put(it.key, it.value) } } private val JsonObject.id: Long? - get() = get(KotlinxDocumentDatabase.ID_PROPERTY_NAME)?.jsonPrimitive?.contentOrNull?.toLong() + get() = get(KotlinxDocumentStore.ID_PROPERTY_NAME)?.jsonPrimitive?.contentOrNull?.toLong() diff --git a/core/src/commonMain/kotlin/kotlinx/document/database/JsonObjectSelectionResult.kt b/core/src/commonMain/kotlin/kotlinx/document/store/JsonObjectSelectionResult.kt similarity index 96% rename from core/src/commonMain/kotlin/kotlinx/document/database/JsonObjectSelectionResult.kt rename to core/src/commonMain/kotlin/kotlinx/document/store/JsonObjectSelectionResult.kt index 5c866e0..d9cb720 100644 --- a/core/src/commonMain/kotlin/kotlinx/document/database/JsonObjectSelectionResult.kt +++ b/core/src/commonMain/kotlin/kotlinx/document/store/JsonObjectSelectionResult.kt @@ -1,4 +1,4 @@ -package kotlinx.document.database +package kotlinx.document.store import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement diff --git a/core/src/commonMain/kotlin/kotlinx/document/database/KotlinxDatabaseCollection.kt b/core/src/commonMain/kotlin/kotlinx/document/store/KotlinxDatabaseCollection.kt similarity index 95% rename from core/src/commonMain/kotlin/kotlinx/document/database/KotlinxDatabaseCollection.kt rename to core/src/commonMain/kotlin/kotlinx/document/store/KotlinxDatabaseCollection.kt index 59a7e3a..460b95c 100644 --- a/core/src/commonMain/kotlin/kotlinx/document/database/KotlinxDatabaseCollection.kt +++ b/core/src/commonMain/kotlin/kotlinx/document/store/KotlinxDatabaseCollection.kt @@ -1,4 +1,4 @@ -package kotlinx.document.database +package kotlinx.document.store import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement diff --git a/core/src/commonMain/kotlin/kotlinx/document/database/KotlinxDocumentDatabaseBuilder.kt b/core/src/commonMain/kotlin/kotlinx/document/store/KotlinxDocumentDatabaseBuilder.kt similarity index 66% rename from core/src/commonMain/kotlin/kotlinx/document/database/KotlinxDocumentDatabaseBuilder.kt rename to core/src/commonMain/kotlin/kotlinx/document/store/KotlinxDocumentDatabaseBuilder.kt index 0464875..93ebcea 100644 --- a/core/src/commonMain/kotlin/kotlinx/document/database/KotlinxDocumentDatabaseBuilder.kt +++ b/core/src/commonMain/kotlin/kotlinx/document/store/KotlinxDocumentDatabaseBuilder.kt @@ -1,6 +1,6 @@ @file:Suppress("RedundantSuppression") -package kotlinx.document.database +package kotlinx.document.store import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule @@ -9,8 +9,8 @@ public class KotlinxDocumentDatabaseBuilder { public var serializersModule: SerializersModule? = null public var store: DataStore? = null - public fun build(): KotlinxDocumentDatabase = - KotlinxDocumentDatabase( + public fun build(): KotlinxDocumentStore = + KotlinxDocumentStore( store = store ?: error("Store must be provided"), json = Json { @@ -22,8 +22,8 @@ public class KotlinxDocumentDatabaseBuilder { } @Suppress("FunctionName") -public inline fun KotlinxDocumentDatabase(block: KotlinxDocumentDatabaseBuilder.() -> Unit): KotlinxDocumentDatabase = +public inline fun KotlinxDocumentStore(block: KotlinxDocumentDatabaseBuilder.() -> Unit): KotlinxDocumentStore = KotlinxDocumentDatabaseBuilder().apply(block).build() @Suppress("FunctionName") -public fun KotlinxDocumentDatabase(store: DataStore): KotlinxDocumentDatabase = KotlinxDocumentDatabase { this.store = store } +public fun KotlinxDocumentStore(store: DataStore): KotlinxDocumentStore = KotlinxDocumentStore { this.store = store } diff --git a/core/src/commonMain/kotlin/kotlinx/document/database/KotlinxDocumentDatabase.kt b/core/src/commonMain/kotlin/kotlinx/document/store/KotlinxDocumentStore.kt similarity index 84% rename from core/src/commonMain/kotlin/kotlinx/document/database/KotlinxDocumentDatabase.kt rename to core/src/commonMain/kotlin/kotlinx/document/store/KotlinxDocumentStore.kt index 0d8328d..a1c09d8 100644 --- a/core/src/commonMain/kotlin/kotlinx/document/database/KotlinxDocumentDatabase.kt +++ b/core/src/commonMain/kotlin/kotlinx/document/store/KotlinxDocumentStore.kt @@ -1,14 +1,14 @@ -package kotlinx.document.database +package kotlinx.document.store import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.document.database.KotlinxDocumentDatabase.Companion.ID_PROPERTY_NAME -import kotlinx.document.database.maps.asCollectionMap -import kotlinx.document.database.maps.asIdGenerator -import kotlinx.document.database.maps.asIndexOfIndexes +import kotlinx.document.store.KotlinxDocumentStore.Companion.ID_PROPERTY_NAME +import kotlinx.document.store.maps.asCollectionMap +import kotlinx.document.store.maps.asIdGenerator +import kotlinx.document.store.maps.asIndexOfIndexes import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement @@ -17,7 +17,7 @@ import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.serializer import kotlin.jvm.JvmName -public class KotlinxDocumentDatabase internal constructor( +public class KotlinxDocumentStore internal constructor( private val store: DataStore, private val json: Json, ) { @@ -32,7 +32,7 @@ public class KotlinxDocumentDatabase internal constructor( private val mutexMap = mutableMapOf() public suspend fun getJsonCollection(name: String): JsonCollection { - store.getMap(COLLECTIONS).put(name, "") + store.getMap(COLLECTIONS).put(name, "1") return JsonCollection( name = name, json = json, @@ -82,7 +82,7 @@ internal suspend fun Flow>.toMap() = } } -public suspend inline fun KotlinxDocumentDatabase.getObjectCollection(name: String): ObjectCollection = +public suspend inline fun KotlinxDocumentStore.getObjectCollection(name: String): ObjectCollection = getJsonCollection(name).toObjectCollection() public inline fun JsonCollection.toObjectCollection(): ObjectCollection = diff --git a/core/src/commonMain/kotlin/kotlinx/document/database/ObjectCollection.kt b/core/src/commonMain/kotlin/kotlinx/document/store/ObjectCollection.kt similarity index 96% rename from core/src/commonMain/kotlin/kotlinx/document/database/ObjectCollection.kt rename to core/src/commonMain/kotlin/kotlinx/document/store/ObjectCollection.kt index ea2ef45..e12f5fa 100644 --- a/core/src/commonMain/kotlin/kotlinx/document/database/ObjectCollection.kt +++ b/core/src/commonMain/kotlin/kotlinx/document/store/ObjectCollection.kt @@ -1,4 +1,4 @@ -package kotlinx.document.database +package kotlinx.document.store import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -46,8 +46,8 @@ public class ObjectCollection( jsonCollection.findById(id) ?.let { json.decodeFromJsonElement(serializer, it) } - public fun iterateAll(fromIndex: Long = 0L): Flow = - jsonCollection.iterateAll(fromIndex) + public fun iterateAll(): Flow = + jsonCollection.iterateAll() .map { json.decodeFromJsonElement(serializer, it) } public suspend fun updateById( diff --git a/core/src/commonMain/kotlin/kotlinx/document/database/PersistentMap.kt b/core/src/commonMain/kotlin/kotlinx/document/store/PersistentMap.kt similarity index 87% rename from core/src/commonMain/kotlin/kotlinx/document/database/PersistentMap.kt rename to core/src/commonMain/kotlin/kotlinx/document/store/PersistentMap.kt index 35fd82e..a597940 100644 --- a/core/src/commonMain/kotlin/kotlinx/document/database/PersistentMap.kt +++ b/core/src/commonMain/kotlin/kotlinx/document/store/PersistentMap.kt @@ -1,4 +1,4 @@ -package kotlinx.document.database +package kotlinx.document.store import kotlinx.coroutines.flow.Flow @@ -31,7 +31,7 @@ public interface PersistentMap : AutoCloseable { defaultValue: () -> V, ): V - public fun entries(fromIndex: Long = 0L): Flow> + public fun entries(): Flow> override fun close() { } diff --git a/core/src/commonMain/kotlin/kotlinx/document/database/SerializableEntry.kt b/core/src/commonMain/kotlin/kotlinx/document/store/SerializableEntry.kt similarity index 83% rename from core/src/commonMain/kotlin/kotlinx/document/database/SerializableEntry.kt rename to core/src/commonMain/kotlin/kotlinx/document/store/SerializableEntry.kt index 9fc349a..75e095a 100644 --- a/core/src/commonMain/kotlin/kotlinx/document/database/SerializableEntry.kt +++ b/core/src/commonMain/kotlin/kotlinx/document/store/SerializableEntry.kt @@ -1,4 +1,4 @@ -package kotlinx.document.database +package kotlinx.document.store import kotlinx.serialization.Serializable diff --git a/core/src/commonMain/kotlin/kotlinx/document/database/Utils.kt b/core/src/commonMain/kotlin/kotlinx/document/store/Utils.kt similarity index 76% rename from core/src/commonMain/kotlin/kotlinx/document/database/Utils.kt rename to core/src/commonMain/kotlin/kotlinx/document/store/Utils.kt index a833b0d..1aaf321 100644 --- a/core/src/commonMain/kotlin/kotlinx/document/database/Utils.kt +++ b/core/src/commonMain/kotlin/kotlinx/document/store/Utils.kt @@ -1,4 +1,4 @@ -package kotlinx.document.database +package kotlinx.document.store import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -13,6 +13,16 @@ public fun Flow.drop(count: Long): Flow { } } +public fun Sequence.drop(count: Long): Sequence { + require(count >= 0) { "Drop count should be non-negative, but had $count" } + return sequence { + var skipped = 0L + forEach { value -> + if (skipped >= count) yield(value) else ++skipped + } + } +} + // available in 1.9.0 but not in the version bundled inside intellijIdea public fun Flow.chunked(size: Int): Flow> { require(size >= 1) { "Expected positive chunk size, but got $size" } diff --git a/core/src/commonMain/kotlin/kotlinx/document/database/maps/Collection.kt b/core/src/commonMain/kotlin/kotlinx/document/store/maps/Collection.kt similarity index 83% rename from core/src/commonMain/kotlin/kotlinx/document/database/maps/Collection.kt rename to core/src/commonMain/kotlin/kotlinx/document/store/maps/Collection.kt index 23a6a83..6f2a95b 100644 --- a/core/src/commonMain/kotlin/kotlinx/document/database/maps/Collection.kt +++ b/core/src/commonMain/kotlin/kotlinx/document/store/maps/Collection.kt @@ -1,10 +1,10 @@ -package kotlinx.document.database.maps +package kotlinx.document.store.maps import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlinx.document.database.PersistentMap -import kotlinx.document.database.SerializableEntry -import kotlinx.document.database.UpdateResult +import kotlinx.document.store.PersistentMap +import kotlinx.document.store.SerializableEntry +import kotlinx.document.store.UpdateResult public fun PersistentMap.asCollectionMap(): Collection = Collection(this) @@ -50,7 +50,7 @@ public class Collection(private val delegate: PersistentMap) : P defaultValue = { defaultValue() }, ) - override fun entries(fromIndex: Long): Flow> = - delegate.entries(fromIndex) + override fun entries(): Flow> = + delegate.entries() .map { SerializableEntry(it.key.toLong(), it.value) } } diff --git a/core/src/commonMain/kotlin/kotlinx/document/database/maps/IdgeneratorMap.kt b/core/src/commonMain/kotlin/kotlinx/document/store/maps/IdgeneratorMap.kt similarity index 84% rename from core/src/commonMain/kotlin/kotlinx/document/database/maps/IdgeneratorMap.kt rename to core/src/commonMain/kotlin/kotlinx/document/store/maps/IdgeneratorMap.kt index 772414f..f77b127 100644 --- a/core/src/commonMain/kotlin/kotlinx/document/database/maps/IdgeneratorMap.kt +++ b/core/src/commonMain/kotlin/kotlinx/document/store/maps/IdgeneratorMap.kt @@ -1,10 +1,10 @@ -package kotlinx.document.database.maps +package kotlinx.document.store.maps import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlinx.document.database.PersistentMap -import kotlinx.document.database.SerializableEntry -import kotlinx.document.database.UpdateResult +import kotlinx.document.store.PersistentMap +import kotlinx.document.store.SerializableEntry +import kotlinx.document.store.UpdateResult public fun PersistentMap.asIdGenerator(): IdGenerator = IdGenerator(this) @@ -50,7 +50,7 @@ public class IdGenerator(private val delegate: PersistentMap) : defaultValue = { defaultValue().toString() }, ).toLong() - override fun entries(fromIndex: Long): Flow> = - delegate.entries(fromIndex) + override fun entries(): Flow> = + delegate.entries() .map { SerializableEntry(it.key, it.value.toLong()) } } diff --git a/core/src/commonMain/kotlin/kotlinx/document/database/maps/Index.kt b/core/src/commonMain/kotlin/kotlinx/document/store/maps/Index.kt similarity index 88% rename from core/src/commonMain/kotlin/kotlinx/document/database/maps/Index.kt rename to core/src/commonMain/kotlin/kotlinx/document/store/maps/Index.kt index 96ad49b..2e71be7 100644 --- a/core/src/commonMain/kotlin/kotlinx/document/database/maps/Index.kt +++ b/core/src/commonMain/kotlin/kotlinx/document/store/maps/Index.kt @@ -1,10 +1,10 @@ -package kotlinx.document.database.maps +package kotlinx.document.store.maps import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlinx.document.database.PersistentMap -import kotlinx.document.database.SerializableEntry -import kotlinx.document.database.UpdateResult +import kotlinx.document.store.PersistentMap +import kotlinx.document.store.SerializableEntry +import kotlinx.document.store.UpdateResult import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement @@ -52,8 +52,8 @@ public class Index( override suspend fun isEmpty(): Boolean = delegate.isEmpty() - override fun entries(fromIndex: Long): Flow>> = - delegate.entries(fromIndex) + override fun entries(): Flow>> = + delegate.entries() .map { SerializableEntry(json.decodeFromString(it.key), it.value.split()) } override fun close() { diff --git a/core/src/commonMain/kotlin/kotlinx/document/database/maps/IndexOfIndexes.kt b/core/src/commonMain/kotlin/kotlinx/document/store/maps/IndexOfIndexes.kt similarity index 85% rename from core/src/commonMain/kotlin/kotlinx/document/database/maps/IndexOfIndexes.kt rename to core/src/commonMain/kotlin/kotlinx/document/store/maps/IndexOfIndexes.kt index b265b15..27793cb 100644 --- a/core/src/commonMain/kotlin/kotlinx/document/database/maps/IndexOfIndexes.kt +++ b/core/src/commonMain/kotlin/kotlinx/document/store/maps/IndexOfIndexes.kt @@ -1,10 +1,10 @@ -package kotlinx.document.database.maps +package kotlinx.document.store.maps import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlinx.document.database.PersistentMap -import kotlinx.document.database.SerializableEntry -import kotlinx.document.database.UpdateResult +import kotlinx.document.store.PersistentMap +import kotlinx.document.store.SerializableEntry +import kotlinx.document.store.UpdateResult import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -56,7 +56,7 @@ public class IndexOfIndexes(private val delegate: PersistentMap) defaultValue = { defaultValue().join() }, ).split() - override fun entries(fromIndex: Long): Flow>> = - delegate.entries(fromIndex) + override fun entries(): Flow>> = + delegate.entries() .map { SerializableEntry(it.key, it.value.split()) } } diff --git a/gradle.properties b/gradle.properties index ab5b028..998ee1c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,8 @@ org.gradle.jvmargs=-Xmx4g kotlin.apple.xcodeCompatibility.nowarn=true org.gradle.parallel=true -kotlin.native.ignoreDisabledTargets=true \ No newline at end of file +kotlin.native.ignoreDisabledTargets=true +kotlin.mpp.enableCInteropCommonization=true +org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled +org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true +android.useAndroidX=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 96ab74f..1492fe2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,23 +1,31 @@ [versions] +androidx-test-core = "1.6.1" +android-test-junit = "1.2.1" +nexus-publish-plugin = "2.0.0" +android-gradle-plugin = "8.7.3" +dokka = "2.0.0-Beta" h2 = "2.2.224" junit = "5.10.3" -kotlin = "2.0.0" -kotlin-browser = "1.0.0-pre.764" -kotlinx-coroutines = "1.8.0" -kotlinx-datetime = "0.6.0" -kotlinx-io = "0.5.0" -kotlinx-serialization-json = "1.7.1" -ktlint-gradle = "12.1.1" -rocksdb = "8.0.0" -ktor = "2.3.12" +kotlin = "2.1.0" +kotlinx-coroutines = "1.9.0" +kotlinx-datetime = "0.6.1" +kotlinx-io = "0.6.0" +kotlinx-serialization-json = "1.7.3" +ktlint-gradle = "12.1.2" +ktor = "3.0.1" logback = "1.5.6" +kotlin-leveldb = "1.0.0-RC.4" +androidx-test-runner = "1.6.2" [libraries] -dokka-gradle-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version = "1.9.20" } +androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" } +android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test-junit" } +nexus-publish-plugin = { module = "io.github.gradle-nexus:publish-plugin", version.ref = "nexus-publish-plugin" } +android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "android-gradle-plugin" } +dokka-gradle-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } h2 = { module = "com.h2database:h2", version.ref = "h2" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } -kotlin-browser = { module = "org.jetbrains.kotlin-wrappers:kotlin-browser", version.ref = "kotlin-browser" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-power-assert-plugin = { module = "org.jetbrains.kotlin:kotlin-power-assert", version.ref = "kotlin" } kotlin-serialization-plugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } @@ -28,15 +36,14 @@ kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.re kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization-json" } ktlint-gradle = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlint-gradle" } -rocksdb-multiplatform = { module = "io.maryk.rocksdb:rocksdb-multiplatform", version.ref = "rocksdb" } ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" } ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } +kotlin-leveldb = { module = "com.github.lamba92:kotlin-leveldb", version.ref = "kotlin-leveldb" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } [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" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-gradle" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e583..a4b76b9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5026cd7..e2847c8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Wed Jun 26 18:49:51 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c787..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +82,12 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +134,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +201,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -205,6 +217,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd3..9d21a21 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,10 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +27,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/kotlin-leveldb b/kotlin-leveldb new file mode 160000 index 0000000..5dff6aa --- /dev/null +++ b/kotlin-leveldb @@ -0,0 +1 @@ +Subproject commit 5dff6aa7c92f2b9084ae48cb61583359428e4bd4 diff --git a/nuke-gradle.bat b/nuke-gradle.bat new file mode 100644 index 0000000..1aae656 --- /dev/null +++ b/nuke-gradle.bat @@ -0,0 +1,11 @@ +@echo off +for /r %%i in (.) do ( + if /i "%%~nxi"==".gradle" ( + echo Deleting: %%i + rd /s /q "%%i" + ) + if /i "%%~nxi"=="build" ( + echo Deleting: %%i + rd /s /q "%%i" + ) +) diff --git a/nuke-gradle.sh b/nuke-gradle.sh new file mode 100644 index 0000000..3922069 --- /dev/null +++ b/nuke-gradle.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +find . \( -name ".gradle" -o -name "build" \) -type d -exec rm -rf {} \; diff --git a/samples/js-http-client/build.gradle.kts b/samples/js-http-client/build.gradle.kts index 71496b7..89bb432 100644 --- a/samples/js-http-client/build.gradle.kts +++ b/samples/js-http-client/build.gradle.kts @@ -26,4 +26,4 @@ kotlin { } } } -} \ No newline at end of file +} diff --git a/samples/js-http-client/src/jsMain/kotlin/kotlinx/document/database/samples/ktor/js/UserClient.kt b/samples/js-http-client/src/jsMain/kotlin/kotlinx/document/store/samples/ktor/js/UserClient.kt similarity index 52% rename from samples/js-http-client/src/jsMain/kotlin/kotlinx/document/database/samples/ktor/js/UserClient.kt rename to samples/js-http-client/src/jsMain/kotlin/kotlinx/document/store/samples/ktor/js/UserClient.kt index c8ab15d..90e4011 100644 --- a/samples/js-http-client/src/jsMain/kotlin/kotlinx/document/database/samples/ktor/js/UserClient.kt +++ b/samples/js-http-client/src/jsMain/kotlin/kotlinx/document/store/samples/ktor/js/UserClient.kt @@ -1,6 +1,6 @@ @file:OptIn(ExperimentalJsExport::class, DelicateCoroutinesApi::class) -package kotlinx.document.database.samples.ktor.js +package kotlinx.document.store.samples.ktor.js import io.ktor.client.HttpClient import io.ktor.client.call.body @@ -11,25 +11,25 @@ import io.ktor.client.request.post import io.ktor.client.request.put import io.ktor.client.request.setBody import io.ktor.serialization.kotlinx.json.json -import kotlin.js.Promise -import kotlin.time.Duration.Companion.days -import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.promise import kotlinx.datetime.Clock import kotlinx.datetime.Instant -import kotlinx.document.database.KotlinxDocumentDatabase -import kotlinx.document.database.browser.IndexedDBStore -import kotlinx.document.database.samples.User +import kotlinx.document.store.KotlinxDocumentStore +import kotlinx.document.store.browser.BrowserStore +import kotlinx.document.store.samples.User import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import kotlin.js.Promise +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.seconds @Serializable data class CacheEntry( val cacheKey: K, val data: V, - val lastUpdate: Instant = Clock.System.now() + val lastUpdate: Instant = Clock.System.now(), ) @JsExport @@ -37,20 +37,22 @@ class UserClient( val protocol: String = "http", val host: String = "localhost", val port: Int = 8080, - cacheDurationInSeconds: Int = 1.days.inWholeSeconds.toInt() + cacheDurationInSeconds: Int = 1.days.inWholeSeconds.toInt(), ) : AutoCloseable { - private val cacheDuration = cacheDurationInSeconds.seconds - private val httpClient = HttpClient(Js) { - install(ContentNegotiation) { - json(Json { - prettyPrint = true - }) + private val httpClient = + HttpClient(Js) { + install(ContentNegotiation) { + json( + Json { + prettyPrint = true + }, + ) + } } - } - private val cache = KotlinxDocumentDatabase(IndexedDBStore) + private val cache = KotlinxDocumentStore(BrowserStore) @Serializable data class GetAllUsersRequest(val page: Int, val pageSize: Int) @@ -68,41 +70,46 @@ class UserClient( // return@promise cacheHit // } // -//// val result = httpClient.get("$protocol://$host:$port/users/all?page=$page&pageSize=$pageSize") +// // val result = httpClient.get("$protocol://$host:$port/users/all?page=$page&pageSize=$pageSize") // .body>() // -//// collection.updateWhere() +// // collection.updateWhere() // // result // // } - fun getUser(id: Int): Promise = GlobalScope.promise { - httpClient.get("$protocol://$host:$port/users/$id") - .body() - } + fun getUser(id: Int): Promise = + GlobalScope.promise { + httpClient.get("$protocol://$host:$port/users/$id") + .body() + } - fun insertUser(user: User): Promise = GlobalScope.promise { - httpClient.post("$protocol://$host:$port/users") { - setBody(user) - }.body() - } + fun insertUser(user: User): Promise = + GlobalScope.promise { + httpClient.post("$protocol://$host:$port/users") { + setBody(user) + }.body() + } - fun updateUser(user: User): Promise = GlobalScope.promise { - httpClient.put("$protocol://$host:$port/users") { - setBody(user) - }.body() - } + fun updateUser(user: User): Promise = + GlobalScope.promise { + httpClient.put("$protocol://$host:$port/users") { + setBody(user) + }.body() + } - fun searchUsers(name: String): Promise> = GlobalScope.promise { - httpClient.get("$protocol://$host:$port/users/search?name=$name") - .body>() - } + fun searchUsers(name: String): Promise> = + GlobalScope.promise { + httpClient.get("$protocol://$host:$port/users/search?name=$name") + .body>() + } - fun loadTestUsers() = GlobalScope.promise { - httpClient.get("$protocol://$host:$port/insertTestUsers") - .status.value - } + fun loadTestUsers() = + GlobalScope.promise { + httpClient.get("$protocol://$host:$port/insertTestUsers") + .status.value + } override fun close() { httpClient.close() diff --git a/samples/ktor-server/build.gradle.kts b/samples/ktor-server/build.gradle.kts index 6be972b..031c223 100644 --- a/samples/ktor-server/build.gradle.kts +++ b/samples/ktor-server/build.gradle.kts @@ -8,12 +8,12 @@ plugins { } application { - mainClass = "kotlinx.document.database.samples.ktor.server.MainKt" + mainClass = "kotlinx.document.store.samples.ktor.server.MainKt" } dependencies { implementation(projects.samples) - implementation(projects.stores.rocksdb) + implementation(projects.stores.leveldb) implementation(libs.ktor.server.cio) implementation(libs.ktor.server.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) @@ -30,4 +30,4 @@ tasks { dbPath.createDirectories() } } -} \ No newline at end of file +} diff --git a/samples/ktor-server/src/main/kotlin/kotlinx/document/database/samples/ktor/server/Main.kt b/samples/ktor-server/src/main/kotlin/kotlinx/document/database/samples/ktor/server/Main.kt deleted file mode 100644 index 79cea50..0000000 --- a/samples/ktor-server/src/main/kotlin/kotlinx/document/database/samples/ktor/server/Main.kt +++ /dev/null @@ -1,35 +0,0 @@ -package kotlinx.document.database.samples.ktor.server - -import io.ktor.server.cio.CIO -import io.ktor.server.engine.embeddedServer -import kotlinx.coroutines.coroutineScope -import kotlinx.document.database.DataStore -import kotlinx.document.database.KotlinxDocumentDatabase -import kotlinx.document.database.getObjectCollection -import kotlinx.document.database.rocksdb.RocksdbDataStore -import kotlinx.document.database.samples.User -import kotlin.io.path.Path -import kotlin.io.path.createDirectories - - -suspend fun main(): Unit = coroutineScope { - val path = Path( - System.getenv("DB_PATH") ?: "./server.db" - ).createDirectories() - - - val db = KotlinxDocumentDatabase( - RocksdbDataStore.open( - path = path, - commitStrategy = DataStore.CommitStrategy.OnChange - ) - ) - val userCollection = db.getObjectCollection("users") - userCollection.createIndex("name") - - val server = embeddedServer(CIO, port = 8080) { - UserCRUDServer(userCollection) - } - - server.start() -} diff --git a/samples/ktor-server/src/main/kotlin/kotlinx/document/store/samples/ktor/server/Main.kt b/samples/ktor-server/src/main/kotlin/kotlinx/document/store/samples/ktor/server/Main.kt new file mode 100644 index 0000000..97a93e4 --- /dev/null +++ b/samples/ktor-server/src/main/kotlin/kotlinx/document/store/samples/ktor/server/Main.kt @@ -0,0 +1,26 @@ +package kotlinx.document.store.samples.ktor.server + +import io.ktor.server.cio.CIO +import io.ktor.server.engine.embeddedServer +import kotlinx.coroutines.coroutineScope +import kotlinx.document.store.KotlinxDocumentStore +import kotlinx.document.store.getObjectCollection +import kotlinx.document.store.leveldb.LevelDBStore +import kotlinx.document.store.samples.User + +suspend fun main() { + val dbPath = System.getenv("DB_PATH") ?: error("DB_PATH environment variable not set") + coroutineScope { + val db = KotlinxDocumentStore(LevelDBStore.open(dbPath)) + val userCollection = db.getObjectCollection("users") + + userCollection.createIndex("name") + + val server = + embeddedServer(CIO, port = 8080) { + UserCRUDServer(userCollection) + } + + server.start() + } +} diff --git a/samples/ktor-server/src/main/kotlin/kotlinx/document/database/samples/ktor/server/UserCRUDServer.kt b/samples/ktor-server/src/main/kotlin/kotlinx/document/store/samples/ktor/server/UserCRUDServer.kt similarity index 78% rename from samples/ktor-server/src/main/kotlin/kotlinx/document/database/samples/ktor/server/UserCRUDServer.kt rename to samples/ktor-server/src/main/kotlin/kotlinx/document/store/samples/ktor/server/UserCRUDServer.kt index bbf0a7f..e7cf3fb 100644 --- a/samples/ktor-server/src/main/kotlin/kotlinx/document/database/samples/ktor/server/UserCRUDServer.kt +++ b/samples/ktor-server/src/main/kotlin/kotlinx/document/store/samples/ktor/server/UserCRUDServer.kt @@ -1,11 +1,10 @@ @file:OptIn(ExperimentalSerializationApi::class) -package kotlinx.document.database.samples.ktor.server +package kotlinx.document.store.samples.ktor.server import io.ktor.http.HttpStatusCode import io.ktor.serialization.kotlinx.json.json import io.ktor.server.application.Application -import io.ktor.server.application.call import io.ktor.server.application.install import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.request.receive @@ -15,12 +14,13 @@ import io.ktor.server.routing.post import io.ktor.server.routing.put import io.ktor.server.routing.route import io.ktor.server.routing.routing +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList -import kotlinx.document.database.ObjectCollection -import kotlinx.document.database.find -import kotlinx.document.database.samples.Page -import kotlinx.document.database.samples.User +import kotlinx.document.store.ObjectCollection +import kotlinx.document.store.find +import kotlinx.document.store.samples.Page +import kotlinx.document.store.samples.User import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject @@ -31,9 +31,11 @@ import kotlinx.serialization.json.longOrNull @Suppress("FunctionName") fun Application.UserCRUDServer(userCollection: ObjectCollection) { install(ContentNegotiation) { - json(Json { - prettyPrint = true - }) + json( + Json { + prettyPrint = true + }, + ) } routing { @@ -45,9 +47,11 @@ fun Application.UserCRUDServer(userCollection: ObjectCollection) { val totalElements = userCollection.size() val totalPages = (totalElements + pageSize - 1) / pageSize - val users = userCollection.iterateAll((page * pageSize).toLong()) - .take(pageSize) - .toList() + val users = + userCollection.iterateAll() + .drop(page * pageSize) + .take(pageSize) + .toList() call.respond(Page(users, page, pageSize, totalElements.toInt(), totalPages.toInt())) } @@ -89,11 +93,12 @@ fun Application.UserCRUDServer(userCollection: ObjectCollection) { call.respond(users) } get("insertTestUsers") { - val users = Thread.currentThread() - .contextClassLoader - .getResourceAsStream("testUsers.json") - ?.let { Json.decodeFromStream>(it) } - ?.map { userCollection.insert(it) } + val users = + Thread.currentThread() + .contextClassLoader + .getResourceAsStream("testUsers.json") + ?.let { Json.decodeFromStream>(it) } + ?.map { userCollection.insert(it) } when (users) { null -> call.respond(HttpStatusCode.InternalServerError) @@ -102,4 +107,4 @@ fun Application.UserCRUDServer(userCollection: ObjectCollection) { } } } -} \ No newline at end of file +} diff --git a/samples/src/commonMain/kotlin/kotlinx/document/database/samples/Data.kt b/samples/src/commonMain/kotlin/kotlinx/document/store/samples/Data.kt similarity index 81% rename from samples/src/commonMain/kotlin/kotlinx/document/database/samples/Data.kt rename to samples/src/commonMain/kotlin/kotlinx/document/store/samples/Data.kt index 41d8417..14d5ead 100644 --- a/samples/src/commonMain/kotlin/kotlinx/document/database/samples/Data.kt +++ b/samples/src/commonMain/kotlin/kotlinx/document/store/samples/Data.kt @@ -1,6 +1,6 @@ @file:OptIn(ExperimentalJsExport::class) -package kotlinx.document.database.samples +package kotlinx.document.store.samples import kotlin.js.ExperimentalJsExport import kotlin.js.JsExport @@ -10,7 +10,7 @@ import kotlinx.serialization.Serializable @JsExport data class User( val name: String, - val age: Int + val age: Int, ) @Serializable @@ -20,5 +20,5 @@ data class Page( val page: Int, val pageSize: Int, val totalElements: Int, - val totalPages: Int + val totalPages: Int, ) diff --git a/settings.gradle.kts b/settings.gradle.kts index 9b98665..a4fe6d2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,12 +1,13 @@ @file:Suppress("UnstableApiUsage") plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" - `gradle-enterprise` + id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" + id("com.gradle.develocity") version "3.18.2" } dependencyResolutionManagement { repositories { + google() mavenCentral() } rulesMode = RulesMode.PREFER_SETTINGS @@ -20,17 +21,46 @@ include( ":tests", ":stores:mvstore", ":stores:browser", - ":stores:rocksdb", + ":stores:leveldb", ":version-catalog", ":samples:js-http-client", ":samples:ktor-server", ":samples:kmp-app", ) -gradleEnterprise { +includeBuild("kotlin-leveldb") { + val endings = listOf( + "jvm", + "js", + "mingwx64", + "linuxx64", + "linuxarm64", + "macosx64", + "macosarm64", + "iosarm64", + "iosx64", + "iosSimulatorarm64", + "watchosarm64", + "watchosx64", + "watchosSimulatorarm64", + "tvosarm64", + "tvosx64", + "tvosSimulatorarm64", + ) + dependencySubstitution { + substitute(module("com.github.lamba92:kotlin-leveldb")).using(project(":")) + endings.forEach { + substitute(module("com.github.lamba92:kotlin-leveldb-$it")).using(project(":")) + } + } +} + +develocity { buildScan { - termsOfServiceUrl = "https://gradle.com/terms-of-service" - termsOfServiceAgree = "yes" - publishAlwaysIf(System.getenv("CI") == "true") + termsOfUseUrl = "https://gradle.com/terms-of-service" + termsOfUseAgree = "yes" + publishing { + onlyIf { System.getenv("CI") == "true" } + } } } \ No newline at end of file diff --git a/stores/browser/build.gradle.kts b/stores/browser/build.gradle.kts index 8a71231..92bf613 100644 --- a/stores/browser/build.gradle.kts +++ b/stores/browser/build.gradle.kts @@ -1,16 +1,18 @@ -import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode - plugins { - convention - kotlin("multiplatform") - kotlin("plugin.serialization") + `publishing-convention` + `kotlin-multiplatform-convention` } kotlin { js { - browser() + browser { + testTask { + useKarma { + useChromeHeadless() + } + } + } } - explicitApi = ExplicitApiMode.Disabled sourceSets { val jsMain by getting { dependencies { diff --git a/stores/browser/src/jsMain/kotlin/keyval/KeyVal.kt b/stores/browser/src/jsMain/kotlin/keyval/KeyVal.kt index 933fae5..d66a982 100644 --- a/stores/browser/src/jsMain/kotlin/keyval/KeyVal.kt +++ b/stores/browser/src/jsMain/kotlin/keyval/KeyVal.kt @@ -6,108 +6,108 @@ package keyval import kotlin.js.Promise -external interface IDBRequest { - var oncomplete: (() -> Unit)? - var onsuccess: (() -> Unit)? - var onabort: (() -> Unit)? - var onerror: (() -> Unit)? - val result: T - val error: dynamic +public external interface IDBRequest { + public var oncomplete: (() -> Unit)? + public var onsuccess: (() -> Unit)? + public var onabort: (() -> Unit)? + public var onerror: (() -> Unit)? + public val result: T + public val error: dynamic } -external interface IDBTransaction { - var oncomplete: (() -> Unit)? - var onsuccess: (() -> Unit)? - var onabort: (() -> Unit)? - var onerror: (() -> Unit)? - val result: dynamic - val error: dynamic +public external interface IDBTransaction { + public var oncomplete: (() -> Unit)? + public var onsuccess: (() -> Unit)? + public var onabort: (() -> Unit)? + public var onerror: (() -> Unit)? + public val result: dynamic + public val error: dynamic } -external interface IDBObjectStore { - fun put( +public external interface IDBObjectStore { + public fun put( value: Any, key: String, ): IDBRequest - fun get(key: String): IDBRequest + public fun get(key: String): IDBRequest - fun delete(key: String): IDBRequest + public fun delete(key: String): IDBRequest - fun clear(): IDBRequest + public fun clear(): IDBRequest - fun openCursor(): IDBRequest + public fun openCursor(): IDBRequest - fun getAll(): IDBRequest> + public fun getAll(): IDBRequest> - fun getAllKeys(): IDBRequest> + public fun getAllKeys(): IDBRequest> - val transaction: IDBTransaction + public val transaction: IDBTransaction } -external interface IDBCursorWithValue { - val key: String - val value: String +public external interface IDBCursorWithValue { + public val key: String + public val value: String @JsName("continue") - fun next() + public fun next() } -external interface UseStore { - operator fun invoke( +public external interface UseStore { + public operator fun invoke( txMode: String, callback: (store: IDBObjectStore) -> Any, ): Promise } -external fun promisifyRequest(request: dynamic): Promise +public external fun promisifyRequest(request: dynamic): Promise -external fun createStore( +public external fun createStore( dbName: String, storeName: String, ): UseStore -external fun get( +public external fun get( key: String, customStore: UseStore = definedExternally, ): Promise -external fun set( +public external fun set( key: String, value: Any, customStore: UseStore = definedExternally, ): Promise -external fun setMany( +public external fun setMany( entries: Array>, customStore: UseStore = definedExternally, ): Promise -external fun getMany( +public external fun getMany( keys: Array, customStore: UseStore = definedExternally, ): Promise> -external fun update( +public external fun update( key: String, updater: (oldValue: Any?) -> Any, customStore: UseStore = definedExternally, ): Promise -external fun del( +public external fun del( key: String, customStore: UseStore = definedExternally, ): Promise -external fun delMany( +public external fun delMany( keys: Array, customStore: UseStore = definedExternally, ): Promise -external fun clear(customStore: UseStore = definedExternally): Promise +public external fun clear(customStore: UseStore = definedExternally): Promise -external fun keys(customStore: UseStore = definedExternally): Promise> +public external fun keys(customStore: UseStore = definedExternally): Promise> -external fun values(customStore: UseStore = definedExternally): Promise> +public external fun values(customStore: UseStore = definedExternally): Promise> -external fun entries(customStore: UseStore = definedExternally): Promise>> +public external fun entries(customStore: UseStore = definedExternally): Promise>> diff --git a/stores/browser/src/jsMain/kotlin/kotlinx/document/store/browser/BrowserStore.kt b/stores/browser/src/jsMain/kotlin/kotlinx/document/store/browser/BrowserStore.kt new file mode 100644 index 0000000..e5cdf04 --- /dev/null +++ b/stores/browser/src/jsMain/kotlin/kotlinx/document/store/browser/BrowserStore.kt @@ -0,0 +1,19 @@ +package kotlinx.document.store.browser + +import kotlinx.coroutines.await +import kotlinx.document.store.AbstractDataStore +import kotlinx.document.store.PersistentMap + +public object BrowserStore : AbstractDataStore() { + override suspend fun getMap(name: String): PersistentMap = withStoreLock { IndexedDBMap(name, getMutex(name)) } + + override suspend fun deleteMap(name: String): Unit = + withStoreLock { + lockAndRemoveMutex(name) { + keyval.keys() + .await() + .filter { it.startsWith(IndexedDBMap.buildPrefix(name)) } + .let { keyval.delMany(it.toTypedArray()).await() } + } + } +} diff --git a/stores/browser/src/jsMain/kotlin/kotlinx/document/database/browser/IndexedDBStore.kt b/stores/browser/src/jsMain/kotlin/kotlinx/document/store/browser/IndexedDBMap.kt similarity index 50% rename from stores/browser/src/jsMain/kotlin/kotlinx/document/database/browser/IndexedDBStore.kt rename to stores/browser/src/jsMain/kotlin/kotlinx/document/store/browser/IndexedDBMap.kt index 8dbd809..0f7b16b 100644 --- a/stores/browser/src/jsMain/kotlin/kotlinx/document/database/browser/IndexedDBStore.kt +++ b/stores/browser/src/jsMain/kotlin/kotlinx/document/store/browser/IndexedDBMap.kt @@ -1,4 +1,4 @@ -package kotlinx.document.database.browser +package kotlinx.document.store.browser import kotlinx.coroutines.await import kotlinx.coroutines.flow.Flow @@ -7,70 +7,64 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.document.database.DataStore -import kotlinx.document.database.PersistentMap -import kotlinx.document.database.SerializableEntry -import kotlinx.document.database.UpdateResult -import kotlinx.document.database.drop - -object IndexedDBStore : DataStore { - override val commitStrategy: DataStore.CommitStrategy = DataStore.CommitStrategy.OnChange - - override suspend fun getMap(name: String): PersistentMap = IndexedDBMap(name) - - override suspend fun deleteMap(name: String) { - keyval.keys().await() - .filter { it.startsWith(name) } - .let { keyval.delMany(it.toTypedArray()).await() } +import kotlinx.document.store.PersistentMap +import kotlinx.document.store.SerializableEntry +import kotlinx.document.store.UpdateResult + +public class IndexedDBMap( + private val name: String, + private val mutex: Mutex, +) : PersistentMap { + public companion object { + private const val SEPARATOR = "." + + internal fun buildPrefix(name: String) = "$name$SEPARATOR" } - override suspend fun close() {} - - override suspend fun commit() {} -} + private val prefixed + get() = buildPrefix(name) -class IndexedDBMap(private val prefix: String) : PersistentMap { - private val mutex = Mutex() + private fun String.prefixed() = "$prefixed$this" - override suspend fun clear() = + override suspend fun clear(): Unit = keyval.keys() .await() - .filter { it.startsWith("${prefix}_") } + .filter { it.startsWith(prefixed) } .let { keyval.delMany(it.toTypedArray()).await() } - override suspend fun size() = + override suspend fun size(): Long = keyval.keys() .await() - .filter { it.startsWith("${prefix}_") } + .filter { it.startsWith(prefixed) } .size .toLong() - override suspend fun isEmpty() = size() == 0L + override suspend fun isEmpty(): Boolean = size() == 0L - override suspend fun get(key: String) = keyval.get("${prefix}_$key").await() + override suspend fun get(key: String): String? = keyval.get(key.prefixed()).await() override suspend fun put( key: String, value: String, - ) = mutex.withLock { unsafePut(key, value) } + ): String? = mutex.withLock { unsafePut(key, value) } private suspend fun IndexedDBMap.unsafePut( key: String, value: String, ): String? { val previous = get(key) - keyval.set("${prefix}_$key", value).await() + keyval.set(key.prefixed(), value).await() return previous } override suspend fun remove(key: String): String? = mutex.withLock { val previous = get(key) - keyval.del("${prefix}_$key").await() + keyval.del(key.prefixed()).await() previous } - override suspend fun containsKey(key: String) = get(key) != null + override suspend fun containsKey(key: String): Boolean = get(key) != null override suspend fun update( key: String, @@ -80,7 +74,7 @@ class IndexedDBMap(private val prefix: String) : PersistentMap { mutex.withLock { val oldValue = get(key) val newValue = oldValue?.let(updater) ?: value - keyval.set("${prefix}_$key", newValue).await() + keyval.set(key.prefixed(), newValue).await() UpdateResult(oldValue, newValue) } @@ -92,16 +86,15 @@ class IndexedDBMap(private val prefix: String) : PersistentMap { get(key) ?: defaultValue().also { unsafePut(key, it) } } - override fun entries(fromIndex: Long): Flow> = + override fun entries(): Flow> = flow { keyval.keys() .await() .asFlow() - .filter { it.startsWith("${prefix}_") } - .drop(fromIndex) + .filter { it.startsWith(prefixed) } .collect { key -> keyval.get(key).await()?.let { value -> - emit(SerializableEntry(key.removePrefix("${prefix}_"), value)) + emit(SerializableEntry(key.removePrefix(prefixed), value)) } } } diff --git a/stores/browser/src/jsTest/kotlin/kotlinx/document/database/browser/tests/BrowserTests.kt b/stores/browser/src/jsTest/kotlin/kotlinx/document/database/browser/tests/BrowserTests.kt deleted file mode 100644 index 8f05d74..0000000 --- a/stores/browser/src/jsTest/kotlin/kotlinx/document/database/browser/tests/BrowserTests.kt +++ /dev/null @@ -1,48 +0,0 @@ -@file:Suppress("unused") - -package kotlinx.document.database.browser.tests - -import kotlinx.coroutines.await -import kotlinx.document.database.browser.IndexedDBStore -import kotlinx.document.database.tests.AbstractDeleteTests -import kotlinx.document.database.tests.AbstractDocumentDatabaseTests -import kotlinx.document.database.tests.AbstractFindTests -import kotlinx.document.database.tests.AbstractIndexTests -import kotlinx.document.database.tests.AbstractInsertTests -import kotlinx.document.database.tests.AbstractObjectCollectionTests -import kotlinx.document.database.tests.AbstractUpdateTests -import kotlinx.document.database.tests.DatabaseDeleter - -class BrowserDeleteTests : - AbstractDeleteTests(IndexedDBStore), - DatabaseDeleter by BrowserDeleter - -class BrowserDocumentDatabaseTests : - AbstractDocumentDatabaseTests(IndexedDBStore), - DatabaseDeleter by BrowserDeleter - -class BrowserIndexTests : - AbstractIndexTests(IndexedDBStore), - DatabaseDeleter by BrowserDeleter - -class BrowserInsertTests : - AbstractInsertTests(IndexedDBStore), - DatabaseDeleter by BrowserDeleter - -class BrowserUpdateTests : - AbstractUpdateTests(IndexedDBStore), - DatabaseDeleter by BrowserDeleter - -class BrowserFindTests : - AbstractFindTests(IndexedDBStore), - DatabaseDeleter by BrowserDeleter - -class BrowserObjectCollectionTests : - AbstractObjectCollectionTests(IndexedDBStore), - DatabaseDeleter by BrowserDeleter - -object BrowserDeleter : DatabaseDeleter { - override suspend fun deleteDatabase() { - keyval.clear().await() - } -} diff --git a/stores/browser/src/jsTest/kotlin/kotlinx/document/store/browser/tests/BrowserTests.kt b/stores/browser/src/jsTest/kotlin/kotlinx/document/store/browser/tests/BrowserTests.kt new file mode 100644 index 0000000..a054da4 --- /dev/null +++ b/stores/browser/src/jsTest/kotlin/kotlinx/document/store/browser/tests/BrowserTests.kt @@ -0,0 +1,52 @@ +@file:Suppress("unused") + +package kotlinx.document.store.browser.tests + +import kotlinx.coroutines.await +import kotlinx.document.store.DataStore +import kotlinx.document.store.browser.BrowserStore +import kotlinx.document.store.tests.AbstractDeleteTests +import kotlinx.document.store.tests.AbstractDocumentDatabaseTests +import kotlinx.document.store.tests.AbstractFindTests +import kotlinx.document.store.tests.AbstractIndexTests +import kotlinx.document.store.tests.AbstractInsertTests +import kotlinx.document.store.tests.AbstractObjectCollectionTests +import kotlinx.document.store.tests.AbstractUpdateTests +import kotlinx.document.store.tests.DataStoreProvider +import kotlinx.document.store.tests.DatabaseDeleter + +class BrowserDeleteTests : + AbstractDeleteTests(BrowserStoreProvider), + DatabaseDeleter by BrowserStoreProvider + +class BrowserDocumentDatabaseTests : + AbstractDocumentDatabaseTests(BrowserStoreProvider), + DatabaseDeleter by BrowserStoreProvider + +class BrowserIndexTests : + AbstractIndexTests(BrowserStoreProvider), + DatabaseDeleter by BrowserStoreProvider + +class BrowserInsertTests : + AbstractInsertTests(BrowserStoreProvider), + DatabaseDeleter by BrowserStoreProvider + +class BrowserUpdateTests : + AbstractUpdateTests(BrowserStoreProvider), + DatabaseDeleter by BrowserStoreProvider + +class BrowserFindTests : + AbstractFindTests(BrowserStoreProvider), + DatabaseDeleter by BrowserStoreProvider + +class BrowserObjectCollectionTests : + AbstractObjectCollectionTests(BrowserStoreProvider), + DatabaseDeleter by BrowserStoreProvider + +object BrowserStoreProvider : DataStoreProvider, DatabaseDeleter { + override suspend fun deleteDatabase(testName: String) { + keyval.clear().await() + } + + override fun provide(testName: String): DataStore = BrowserStore +} diff --git a/stores/leveldb/build.gradle.kts b/stores/leveldb/build.gradle.kts new file mode 100644 index 0000000..7aedeed --- /dev/null +++ b/stores/leveldb/build.gradle.kts @@ -0,0 +1,110 @@ +plugins { + `publishing-convention` + `kotlin-multiplatform-with-android-convention` +} + +kotlin { + jvm() + androidTarget() + + mingwX64() + + linuxX64() + linuxArm64() + + macosArm64() + macosX64() + + iosArm64() + iosX64() + iosSimulatorArm64() + + watchosArm64() + watchosX64() + watchosSimulatorArm64() + + tvosArm64() + tvosX64() + tvosSimulatorArm64() + + androidNativeX64() + androidNativeX86() + androidNativeArm64() + androidNativeArm32() + + applyDefaultHierarchyTemplate() + + sourceSets { + commonMain { + dependencies { + api(projects.core) + api(libs.kotlin.leveldb) + } + } + + commonTest { + dependencies { + implementation(projects.tests) + } + } + + jvmTest { + dependencies { + runtimeOnly(libs.junit.jupiter.engine) + implementation(libs.junit.jupiter.api) + implementation(kotlin("test-junit5")) + } + } + + val nativeDesktopTest by creating { + dependsOn(commonTest.get()) + } + + mingwTest { + dependsOn(nativeDesktopTest) + } + + linuxTest { + dependsOn(nativeDesktopTest) + } + + macosTest { + dependsOn(nativeDesktopTest) + } + + val appleMobileTest by creating { + dependsOn(commonTest.get()) + } + iosTest { + dependsOn(appleMobileTest) + } + watchosTest { + dependsOn(appleMobileTest) + } + tvosTest { + dependsOn(appleMobileTest) + } + + val commonJvmTest by creating { + dependsOn(commonTest.get()) + } + + androidInstrumentedTest { + dependsOn(commonJvmTest) + } + androidUnitTest { + dependsOn(commonJvmTest) + } + jvmTest { + dependsOn(commonJvmTest) + } + + androidInstrumentedTest { + dependencies { + implementation(libs.androidx.test.runner) + implementation(libs.androidx.test.core) + implementation(libs.android.test.junit) + } + } + } +} diff --git a/stores/leveldb/src/androidInstrumentedTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.android.kt b/stores/leveldb/src/androidInstrumentedTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.android.kt new file mode 100644 index 0000000..6c02b9c --- /dev/null +++ b/stores/leveldb/src/androidInstrumentedTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.android.kt @@ -0,0 +1,12 @@ +package kotlinx.document.store.tests.leveldb + +import androidx.test.platform.app.InstrumentationRegistry + +actual val DB_PATH: String + get() = + InstrumentationRegistry + .getInstrumentation() + .targetContext + .filesDir + .resolve("testdb") + .absolutePath diff --git a/stores/leveldb/src/androidNativeTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.androidNative.kt b/stores/leveldb/src/androidNativeTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.androidNative.kt new file mode 100644 index 0000000..6601992 --- /dev/null +++ b/stores/leveldb/src/androidNativeTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.androidNative.kt @@ -0,0 +1,16 @@ +package kotlinx.document.store.tests.leveldb + +import kotlinx.io.files.Path + +actual val DB_PATH: String + get() = error() + +actual fun Path.createDirectories(): Path { + error() +} + +private fun error(): Nothing = error("Kotlin/Native has no tests suite for Android Native") + +actual fun Path.resolve(path: String): Path { + error() +} diff --git a/stores/leveldb/src/androidNativeTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.linux.kt b/stores/leveldb/src/androidNativeTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.linux.kt new file mode 100644 index 0000000..3004858 --- /dev/null +++ b/stores/leveldb/src/androidNativeTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.linux.kt @@ -0,0 +1,53 @@ +package kotlinx.document.store.tests.leveldb + +import kotlinx.cinterop.MemScope +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.pointed +import kotlinx.cinterop.ptr +import kotlinx.cinterop.toKString +import platform.posix.S_IFDIR +import platform.posix.S_IFMT +import platform.posix.closedir +import platform.posix.lstat +import platform.posix.opendir +import platform.posix.readdir +import platform.posix.rmdir +import platform.posix.stat +import platform.posix.unlink + +actual fun deleteFolderRecursively(path: String): Unit = + memScoped { + deleteFolderRecursively(path) + } + +private fun MemScope.deleteFolderRecursively(path: String) { + val dir = opendir(path) ?: error("Failed to open directory: $path") + try { + while (true) { + val entry = readdir(dir) ?: break + val name = entry.pointed.d_name.toKString() + if (name == "." || name == "..") continue + val fullPath = "$path/$name" + val statBuf = alloc() + if (lstat(fullPath, statBuf.ptr) != 0) { + error("Failed to stat file: $fullPath") + } + + when (S_IFDIR) { + statBuf.st_mode.toInt() and S_IFMT -> deleteFolderRecursively(fullPath) + else -> + if (unlink(fullPath) != 0) { + error("Failed to delete file: $fullPath") + } + } + } + + // Delete the directory itself + if (rmdir(path) != 0) { + error("Failed to delete directory: $path") + } + } finally { + closedir(dir) + } +} diff --git a/stores/leveldb/src/androidUnitTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.android.kt b/stores/leveldb/src/androidUnitTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.android.kt new file mode 100644 index 0000000..be41cde --- /dev/null +++ b/stores/leveldb/src/androidUnitTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.android.kt @@ -0,0 +1,4 @@ +package kotlinx.document.store.tests.leveldb + +actual val DB_PATH: String + get() = error("No unit tests are executed on Android") diff --git a/stores/leveldb/src/appleMobileTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.appleMobile.kt b/stores/leveldb/src/appleMobileTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.appleMobile.kt new file mode 100644 index 0000000..715b47d --- /dev/null +++ b/stores/leveldb/src/appleMobileTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.appleMobile.kt @@ -0,0 +1,29 @@ +package kotlinx.document.store.tests.leveldb + +import platform.Foundation.NSFileManager +import platform.Foundation.NSTemporaryDirectory +import platform.Foundation.NSURL + +actual val DB_PATH: String + get() = + createTestFilePath() + ?: error("Failed to create test file for database") + +fun createTestFilePath(): String? { + // Use the temporary directory for test files + val tmpDir = NSURL.fileURLWithPath(NSTemporaryDirectory()) + + // Create a subdirectory for organization, if desired + val testDir = + tmpDir.URLByAppendingPathComponent("testDir") + ?: error("Failed to create test directory") + NSFileManager.defaultManager.createDirectoryAtURL( + url = testDir, + withIntermediateDirectories = true, + attributes = null, + error = null, + ) + + // Return the absolute path as a String + return testDir.path +} diff --git a/stores/leveldb/src/appleTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.apple.kt b/stores/leveldb/src/appleTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.apple.kt new file mode 100644 index 0000000..6692734 --- /dev/null +++ b/stores/leveldb/src/appleTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.apple.kt @@ -0,0 +1,50 @@ +@file:OptIn(BetaInteropApi::class) + +package kotlinx.document.store.tests.leveldb + +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.BooleanVar +import kotlinx.cinterop.ObjCObjectVar +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.value +import kotlinx.io.files.Path +import platform.Foundation.NSError +import platform.Foundation.NSFileManager + +actual fun deleteFolderRecursively(path: String): Unit = + memScoped { + val fileManager = NSFileManager.defaultManager + val isDirectoryBoolean = alloc() + isDirectoryBoolean.value = true + if (!fileManager.fileExistsAtPath(path, isDirectoryBoolean.ptr)) { + return + } + val errorPtr = alloc>() + val success = fileManager.removeItemAtPath(path, error = errorPtr.ptr) + val nsError = errorPtr.value + if (!success && nsError != null) { + error("Error deleting folder: ${nsError.localizedDescription}") + } + } + +actual fun Path.createDirectories(): Path { + memScoped { + val fileManager = NSFileManager.defaultManager() + val errorPtr = alloc>() + + fileManager.createDirectoryAtPath( + path = this@createDirectories.toString(), + withIntermediateDirectories = true, + attributes = null, + error = errorPtr.ptr, + ) + + val nsError = errorPtr.value + if (nsError != null) error(nsError.localizedDescription) + } + return this +} + +actual fun Path.resolve(path: String): Path = Path(toString() + "/" + path) diff --git a/stores/leveldb/src/commonJvmTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.commonJvm.kt b/stores/leveldb/src/commonJvmTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.commonJvm.kt new file mode 100644 index 0000000..ef436d8 --- /dev/null +++ b/stores/leveldb/src/commonJvmTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.commonJvm.kt @@ -0,0 +1,18 @@ +package kotlinx.document.store.tests.leveldb + +import kotlinx.io.files.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteRecursively +import kotlin.io.path.Path as JavaNioPath +import kotlinx.io.files.Path as KotlinPath + +actual fun deleteFolderRecursively(path: String) { + JavaNioPath(path).deleteRecursively() +} + +actual fun KotlinPath.createDirectories(): KotlinPath { + JavaNioPath(toString()).createDirectories() + return this +} + +actual fun Path.resolve(path: String): Path = KotlinPath(JavaNioPath(toString()).resolve(path).toString()) diff --git a/stores/leveldb/src/commonMain/kotlin/kotlinx/document/store/leveldb/LevelDBPersistentMap.kt b/stores/leveldb/src/commonMain/kotlin/kotlinx/document/store/leveldb/LevelDBPersistentMap.kt new file mode 100644 index 0000000..1f67248 --- /dev/null +++ b/stores/leveldb/src/commonMain/kotlin/kotlinx/document/store/leveldb/LevelDBPersistentMap.kt @@ -0,0 +1,111 @@ +package kotlinx.document.store.leveldb + +import com.github.lamba92.leveldb.LevelDB +import com.github.lamba92.leveldb.batch +import com.github.lamba92.leveldb.resolve +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import kotlinx.document.store.PersistentMap +import kotlinx.document.store.SerializableEntry +import kotlinx.document.store.UpdateResult + +public class LevelDBPersistentMap( + private val delegate: LevelDB, + private val prefix: String, + private val mutex: Mutex, +) : PersistentMap { + private fun String.prefixed() = "$prefix.$this" + + override suspend fun get(key: String): String? = + withContext(Dispatchers.IO) { + delegate.get(key.prefixed()) + } + + override suspend fun put( + key: String, + value: String, + ): String? = + mutex.withLock { + val previousValue = delegate.get(key.prefixed()) + val previousSize = delegate.get("sizes.$prefix")?.toLong() + delegate.batch { + put(key.prefixed(), value) + val nextSize = + when (previousSize) { + null -> "1" + else -> (previousSize + 1).toString() + } + put("sizes.$prefix", nextSize) + } + previousValue + } + + override suspend fun remove(key: String): String? = + withContext(Dispatchers.IO) { + mutex.withLock { + val prefixed = key.prefixed() + val previous = delegate.get(prefixed) + delegate.delete(prefixed) + delegate.get("sizes.$prefix") + ?.toLong() + ?.let { delegate.put("sizes.$prefix", (it - 1).toString()) } + previous + } + } + + override suspend fun containsKey(key: String): Boolean = get(key) != null + + override suspend fun clear(): Unit = delegate.deletePrefix(prefix) + + override suspend fun size(): Long = + withContext(Dispatchers.IO) { + delegate.get("sizes.$prefix")?.toLong() ?: 0L + } + + override suspend fun isEmpty(): Boolean = size() == 0L + + override fun entries(): Flow> = + flow { + val sequence = delegate.scan("$prefix.") + try { + sequence.map { it.resolve() } + .takeWhile { (key, _) -> key.startsWith("$prefix.") } + .forEach { emit(SerializableEntry(it.key.removePrefix("$prefix."), it.value)) } + } finally { + withContext(NonCancellable) { + sequence.close() + } + } + } + + override suspend fun getOrPut( + key: String, + defaultValue: () -> String, + ): String = + mutex.withLock { + withContext(Dispatchers.IO) { + delegate.get(key.prefixed()) + ?: defaultValue().also { delegate.put(key.prefixed(), it) } + } + } + + override suspend fun update( + key: String, + value: String, + updater: (String) -> String, + ): UpdateResult = + mutex.withLock { + withContext(Dispatchers.IO) { + val previous = delegate.get(key.prefixed()) + val newValue = previous?.let(updater) ?: value + delegate.put(key.prefixed(), newValue) + UpdateResult(previous, newValue) + } + } +} diff --git a/stores/leveldb/src/commonMain/kotlin/kotlinx/document/store/leveldb/LevelDBStore.kt b/stores/leveldb/src/commonMain/kotlin/kotlinx/document/store/leveldb/LevelDBStore.kt new file mode 100644 index 0000000..81e32bf --- /dev/null +++ b/stores/leveldb/src/commonMain/kotlin/kotlinx/document/store/leveldb/LevelDBStore.kt @@ -0,0 +1,58 @@ +package kotlinx.document.store.leveldb + +import com.github.lamba92.leveldb.LevelDB +import com.github.lamba92.leveldb.LevelDBOptions +import com.github.lamba92.leveldb.batch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.withContext +import kotlinx.document.store.AbstractDataStore +import kotlinx.document.store.PersistentMap +import kotlinx.io.files.Path + +public class LevelDBStore(private val delegate: LevelDB) : AbstractDataStore() { + public companion object { + public fun open( + path: Path, + options: LevelDBOptions = LevelDBOptions.DEFAULT, + ): LevelDBStore = open(path.toString(), options) + + public fun open( + path: String, + options: LevelDBOptions = LevelDBOptions.DEFAULT, + ): LevelDBStore = LevelDBStore(LevelDB(path, options)) + } + + override suspend fun getMap(name: String): PersistentMap = + withStoreLock { + LevelDBPersistentMap( + delegate = delegate, + prefix = name, + mutex = getMutex(name), + ) + } + + override suspend fun deleteMap(name: String): Unit = + withStoreLock { + lockAndRemoveMutex(name) { delegate.deletePrefix(name) } + } + + override suspend fun close() { + withContext(Dispatchers.IO) { + delegate.close() + } + } +} + +internal suspend fun LevelDB.deletePrefix(prefix: String) = + withContext(Dispatchers.IO) { + batch { + val sequence = scan(prefix) + sequence + .map { it.key.value } + .takeWhile { it.startsWith(prefix) } + .forEach { delete(it) } + delete("sizes.$prefix") + sequence.close() + } + } diff --git a/stores/leveldb/src/commonTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.kt b/stores/leveldb/src/commonTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.kt new file mode 100644 index 0000000..0f17cfc --- /dev/null +++ b/stores/leveldb/src/commonTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.kt @@ -0,0 +1,74 @@ +@file:Suppress("unused") + +package kotlinx.document.store.tests.leveldb + +import com.github.lamba92.leveldb.LevelDB +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.withContext +import kotlinx.document.store.DataStore +import kotlinx.document.store.leveldb.LevelDBStore +import kotlinx.document.store.tests.AbstractDeleteTests +import kotlinx.document.store.tests.AbstractDocumentDatabaseTests +import kotlinx.document.store.tests.AbstractFindTests +import kotlinx.document.store.tests.AbstractIndexTests +import kotlinx.document.store.tests.AbstractInsertTests +import kotlinx.document.store.tests.AbstractObjectCollectionTests +import kotlinx.document.store.tests.AbstractUpdateTests +import kotlinx.document.store.tests.DataStoreProvider +import kotlinx.document.store.tests.DatabaseDeleter +import kotlinx.io.files.Path + +class LevelDBDeleteTests : + AbstractDeleteTests(LevelDBStoreProvider), + DatabaseDeleter by LevelDBStoreProvider + +class LevelDBDocumentDatabaseTests : + AbstractDocumentDatabaseTests(LevelDBStoreProvider), + DatabaseDeleter by LevelDBStoreProvider + +class LevelDBIndexTests : + AbstractIndexTests(LevelDBStoreProvider), + DatabaseDeleter by LevelDBStoreProvider + +class LevelDBInsertTests : + AbstractInsertTests(LevelDBStoreProvider), + DatabaseDeleter by LevelDBStoreProvider + +class LevelDBUpdateTests : + AbstractUpdateTests(LevelDBStoreProvider), + DatabaseDeleter by LevelDBStoreProvider + +class LevelDBFindTests : + AbstractFindTests(LevelDBStoreProvider), + DatabaseDeleter by LevelDBStoreProvider + +class LevelDBObjectCollectionTests : + AbstractObjectCollectionTests(LevelDBStoreProvider), + DatabaseDeleter by LevelDBStoreProvider + +object LevelDBStoreProvider : DataStoreProvider, DatabaseDeleter { + private fun getDbPath(testName: String) = Path(DB_PATH).resolve(testName) + + override suspend fun deleteDatabase(testName: String) = + withContext(Dispatchers.IO) { + deleteFolderRecursively(getDbPath(testName).toString()) + } + + override fun provide(testName: String): DataStore = + LevelDBStore( + LevelDB( + getDbPath(testName) + .createDirectories() + .toString(), + ), + ) +} + +expect fun Path.resolve(path: String): Path + +expect fun Path.createDirectories(): Path + +expect fun deleteFolderRecursively(path: String) + +expect val DB_PATH: String diff --git a/stores/leveldb/src/jvmTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.jvm.kt b/stores/leveldb/src/jvmTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.jvm.kt new file mode 100644 index 0000000..4818fd9 --- /dev/null +++ b/stores/leveldb/src/jvmTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.jvm.kt @@ -0,0 +1,4 @@ +package kotlinx.document.store.tests.leveldb + +actual val DB_PATH: String + get() = System.getenv("DB_PATH") ?: error("DB_PATH not set") diff --git a/stores/leveldb/src/linuxTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.linux.kt b/stores/leveldb/src/linuxTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.linux.kt new file mode 100644 index 0000000..e4c3743 --- /dev/null +++ b/stores/leveldb/src/linuxTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.linux.kt @@ -0,0 +1,111 @@ +package kotlinx.document.store.tests.leveldb + +import kotlinx.cinterop.MemScope +import kotlinx.cinterop.alloc +import kotlinx.cinterop.convert +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.pointed +import kotlinx.cinterop.ptr +import kotlinx.cinterop.toKString +import kotlinx.io.files.Path +import platform.posix.S_IFDIR +import platform.posix.S_IFMT +import platform.posix.S_IROTH +import platform.posix.S_IRWXG +import platform.posix.S_IRWXU +import platform.posix.S_IXOTH +import platform.posix.closedir +import platform.posix.lstat +import platform.posix.mkdir +import platform.posix.opendir +import platform.posix.readdir +import platform.posix.rmdir +import platform.posix.stat +import platform.posix.unlink + +actual fun deleteFolderRecursively(path: String) { +// println("Deleting folder recursively: $path") + // Check if the path exists and is a directory + memScoped { + val statBuf = alloc() + if (lstat(path, statBuf.ptr) != 0) { +// println("Path does not exist: $path") + return + } + + if (statBuf.st_mode.toInt() and S_IFMT != S_IFDIR) { + error("Path is not a directory: $path") + } + + // Proceed to delete the directory recursively + deleteFolderRecursivelyInternal(path) + } +} + +private fun MemScope.deleteFolderRecursivelyInternal(path: String) { + val dir = opendir(path) ?: error("Failed to open directory: $path") + try { + while (true) { + val entry = readdir(dir) ?: break + val name = entry.pointed.d_name.toKString() + if (name == "." || name == "..") continue + val fullPath = "$path/$name" + val statBuf = alloc() + if (lstat(fullPath, statBuf.ptr) != 0) { + error("Failed to stat file: $fullPath") + } + + when (S_IFDIR) { + statBuf.st_mode.toInt() and S_IFMT -> deleteFolderRecursivelyInternal(fullPath) + else -> + if (unlink(fullPath) != 0) { + error("Failed to delete file: $fullPath") + } + } + } + + // Delete the directory itself + if (rmdir(path) != 0) { + error("Failed to delete directory: $path") + } + } finally { + closedir(dir) + } +} + +actual fun Path.createDirectories(): Path { + val pathString = toString() + val parts = pathString.split("/") + var currentPath = + when { + pathString.startsWith("/") -> "/" + else -> "" + } + println("Parts: $parts") + memScoped { + val statBuf = alloc() + for (part in parts) { + if (part.isEmpty()) { + continue // Skip empty parts + } + + currentPath += part + + // Check if the directory exists + val dirExists = + stat(currentPath, statBuf.ptr) == 0 && (statBuf.st_mode.toInt() and S_IFDIR != 0) + + if (!dirExists) { + // Create the directory + if (mkdir(currentPath, (S_IRWXU or S_IRWXG or S_IROTH or S_IXOTH).convert()) != 0) { + error("Failed to create directory: $currentPath") + } + } + + currentPath += "/" + } + } + return this +} + +actual fun Path.resolve(path: String): Path = Path(toString() + "/" + path) diff --git a/stores/leveldb/src/mingwTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.mingw.kt b/stores/leveldb/src/mingwTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.mingw.kt new file mode 100644 index 0000000..697c90f --- /dev/null +++ b/stores/leveldb/src/mingwTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.mingw.kt @@ -0,0 +1,70 @@ +package kotlinx.document.store.tests.leveldb + +import kotlinx.cinterop.alloc +import kotlinx.cinterop.convert +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.toKString +import kotlinx.cinterop.wcstr +import kotlinx.io.files.Path +import platform.posix.strerror +import platform.windows.CreateDirectoryW +import platform.windows.FILE_ATTRIBUTE_DIRECTORY +import platform.windows.FOF_NOCONFIRMATION +import platform.windows.FOF_NOERRORUI +import platform.windows.FOF_SILENT +import platform.windows.FO_DELETE +import platform.windows.GetFileAttributesW +import platform.windows.GetLastError +import platform.windows.INVALID_FILE_ATTRIBUTES +import platform.windows.SHFILEOPSTRUCTW +import platform.windows.SHFileOperationW + +actual fun deleteFolderRecursively(path: String) { + memScoped { + val attributes = GetFileAttributesW(path) + if (attributes == INVALID_FILE_ATTRIBUTES || attributes and FILE_ATTRIBUTE_DIRECTORY.toUInt() == 0u) { + return + } + + // Proceed to delete the folder + val shFileOp = alloc() + + shFileOp.hwnd = null + shFileOp.wFunc = FO_DELETE.toUInt() + shFileOp.pFrom = path.wcstr.ptr + shFileOp.pTo = null + shFileOp.fFlags = (FOF_NOCONFIRMATION or FOF_SILENT or FOF_NOERRORUI).toUShort() + + val result = SHFileOperationW(shFileOp.ptr) + if (result != 0) { + val errorMessage = strerror(result)?.toKString() ?: "Unknown error" + error("Error deleting folder: $errorMessage (code: $result)") + } + } +} + +actual fun Path.createDirectories(): Path { + val parts = toString().split("\\") + var currentPath = "" + + for (part in parts) { + if (part.isEmpty()) continue // Skip empty parts + currentPath += if (currentPath.isEmpty()) part else "\\$part" + + // Check if the directory exists + val attributes = GetFileAttributesW(currentPath) + val directoryExists = attributes != INVALID_FILE_ATTRIBUTES && (attributes and FILE_ATTRIBUTE_DIRECTORY.convert()) != 0u + + if (!directoryExists) { + // Create the directory + if (CreateDirectoryW(currentPath, null) == 0) { // Failed to create directory + val error = GetLastError() + error("Failed to create directory: $currentPath, Error: $error") + } + } + } + return this +} + +actual fun Path.resolve(path: String): Path = Path(toString() + "\\" + path) diff --git a/stores/leveldb/src/nativeDesktopTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.nativeDesktop.kt b/stores/leveldb/src/nativeDesktopTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.nativeDesktop.kt new file mode 100644 index 0000000..02b1c2d --- /dev/null +++ b/stores/leveldb/src/nativeDesktopTest/kotlin/kotlinx/document/store/tests/leveldb/LeveldbTests.nativeDesktop.kt @@ -0,0 +1,6 @@ +package kotlinx.document.store.tests.leveldb + +import kotlinx.cinterop.toKString + +actual val DB_PATH: String + get() = platform.posix.getenv("DB_PATH")?.toKString() ?: error("DB_PATH not set") diff --git a/stores/mvstore/build.gradle.kts b/stores/mvstore/build.gradle.kts index 4a34166..692b6f3 100644 --- a/stores/mvstore/build.gradle.kts +++ b/stores/mvstore/build.gradle.kts @@ -1,7 +1,6 @@ plugins { - convention - kotlin("jvm") - kotlin("plugin.serialization") + `publishing-convention` + `kotlin-jvm-convention` } dependencies { @@ -14,3 +13,7 @@ dependencies { testImplementation(libs.kotlinx.datetime) testImplementation(kotlin("test-junit5")) } + +publishing.publications.register("main") { + from(components["kotlin"]) +} diff --git a/stores/mvstore/src/main/kotlin/kotlinx/document/database/mvstore/MVDataStore.kt b/stores/mvstore/src/main/kotlin/kotlinx/document/database/mvstore/MVDataStore.kt deleted file mode 100644 index 2771c16..0000000 --- a/stores/mvstore/src/main/kotlin/kotlinx/document/database/mvstore/MVDataStore.kt +++ /dev/null @@ -1,76 +0,0 @@ -package kotlinx.document.database.mvstore - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.document.database.DataStore -import kotlinx.document.database.PersistentMap -import org.h2.mvstore.MVStore -import java.nio.file.Path -import kotlin.io.path.absolutePathString - -public class MVDataStore private constructor( - internal val delegate: MVStore, - override val commitStrategy: DataStore.CommitStrategy, -) : DataStore { - private val scope by lazy { CoroutineScope(SupervisorJob()) } - - init { - if (commitStrategy is DataStore.CommitStrategy.Periodic) { - scope.launch { - while (true) { - delay(commitStrategy.interval) - commit() - } - } - } - } - - public companion object { - public fun open( - path: Path, - commitStrategy: DataStore.CommitStrategy, - ): MVDataStore = - MVStore - .Builder() - .fileName(path.absolutePathString()) - .autoCommitDisabled() - .open() - .let { - MVDataStore( - delegate = it, - commitStrategy = commitStrategy, - ) - } - } - - override suspend fun getMap(name: String): PersistentMap = - MVPersistentMap( - delegate = withContext(Dispatchers.IO) { delegate.openMap(name) }, - commitFunction = if (commitStrategy is DataStore.CommitStrategy.OnChange) ::commit else null, - ) - - override suspend fun deleteMap(name: String) { - withContext(Dispatchers.IO) { delegate.removeMap(name) } - } - - override suspend fun close() { - withContext(Dispatchers.IO) { - delegate.close() - } - if (commitStrategy is DataStore.CommitStrategy.Periodic) { - scope.cancel() - } - } - - override suspend fun commit() { - withContext(Dispatchers.IO) { - delegate.commit() - delegate.sync() - } - } -} diff --git a/stores/mvstore/src/main/kotlin/kotlinx/document/store/mvstore/MVDataStore.kt b/stores/mvstore/src/main/kotlin/kotlinx/document/store/mvstore/MVDataStore.kt new file mode 100644 index 0000000..211c93b --- /dev/null +++ b/stores/mvstore/src/main/kotlin/kotlinx/document/store/mvstore/MVDataStore.kt @@ -0,0 +1,39 @@ +package kotlinx.document.store.mvstore + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.document.store.DataStore +import kotlinx.document.store.PersistentMap +import org.h2.mvstore.MVStore +import kotlin.io.path.absolutePathString +import java.nio.file.Path as JavaNioPath +import kotlin.io.path.Path as JavaNioPath +import kotlinx.io.files.Path as KotlinxIoPath + +public class MVDataStore(private val delegate: MVStore) : DataStore { + public companion object { + public fun open(path: String): MVDataStore = open(JavaNioPath(path)) + + public fun open(path: KotlinxIoPath): MVDataStore = open(path.toString()) + + public fun open(path: JavaNioPath): MVDataStore = + MVStore + .Builder() + .fileName(path.absolutePathString()) + .open() + .let { MVDataStore(it) } + } + + override suspend fun getMap(name: String): PersistentMap = + MVPersistentMap(delegate = withContext(Dispatchers.IO) { delegate.openMap(name) }) + + override suspend fun deleteMap(name: String) { + withContext(Dispatchers.IO) { delegate.removeMap(name) } + } + + override suspend fun close() { + withContext(Dispatchers.IO) { + delegate.close() + } + } +} diff --git a/stores/mvstore/src/main/kotlin/kotlinx/document/database/mvstore/MVPersistentMap.kt b/stores/mvstore/src/main/kotlin/kotlinx/document/store/mvstore/MVPersistentMap.kt similarity index 73% rename from stores/mvstore/src/main/kotlin/kotlinx/document/database/mvstore/MVPersistentMap.kt rename to stores/mvstore/src/main/kotlin/kotlinx/document/store/mvstore/MVPersistentMap.kt index 12663b1..7e0edfb 100644 --- a/stores/mvstore/src/main/kotlin/kotlinx/document/database/mvstore/MVPersistentMap.kt +++ b/stores/mvstore/src/main/kotlin/kotlinx/document/store/mvstore/MVPersistentMap.kt @@ -1,4 +1,4 @@ -package kotlinx.document.database.mvstore +package kotlinx.document.store.mvstore import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -7,14 +7,12 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import kotlinx.document.database.PersistentMap -import kotlinx.document.database.UpdateResult -import kotlinx.document.database.drop +import kotlinx.document.store.PersistentMap +import kotlinx.document.store.UpdateResult import org.h2.mvstore.MVMap public class MVPersistentMap( private val delegate: MVMap, - private val commitFunction: (suspend () -> Unit)?, ) : PersistentMap { override suspend fun get(key: K): V? = withContext(Dispatchers.IO) { delegate[key] } @@ -24,13 +22,11 @@ public class MVPersistentMap( ): V? = withContext(Dispatchers.IO) { delegate.put(key, value) - .also { commitFunction?.invoke() } } override suspend fun remove(key: K): V? = withContext(Dispatchers.IO) { delegate.remove(key) - .also { commitFunction?.invoke() } } override suspend fun containsKey(key: K): Boolean = withContext(Dispatchers.IO) { delegate.containsKey(key) } @@ -38,17 +34,15 @@ public class MVPersistentMap( override suspend fun clear(): Unit = withContext(Dispatchers.IO) { delegate.clear() - .also { commitFunction?.invoke() } } override suspend fun size(): Long = withContext(Dispatchers.IO) { delegate.sizeAsLong() } override suspend fun isEmpty(): Boolean = withContext(Dispatchers.IO) { delegate.isEmpty() } - override fun entries(fromIndex: Long): Flow> = + override fun entries(): Flow> = delegate.entries .asFlow() - .drop(fromIndex) .flowOn(Dispatchers.IO) private val mutex = Mutex() @@ -58,9 +52,7 @@ public class MVPersistentMap( defaultValue: () -> V, ): V = withContext(Dispatchers.IO) { - mutex.withLock { - delegate.getOrPut(key, defaultValue) - }.also { commitFunction?.invoke() } + mutex.withLock { delegate.getOrPut(key, defaultValue) } } override suspend fun update( @@ -74,8 +66,6 @@ public class MVPersistentMap( val newValue = oldValue?.let(updater) ?: value delegate[key] = newValue UpdateResult(oldValue, newValue) - }.also { commitFunction?.invoke() } + } } - - override fun close() {} } diff --git a/stores/mvstore/src/test/kotlin/kotlinx/document/database/tests/mvstore/MVStoreTests.kt b/stores/mvstore/src/test/kotlin/kotlinx/document/database/tests/mvstore/MVStoreTests.kt deleted file mode 100644 index 35eeea0..0000000 --- a/stores/mvstore/src/test/kotlin/kotlinx/document/database/tests/mvstore/MVStoreTests.kt +++ /dev/null @@ -1,92 +0,0 @@ -@file:Suppress("unused") - -package kotlinx.document.database.tests.mvstore - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.document.database.DataStore -import kotlinx.document.database.mvstore.MVDataStore -import kotlinx.document.database.tests.AbstractCacheOverflowTests -import kotlinx.document.database.tests.AbstractDeleteTests -import kotlinx.document.database.tests.AbstractDocumentDatabaseTests -import kotlinx.document.database.tests.AbstractFindTests -import kotlinx.document.database.tests.AbstractIndexTests -import kotlinx.document.database.tests.AbstractInsertTests -import kotlinx.document.database.tests.AbstractObjectCollectionTests -import kotlinx.document.database.tests.AbstractOnChangeCommitStrategyTests -import kotlinx.document.database.tests.AbstractUpdateTests -import kotlinx.document.database.tests.DatabaseDeleter -import java.io.File -import kotlin.io.path.Path -import kotlin.io.path.deleteIfExists - -class MVStoreDeleteTests : - AbstractDeleteTests(MVDataStore.open(Path(DB_PATH), DataStore.CommitStrategy.OnChange)), - DatabaseDeleter by MVStoreDeleter - -class MVStoreDocumentDatabaseTests : - AbstractDocumentDatabaseTests(MVDataStore.open(Path(DB_PATH), DataStore.CommitStrategy.OnChange)), - DatabaseDeleter by MVStoreDeleter - -class MVStoreIndexTests : - AbstractIndexTests(MVDataStore.open(Path(DB_PATH), DataStore.CommitStrategy.OnChange)), - DatabaseDeleter by MVStoreDeleter - -class MVStoreInsertTests : - AbstractInsertTests(MVDataStore.open(Path(DB_PATH), DataStore.CommitStrategy.OnChange)), - DatabaseDeleter by MVStoreDeleter - -class MVStoreUpdateTests : - AbstractUpdateTests(MVDataStore.open(Path(DB_PATH), DataStore.CommitStrategy.OnChange)), - DatabaseDeleter by MVStoreDeleter - -class MVStoreFindTests : - AbstractFindTests(MVDataStore.open(Path(DB_PATH), DataStore.CommitStrategy.OnChange)), - DatabaseDeleter by MVStoreDeleter - -class MVStoreObjectCollectionTests : - AbstractObjectCollectionTests(MVDataStore.open(Path(DB_PATH), DataStore.CommitStrategy.OnChange)), - DatabaseDeleter by MVStoreDeleter - -// todo flaky -// class MVStorePeriodicCommitStrategyTests : -// AbstractPeriodicCommitStrategyTests( -// MVDataStore.open( -// Path(DB_PATH), -// DataStore.CommitStrategy.Periodic(commitInterval), -// ), -// ), -// DatabaseDeleter by MVStoreDeleter { -// override fun getUnsavedMemory() = (store as MVDataStore).delegate.unsavedMemory -// -// override fun getTotalCacheMemorySize(): Int = (store as MVDataStore).delegate.autoCommitMemory -// } - -class MVStoreOnChangeCommitStrategyTests : - AbstractOnChangeCommitStrategyTests(store = MVDataStore.open(Path(DB_PATH), DataStore.CommitStrategy.OnChange)), - DatabaseDeleter by MVStoreDeleter { - override fun getStoreFileSize(): Long = File(DB_PATH).listFiles()?.sumOf { it.length() } ?: File(DB_PATH).length() -} - -class MVStoreCacheOverflowTests : - AbstractCacheOverflowTests( - MVDataStore.open( - Path(DB_PATH), - DataStore.CommitStrategy.Periodic(commitInterval), - ), - ), - DatabaseDeleter by MVStoreDeleter { - override fun getUnsavedMemory() = (store as MVDataStore).delegate.unsavedMemory - - override fun getTotalCacheMemorySize(): Int = (store as MVDataStore).delegate.autoCommitMemory -} - -object MVStoreDeleter : DatabaseDeleter { - override suspend fun deleteDatabase() { - withContext(Dispatchers.IO) { - Path(DB_PATH).deleteIfExists() - } - } -} - -val DB_PATH: String by System.getenv() diff --git a/stores/mvstore/src/test/kotlin/kotlinx/document/store/tests/mvstore/MVStoreTests.kt b/stores/mvstore/src/test/kotlin/kotlinx/document/store/tests/mvstore/MVStoreTests.kt new file mode 100644 index 0000000..cf9936e --- /dev/null +++ b/stores/mvstore/src/test/kotlin/kotlinx/document/store/tests/mvstore/MVStoreTests.kt @@ -0,0 +1,66 @@ +@file:Suppress("unused") + +package kotlinx.document.store.tests.mvstore + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.document.store.mvstore.MVDataStore +import kotlinx.document.store.tests.AbstractDeleteTests +import kotlinx.document.store.tests.AbstractDocumentDatabaseTests +import kotlinx.document.store.tests.AbstractFindTests +import kotlinx.document.store.tests.AbstractIndexTests +import kotlinx.document.store.tests.AbstractInsertTests +import kotlinx.document.store.tests.AbstractObjectCollectionTests +import kotlinx.document.store.tests.AbstractUpdateTests +import kotlinx.document.store.tests.DataStoreProvider +import kotlinx.document.store.tests.DatabaseDeleter +import kotlin.io.path.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteRecursively + +object MVDataStoreProvider : DataStoreProvider, DatabaseDeleter { + private fun getDbPath(testName: String) = Path(DB_PATH).resolve("$testName.mv.db") + + override fun provide(testName: String) = + MVDataStore.open( + getDbPath(testName) + .apply { parent.createDirectories() } + .absolutePathString(), + ) + + override suspend fun deleteDatabase(testName: String) = + withContext(Dispatchers.IO) { + getDbPath(testName).deleteRecursively() + } +} + +class MVStoreDeleteTests : + AbstractDeleteTests(MVDataStoreProvider), + DatabaseDeleter by MVDataStoreProvider + +class MVStoreDocumentDatabaseTests : + AbstractDocumentDatabaseTests(MVDataStoreProvider), + DatabaseDeleter by MVDataStoreProvider + +class MVStoreIndexTests : + AbstractIndexTests(MVDataStoreProvider), + DatabaseDeleter by MVDataStoreProvider + +class MVStoreInsertTests : + AbstractInsertTests(MVDataStoreProvider), + DatabaseDeleter by MVDataStoreProvider + +class MVStoreUpdateTests : + AbstractUpdateTests(MVDataStoreProvider), + DatabaseDeleter by MVDataStoreProvider + +class MVStoreFindTests : + AbstractFindTests(MVDataStoreProvider), + DatabaseDeleter by MVDataStoreProvider + +class MVStoreObjectCollectionTests : + AbstractObjectCollectionTests(MVDataStoreProvider), + DatabaseDeleter by MVDataStoreProvider + +val DB_PATH: String by System.getenv() diff --git a/stores/rocksdb/build.gradle.kts b/stores/rocksdb/build.gradle.kts deleted file mode 100644 index edea744..0000000 --- a/stores/rocksdb/build.gradle.kts +++ /dev/null @@ -1,32 +0,0 @@ -plugins { - kotlin("multiplatform") - convention -} - -kotlin { - - jvm() - - if (System.getenv("ENABLE_ROCKSDB_NATIVE") == "true") { - macosArm64() - macosX64() - iosArm64() - iosX64() - } - - sourceSets { - commonMain { - dependencies { - api(projects.core) - api(libs.rocksdb.multiplatform) - api(libs.kotlinx.io.core) - } - } - - commonTest { - dependencies { - implementation(projects.tests) - } - } - } -} diff --git a/stores/rocksdb/src/commonMain/kotlin/kotlinx/document/database/rocksdb/RocksdbDataStore.kt b/stores/rocksdb/src/commonMain/kotlin/kotlinx/document/database/rocksdb/RocksdbDataStore.kt deleted file mode 100644 index 851bad0..0000000 --- a/stores/rocksdb/src/commonMain/kotlin/kotlinx/document/database/rocksdb/RocksdbDataStore.kt +++ /dev/null @@ -1,105 +0,0 @@ -package kotlinx.document.database.rocksdb - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.document.database.DataStore -import kotlinx.document.database.PersistentMap -import maryk.rocksdb.Options -import maryk.rocksdb.RocksDB -import maryk.rocksdb.openRocksDB -import maryk.rocksdb.use -import org.rocksdb.WriteOptions -import java.nio.file.Path -import kotlin.io.path.absolutePathString - -public class RocksdbDataStore( - internal val delegate: RocksDB, - override val commitStrategy: DataStore.CommitStrategy, -) : DataStore { - private val scope by lazy { CoroutineScope(SupervisorJob()) } - - init { - if (commitStrategy is DataStore.CommitStrategy.Periodic) { - scope.launch { - while (true) { - delay(commitStrategy.interval) - commit() - } - } - } - } - - public companion object { - public fun open( - path: Path, - commitStrategy: DataStore.CommitStrategy, - ): RocksdbDataStore = open(Options().setCreateIfMissing(true), path, commitStrategy) - - public fun open( - options: Options, - path: Path, - commitStrategy: DataStore.CommitStrategy, - ): RocksdbDataStore { - val datastore = - RocksdbDataStore( - delegate = openRocksDB(options = options, path = path.absolutePathString()), - commitStrategy = commitStrategy, - ) - WriteOptions().apply { - when (commitStrategy) { - is DataStore.CommitStrategy.OnChange -> { - setDisableWAL(false) // Enable Write Ahead Logging (auto commit) - setSync(true) - } - - is DataStore.CommitStrategy.Periodic -> { - setDisableWAL(true) // Disable Write Ahead Logging (manual commit) - setSync(false) - } - } - } - return datastore - } - } - - override suspend fun getMap(name: String): PersistentMap = RocksdbPersistentMap(delegate, name) - - override suspend fun deleteMap(name: String) { - delegate.deletePrefix(name) - } - - override suspend fun commit() { - withContext(Dispatchers.IO) { delegate.flushWal(true) } - } - - override suspend fun close() { - withContext(Dispatchers.IO) { - delegate.flushWal(true) - delegate.close() - } - if (commitStrategy is DataStore.CommitStrategy.Periodic) { - scope.cancel() - } - } -} - -public suspend fun RocksDB.deletePrefix(prefix: String): Unit = - withContext(Dispatchers.IO) { - newIterator().use { iterator -> - iterator.seek(prefix.encodeToByteArray()) - while (iterator.isValid()) { - val key = iterator.key() - val keyString = key.decodeToString() - when { - keyString.startsWith(prefix) -> delete(key) - else -> break - } - iterator.next() - } - } - } diff --git a/stores/rocksdb/src/commonMain/kotlin/kotlinx/document/database/rocksdb/RocksdbPersistentMap.kt b/stores/rocksdb/src/commonMain/kotlin/kotlinx/document/database/rocksdb/RocksdbPersistentMap.kt deleted file mode 100644 index fe32a64..0000000 --- a/stores/rocksdb/src/commonMain/kotlin/kotlinx/document/database/rocksdb/RocksdbPersistentMap.kt +++ /dev/null @@ -1,128 +0,0 @@ -package kotlinx.document.database.rocksdb - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import kotlinx.document.database.PersistentMap -import kotlinx.document.database.SerializableEntry -import kotlinx.document.database.UpdateResult -import maryk.rocksdb.RocksDB -import maryk.rocksdb.use - -public class RocksdbPersistentMap( - private val delegate: RocksDB, - private val prefix: String, -) : PersistentMap { - private val mutex = Mutex() - - private fun String.prefixed() = "${prefix}_$this".encodeToByteArray() - - override suspend fun get(key: String): String? = - withContext(Dispatchers.IO) { - delegate[key.prefixed()]?.decodeToString() - } - - override suspend fun put( - key: String, - value: String, - ): String? = mutex.withLock { unsafePut(key, value) } - - private suspend fun unsafePut( - key: String, - value: String, - ): String? = - withContext(Dispatchers.IO) { - val prefixed = key.prefixed() - val previous = delegate[prefixed]?.decodeToString() - delegate.put(prefixed, value.encodeToByteArray()) - previous - } - - override suspend fun remove(key: String): String? = - mutex.withLock { - withContext(Dispatchers.IO) { - val prefixed = key.prefixed() - val previous = delegate[prefixed]?.decodeToString() - delegate.delete(prefixed) - previous - } - } - - override suspend fun containsKey(key: String): Boolean = get(key) != null - - override suspend fun clear(): Unit = delegate.deletePrefix(prefix) - - override suspend fun size(): Long = - mutex.withLock { - var count = 0L - withContext(Dispatchers.IO) { - delegate.newIterator().use { - it.seek(prefix.encodeToByteArray()) - while (it.isValid() && it.key().decodeToString().startsWith(prefix)) { - count++ - it.next() - } - } - count - } - } - - override suspend fun isEmpty(): Boolean = size() == 0L - - override fun entries(fromIndex: Long): Flow> = - flow { - delegate.newIterator().use { - it.seek("${prefix}_".encodeToByteArray()) - var dropped = 0L - while (it.isValid()) { - // check if we need to skip some entries - if (dropped++ < fromIndex) { - it.next() - continue - } - - val key = it.key().decodeToString() - when { - key.startsWith(prefix) -> - emit( - SerializableEntry( - key = key.removePrefix("${prefix}_"), - value = it.value().decodeToString(), - ), - ) - - else -> break - } - it.next() - } - } - }.flowOn(Dispatchers.IO) - - override suspend fun getOrPut( - key: String, - defaultValue: () -> String, - ): String = - mutex.withLock { - withContext(Dispatchers.IO) { - delegate[key.prefixed()]?.decodeToString() ?: defaultValue().also { unsafePut(key, it) } - } - } - - override suspend fun update( - key: String, - value: String, - updater: (String) -> String, - ): UpdateResult = - mutex.withLock { - withContext(Dispatchers.IO) { - val previous = delegate[key.prefixed()]?.decodeToString() - val newValue = previous?.let(updater) ?: value - unsafePut(key, newValue) - UpdateResult(previous, newValue) - } - } -} diff --git a/stores/rocksdb/src/commonTest/kotlin/kotlinx/document/database/tests/rocksdb/RocksdbTests.kt b/stores/rocksdb/src/commonTest/kotlin/kotlinx/document/database/tests/rocksdb/RocksdbTests.kt deleted file mode 100644 index fa56046..0000000 --- a/stores/rocksdb/src/commonTest/kotlin/kotlinx/document/database/tests/rocksdb/RocksdbTests.kt +++ /dev/null @@ -1,69 +0,0 @@ -@file:Suppress("unused") - -package kotlinx.document.database.tests.rocksdb - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.document.database.DataStore -import kotlinx.document.database.rocksdb.RocksdbDataStore -import kotlinx.document.database.tests.AbstractDeleteTests -import kotlinx.document.database.tests.AbstractDocumentDatabaseTests -import kotlinx.document.database.tests.AbstractFindTests -import kotlinx.document.database.tests.AbstractIndexTests -import kotlinx.document.database.tests.AbstractInsertTests -import kotlinx.document.database.tests.AbstractObjectCollectionTests -import kotlinx.document.database.tests.AbstractOnChangeCommitStrategyTests -import kotlinx.document.database.tests.AbstractUpdateTests -import kotlinx.document.database.tests.DatabaseDeleter -import java.io.File -import kotlin.io.path.Path -import kotlin.io.path.deleteRecursively - -class RocksdbDeleteTests : - AbstractDeleteTests(RocksdbDataStore.open(Path(DB_PATH), DataStore.CommitStrategy.OnChange)), - DatabaseDeleter by RocksdbDeleter - -class RocksdbDocumentDatabaseTests : - AbstractDocumentDatabaseTests(RocksdbDataStore.open(Path(DB_PATH), DataStore.CommitStrategy.OnChange)), - DatabaseDeleter by RocksdbDeleter - -class RocksdbIndexTests : - AbstractIndexTests(RocksdbDataStore.open(Path(DB_PATH), DataStore.CommitStrategy.OnChange)), - DatabaseDeleter by RocksdbDeleter - -class RocksdbInsertTests : - AbstractInsertTests(RocksdbDataStore.open(Path(DB_PATH), DataStore.CommitStrategy.OnChange)), - DatabaseDeleter by RocksdbDeleter - -class RocksdbUpdateTests : - AbstractUpdateTests(RocksdbDataStore.open(Path(DB_PATH), DataStore.CommitStrategy.OnChange)), - DatabaseDeleter by RocksdbDeleter - -class RocksdbFindTests : - AbstractFindTests(RocksdbDataStore.open(Path(DB_PATH), DataStore.CommitStrategy.OnChange)), - DatabaseDeleter by RocksdbDeleter - -class RocksdbObjectCollectionTests : - AbstractObjectCollectionTests(RocksdbDataStore.open(Path(DB_PATH), DataStore.CommitStrategy.OnChange)), - DatabaseDeleter by RocksdbDeleter - -class RocksdbOnChangeCommitStrategyTests : - AbstractOnChangeCommitStrategyTests( - RocksdbDataStore.open(Path(DB_PATH), DataStore.CommitStrategy.OnChange), - ), - DatabaseDeleter by RocksdbDeleter { - override fun getStoreFileSize(): Long = File(DB_PATH).listFiles()?.sumOf { it.length() } ?: File(DB_PATH).length() -} - -// TODO implement RocksdbCommitStrategies Tests - -object RocksdbDeleter : DatabaseDeleter { - override suspend fun deleteDatabase() = - withContext(Dispatchers.IO) { - Path(DB_PATH).toRealPath() - }.deleteRecursively() -} - -// expect suspend fun Path.deleteRecursively() - -expect val DB_PATH: String diff --git a/stores/rocksdb/src/jvmTest/kotlin/kotlinx/document/database/tests/rocksdb/RocksdbTests.jvm.kt b/stores/rocksdb/src/jvmTest/kotlin/kotlinx/document/database/tests/rocksdb/RocksdbTests.jvm.kt deleted file mode 100644 index 68bbd0b..0000000 --- a/stores/rocksdb/src/jvmTest/kotlin/kotlinx/document/database/tests/rocksdb/RocksdbTests.jvm.kt +++ /dev/null @@ -1,3 +0,0 @@ -package kotlinx.document.database.tests.rocksdb - -actual val DB_PATH: String by System.getenv() diff --git a/stores/rocksdb/src/nativeTest/kotlin/kotlinx/document/database/tests/rocksdb/RocksdbTests.native.kt b/stores/rocksdb/src/nativeTest/kotlin/kotlinx/document/database/tests/rocksdb/RocksdbTests.native.kt deleted file mode 100644 index c277a58..0000000 --- a/stores/rocksdb/src/nativeTest/kotlin/kotlinx/document/database/tests/rocksdb/RocksdbTests.native.kt +++ /dev/null @@ -1,62 +0,0 @@ -package kotlinx.document.database.tests.rocksdb - -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.pointed -import kotlinx.cinterop.toKString -import kotlinx.cinterop.toKStringFromUtf8 -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.IO -import kotlinx.coroutines.withContext -import kotlinx.io.files.Path -import platform.posix.DT_DIR -import platform.posix.closedir -import platform.posix.getenv -import platform.posix.opendir -import platform.posix.readdir -import platform.posix.remove -import platform.posix.rmdir - -actual val DB_PATH: String - get() = getenv("DB_PATH")?.toKStringFromUtf8() ?: error("DB_PATH not set") - -actual suspend fun Path.deleteRecursively() = - withContext(Dispatchers.IO) { - deleteFolderRecursively(this@deleteRecursively.toString()) - } - -fun deleteFolderRecursively(path: String) { - // Create a memory scope to manage memory allocation and deallocation automatically - memScoped { - // Open the directory specified by the path - val dir = opendir(path) ?: return@memScoped // If the directory can't be opened, exit the scope - println("Deleting folder $path") - try { - while (true) { - // Read the next entry in the directory - val entry = readdir(dir) ?: break // If there are no more entries, exit the loop - // Get the name of the entry - val name = entry.pointed.d_name.toKString() - // Skip the special entries "." and ".." which refer to the current and parent directories - if (name == "." || name == "..") continue - - // Construct the full path of the entry - val fullPath = "$path/$name" - // Check if the entry is a directory - if (entry.pointed.d_type.toInt() == DT_DIR) { - // If it's a directory, call this function recursively to delete its contents - deleteFolderRecursively(fullPath) - } else { - // If it's a file, delete it - println("Deleting $name") - remove(fullPath) - } - } - } finally { - // Close the directory to free resources - closedir(dir) - } - // After all contents have been processed, remove the directory itself - rmdir(path) - println("Deleted folder $path") - } -} diff --git a/tests/build.gradle.kts b/tests/build.gradle.kts index f3bc925..e890940 100644 --- a/tests/build.gradle.kts +++ b/tests/build.gradle.kts @@ -1,27 +1,41 @@ @file:Suppress("OPT_IN_USAGE") -import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode - plugins { - convention - kotlin("multiplatform") - kotlin("plugin.serialization") + `publishing-convention` + `kotlin-multiplatform-convention` kotlin("plugin.power-assert") } kotlin { - jvm() js { browser() } + + mingwX64() + + linuxX64() + linuxArm64() + macosArm64() macosX64() + iosArm64() iosX64() iosSimulatorArm64() - explicitApi = ExplicitApiMode.Disabled + watchosArm64() + watchosX64() + watchosSimulatorArm64() + + tvosArm64() + tvosX64() + tvosSimulatorArm64() + + androidNativeX64() + androidNativeX86() + androidNativeArm64() + androidNativeArm32() sourceSets { @@ -36,21 +50,13 @@ kotlin { jvmMain { dependencies { + api(libs.junit.jupiter.api) api(kotlin("test-junit5")) } } - - jsMain { - dependencies { - api(kotlin("test-js")) - } - } } } powerAssert { - functions = - setOf( - "kotlin.test.assertEquals", - ) + functions = setOf("kotlin.test.assertEquals") } diff --git a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractCacheOverflowTests.kt b/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractCacheOverflowTests.kt deleted file mode 100644 index 4b91882..0000000 --- a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractCacheOverflowTests.kt +++ /dev/null @@ -1,52 +0,0 @@ -package kotlinx.document.database.tests - -import kotlinx.document.database.DataStore -import kotlinx.document.database.getObjectCollection -import kotlin.js.JsName -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue -import kotlin.time.Duration.Companion.days - -abstract class AbstractCacheOverflowTests(val store: DataStore) : BaseTest(store) { - companion object { - val commitInterval = 1.days - } - - abstract fun getUnsavedMemory(): Int - - abstract fun getTotalCacheMemorySize(): Int - - @Test - @JsName("cache_is_flushed_when_memory_is_full") - fun `test if Cache Is flushed when memory is full`() = - runDatabaseTest { - assertTrue( - actual = store.commitStrategy is DataStore.CommitStrategy.Periodic, - message = "Commit policy should be periodic", - ) - assertEquals( - expected = 1.days, - actual = (store.commitStrategy as DataStore.CommitStrategy.Periodic).interval, - message = "Commit interval should be set to 1 day for this test", - ) - - val memorySize = getTotalCacheMemorySize() - val collection = db.getObjectCollection("test") - - var lastAvailableMemory = getTotalCacheMemorySize() - getUnsavedMemory() - val testUsers = TestUser.generateUsers(1000) - - while (true) { - testUsers.forEach { collection.insert(it) } - val actualAvailableMemory = memorySize - getUnsavedMemory() - - if (lastAvailableMemory < actualAvailableMemory) { - assertTrue(lastAvailableMemory < actualAvailableMemory) - break - } - assertTrue(lastAvailableMemory >= actualAvailableMemory) - lastAvailableMemory = actualAvailableMemory - } - } -} diff --git a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractObjectCollectionTests.kt b/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractObjectCollectionTests.kt deleted file mode 100644 index ccc35ef..0000000 --- a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractObjectCollectionTests.kt +++ /dev/null @@ -1,74 +0,0 @@ -package kotlinx.document.database.tests - -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.document.database.DataStore -import kotlinx.document.database.getObjectCollection -import kotlinx.document.database.tests.TestUser.Companion.Luigi -import kotlinx.document.database.tests.TestUser.Companion.Mario -import kotlinx.document.database.updateWhere -import kotlin.js.JsName -import kotlin.test.Test -import kotlin.test.assertFails -import kotlin.time.Duration.Companion.seconds - -abstract class AbstractObjectCollectionTests(store: DataStore) : BaseTest(store) { - @Test - @JsName("gets_all_collection_names") - fun `Fails if collection type is not serializable`() = - runDatabaseTest { - assertFails { - val collection = db.getObjectCollection<() -> Unit>("test") - collection.insert { } - } - } - - @Test - @JsName("fails_if_collection_type_is_primitive") - fun `Fails if collection type is primitive`() = - runDatabaseTest { - val collection = db.getObjectCollection("test") - assertFails { collection.insert(1L) } - } - - @Test - @JsName("fails_if_collection_type_is_array_like") - fun `Fails if collection type is array-like`() = - runDatabaseTest { - val collection = db.getObjectCollection>("test") - assertFails { collection.insert(listOf(Mario, Luigi)) } - } - - @Test - @JsName("Concurrent_modification") - fun `Concurrent modification`() = - runDatabaseTest { - val collection = db.getObjectCollection("test") - collection.insert(Mario) - - val mutex = Mutex(true) - - launch { - collection.updateWhere( - TestUser::name.name, - Mario.name, - ) { - mutex.lock() - it - } - } - - launch { - delay(2.seconds) - mutex.unlock() - } - - collection.updateWhere( - TestUser::name.name, - Mario.name, - ) { - it - } - } -} diff --git a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractOnChangeCommitStrategyTests.kt b/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractOnChangeCommitStrategyTests.kt deleted file mode 100644 index c6c6f89..0000000 --- a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractOnChangeCommitStrategyTests.kt +++ /dev/null @@ -1,32 +0,0 @@ -package kotlinx.document.database.tests - -import kotlinx.document.database.DataStore -import kotlinx.document.database.getObjectCollection -import kotlin.js.JsName -import kotlin.test.Test -import kotlin.test.assertTrue - -abstract class AbstractOnChangeCommitStrategyTests(val store: DataStore) : BaseTest(store) { - abstract fun getStoreFileSize(): Long - - @Test - @JsName("cache_is_periodically_flushed") - fun `test if insert are directly committed on disk`() = - runDatabaseTest { - assertTrue( - actual = store.commitStrategy is DataStore.CommitStrategy.OnChange, - message = "Commit policy should be OnChange", - ) - - val collection = db.getObjectCollection("test") - - var fileSize = getStoreFileSize() - - TestUser.generateUsers(100).forEach { - collection.insert(it) - val actualFileSize = getStoreFileSize() - assertTrue(actualFileSize > fileSize) - fileSize = actualFileSize - } - } -} diff --git a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractPeriodicCommitStrategyTests.kt b/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractPeriodicCommitStrategyTests.kt deleted file mode 100644 index abd829a..0000000 --- a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractPeriodicCommitStrategyTests.kt +++ /dev/null @@ -1,51 +0,0 @@ -package kotlinx.document.database.tests - -import kotlinx.document.database.DataStore -import kotlinx.document.database.getObjectCollection -import kotlin.js.JsName -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue -import kotlin.time.Duration.Companion.seconds -import kotlin.time.TimeSource - -abstract class AbstractPeriodicCommitStrategyTests(val store: DataStore) : BaseTest(store) { - companion object { - val commitInterval = 3.seconds - } - - abstract fun getUnsavedMemory(): Int - - abstract fun getTotalCacheMemorySize(): Int - - @Test - @JsName("cache_is_periodically_flushed") - fun `test if Cache Is Periodically Flushed correctly`() = - runDatabaseTest { - assertTrue( - actual = store.commitStrategy is DataStore.CommitStrategy.Periodic, - message = "Commit policy should be periodic", - ) - assertEquals( - expected = commitInterval, - actual = (store.commitStrategy as DataStore.CommitStrategy.Periodic).interval, - message = "Commit interval should be taken from the companion object", - ) - - val collection = db.getObjectCollection("test") - - val initialUnsavedMemorySize = getUnsavedMemory().also(::println) - TestUser.generateUsers(100).forEach { collection.insert(it) } - val afterOpUnsavedMemorySize = getUnsavedMemory().also(::println) - assertTrue("Memory cache size does not increase as expected") { - afterOpUnsavedMemorySize > initialUnsavedMemorySize - } - - val start = TimeSource.Monotonic.markNow() - - while (start.elapsedNow() < commitInterval) { // wait for the commit interval - } - - assertTrue("Memory cache size does not decrease as expected") { afterOpUnsavedMemorySize > getUnsavedMemory().also(::println) } - } -} diff --git a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/BaseTest.kt b/tests/src/commonMain/kotlin/kotlinx/document/database/tests/BaseTest.kt deleted file mode 100644 index f665d11..0000000 --- a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/BaseTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -package kotlinx.document.database.tests - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.test.runTest -import kotlinx.document.database.DataStore -import kotlinx.document.database.KotlinxDocumentDatabase -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds - -abstract class BaseTest(store: DataStore) : DatabaseDeleter { - val db = KotlinxDocumentDatabase(store) - - protected fun runDatabaseTest( - context: CoroutineContext = EmptyCoroutineContext, - timeout: Duration = 60.seconds, - testBody: suspend CoroutineScope.() -> Unit, - ) = runTest(context, timeout) { - try { - coroutineScope(testBody) - } finally { - db.close() - deleteDatabase() - } - } -} - -interface DatabaseDeleter { - suspend fun deleteDatabase() -} diff --git a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractDeleteTests.kt b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractDeleteTests.kt similarity index 68% rename from tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractDeleteTests.kt rename to tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractDeleteTests.kt index 45721e6..d00c05f 100644 --- a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractDeleteTests.kt +++ b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractDeleteTests.kt @@ -1,22 +1,33 @@ @file:Suppress("FunctionName") -package kotlinx.document.database.tests +package kotlinx.document.store.tests import kotlinx.coroutines.flow.count -import kotlinx.document.database.DataStore -import kotlinx.document.database.getObjectCollection -import kotlinx.document.database.removeWhere +import kotlinx.coroutines.test.TestResult +import kotlinx.document.store.getObjectCollection +import kotlinx.document.store.removeWhere import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.encodeToJsonElement import kotlin.js.JsName import kotlin.test.Test import kotlin.test.assertEquals -abstract class AbstractDeleteTests(store: DataStore) : BaseTest(store) { +public abstract class AbstractDeleteTests(store: DataStoreProvider) : BaseTest(store) { + public companion object { + public const val TEST_NAME_1: String = "deletes_collection" + public const val TEST_NAME_2: String = "deleting_a_collection_actually_clears_it" + public const val TEST_NAME_3: String = "deleting_a_collection_that_does_not_exist_does_nothing" + public const val TEST_NAME_4: String = "deletes_a_document_with_index" + public const val TEST_NAME_5: String = "deletes_a_document_using_selector_without_index" + public const val TEST_NAME_6: String = "deletes_a_document_using_selector_with_index" + public const val TEST_NAME_7: String = "deletes_a_document_using_complex_selector" + public const val TEST_NAME_8: String = "deletes_a_document_using_complex_selector_with_index" + } + @Test - @JsName("deletes_collection") - fun `deletes collection`() = - runDatabaseTest { + @JsName(TEST_NAME_1) + public fun `deletes collection`(): TestResult = + runDatabaseTest(TEST_NAME_1) { db -> db.getObjectCollection("test") db.deleteCollection("test") assertEquals( @@ -27,9 +38,9 @@ abstract class AbstractDeleteTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("deleting_a_collection_actually_clears_it") - fun `deleting a collection actually clears it`() = - runDatabaseTest { + @JsName(TEST_NAME_2) + public fun `deleting a collection actually clears it`(): TestResult = + runDatabaseTest(TEST_NAME_2) { db -> db.getObjectCollection("test").insert(TestUser.Mario) db.deleteCollection("test") assertEquals( @@ -40,9 +51,9 @@ abstract class AbstractDeleteTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("deleting_a_collection_that_does_not_exist_does_nothing") - fun `deleting a collection that does not exist does nothing`() = - runDatabaseTest { + @JsName(TEST_NAME_3) + public fun `deleting a collection that does not exist does nothing`(): TestResult = + runDatabaseTest(TEST_NAME_3) { db -> db.deleteCollection("test") assertEquals( expected = 0, @@ -52,9 +63,9 @@ abstract class AbstractDeleteTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("deletes_a_document_with_index") - fun `deletes a document with index`() = - runDatabaseTest { + @JsName(TEST_NAME_4) + public fun `deletes a document with index`(): TestResult = + runDatabaseTest(TEST_NAME_4) { db -> val collection = db.getObjectCollection("test") collection.createIndex("name") val marioWithId = collection.insert(TestUser.Mario) @@ -77,9 +88,9 @@ abstract class AbstractDeleteTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("deletes_a_document_using_selector_without_index") - fun `deletes a document using selector without index`() = - runDatabaseTest { + @JsName(TEST_NAME_5) + public fun `deletes a document using selector without index`(): TestResult = + runDatabaseTest(TEST_NAME_5) { db -> val collection = db.getObjectCollection("test") collection.insert(TestUser.Mario) collection.removeWhere( @@ -94,9 +105,9 @@ abstract class AbstractDeleteTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("deletes_a_document_using_selector_with_index") - fun `deletes a document using selector with index`() = - runDatabaseTest { + @JsName(TEST_NAME_6) + public fun `deletes a document using selector with index`(): TestResult = + runDatabaseTest(TEST_NAME_6) { db -> val collection = db.getObjectCollection("test") collection.createIndex("name") collection.insert(TestUser.Mario) @@ -121,9 +132,9 @@ abstract class AbstractDeleteTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("deletes_a_document_using_complex_selector") - fun `deletes a document using complex selector`() = - runDatabaseTest { + @JsName(TEST_NAME_7) + public fun `deletes a document using complex selector`(): TestResult = + runDatabaseTest(TEST_NAME_7) { db -> val collection = db.getObjectCollection("test") collection.insert(TestUser.Mario) collection.removeWhere( @@ -138,9 +149,9 @@ abstract class AbstractDeleteTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("deletes_a_document_using_complex_selector_with_index") - fun `deletes a document using complex selector with index`() = - runDatabaseTest { + @JsName(TEST_NAME_8) + public fun `deletes a document using complex selector with index`(): TestResult = + runDatabaseTest(TEST_NAME_8) { db -> val collection = db.getObjectCollection("test") collection.createIndex("addresses.$0") collection.insert(TestUser.Mario) diff --git a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractDocumentDatabaseTests.kt b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractDocumentDatabaseTests.kt similarity index 51% rename from tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractDocumentDatabaseTests.kt rename to tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractDocumentDatabaseTests.kt index a98b1f5..e41309a 100644 --- a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractDocumentDatabaseTests.kt +++ b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractDocumentDatabaseTests.kt @@ -1,19 +1,23 @@ @file:Suppress("FunctionName") -package kotlinx.document.database.tests +package kotlinx.document.store.tests import kotlinx.coroutines.flow.toList -import kotlinx.document.database.DataStore -import kotlinx.document.database.getObjectCollection +import kotlinx.coroutines.test.TestResult +import kotlinx.document.store.getObjectCollection import kotlin.js.JsName import kotlin.test.Test import kotlin.test.assertEquals -abstract class AbstractDocumentDatabaseTests(store: DataStore) : BaseTest(store) { +public abstract class AbstractDocumentDatabaseTests(store: DataStoreProvider) : BaseTest(store) { + public companion object { + public const val TEST_NAME: String = "gets_all_collection_names" + } + @Test - @JsName("gets_all_collection_names") - fun `gets all collection names`() = - runDatabaseTest { + @JsName(TEST_NAME) + public fun `gets all collection names`(): TestResult = + runDatabaseTest(TEST_NAME) { db -> db.getObjectCollection("test") db.getObjectCollection("test2") diff --git a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractFindTests.kt b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractFindTests.kt similarity index 57% rename from tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractFindTests.kt rename to tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractFindTests.kt index ab28fe0..9237b67 100644 --- a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractFindTests.kt +++ b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractFindTests.kt @@ -1,19 +1,26 @@ -package kotlinx.document.database.tests +@file:Suppress("FunctionName") + +package kotlinx.document.store.tests import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.single -import kotlinx.document.database.DataStore -import kotlinx.document.database.find -import kotlinx.document.database.getObjectCollection +import kotlinx.coroutines.test.TestResult +import kotlinx.document.store.find +import kotlinx.document.store.getObjectCollection import kotlin.js.JsName import kotlin.test.Test import kotlin.test.assertEquals -abstract class AbstractFindTests(store: DataStore) : BaseTest(store) { +public abstract class AbstractFindTests(store: DataStoreProvider) : BaseTest(store) { + public companion object { + public const val TEST_NAME_1: String = "finds_a_document_using_index" + public const val TEST_NAME_2: String = "finds_a_document_using_complex_index" + } + @Test - @JsName("finds_a_document_using_index") - fun `finds a document using index`() = - runDatabaseTest { + @JsName(TEST_NAME_1) + public fun `finds a document using index`(): TestResult = + runDatabaseTest(TEST_NAME_1) { db -> val collection = db.getObjectCollection("test") collection.createIndex("name") @@ -27,9 +34,9 @@ abstract class AbstractFindTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("finds_a_document_using_complex_index") - fun `finds a document using complex index`() = - runDatabaseTest { + @JsName(TEST_NAME_2) + public fun `finds a document using complex index`(): TestResult = + runDatabaseTest(TEST_NAME_2) { db -> val collection = db.getObjectCollection("test") collection.createIndex("addresses.$0") diff --git a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractIndexTests.kt b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractIndexTests.kt similarity index 77% rename from tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractIndexTests.kt rename to tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractIndexTests.kt index e4fb1d9..72c4c7d 100644 --- a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractIndexTests.kt +++ b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractIndexTests.kt @@ -1,10 +1,10 @@ @file:Suppress("FunctionName") -package kotlinx.document.database.tests +package kotlinx.document.store.tests import kotlinx.coroutines.flow.first -import kotlinx.document.database.DataStore -import kotlinx.document.database.getObjectCollection +import kotlinx.coroutines.test.TestResult +import kotlinx.document.store.getObjectCollection import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonPrimitive @@ -16,19 +16,25 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -abstract class AbstractIndexTests(store: DataStore) : BaseTest(store) { - companion object { - val json = +public abstract class AbstractIndexTests(store: DataStoreProvider) : BaseTest(store) { + public companion object { + public val json: Json = Json { prettyPrint = true allowStructuredMapKeys = true } + + public const val TEST_NAME_1: String = "id_is_correctly_increased_after_insert" + public const val TEST_NAME_2: String = "index_is_correctly_created_after_insert" + public const val TEST_NAME_3: String = "index_is_correctly_created_before_insert" + public const val TEST_NAME_4: String = "index_is_correctly_created_after_insert_and_update" + public const val TEST_NAME_5: String = "object_index_is_created_before_insert_test" } @Test - @JsName("id_is_correctly_increased_after_insert") - fun `id is correctly increased after insert`() = - runDatabaseTest { + @JsName(TEST_NAME_1) + public fun `id is correctly increased after insert`(): TestResult = + runDatabaseTest(TEST_NAME_1) { db -> val collection = db.getObjectCollection("test") collection.insert(TestUser.Mario) @@ -53,9 +59,9 @@ abstract class AbstractIndexTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("index_is_correctly_created_after_insert") - fun `index is correctly created after insert`() = - runDatabaseTest { + @JsName(TEST_NAME_2) + public fun `index is correctly created after insert`(): TestResult = + runDatabaseTest(TEST_NAME_2) { db -> val collection = db.getObjectCollection("test") val userId = collection.insert(TestUser.Mario).id ?: error("No id found") collection.createIndex("name") @@ -69,9 +75,9 @@ abstract class AbstractIndexTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("index_is_correctly_created_before_insert") - fun `index is correctly created before insert`() = - runDatabaseTest { + @JsName(TEST_NAME_3) + public fun `index is correctly created before insert`(): TestResult = + runDatabaseTest(TEST_NAME_3) { db -> val collection = db.getObjectCollection("test") collection.createIndex("name") val userId = collection.insert(TestUser.Mario).id ?: error("No id found") @@ -84,9 +90,9 @@ abstract class AbstractIndexTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("index_is_correctly_created_after_insert_and_update") - fun `index is correctly created after insert and update`() = - runDatabaseTest { + @JsName(TEST_NAME_4) + public fun `index is correctly created after insert and update`(): TestResult = + runDatabaseTest(TEST_NAME_4) { db -> val collection = db.getObjectCollection("test") val marioId = collection.insert(TestUser.Mario).id ?: error("No id found") collection.createIndex("name") @@ -109,9 +115,9 @@ abstract class AbstractIndexTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("object_index_is_created_before_insert_test") - fun `object index is created before insert test`() = - runDatabaseTest { + @JsName(TEST_NAME_5) + public fun `object index is created before insert test`(): TestResult = + runDatabaseTest(TEST_NAME_5) { db -> val collection = db.getObjectCollection("test") collection.createIndex("addresses.$0") collection.createIndex("addresses.$1") diff --git a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractInsertTests.kt b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractInsertTests.kt similarity index 70% rename from tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractInsertTests.kt rename to tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractInsertTests.kt index 94a2945..3c17830 100644 --- a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractInsertTests.kt +++ b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractInsertTests.kt @@ -1,20 +1,28 @@ -package kotlinx.document.database.tests +@file:Suppress("FunctionName") + +package kotlinx.document.store.tests import kotlinx.coroutines.flow.first -import kotlinx.document.database.DataStore -import kotlinx.document.database.find -import kotlinx.document.database.getObjectCollection +import kotlinx.coroutines.test.TestResult +import kotlinx.document.store.find +import kotlinx.document.store.getObjectCollection import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.encodeToJsonElement import kotlin.js.JsName import kotlin.test.Test import kotlin.test.assertEquals -abstract class AbstractInsertTests(store: DataStore) : BaseTest(store) { +public abstract class AbstractInsertTests(store: DataStoreProvider) : BaseTest(store) { + public companion object { + public const val TEST_NAME_1: String = "inserts_and_retrieves_a_document_without_index" + public const val TEST_NAME_2: String = "inserts_and_retrieves_a_document_with_index" + public const val TEST_NAME_3: String = "inserts_and_retrieves_a_document_using_complex_index" + } + @Test - @JsName("inserts_and_retrieves_a_document_without_index") - fun `inserts and retrieves a document without index`() = - runDatabaseTest { + @JsName(TEST_NAME_1) + public fun `inserts and retrieves a document without index`(): TestResult = + runDatabaseTest(TEST_NAME_1) { db -> val collection = db.getObjectCollection("test") val testUser = TestUser( @@ -33,9 +41,9 @@ abstract class AbstractInsertTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("inserts_and_retrieves_a_document_with_index") - fun `inserts and retrieves a document with index`() = - runDatabaseTest { + @JsName(TEST_NAME_2) + public fun `inserts and retrieves a document with index`(): TestResult = + runDatabaseTest(TEST_NAME_2) { db -> val collection = db.getObjectCollection("test") collection.createIndex("name") @@ -60,9 +68,9 @@ abstract class AbstractInsertTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("inserts_and_retrieves_a_document_using_complex_index") - fun `inserts and retrieves a document using complex index`() = - runDatabaseTest { + @JsName(TEST_NAME_3) + public fun `inserts and retrieves a document using complex index`(): TestResult = + runDatabaseTest(TEST_NAME_3) { db -> val collection = db.getObjectCollection("test") collection.createIndex("addresses.$0") diff --git a/tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractObjectCollectionTests.kt b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractObjectCollectionTests.kt new file mode 100644 index 0000000..b0c7039 --- /dev/null +++ b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractObjectCollectionTests.kt @@ -0,0 +1,84 @@ +@file:Suppress("FunctionName") + +package kotlinx.document.store.tests + +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.test.TestResult +import kotlinx.document.store.getObjectCollection +import kotlinx.document.store.tests.TestUser.Companion.Luigi +import kotlinx.document.store.tests.TestUser.Companion.Mario +import kotlinx.document.store.updateWhere +import kotlin.js.JsName +import kotlin.test.Test +import kotlin.test.assertFails +import kotlin.time.Duration.Companion.seconds + +public abstract class AbstractObjectCollectionTests(store: DataStoreProvider) : BaseTest(store) { + public companion object { + public const val TEST_NAME_1: String = "gets_all_collection_names" + public const val TEST_NAME_2: String = "fails_if_collection_type_is_not_serializable" + public const val TEST_NAME_3: String = "fails_if_collection_type_is_primitive" + public const val TEST_NAME_4: String = "fails_if_collection_type_is_array_like" + public const val TEST_NAME_5: String = "Concurrent_modification" + } + + @Test + @JsName(TEST_NAME_1) + public fun `Fails if collection type is not serializable`(): TestResult = + runDatabaseTest(TEST_NAME_1) { db -> + assertFails { + val collection = db.getObjectCollection<() -> Unit>("test") + collection.insert { } + } + } + + @Test + @JsName(TEST_NAME_2) + public fun `Fails if collection type is primitive`(): TestResult = + runDatabaseTest(TEST_NAME_2) { db -> + val collection = db.getObjectCollection("test") + assertFails { collection.insert(1L) } + } + + @Test + @JsName(TEST_NAME_3) + public fun `Fails if collection type is array-like`(): TestResult = + runDatabaseTest(TEST_NAME_3) { db -> + val collection = db.getObjectCollection>("test") + assertFails { collection.insert(listOf(Mario, Luigi)) } + } + + @Test + @JsName(TEST_NAME_4) + public fun `Concurrent modification`(): TestResult = + runDatabaseTest(TEST_NAME_4) { db -> + val collection = db.getObjectCollection("test") + collection.insert(Mario) + + val mutex = Mutex(true) + + launch { + collection.updateWhere( + fieldSelector = TestUser::name.name, + fieldValue = Mario.name, + ) { + mutex.lock() + it + } + } + + launch { + delay(2.seconds) + mutex.unlock() + } + + collection.updateWhere( + fieldSelector = TestUser::name.name, + fieldValue = Mario.name, + ) { + it + } + } +} diff --git a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractUpdateTests.kt b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractUpdateTests.kt similarity index 73% rename from tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractUpdateTests.kt rename to tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractUpdateTests.kt index a073972..d7ebfb7 100644 --- a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/AbstractUpdateTests.kt +++ b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/AbstractUpdateTests.kt @@ -1,20 +1,29 @@ -package kotlinx.document.database.tests +@file:Suppress("FunctionName") + +package kotlinx.document.store.tests import kotlinx.coroutines.flow.single -import kotlinx.document.database.DataStore -import kotlinx.document.database.getObjectCollection -import kotlinx.document.database.updateWhere +import kotlinx.coroutines.test.TestResult +import kotlinx.document.store.getObjectCollection +import kotlinx.document.store.updateWhere import kotlinx.serialization.json.JsonPrimitive import kotlin.js.JsName import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -abstract class AbstractUpdateTests(store: DataStore) : BaseTest(store) { +public abstract class AbstractUpdateTests(store: DataStoreProvider) : BaseTest(store) { + public companion object { + public const val TEST_NAME_1: String = "updates_a_document_without_index" + public const val TEST_NAME_2: String = "updates_a_document_with_index" + public const val TEST_NAME_3: String = "upsert_inserts_a_document_without_index" + public const val TEST_NAME_4: String = "upsert_inserts_a_document_with_index" + } + @Test - @JsName("updates_a_document_without_index") - fun `updates a document without index`() = - runDatabaseTest { + @JsName(TEST_NAME_1) + public fun `updates a document without index`(): TestResult = + runDatabaseTest(TEST_NAME_1) { db -> val collection = db.getObjectCollection("test") val marioWithId = collection.insert(TestUser.Mario) @@ -30,9 +39,9 @@ abstract class AbstractUpdateTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("updates_a_document_with_index") - fun `updates a document with index`() = - runDatabaseTest { + @JsName(TEST_NAME_2) + public fun `updates a document with index`(): TestResult = + runDatabaseTest(TEST_NAME_2) { db -> val collection = db.getObjectCollection("test") collection.createIndex("name") val marioWithId = collection.insert(TestUser.Mario) @@ -59,9 +68,9 @@ abstract class AbstractUpdateTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("upsert_inserts_a_document_without_index") - fun `upsert inserts a document without index`() = - runDatabaseTest { + @JsName(TEST_NAME_3) + public fun `upsert inserts a document without index`(): TestResult = + runDatabaseTest(TEST_NAME_3) { db -> val collection = db.getObjectCollection("test") collection.updateWhere( @@ -81,9 +90,9 @@ abstract class AbstractUpdateTests(store: DataStore) : BaseTest(store) { } @Test - @JsName("upsert_inserts_a_document_with_index") - fun `upsert inserts a document with index`() = - runDatabaseTest { + @JsName(TEST_NAME_4) + public fun `upsert inserts a document with index`(): TestResult = + runDatabaseTest(TEST_NAME_4) { db -> val collection = db.getObjectCollection("test") collection.createIndex("name") diff --git a/tests/src/commonMain/kotlin/kotlinx/document/store/tests/BaseTest.kt b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/BaseTest.kt new file mode 100644 index 0000000..48c9716 --- /dev/null +++ b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/BaseTest.kt @@ -0,0 +1,43 @@ +package kotlinx.document.store.tests + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.test.TestResult +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import kotlinx.document.store.DataStore +import kotlinx.document.store.KotlinxDocumentStore +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes + +public abstract class BaseTest(private val storeProvider: DataStoreProvider) : DatabaseDeleter { + protected fun runDatabaseTest( + testName: String, + context: CoroutineContext = EmptyCoroutineContext, + timeout: Duration = 1.minutes, + testBody: suspend CoroutineScope.(db: KotlinxDocumentStore) -> Unit, + ): TestResult = + runTest(context, timeout) { + deleteDatabase(testName) + val store = storeProvider.provide(testName) + val db = KotlinxDocumentStore(store) + try { + coroutineScope { testBody(db) } + } finally { + withContext(NonCancellable) { + db.close() + } + } + } +} + +public fun interface DataStoreProvider { + public fun provide(testName: String): DataStore +} + +public fun interface DatabaseDeleter { + public suspend fun deleteDatabase(testName: String) +} diff --git a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/TestUtils.kt b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/TestUtils.kt similarity index 84% rename from tests/src/commonMain/kotlin/kotlinx/document/database/tests/TestUtils.kt rename to tests/src/commonMain/kotlin/kotlinx/document/store/tests/TestUtils.kt index 1a3aae0..09d1e02 100644 --- a/tests/src/commonMain/kotlin/kotlinx/document/database/tests/TestUtils.kt +++ b/tests/src/commonMain/kotlin/kotlinx/document/store/tests/TestUtils.kt @@ -1,4 +1,4 @@ -package kotlinx.document.database.tests +package kotlinx.document.store.tests import kotlinx.datetime.Clock import kotlinx.datetime.Instant @@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable import kotlin.time.Duration.Companion.days @Serializable -data class TestUser( +public data class TestUser( val name: String, val age: Int, val isAdult: Boolean = true, @@ -15,8 +15,8 @@ data class TestUser( val addresses: List
= emptyList(), @SerialName("_id") val id: Long? = null, ) { - companion object { - val Mario = + public companion object { + public val Mario: TestUser = TestUser( name = "mario", age = 20, @@ -27,7 +27,7 @@ data class TestUser( Address("New York", 3), ), ) - val Luigi = + public val Luigi: TestUser = TestUser( name = "luigi", age = 20, @@ -39,7 +39,7 @@ data class TestUser( ), ) - fun generateUsers(count: Int): Sequence = + public fun generateUsers(count: Int): Sequence = sequence { var counter = 0 while (true) { @@ -51,7 +51,7 @@ data class TestUser( } @Serializable -data class Address( +public data class Address( val street: String, val number: Int, ) diff --git a/version-catalog/build.gradle.kts b/version-catalog/build.gradle.kts index f323e01..5f157a3 100644 --- a/version-catalog/build.gradle.kts +++ b/version-catalog/build.gradle.kts @@ -2,7 +2,7 @@ import kotlin.io.path.createDirectories import kotlin.io.path.writeText plugins { - convention + versions `version-catalog` `maven-publish` } diff --git a/version-catalog/libs.versions.toml b/version-catalog/libs.versions.toml index d40c45f..adf0267 100644 --- a/version-catalog/libs.versions.toml +++ b/version-catalog/libs.versions.toml @@ -1,10 +1,10 @@ [versions] -kotlinx-document-store="%%%VERSION%%%" +kotlin-document-store="%%%VERSION%%%" [libraries] -core = { module = "com.github.lamba92:kotlinx-document-store-core", version.ref = "kotlinx-document-store" } -mvstore = { module = "com.github.lamba92:kotlinx-document-store-mvstore", version.ref = "kotlinx-document-store" } -browser = { module = "com.github.lamba92:kotlinx-document-store-browser", version.ref = "kotlinx-document-store" } -rocksdb = { module = "com.github.lamba92:kotlinx-document-store-rocksdb", version.ref = "kotlinx-document-store" } - +core = { module = "com.github.lamba92:kotlin-document-store-core", version.ref = "kotlin-document-store" } +mvstore = { module = "com.github.lamba92:kotlin-document-store-mvstore", version.ref = "kotlin-document-store" } +browser = { module = "com.github.lamba92:kotlin-document-store-browser", version.ref = "kotlin-document-store" } +leveldb = { module = "com.github.lamba92:kotlin-document-store-leveldb", version.ref = "kotlin-document-store" } +tests = { module = "com.github.lamba92:kotlin-document-store-tests", version.ref = "kotlin-document-store" }