diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..32dd675 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,4 @@ +FROM mcr.microsoft.com/devcontainers/java:21 + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends libpq-dev diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..902f9b0 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,12 @@ +{ + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "features": { + "ghcr.io/devcontainers/features/sshd:1": {} + }, + + "forwardPorts": [ + 5432 + ] +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..adea18b --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,38 @@ +version: '3.8' + +services: + app: + container_name: javadev + # https://youtrack.jetbrains.com/issue/KT-36871/Support-Aarch64-Linux-as-a-host-for-the-Kotlin-Native + platform: "linux/amd64" + build: + context: . + dockerfile: Dockerfile + environment: + POSTGRES_HOSTNAME: postgresdb + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + + volumes: + - ../..:/workspaces:cached + + command: sleep infinity + + network_mode: service:db + + db: + container_name: postgresdb + image: postgres:latest + restart: unless-stopped + healthcheck: + test: [ "CMD-SHELL", "pg_isready" ] + interval: 1s + timeout: 5s + retries: 10 + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + ports: + - "5432:5432" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 99c0223..346adc8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,3 +13,15 @@ updates: assignees: - "hfhbd" rebase-strategy: "disabled" + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: "daily" + assignees: + - "hfhbd" + - package-ecosystem: "docker" + directory: "/.devcontainer" + schedule: + interval: "daily" + assignees: + - "hfhbd" diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..993f93e --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,11 @@ +changelog: + categories: + - title: Features + labels: + - '*' + exclude: + labels: + - dependencies + - title: Updated Dependencies + labels: + - dependencies diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index e9d68be..1e4fad3 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -11,21 +11,21 @@ jobs: steps: - name: Set environment for version run: long="${{ github.ref }}"; version=${long#"refs/tags/v"}; echo "version=${version}" >> $GITHUB_ENV - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - uses: Homebrew/actions/setup-homebrew@master id: set-up-homebrew - - uses: gradle/wrapper-validation-action@v1 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: - distribution: 'adopt' - java-version: 11 + distribution: 'temurin' + java-version: 21 + - uses: gradle/actions/setup-gradle@v4 - run: brew install libpq - - name: Build with Gradle - run: ./gradlew assemble + env: + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: true - name: Publish - run: ./gradlew -Pversion=$version -Dorg.gradle.parallel=false publish closeAndReleaseStagingRepository + run: ./gradlew -Pversion=$version -Dorg.gradle.parallel=false --no-configuration-cache publish closeAndReleaseStagingRepositories env: - SIGNING_PRIVATE_KEY: ${{ secrets.SIGNING_PRIVATE_KEY }} - SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - SONARTYPE_APIKEY: ${{ secrets.SONARTYPE_APIKEY }} - SONARTYPE_APITOKEN: ${{ secrets.SONARTYPE_APITOKEN }} + ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_PRIVATE_KEY }} + ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} + ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONARTYPE_APIKEY }} + ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONARTYPE_APITOKEN }} diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 2bab7e9..df0f9d8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -8,36 +8,28 @@ on: jobs: build: - runs-on: ubuntu-latest - services: - postgres: - image: postgres - env: - POSTGRES_PASSWORD: password - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 + runs-on: ${{ matrix.os }} + permissions: + contents: write + strategy: + matrix: + os: [ 'ubuntu-latest', 'ubuntu-24.04', 'macos-latest' ] + + env: + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - uses: Homebrew/actions/setup-homebrew@master id: set-up-homebrew - - uses: gradle/wrapper-validation-action@v1 - run: brew install libpq - - uses: actions/setup-java@v3 + - uses: ikalnytskyi/action-setup-postgres@v7 + with: + password: password + - uses: actions/setup-java@v4 with: - distribution: 'adopt' - java-version: 11 + distribution: 'temurin' + java-version: 21 + - uses: gradle/actions/setup-gradle@v4 - run: ./gradlew assemble - run: ./gradlew build - - uses: actions/upload-artifact@v3 - with: - path: | - postgres-native-sqldelight-dialect/build/reports/tests - postgres-native-sqldelight-driver/build/reports/tests - testing/build/reports/tests - if: failure() diff --git a/.github/workflows/Docs.yml b/.github/workflows/Docs.yml new file mode 100644 index 0000000..7410a1a --- /dev/null +++ b/.github/workflows/Docs.yml @@ -0,0 +1,45 @@ +name: Docs + +on: + release: + types: [ created ] + workflow_dispatch: + +concurrency: + group: "docs" + cancel-in-progress: false + +env: + GRADLE_OPTS: -Dorg.gradle.caching=true + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - uses: actions/configure-pages@v5 + - uses: actions/checkout@v5 + - uses: Homebrew/actions/setup-homebrew@master + id: set-up-homebrew + - run: brew install libpq + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 21 + - uses: gradle/actions/setup-gradle@v4 + - name: Generate Docs + run: ./gradlew :dokkaHtmlMultiModule --no-configuration-cache + - uses: actions/upload-pages-artifact@v4 + with: + path: build/dokka/htmlMultiModule + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml new file mode 100644 index 0000000..9259ece --- /dev/null +++ b/.github/workflows/dependencies.yml @@ -0,0 +1,32 @@ +name: Dependency review for pull requests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + dependency-submission: + runs-on: ubuntu-latest + permissions: + contents: write + + env: + GRADLE_OPTS: -Dorg.gradle.caching=true + + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 21 + - uses: gradle/actions/dependency-submission@v4 + + dependency-review: + runs-on: ubuntu-latest + needs: dependency-submission + if: github.event_name == 'pull_request' + + steps: + - uses: actions/dependency-review-action@v4 diff --git a/.gitignore b/.gitignore index e0d53d8..aa238fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .gradle .idea +.kotlin build diff --git a/README.md b/README.md index de0494c..2d63d9c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Module postgres-native-sqldelight +# PostgreSQL native SQLDelight driver A native Postgres driver using libpq. @@ -6,11 +6,14 @@ You can use the driver with [SQLDelight](https://github.com/cashapp/sqldelight), - [Source code](https://github.com/hfhbd/postgres-native-sqldelight) +> Keep in mind, until now, this is only a single-threaded wrapper over libpq using 1 connection only. There is no +> connection pool nor multithread support (like JDBC or R2DBC). + ## Install You need `libpq` installed and available in your `$PATH`. -This package is uploaded to MavenCentral and supports macOS, linuxX64. +This package is uploaded to MavenCentral and supports macOS and linuxX64. Windows is currently not supported. ````kotlin @@ -22,11 +25,12 @@ dependencies { implementation("app.softwork:postgres-native-sqldelight-driver:LATEST") } +// optional SQLDelight setup: sqldelight { - database("NativePostgres") { + databases.register("NativePostgres") { dialect("app.softwork:postgres-native-sqldelight-dialect:LATEST") } - linkSqlite = false + linkSqlite.set(false) } ```` @@ -44,10 +48,12 @@ val driver = PostgresNativeDriver( ) ``` +### Listeners + This driver supports local and remote listeners. -Local listeners only notify this client, ideally for testing or using the database with only one client at a time with -SQLDelight only. -Remote listener support uses `NOTIFY` and `LISTEN`, so you can use this with multiple clients or with existing database +Local listeners only notify this client, ideally for testing or using the database with only one client at a time. +Remote listener support uses `NOTIFY` and `LISTEN`, so you can use this to sync multiple clients or with existing +database triggers. SQLDelight uses and expects the table name as payload, but you can provide a mapper function. @@ -64,33 +70,13 @@ The identifier is used to reuse prepared statements. driver.execute(identifier = null, sql = "INSERT INTO foo VALUES (42)", parameters = 0, binders = null) ``` -It also supports a real lazy cursor or a flow: +It also supports a real lazy cursor by using a `Flow`. The `fetchSize` parameter defines how many rows are fetched at +once: ```kotlin -val names: List = driver.executeQueryWithNativeCursor( - identifier = null, - sql = "SELECT name from foo", - mapper = { cursor -> - buildList { - while (cursor.next()) { - add( - Simple( - index = cursor.getLong(0)!!.toInt(), - name = cursor.getString(1), - byteArray = cursor.getBytes(2) - ) - ) - } - } - }, - parameters = 0, - fetchSize = 100, - binders = null -) - -val namesFlow: Flow = driver.executeQueryAsFlow( +val namesFlow: Flow = driver.executeQueryAsFlow( identifier = null, - sql = "SELECT name from foo", + sql = "SELECT index, name, bytes FROM foo", mapper = { cursor -> Simple( index = cursor.getLong(0)!!.toInt(), @@ -104,10 +90,10 @@ val namesFlow: Flow = driver.executeQueryAsFlow( ) ``` -And for bulk imports, use the `copy` method: +And for bulk imports, use the `copy` method. You need to enable `COPY` first: ```kotlin -driver.execute(514394779, """COPY foo FROM STDIN (FORMAT CSV)""", 0) +driver.execute(514394779, "COPY foo FROM STDIN (FORMAT CSV)", 0) val rows = driver.copy("1,2,3\n4,5,6\n") ``` @@ -115,31 +101,42 @@ val rows = driver.copy("1,2,3\n4,5,6\n") Apache 2 -This library uses some socket native code from [ktor](https://github.com/ktorio/ktor), licensed under Apache 2. - ## Contributing -You need libpq installed: https://formulae.brew.sh/formula/libpq#default +### Devcontainers + +Start the devcontainer, that's it. + +### Local + +#### docker compose + +This is the preferred local option. The `app` service contains the JVM as well as libpq. -You have to add the compiler flags to your path too. -The exact commands depend on your config, but you will get them during installing libpq with homebrew. +#### Manual -Sample commands: +You need to install `libpq`, eg using Homebrew: https://formulae.brew.sh/formula/libpq#default + +For installation using homebrew, the default path is already added. + +Otherwise, you have to add the compiler flags to +the [libpq.def](postgres-native-sqldelight-driver/src/nativeInterop/cinterop/libpq.def). +The exact flags depend on your config, for example: ``` -If you need to have libpq first in your PATH, run: - echo 'export PATH="/home/linuxbrew/.linuxbrew/opt/libpq/bin:$PATH"' >> /home/runner/.bash_profile For compilers to find libpq you may need to set: export LDFLAGS="-L/home/linuxbrew/.linuxbrew/opt/libpq/lib" export CPPFLAGS="-I/home/linuxbrew/.linuxbrew/opt/libpq/include" ``` -### Testing +##### Testing + +If you installed libpq with homebrew, it will install the platform-specific artifact. -If you install libpq with homebrew, it will install the platform-specific artifact. +To test other platforms, eg. linux x64 on macOS, you need to install the platform-specific libpq of linux x64 too. -| Host | Supported test targets | -|-------------|------------------------| -| linux x64 | linux x64 | -| macOS x64 | macOS x64, linux x64 | -| macOS arm64 | macOS arm64 | +To start the postgres instance, you can use docker: + +```sh +docker run -e POSTGRES_PASSWORD=password -p 5432:5432 postgres +``` diff --git a/build.gradle.kts b/build.gradle.kts index 906184a..b8e4a83 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,103 +1,15 @@ -import app.cash.licensee.* -import org.jetbrains.kotlin.gradle.dsl.* -import java.util.* - plugins { - kotlin("multiplatform") version "1.7.22" apply false - `maven-publish` - signing - id("io.github.gradle-nexus.publish-plugin") version "1.1.0" - id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.12.1" - id("com.github.johnrengelman.shadow") version "7.1.2" apply false - id("com.alecstrong.grammar.kit.composer") version "0.1.10" apply false - id("app.cash.sqldelight") version "2.0.0-alpha04" apply false - id("app.cash.licensee") version "1.6.0" apply false -} - -repositories { - mavenCentral() + id("io.github.gradle-nexus.publish-plugin") + id("org.jetbrains.dokka") } -group = "app.softwork" - -subprojects { - if (this.name == "testing") { - return@subprojects - } - - plugins.apply("app.cash.licensee") - configure { - allow("Apache-2.0") - allow("MIT") - allowUrl("https://jdbc.postgresql.org/about/license.html") - } - - afterEvaluate { - configure { - explicitApi() - sourceSets { - all { - languageSettings.progressiveMode = true - } - } - } - } - - plugins.apply("org.gradle.maven-publish") - plugins.apply("org.gradle.signing") - val emptyJar by tasks.creating(Jar::class) { } - - group = "app.softwork" - - publishing { - publications.all { - this as MavenPublication - artifact(emptyJar) { - classifier = "javadoc" - } - pom { - name.set("app.softwork Postgres Native Driver and SqlDelight Dialect") - description.set("A Postgres native driver including support for SqlDelight") - url.set("https://github.com/hfhbd/kotlinx-serialization-csv") - licenses { - license { - name.set("The Apache License, Version 2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") - } - } - developers { - developer { - id.set("hfhbd") - name.set("Philip Wedemann") - email.set("mybztg+mavencentral@icloud.com") - } - } - scm { - connection.set("scm:git://github.com/hfhbd/SqlDelightNativePostgres.git") - developerConnection.set("scm:git://github.com/hfhbd/SqlDelightNativePostgres.git") - url.set("https://github.com/hfhbd/SqlDelightNativePostgres") - } - } - } - } - - (System.getProperty("signing.privateKey") ?: System.getenv("SIGNING_PRIVATE_KEY"))?.let { - String(Base64.getDecoder().decode(it)).trim() - }?.let { key -> - println("found key, config signing") - signing { - val signingPassword = System.getProperty("signing.password") ?: System.getenv("SIGNING_PASSWORD") - useInMemoryPgpKeys(key, signingPassword) - sign(publishing.publications) - } - } +tasks.dokkaHtmlMultiModule { + includes.from("README.md") } nexusPublishing { - repositories { + this.repositories { sonatype { - username.set(System.getProperty("sonartype.apiKey") ?: System.getenv("SONARTYPE_APIKEY")) - password.set(System.getProperty("sonartype.apiToken") ?: System.getenv("SONARTYPE_APITOKEN")) nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) } diff --git a/gradle.properties b/gradle.properties index f683daf..35f98a5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,9 @@ kotlin.code.style=official kotlin.mpp.enableCInteropCommonization=true -kotlin.native.cacheKind.macosArm64=none -kotlin.native.cacheKind.linuxX64=none - org.gradle.parallel=true org.gradle.jvmargs=-Xmx2048m +org.gradle.configuration-cache=true +org.gradle.configureondemand=true + +group=app.softwork diff --git a/gradle/build-logic/build.gradle.kts b/gradle/build-logic/build.gradle.kts new file mode 100644 index 0000000..003cb81 --- /dev/null +++ b/gradle/build-logic/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + `kotlin-dsl` +} + +dependencies { + implementation(libs.plugins.kotlin.jvm.toDep()) + implementation(libs.plugins.kotlin.serialization.toDep()) + implementation(libs.plugins.grammarKit.toDep()) + implementation(libs.plugins.publish.toDep()) + implementation(libs.plugins.binary.toDep()) + implementation(libs.plugins.sqldelight.toDep()) + implementation(libs.plugins.licensee.toDep()) + implementation(libs.plugins.dokka.toDep()) +} + +fun Provider.toDep() = map { + "${it.pluginId}:${it.pluginId}.gradle.plugin:${it.version}" +} + +kotlin.jvmToolchain(17) diff --git a/gradle/build-logic/settings.gradle.kts b/gradle/build-logic/settings.gradle.kts new file mode 100644 index 0000000..10af82a --- /dev/null +++ b/gradle/build-logic/settings.gradle.kts @@ -0,0 +1,16 @@ +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + gradlePluginPortal() + } + versionCatalogs.register("libs") { + from(files("../libs.versions.toml")) + } +} + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +rootProject.name = "build-logic" diff --git a/gradle/build-logic/src/main/kotlin/MyRepos.kt b/gradle/build-logic/src/main/kotlin/MyRepos.kt new file mode 100644 index 0000000..bc41370 --- /dev/null +++ b/gradle/build-logic/src/main/kotlin/MyRepos.kt @@ -0,0 +1,11 @@ +import org.gradle.api.artifacts.dsl.RepositoryHandler +import org.gradle.kotlin.dsl.maven + +fun RepositoryHandler.repos() { + mavenCentral() + + maven(url = "https://www.jetbrains.com/intellij-repository/releases") + maven(url = "https://cache-redirector.jetbrains.com/intellij-dependencies") + maven(url = "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-ide-plugin-dependencies/") + maven(url = "https://maven.pkg.jetbrains.space/public/p/ktor/eap") +} diff --git a/gradle/build-logic/src/main/kotlin/MyRepos.settings.gradle.kts b/gradle/build-logic/src/main/kotlin/MyRepos.settings.gradle.kts new file mode 100644 index 0000000..964ff0e --- /dev/null +++ b/gradle/build-logic/src/main/kotlin/MyRepos.settings.gradle.kts @@ -0,0 +1,5 @@ +dependencyResolutionManagement { + repositories { + repos() + } +} diff --git a/gradle/build-logic/src/main/kotlin/exclude.gradle.kts b/gradle/build-logic/src/main/kotlin/exclude.gradle.kts new file mode 100644 index 0000000..b3592a6 --- /dev/null +++ b/gradle/build-logic/src/main/kotlin/exclude.gradle.kts @@ -0,0 +1,12 @@ +configurations.configureEach { + exclude(group = "com.jetbrains.rd") + exclude(group = "com.github.jetbrains", module = "jetCheck") + exclude(group = "com.jetbrains.infra") + + exclude(group = "org.roaringbitmap") + + exclude(group = "ai.grazie.spell") + exclude(group = "ai.grazie.model") + exclude(group = "ai.grazie.utils") + exclude(group = "ai.grazie.nlp") +} diff --git a/gradle/build-logic/src/main/kotlin/publish.gradle.kts b/gradle/build-logic/src/main/kotlin/publish.gradle.kts new file mode 100644 index 0000000..92093b8 --- /dev/null +++ b/gradle/build-logic/src/main/kotlin/publish.gradle.kts @@ -0,0 +1,67 @@ +import org.gradle.api.publish.maven.* +import org.gradle.api.tasks.bundling.* +import org.gradle.kotlin.dsl.* +import java.util.* + +plugins { + id("maven-publish") + id("signing") +} + +val emptyJar by tasks.registering(Jar::class) { } + +publishing { + publications.configureEach { + this as MavenPublication + if (project.name != "postgres-native-sqldelight-dialect") { + artifact(emptyJar) { + classifier = "javadoc" + } + } + pom { + name.set("app.softwork Postgres Native Driver and SqlDelight Dialect") + description.set("A Postgres native driver including support for SqlDelight") + url.set("https://github.com/hfhbd/SqlDelightNativePostgres") + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + developers { + developer { + id.set("hfhbd") + name.set("Philip Wedemann") + email.set("mybztg+mavencentral@icloud.com") + } + } + scm { + connection.set("scm:git://github.com/hfhbd/SqlDelightNativePostgres.git") + developerConnection.set("scm:git://github.com/hfhbd/SqlDelightNativePostgres.git") + url.set("https://github.com/hfhbd/SqlDelightNativePostgres") + } + } + } +} + +signing { + val signingKey: String? by project + val signingPassword: String? by project + signingKey?.let { + useInMemoryPgpKeys(String(Base64.getDecoder().decode(it)).trim(), signingPassword) + sign(publishing.publications) + } +} + +// https://youtrack.jetbrains.com/issue/KT-46466 +val signingTasks = tasks.withType() +tasks.withType().configureEach { + dependsOn(signingTasks) +} + +tasks.withType().configureEach { + isPreserveFileTimestamps = false + isReproducibleFileOrder = true + filePermissions {} + dirPermissions {} +} diff --git a/gradle/build-logic/src/main/kotlin/repos.gradle.kts b/gradle/build-logic/src/main/kotlin/repos.gradle.kts new file mode 100644 index 0000000..5aaa3eb --- /dev/null +++ b/gradle/build-logic/src/main/kotlin/repos.gradle.kts @@ -0,0 +1,3 @@ +repositories { + repos() +} diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 0000000..63e5bbd --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,2 @@ +#This file is generated by updateDaemonJvm +toolchainVersion=21 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..aa6cf70 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,28 @@ +[versions] +kotlin = "2.1.21" +sqldelight = "2.0.2" +idea = "222.4459.24" +coroutines = "1.10.2" + +[libraries] +sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqldelight" } +sqldelight-postgresql-dialect = { module = "app.cash.sqldelight:postgresql-dialect", version.ref = "sqldelight" } +sqldelight-dialect-api = { module = "app.cash.sqldelight:dialect-api", version.ref = "sqldelight" } +sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } +sqldelight-compiler-env = { module = "app.cash.sqldelight:compiler-env", version.ref = "sqldelight" } + +coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +ktor-network = { module = "io.ktor:ktor-network", version = "3.2.1" } +datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.6.2" } +serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version = "1.8.1" } + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +grammarKit = { id = "com.alecstrong.grammar.kit.composer", version = "0.1.12" } +publish = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" } +binary = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.17.0" } +sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } +licensee = { id = "app.cash.licensee", version = "1.12.0" } +dokka = { id = "org.jetbrains.dokka", version = "1.9.20" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e583..2c35211 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 8049c68..09523c0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index a69d9cb..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" \ diff --git a/gradlew.bat b/gradlew.bat index f127cfd..9b42019 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,91 +1,94 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -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. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -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. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -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! -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 - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@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 +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +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 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +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 + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +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! +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 + +:omega diff --git a/postgres-native-sqldelight-dialect/build.gradle.kts b/postgres-native-sqldelight-dialect/build.gradle.kts index 14fbcd2..e3217e0 100644 --- a/postgres-native-sqldelight-dialect/build.gradle.kts +++ b/postgres-native-sqldelight-dialect/build.gradle.kts @@ -1,11 +1,11 @@ -import groovy.util.* -import org.jetbrains.grammarkit.tasks.* - plugins { kotlin("jvm") - `maven-publish` - id("com.github.johnrengelman.shadow") id("com.alecstrong.grammar.kit.composer") + id("org.jetbrains.kotlinx.binary-compatibility-validator") + id("app.cash.licensee") + id("repos") + id("publish") + id("exclude") } java { @@ -13,121 +13,43 @@ java { withSourcesJar() } -repositories { - mavenCentral() - maven(url = "https://www.jetbrains.com/intellij-repository/releases") - maven(url = "https://cache-redirector.jetbrains.com/intellij-dependencies") - maven(url = "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-ide-plugin-dependencies/") - maven(url = "https://maven.pkg.jetbrains.space/public/p/ktor/eap") -} - -val idea = "222.4459.24" - grammarKit { - intellijRelease.set(idea) -} - -// https://youtrack.jetbrains.com/issue/IDEA-301677 -val grammar = configurations.create("grammar") { - isCanBeResolved = true - isCanBeConsumed = false + intellijRelease.set(libs.versions.idea) } dependencies { - api("app.cash.sqldelight:postgresql-dialect:2.0.0-alpha04") + api(libs.sqldelight.postgresql.dialect) - compileOnly("app.cash.sqldelight:dialect-api:2.0.0-alpha04") + api(libs.sqldelight.dialect.api) - grammar("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") - compileOnly("com.jetbrains.intellij.platform:core-impl:$idea") - compileOnly("com.jetbrains.intellij.platform:util-ui:$idea") - compileOnly("com.jetbrains.intellij.platform:project-model-impl:$idea") - compileOnly("com.jetbrains.intellij.platform:analysis-impl:$idea") + compileOnly(libs.sqldelight.compiler.env) - testImplementation("com.jetbrains.intellij.platform:core-impl:$idea") - testImplementation("com.jetbrains.intellij.platform:util-ui:$idea") - testImplementation("com.jetbrains.intellij.platform:project-model-impl:$idea") - testImplementation("com.jetbrains.intellij.platform:analysis-impl:$idea") - testImplementation(kotlin("test-junit")) + testImplementation(kotlin("test")) + testImplementation(libs.sqldelight.compiler.env) } kotlin { - target.compilations.all { - kotlinOptions.allWarningsAsErrors = true - } -} - -configurations.all { - exclude(group = "com.jetbrains.rd") - exclude(group = "com.github.jetbrains", module = "jetCheck") - exclude(group = "org.roaringbitmap") - exclude(group = "com.jetbrains.intellij.remoteDev") - exclude(group = "com.jetbrains.intellij.spellchecker") -} - -tasks { - val generateapp_softwork_sqldelight_postgresdialect_PostgreSqlNativeParser by getting(GenerateParserTask::class) { - classpath.from(grammar) - } - generateParser { - classpath.from(grammar) - } -} - -tasks.shadowJar { - classifier = "" - include("*.jar") - include("app/cash/sqldelight/**") - include("app/softwork/sqldelight/postgresdialect/**") - include("META-INF/services/*") -} + jvmToolchain(17) -tasks.jar.configure { - // Prevents shadowJar (with classifier = '') and this task from writing to the same path. - enabled = false -} - -configurations { - fun conf(it: Configuration) { - it.outgoing.artifacts.removeIf { it.buildDependencies.getDependencies(null).contains(tasks.jar.get()) } - it.outgoing.artifact(tasks.shadowJar) + target.compilations.configureEach { + kotlinOptions.allWarningsAsErrors = true } - apiElements.configure { - conf(this) + explicitApi() + sourceSets { + configureEach { + languageSettings.progressiveMode = true + } } - runtimeElements.configure { conf(this) } -} - -artifacts { - runtimeOnly(tasks.shadowJar) - archives(tasks.shadowJar) } -// Disable Gradle module.json as it lists wrong dependencies -tasks.withType { - enabled = false +licensee { + allow("Apache-2.0") + allow("MIT") + allowUrl("https://jdbc.postgresql.org/about/license.html") } -// Remove dependencies from POM: uber jar has no dependencies publishing { - publications { - withType { - if (name == "pluginMaven") { - pom.withXml { - val pomNode = asNode() - - val dependencyNodes: NodeList = pomNode.get("dependencies") as NodeList - dependencyNodes.forEach { - (it as Node).parent().remove(it) - } - } - } - artifact(tasks.emptyJar) { - classifier = "sources" - } - } - create("shadow", MavenPublication::class.java) { - project.shadow.component(this) - } + publications.register("maven") { + from(components["java"]) } } diff --git a/postgres-native-sqldelight-dialect/src/main/kotlin/app/softwork/sqldelight/postgresdialect/PostgresNativeDialect.kt b/postgres-native-sqldelight-dialect/src/main/kotlin/app/softwork/sqldelight/postgresdialect/PostgresNativeDialect.kt index 6a033d9..4da31ad 100644 --- a/postgres-native-sqldelight-dialect/src/main/kotlin/app/softwork/sqldelight/postgresdialect/PostgresNativeDialect.kt +++ b/postgres-native-sqldelight-dialect/src/main/kotlin/app/softwork/sqldelight/postgresdialect/PostgresNativeDialect.kt @@ -25,6 +25,9 @@ public class PostgresNativeDialect : SqlDelightDialect by PostgreSqlDialect() { preparedStatementType = ClassName("app.softwork.sqldelight.postgresdriver", "PostgresPreparedStatement") ) + override val asyncRuntimeTypes: RuntimeTypes + get() = error("Async native driver is not yet supported") + override fun typeResolver(parentResolver: TypeResolver): TypeResolver = PostgresNativeTypeResolver(parentResolver) } @@ -80,10 +83,10 @@ private enum class PostgreSqlType(override val javaType: TypeName): DialectType TIME(ClassName("kotlinx.datetime", "LocalTime")), TIMESTAMP(ClassName("kotlinx.datetime", "LocalDateTime")), TIMESTAMP_TIMEZONE(ClassName("kotlinx.datetime", "Instant")), - INTERVAL(ClassName("kotlin.time", "Duration")), - UUID(ClassName("kotlinx.uuid", "UUID")); + INTERVAL(ClassName("kotlinx.datetime", "DateTimePeriod")), + UUID(ClassName("kotlin.uuid", "Uuid")); - override fun prepareStatementBinder(columnIndex: String, value: CodeBlock): CodeBlock { + override fun prepareStatementBinder(columnIndex: CodeBlock, value: CodeBlock): CodeBlock { return CodeBlock.builder() .add( when (this) { @@ -93,10 +96,10 @@ private enum class PostgreSqlType(override val javaType: TypeName): DialectType TIMESTAMP -> "bindLocalTimestamp" TIMESTAMP_TIMEZONE -> "bindTimestamp" INTERVAL -> "bindInterval" - UUID -> "bindUUID" + UUID -> "bindUuid" } ) - .add("($columnIndex, %L)\n", value) + .add("(%L, %L)\n", columnIndex, value) .build() } @@ -109,7 +112,7 @@ private enum class PostgreSqlType(override val javaType: TypeName): DialectType TIMESTAMP -> "$cursorName.getLocalTimestamp($columnIndex)" TIMESTAMP_TIMEZONE -> "$cursorName.getTimestamp($columnIndex)" INTERVAL -> "$cursorName.getInterval($columnIndex)" - UUID -> "$cursorName.getUUID($columnIndex)" + UUID -> "$cursorName.getUuid($columnIndex)" } ) } diff --git a/postgres-native-sqldelight-driver/api/postgres-native-sqldelight-driver.klib.api b/postgres-native-sqldelight-driver/api/postgres-native-sqldelight-driver.klib.api new file mode 100644 index 0000000..7c06973 --- /dev/null +++ b/postgres-native-sqldelight-driver/api/postgres-native-sqldelight-driver.klib.api @@ -0,0 +1,74 @@ +// Klib ABI Dump +// Targets: [linuxArm64, linuxX64, macosArm64, macosX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +sealed interface app.softwork.sqldelight.postgresdriver/ListenerSupport { // app.softwork.sqldelight.postgresdriver/ListenerSupport|null[0] + final class Local : app.softwork.sqldelight.postgresdriver/ScopedListenerSupport { // app.softwork.sqldelight.postgresdriver/ListenerSupport.Local|null[0] + constructor (kotlinx.coroutines/CoroutineScope, kotlinx.coroutines.flow/Flow, kotlin.coroutines/SuspendFunction1) // app.softwork.sqldelight.postgresdriver/ListenerSupport.Local.|(kotlinx.coroutines.CoroutineScope;kotlinx.coroutines.flow.Flow;kotlin.coroutines.SuspendFunction1){}[0] + + final val notificationScope // app.softwork.sqldelight.postgresdriver/ListenerSupport.Local.notificationScope|{}notificationScope[0] + final fun (): kotlinx.coroutines/CoroutineScope // app.softwork.sqldelight.postgresdriver/ListenerSupport.Local.notificationScope.|(){}[0] + } + + final class Remote : app.softwork.sqldelight.postgresdriver/ScopedListenerSupport { // app.softwork.sqldelight.postgresdriver/ListenerSupport.Remote|null[0] + constructor (kotlinx.coroutines/CoroutineScope, kotlin/Function1 = ...) // app.softwork.sqldelight.postgresdriver/ListenerSupport.Remote.|(kotlinx.coroutines.CoroutineScope;kotlin.Function1){}[0] + + final val notificationScope // app.softwork.sqldelight.postgresdriver/ListenerSupport.Remote.notificationScope|{}notificationScope[0] + final fun (): kotlinx.coroutines/CoroutineScope // app.softwork.sqldelight.postgresdriver/ListenerSupport.Remote.notificationScope.|(){}[0] + } + + final object Companion { // app.softwork.sqldelight.postgresdriver/ListenerSupport.Companion|null[0] + final fun Local(kotlinx.coroutines/CoroutineScope): app.softwork.sqldelight.postgresdriver/ListenerSupport.Local // app.softwork.sqldelight.postgresdriver/ListenerSupport.Companion.Local|Local(kotlinx.coroutines.CoroutineScope){}[0] + } + + final object None : app.softwork.sqldelight.postgresdriver/ListenerSupport // app.softwork.sqldelight.postgresdriver/ListenerSupport.None|null[0] +} + +final class app.softwork.sqldelight.postgresdriver/PostgresNativeDriver : app.cash.sqldelight.db/SqlDriver { // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver|null[0] + constructor (kotlinx.cinterop/CPointer, app.softwork.sqldelight.postgresdriver/ListenerSupport) // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.|(kotlinx.cinterop.CPointer;app.softwork.sqldelight.postgresdriver.ListenerSupport){}[0] + + final fun <#A1: kotlin/Any?> executeQuery(kotlin/Int?, kotlin/String, kotlin/Function1>, kotlin/Int, kotlin/Function1?): app.cash.sqldelight.db/QueryResult<#A1> // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.executeQuery|executeQuery(kotlin.Int?;kotlin.String;kotlin.Function1>;kotlin.Int;kotlin.Function1?){0§}[0] + final fun <#A1: kotlin/Any?> executeQueryAsFlow(kotlin/Int?, kotlin/String, kotlin.coroutines/SuspendFunction1, kotlin/Int, kotlin/Int = ..., kotlin/Function1?): kotlinx.coroutines.flow/Flow<#A1> // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.executeQueryAsFlow|executeQueryAsFlow(kotlin.Int?;kotlin.String;kotlin.coroutines.SuspendFunction1;kotlin.Int;kotlin.Int;kotlin.Function1?){0§}[0] + final fun addListener(kotlin/Array..., app.cash.sqldelight/Query.Listener) // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.addListener|addListener(kotlin.Array...;app.cash.sqldelight.Query.Listener){}[0] + final fun close() // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.close|close(){}[0] + final fun copy(kotlin.sequences/Sequence): kotlin/Long // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.copy|copy(kotlin.sequences.Sequence){}[0] + final fun currentTransaction(): app.cash.sqldelight/Transacter.Transaction? // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.currentTransaction|currentTransaction(){}[0] + final fun execute(kotlin/Int?, kotlin/String, kotlin/Int, kotlin/Function1?): app.cash.sqldelight.db/QueryResult.Value // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.execute|execute(kotlin.Int?;kotlin.String;kotlin.Int;kotlin.Function1?){}[0] + final fun newTransaction(): app.cash.sqldelight.db/QueryResult.Value // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.newTransaction|newTransaction(){}[0] + final fun notifyListeners(kotlin/Array...) // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.notifyListeners|notifyListeners(kotlin.Array...){}[0] + final fun removeListener(kotlin/Array..., app.cash.sqldelight/Query.Listener) // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.removeListener|removeListener(kotlin.Array...;app.cash.sqldelight.Query.Listener){}[0] +} + +final class app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement : app.cash.sqldelight.db/SqlPreparedStatement { // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement|null[0] + final fun bindBoolean(kotlin/Int, kotlin/Boolean?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindBoolean|bindBoolean(kotlin.Int;kotlin.Boolean?){}[0] + final fun bindBytes(kotlin/Int, kotlin/ByteArray?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindBytes|bindBytes(kotlin.Int;kotlin.ByteArray?){}[0] + final fun bindDate(kotlin/Int, kotlinx.datetime/LocalDate?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindDate|bindDate(kotlin.Int;kotlinx.datetime.LocalDate?){}[0] + final fun bindDouble(kotlin/Int, kotlin/Double?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindDouble|bindDouble(kotlin.Int;kotlin.Double?){}[0] + final fun bindInterval(kotlin/Int, kotlinx.datetime/DateTimePeriod?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindInterval|bindInterval(kotlin.Int;kotlinx.datetime.DateTimePeriod?){}[0] + final fun bindLocalTimestamp(kotlin/Int, kotlinx.datetime/LocalDateTime?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindLocalTimestamp|bindLocalTimestamp(kotlin.Int;kotlinx.datetime.LocalDateTime?){}[0] + final fun bindLong(kotlin/Int, kotlin/Long?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindLong|bindLong(kotlin.Int;kotlin.Long?){}[0] + final fun bindString(kotlin/Int, kotlin/String?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindString|bindString(kotlin.Int;kotlin.String?){}[0] + final fun bindTime(kotlin/Int, kotlinx.datetime/LocalTime?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindTime|bindTime(kotlin.Int;kotlinx.datetime.LocalTime?){}[0] + final fun bindTimestamp(kotlin/Int, kotlinx.datetime/Instant?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindTimestamp|bindTimestamp(kotlin.Int;kotlinx.datetime.Instant?){}[0] + final fun bindUuid(kotlin/Int, kotlin.uuid/Uuid?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindUuid|bindUuid(kotlin.Int;kotlin.uuid.Uuid?){}[0] +} + +sealed class app.softwork.sqldelight.postgresdriver/PostgresCursor : app.cash.sqldelight.db/SqlCursor { // app.softwork.sqldelight.postgresdriver/PostgresCursor|null[0] + final fun getDate(kotlin/Int): kotlinx.datetime/LocalDate? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getDate|getDate(kotlin.Int){}[0] + final fun getInterval(kotlin/Int): kotlinx.datetime/DateTimePeriod? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getInterval|getInterval(kotlin.Int){}[0] + final fun getLocalTimestamp(kotlin/Int): kotlinx.datetime/LocalDateTime? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getLocalTimestamp|getLocalTimestamp(kotlin.Int){}[0] + final fun getTime(kotlin/Int): kotlinx.datetime/LocalTime? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getTime|getTime(kotlin.Int){}[0] + final fun getTimestamp(kotlin/Int): kotlinx.datetime/Instant? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getTimestamp|getTimestamp(kotlin.Int){}[0] + final fun getUuid(kotlin/Int): kotlin.uuid/Uuid? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getUuid|getUuid(kotlin.Int){}[0] + open fun getBoolean(kotlin/Int): kotlin/Boolean? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getBoolean|getBoolean(kotlin.Int){}[0] + open fun getBytes(kotlin/Int): kotlin/ByteArray? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getBytes|getBytes(kotlin.Int){}[0] + open fun getDouble(kotlin/Int): kotlin/Double? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getDouble|getDouble(kotlin.Int){}[0] + open fun getLong(kotlin/Int): kotlin/Long? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getLong|getLong(kotlin.Int){}[0] + open fun getString(kotlin/Int): kotlin/String? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getString|getString(kotlin.Int){}[0] +} + +final fun app.softwork.sqldelight.postgresdriver/PostgresNativeDriver(kotlin/String, kotlin/String, kotlin/String, kotlin/String, kotlin/Int = ..., kotlin/String? = ..., app.softwork.sqldelight.postgresdriver/ListenerSupport = ...): app.softwork.sqldelight.postgresdriver/PostgresNativeDriver // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver|PostgresNativeDriver(kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.Int;kotlin.String?;app.softwork.sqldelight.postgresdriver.ListenerSupport){}[0] diff --git a/postgres-native-sqldelight-driver/build.gradle.kts b/postgres-native-sqldelight-driver/build.gradle.kts index 807bbf3..082c2ca 100644 --- a/postgres-native-sqldelight-driver/build.gradle.kts +++ b/postgres-native-sqldelight-driver/build.gradle.kts @@ -2,17 +2,25 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.* plugins { kotlin("multiplatform") -} - -repositories { - mavenCentral() + id("org.jetbrains.kotlinx.binary-compatibility-validator") + id("app.cash.licensee") + id("repos") + id("publish") + id("org.jetbrains.dokka") } kotlin { + explicitApi() + + compilerOptions { + progressiveMode.set(true) + optIn.add("kotlin.uuid.ExperimentalUuidApi") + } + fun KotlinNativeTarget.config() { - compilations.getByName("main") { + compilations.named("main") { cinterops { - val libpq by creating { + register("libpq") { defFile(project.file("src/nativeInterop/cinterop/libpq.def")) } } @@ -22,29 +30,45 @@ kotlin { macosArm64 { config() } macosX64 { config() } linuxX64 { config() } + linuxArm64 { config() } // mingwX64 { config() } - targets.all { - compilations.all { - kotlinOptions.allWarningsAsErrors = true - } - } - sourceSets { commonMain { dependencies { - api("io.ktor:ktor-network:2.1.3") - api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") - api("app.cash.sqldelight:runtime:2.0.0-alpha04") - api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") - api("app.softwork:kotlinx-uuid-core:0.0.17") + implementation(libs.ktor.network) + api(libs.coroutines.core) + api(libs.sqldelight.runtime) + api(libs.datetime) } } commonTest { dependencies { implementation(kotlin("test")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2") } } } } + +licensee { + allow("Apache-2.0") +} + +tasks.dokkaHtmlPartial { + dokkaSourceSets.configureEach { + externalDocumentationLink("https://cashapp.github.io/sqldelight/2.0.0/2.x/") + externalDocumentationLink( + url = "https://kotlinlang.org/api/kotlinx-datetime/", + packageListUrl = "https://kotlinlang.org/api/kotlinx-datetime/kotlinx-datetime/package-list", + ) + externalDocumentationLink("https://uuid.softwork.app") + externalDocumentationLink("https://kotlinlang.org/api/kotlinx.coroutines/") + } +} + +apiValidation { + klib { + enabled = true + } +} diff --git a/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/Ktor_select.kt b/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/Ktor_select.kt deleted file mode 100644 index bddf633..0000000 --- a/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/Ktor_select.kt +++ /dev/null @@ -1,386 +0,0 @@ -/* - * Copyright (C) 2022 JetBrains s.r.o and contributors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package app.softwork.sqldelight.postgresdriver - -import io.ktor.network.interop.* -import io.ktor.network.selector.* -import io.ktor.network.util.* -import io.ktor.util.* -import io.ktor.util.collections.* -import io.ktor.utils.io.core.* -import io.ktor.utils.io.errors.* -import kotlinx.atomicfu.* -import kotlinx.atomicfu.locks.* -import kotlinx.cinterop.* -import kotlinx.coroutines.* -import platform.posix.* -import kotlin.coroutines.* -import kotlin.math.* - -// COPIED FROM KTOR -// License Apache-2.0 -// Changes: Make all Ktor internal classes private -// Reason: https://youtrack.jetbrains.com/issue/KTOR-5035/Remove-check-for-internal-class-in-Select - -internal fun SelectorManager(): SelectorManager = WorkerSelectorManager() - -@OptIn(ExperimentalCoroutinesApi::class) -private class WorkerSelectorManager : SelectorManager { - private val selectorContext = newSingleThreadContext("WorkerSelectorManager") - private val job = Job() - override val coroutineContext: CoroutineContext = selectorContext + job - - private val selector = SelectorHelper() - - init { - selector.start(this) - } - - override fun notifyClosed(selectable: Selectable) { - selector.notifyClosed(selectable.descriptor) - } - - override suspend fun select( - selectable: Selectable, - interest: SelectInterest - ) { - return suspendCancellableCoroutine { continuation -> - val selectorState = EventInfo(selectable.descriptor, interest, continuation) - if (!selector.interest(selectorState)) { - continuation.resumeWithException(CancellationException("Selector closed.")) - } - } - } - - override fun close() { - selector.requestTermination() - selectorContext.close() - } -} - - -@OptIn(InternalAPI::class) -private class SelectorHelper { - private val wakeupSignal = SignalPoint() - private val interestQueue = LockFreeMPSCQueue() - private val closeQueue = LockFreeMPSCQueue() - - private val wakeupSignalEvent = EventInfo( - wakeupSignal.selectionDescriptor, - SelectInterest.READ, - Continuation(EmptyCoroutineContext) { - } - ) - - fun interest(event: EventInfo): Boolean { - if (interestQueue.addLast(event)) { - wakeupSignal.signal() - return true - } - - return false - } - - fun start(scope: CoroutineScope) { - scope.launch(CoroutineName("selector")) { - selectionLoop() - }.invokeOnCompletion { - cleanup() - } - } - - fun requestTermination() { - interestQueue.close() - wakeupSignal.signal() - } - - private fun cleanup() { - wakeupSignal.close() - } - - fun notifyClosed(descriptor: Int) { - closeQueue.addLast(descriptor) - wakeupSignal.signal() - } - - private fun selectionLoop(): Unit = memScoped { - val readSet = alloc() - val writeSet = alloc() - val errorSet = alloc() - - val completed = mutableSetOf() - val watchSet = mutableSetOf() - val closeSet = mutableSetOf() - - while (!interestQueue.isClosed) { - watchSet.add(wakeupSignalEvent) - var maxDescriptor = fillHandlers(watchSet, readSet, writeSet, errorSet) - if (maxDescriptor == 0) continue - - maxDescriptor = max(maxDescriptor + 1, wakeupSignalEvent.descriptor + 1) - - try { - pselectBridge(maxDescriptor + 1, readSet.ptr, writeSet.ptr, errorSet.ptr).check() - } catch (_: PosixException.BadFileDescriptorException) { - // Thrown if the descriptor was closed. - } - - processSelectedEvents(watchSet, closeSet, completed, readSet, writeSet, errorSet) - } - - val exception = CancellationException("Selector closed") - while (!interestQueue.isEmpty) { - interestQueue.removeFirstOrNull()?.fail(exception) - } - - for (item in watchSet) { - item.fail(exception) - } - } - - private fun fillHandlers( - watchSet: MutableSet, - readSet: fd_set, - writeSet: fd_set, - errorSet: fd_set - ): Int { - var maxDescriptor = 0 - - select_fd_clear(readSet.ptr) - select_fd_clear(writeSet.ptr) - select_fd_clear(errorSet.ptr) - - while (true) { - val event = interestQueue.removeFirstOrNull() ?: break - watchSet.add(event) - } - - for (event in watchSet) { - addInterest(event, readSet, writeSet, errorSet) - maxDescriptor = max(maxDescriptor, event.descriptor) - } - - return maxDescriptor - } - - private fun addInterest( - event: EventInfo, - readSet: fd_set, - writeSet: fd_set, - errorSet: fd_set - ) { - val set = descriptorSetByInterestKind(event, readSet, writeSet) - - select_fd_add(event.descriptor, set.ptr) - select_fd_add(event.descriptor, errorSet.ptr) - - check(select_fd_isset(event.descriptor, set.ptr) != 0) - check(select_fd_isset(event.descriptor, errorSet.ptr) != 0) - } - - private fun processSelectedEvents( - watchSet: MutableSet, - closeSet: MutableSet, - completed: MutableSet, - readSet: fd_set, - writeSet: fd_set, - errorSet: fd_set - ) { - while (true) { - val event = closeQueue.removeFirstOrNull() ?: break - closeSet.add(event) - } - - for (event in watchSet) { - if (event.descriptor in closeSet) { - completed.add(event) - continue - } - - val set = descriptorSetByInterestKind(event, readSet, writeSet) - - if (select_fd_isset(event.descriptor, errorSet.ptr) != 0) { - completed.add(event) - event.fail(IOException("Fail to select descriptor ${event.descriptor} for ${event.interest}")) - continue - } - - if (select_fd_isset(event.descriptor, set.ptr) == 0) continue - - if (event.descriptor == wakeupSignal.selectionDescriptor) { - wakeupSignal.check() - continue - } - - completed.add(event) - event.complete() - } - - for (descriptor in closeSet) { - close(descriptor) - } - closeSet.clear() - - watchSet.removeAll(completed) - completed.clear() - } - - private fun descriptorSetByInterestKind( - event: EventInfo, - readSet: fd_set, - writeSet: fd_set - ): fd_set = when (event.interest) { - SelectInterest.READ -> readSet - SelectInterest.WRITE -> writeSet - SelectInterest.ACCEPT -> readSet - else -> error("Unsupported interest ${event.interest}.") - } -} - -private data class EventInfo( - val descriptor: Int, - val interest: SelectInterest, - private val continuation: Continuation -) { - - fun complete() { - continuation.resume(Unit) - } - - fun fail(cause: Throwable) { - continuation.resumeWithException(cause) - } - - override fun toString(): String = "EventInfo[$descriptor, $interest]" -} - -private class SignalPoint : Closeable { - private val readDescriptor: Int - private val writeDescriptor: Int - private var remaining: Int by atomic(0) - private val lock = SynchronizedObject() - private var closed = false - - val selectionDescriptor: Int - get() = readDescriptor - - init { - val (read, write) = memScoped { - val pipeDescriptors = allocArray(2) - pipe(pipeDescriptors).check() - - repeat(2) { index -> - makeNonBlocking(pipeDescriptors[index]) - } - - Pair(pipeDescriptors[0], pipeDescriptors[1]) - } - - readDescriptor = read - writeDescriptor = write - } - - fun check() { - synchronized(lock) { - if (closed) return@synchronized - while (remaining > 0) { - remaining -= readFromPipe() - } - } - } - - @OptIn(UnsafeNumber::class) - fun signal() { - synchronized(lock) { - if (closed) return@synchronized - - if (remaining > 0) return - - memScoped { - val array = allocArray(1) - array[0] = 7 - // note: here we ignore the result of write intentionally - // we simply don't care whether the buffer is full or the pipe is already closed - val result = write(writeDescriptor, array, 1.convert()) - if (result < 0) return - - remaining += result.toInt() - } - } - } - - override fun close() { - synchronized(lock) { - if (closed) return@synchronized - closed = true - - close(writeDescriptor) - readFromPipe() - close(readDescriptor) - } - } - - @OptIn(UnsafeNumber::class) - private fun readFromPipe(): Int { - var count = 0 - - memScoped { - val buffer = allocArray(1024) - - do { - val result = read(readDescriptor, buffer, 1024.convert()).convert() - if (result < 0) { - when (val error = PosixException.forErrno()) { - is PosixException.TryAgainException -> {} - else -> throw error - } - - break - } - - if (result == 0) { - break - } - - count += result - } while (true) - } - - return count - } - - private fun makeNonBlocking(descriptor: Int) { - fcntl(descriptor, F_SETFL, fcntl(descriptor, F_GETFL) or O_NONBLOCK).check() - } -} - -private inline fun Int.check( - block: (Int) -> Boolean = { it >= 0 } -): Int { - if (!block(this)) { - throw PosixException.forErrno() - } - - return this -} - -internal expect fun pselectBridge( - descriptor: Int, - readSet: CPointer, - writeSet: CPointer, - errorSet: CPointer -): Int diff --git a/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/ListenerSupport.kt b/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/ListenerSupport.kt index d48de01..f68f847 100644 --- a/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/ListenerSupport.kt +++ b/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/ListenerSupport.kt @@ -30,6 +30,7 @@ public sealed interface ListenerSupport { ) : ScopedListenerSupport { override val notificationScope: CoroutineScope = notificationScope + Job() + @ExperimentalForeignApi internal fun remoteListener(conn: CPointer): Flow = channelFlow { val selector = SelectorManager() diff --git a/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/NoCursor.kt b/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/NoCursor.kt index ca8e09f..52f157d 100644 --- a/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/NoCursor.kt +++ b/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/NoCursor.kt @@ -4,6 +4,7 @@ import app.cash.sqldelight.db.* import kotlinx.cinterop.* import libpq.* +@ExperimentalForeignApi internal class NoCursor( result: CPointer ) : PostgresCursor(result), Closeable { @@ -14,12 +15,12 @@ internal class NoCursor( private val maxRowIndex = PQntuples(result) - 1 override var currentRowIndex = -1 - override fun next(): Boolean { + override fun next(): QueryResult.Value { return if (currentRowIndex < maxRowIndex) { currentRowIndex += 1 - true + QueryResult.Value(true) } else { - false + QueryResult.Value(false) } } } diff --git a/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/PostgresCursor.kt b/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/PostgresCursor.kt index 0defd6f..bf99eff 100644 --- a/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/PostgresCursor.kt +++ b/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/PostgresCursor.kt @@ -3,10 +3,10 @@ package app.softwork.sqldelight.postgresdriver import app.cash.sqldelight.db.* import kotlinx.cinterop.* import kotlinx.datetime.* -import kotlinx.uuid.* import libpq.* -import kotlin.time.* +import kotlin.uuid.* +@OptIn(ExperimentalForeignApi::class) public sealed class PostgresCursor( internal var result: CPointer ) : SqlCursor { @@ -66,6 +66,6 @@ public sealed class PostgresCursor( Instant.parse(it.replace(" ", "T")) } - public fun getInterval(index: Int): Duration? = getString(index)?.let { Duration.parseIsoString(it) } - public fun getUUID(index: Int): UUID? = getString(index)?.toUUID() + public fun getInterval(index: Int): DateTimePeriod? = getString(index)?.let { DateTimePeriod.parse(it) } + public fun getUuid(index: Int): Uuid? = getString(index)?.let { Uuid.parse(it) } } diff --git a/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeDriver.kt b/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeDriver.kt index af9ae35..5b5a75d 100644 --- a/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeDriver.kt +++ b/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeDriver.kt @@ -1,12 +1,17 @@ package app.softwork.sqldelight.postgresdriver -import app.cash.sqldelight.* +import app.cash.sqldelight.Query +import app.cash.sqldelight.Transacter import app.cash.sqldelight.db.* import kotlinx.cinterop.* -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import libpq.* +@OptIn(ExperimentalForeignApi::class) public class PostgresNativeDriver( private val conn: CPointer, private val listenerSupport: ListenerSupport @@ -44,7 +49,7 @@ public class PostgresNativeDriver( } } - override fun addListener(listener: Query.Listener, queryKeys: Array) { + override fun addListener(vararg queryKeys: String, listener: Query.Listener) { when (listenerSupport) { ListenerSupport.None -> return is ListenerSupport.Local -> { @@ -67,7 +72,7 @@ public class PostgresNativeDriver( } } - override fun notifyListeners(queryKeys: Array) { + override fun notifyListeners(vararg queryKeys: String) { when (listenerSupport) { is ListenerSupport.Local -> { listenerSupport.notificationScope.launch { @@ -88,7 +93,7 @@ public class PostgresNativeDriver( } } - override fun removeListener(listener: Query.Listener, queryKeys: Array) { + override fun removeListener(vararg queryKeys: String, listener: Query.Listener) { val queryListeners = listeners[listener] if (queryListeners != null) { if (listenerSupport is ListenerSupport.Remote) { @@ -163,13 +168,21 @@ public class PostgresNativeDriver( private fun preparedStatementExists(identifier: Int): Boolean { val result = - executeQuery(null, "SELECT name FROM pg_prepared_statements WHERE name = $1", parameters = 1, binders = { - bindString(0, identifier.toString()) - }, mapper = { - if (it.next()) { - it.getString(0) - } else null - }) + executeQuery( + null, + "SELECT name FROM pg_prepared_statements WHERE name = $1", + parameters = 1, + binders = { + bindString(0, identifier.toString()) + }, + mapper = { + it.next().map { next -> + if (next) { + it.getString(0) + } else null + } + } + ) return result.value != null } @@ -206,10 +219,10 @@ public class PostgresNativeDriver( override fun executeQuery( identifier: Int?, sql: String, - mapper: (SqlCursor) -> R, + mapper: (SqlCursor) -> QueryResult, parameters: Int, binders: (SqlPreparedStatement.() -> Unit)? - ): QueryResult.Value { + ): QueryResult { val preparedStatement = preparedStatement(parameters, binders) val result = if (identifier != null) { checkPreparedStatement(identifier, sql, parameters, preparedStatement) @@ -239,8 +252,7 @@ public class PostgresNativeDriver( } }.check(conn) - val value = NoCursor(result).use(mapper) - return QueryResult.Value(value = value) + return NoCursor(result).use(mapper) } internal companion object { @@ -263,7 +275,7 @@ public class PostgresNativeDriver( private inner class Transaction( override val enclosingTransaction: Transacter.Transaction? ) : Transacter.Transaction() { - override fun endTransaction(successful: Boolean): QueryResult.Unit { + override fun endTransaction(successful: Boolean): QueryResult.Value { if (enclosingTransaction == null) { if (successful) { conn.exec("END") @@ -278,10 +290,15 @@ public class PostgresNativeDriver( // Custom functions - public fun copy(stdin: String): Long { - val status = PQputCopyData(conn, stdin, stdin.encodeToByteArray().size) - check(status == 1) { - conn.error() + /** + * Each element of stdin can be up to 2 GB. + */ + public fun copy(stdin: Sequence): Long { + for (std in stdin) { + val status = PQputCopyData(conn, std, std.encodeToByteArray().size) + check(status == 1) { + conn.error() + } } val end = PQputCopyEnd(conn, null) check(end == 1) { @@ -296,14 +313,17 @@ public class PostgresNativeDriver( sql: String, mapper: suspend (PostgresCursor) -> R, parameters: Int, - fetchSize: Int = 1, + fetchSize: Int = 10, binders: (PostgresPreparedStatement.() -> Unit)? ): Flow = flow { val (result, cursorName) = prepareQuery(identifier, sql, parameters, binders) - RealCursor(result, cursorName, conn, fetchSize).use { - while (it.next()) { - emit(mapper(it)) + val cursor = RealCursor(result, cursorName, conn, fetchSize) + try { + while (cursor.next().value) { + emit(mapper(cursor)) } + } finally { + cursor.close() } } @@ -347,37 +367,28 @@ public class PostgresNativeDriver( } }.check(conn) to cursorName } - - public fun executeQueryWithNativeCursor( - identifier: Int?, - sql: String, - mapper: (PostgresCursor) -> R, - parameters: Int, - fetchSize: Int = 1, - binders: (PostgresPreparedStatement.() -> Unit)? - ): QueryResult.Value { - val (result, cursorName) = prepareQuery(identifier, sql, parameters, binders) - val value = RealCursor(result, cursorName, conn, fetchSize).use(mapper) - return QueryResult.Value(value = value) - } } +@ExperimentalForeignApi private fun CPointer?.error(): String { val errorMessage = PQerrorMessage(this)!!.toKString() PQfinish(this) return errorMessage } +@ExperimentalForeignApi internal fun CPointer?.clear() { PQclear(this) } +@ExperimentalForeignApi internal fun CPointer.exec(sql: String) { val result = PQexec(this, sql) result.check(this) result.clear() } +@ExperimentalForeignApi internal fun CPointer?.check(conn: CPointer): CPointer { val status = PQresultStatus(this) check(status == PGRES_TUPLES_OK || status == PGRES_COMMAND_OK || status == PGRES_COPY_IN) { @@ -386,6 +397,7 @@ internal fun CPointer?.check(conn: CPointer): CPointer.escaped(value: String): String { val cString = PQescapeIdentifier(this, value, value.length.convert()) val escaped = cString!!.toKString() @@ -393,6 +405,7 @@ private fun CPointer.escaped(value: String): String { return escaped } +@OptIn(ExperimentalForeignApi::class) public fun PostgresNativeDriver( host: String, database: String, @@ -411,8 +424,19 @@ public fun PostgresNativeDriver( pwd = password, pgoptions = options ) - require(PQstatus(conn) == ConnStatusType.CONNECTION_OK) { + val status = PQstatus(conn) + if (status == ConnStatusType.CONNECTION_BAD) { + throw IllegalArgumentException(conn.error()) + } + require(status == ConnStatusType.CONNECTION_OK) { conn.error() } return PostgresNativeDriver(conn!!, listenerSupport = listenerSupport) } + +private fun QueryResult.map(action: (T) -> R): QueryResult = when (this) { + is QueryResult.Value -> QueryResult.Value(action(value)) + is QueryResult.AsyncValue -> QueryResult.AsyncValue { + action(await()) + } +} diff --git a/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/PostgresPreparedStatement.kt b/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/PostgresPreparedStatement.kt index 2b6cd5c..238794d 100644 --- a/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/PostgresPreparedStatement.kt +++ b/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/PostgresPreparedStatement.kt @@ -3,10 +3,10 @@ package app.softwork.sqldelight.postgresdriver import app.cash.sqldelight.db.* import kotlinx.cinterop.* import kotlinx.datetime.* -import kotlinx.uuid.* -import kotlin.time.* +import kotlin.uuid.* public class PostgresPreparedStatement internal constructor(private val parameters: Int) : SqlPreparedStatement { + @ExperimentalForeignApi internal fun values(scope: AutofreeScope): CValuesRef> = createValues(parameters) { value = when (val value = _values[it]) { null -> null @@ -76,11 +76,11 @@ public class PostgresPreparedStatement internal constructor(private val paramete bind(index, value?.toString(), timestampTzOid) } - public fun bindInterval(index: Int, value: Duration?) { - bind(index, value?.toIsoString(), intervalOid) + public fun bindInterval(index: Int, value: DateTimePeriod?) { + bind(index, value?.toString(), intervalOid) } - public fun bindUUID(index: Int, value: UUID?) { + public fun bindUuid(index: Int, value: Uuid?) { bind(index, value?.toString(), uuidOid) } diff --git a/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/RealCursor.kt b/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/RealCursor.kt index ea2b52d..d4c6b66 100644 --- a/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/RealCursor.kt +++ b/postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/RealCursor.kt @@ -5,8 +5,9 @@ import kotlinx.cinterop.* import libpq.* /** - * Must be inside a transaction! + * Must be used inside a transaction! */ +@ExperimentalForeignApi internal class RealCursor( result: CPointer, private val name: String, @@ -22,7 +23,7 @@ internal class RealCursor( override var currentRowIndex = -1 private var maxRowIndex = -1 - override fun next(): Boolean { + override fun next(): QueryResult.Value { if (currentRowIndex == maxRowIndex) { currentRowIndex = -1 } @@ -32,7 +33,7 @@ internal class RealCursor( } return if (currentRowIndex < maxRowIndex) { currentRowIndex += 1 - true - } else false + QueryResult.Value(true) + } else QueryResult.Value(false) } } diff --git a/postgres-native-sqldelight-driver/src/commonTest/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeDriverTest.kt b/postgres-native-sqldelight-driver/src/commonTest/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeDriverTest.kt deleted file mode 100644 index f4097ec..0000000 --- a/postgres-native-sqldelight-driver/src/commonTest/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeDriverTest.kt +++ /dev/null @@ -1,330 +0,0 @@ -package app.softwork.sqldelight.postgresdriver - -import app.cash.sqldelight.* -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.test.* -import kotlin.test.* -import kotlin.time.Duration.Companion.seconds - -@ExperimentalCoroutinesApi -class PostgresNativeDriverTest { - @Test - fun simpleTest() = runTest { - val driver = PostgresNativeDriver( - host = "localhost", - port = 5432, - user = "postgres", - database = "postgres", - password = "password" - ) - assertEquals(0, driver.execute(null, "DROP TABLE IF EXISTS baz;", parameters = 0).value) - assertEquals( - 0, - driver.execute(null, "CREATE TABLE baz(a INT PRIMARY KEY, foo TEXT, b BYTEA);", parameters = 0).value - ) - repeat(5) { - val result = driver.execute(null, "INSERT INTO baz VALUES ($it)", parameters = 0) - assertEquals(1, result.value) - } - - val result = driver.execute(null, "INSERT INTO baz VALUES ($1, $2, $3), ($4, $5, $6)", parameters = 6) { - bindLong(0, 5) - bindString(1, "bar 0") - bindBytes(2, byteArrayOf(1.toByte(), 2.toByte())) - - bindLong(3, 6) - bindString(4, "bar 1") - bindBytes(5, byteArrayOf(16.toByte(), 12.toByte())) - }.value - assertEquals(2, result) - val notPrepared = driver.executeQuery(null, "SELECT * FROM baz LIMIT 1;", parameters = 0, mapper = { - assertTrue(it.next()) - Simple( - index = it.getLong(0)!!.toInt(), - name = it.getString(1), - byteArray = it.getBytes(2) - ) - }) - assertEquals(Simple(0, null, null), notPrepared.value) - val preparedStatement = driver.executeQuery( - 42, - sql = "SELECT * FROM baz;", - parameters = 0, binders = null, - mapper = { - buildList { - while (it.next()) { - add( - Simple( - index = it.getLong(0)!!.toInt(), - name = it.getString(1), - byteArray = it.getBytes(2) - ) - ) - } - } - } - ).value - - assertEquals(7, preparedStatement.size) - assertEquals( - List(5) { - Simple(it, null, null) - } + listOf( - Simple(5, "bar 0", byteArrayOf(1.toByte(), 2.toByte())), - Simple(6, "bar 1", byteArrayOf(16.toByte(), 12.toByte())), - ), - preparedStatement - ) - - expect(7) { - val cursorList = driver.executeQueryWithNativeCursor( - -99, - "SELECT * FROM baz", - fetchSize = 4, - parameters = 0, - binders = null, - mapper = { - buildList { - while (it.next()) { - add( - Simple( - index = it.getLong(0)!!.toInt(), - name = it.getString(1), - byteArray = it.getBytes(2) - ) - ) - } - } - }).value - cursorList.size - } - - expect(7) { - val cursorList = driver.executeQueryWithNativeCursor( - -5, - "SELECT * FROM baz", - fetchSize = 1, - parameters = 0, - binders = null, - mapper = { - buildList { - while (it.next()) { - add( - Simple( - index = it.getLong(0)!!.toInt(), - name = it.getString(1), - byteArray = it.getBytes(2) - ) - ) - } - } - }).value - cursorList.size - } - - val cursorFlow = driver.executeQueryAsFlow( - -42, - "SELECT * FROM baz", - fetchSize = 1, - parameters = 0, - binders = null, - mapper = { - Simple( - index = it.getLong(0)!!.toInt(), - name = it.getString(1), - byteArray = it.getBytes(2) - ) - }) - assertEquals(7, cursorFlow.count()) - assertEquals(4, cursorFlow.take(4).count()) - - expect(0) { - val cursorList = driver.executeQueryWithNativeCursor( - -100, - "SELECT * FROM baz WHERE a = -1", - fetchSize = 1, - parameters = 0, - binders = null, - mapper = { - buildList { - while (it.next()) { - add( - Simple( - index = it.getLong(0)!!.toInt(), - name = it.getString(1), - byteArray = it.getBytes(2) - ) - ) - } - } - }).value - cursorList.size - } - } - - private data class Simple(val index: Int, val name: String?, val byteArray: ByteArray?) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - - other as Simple - - if (index != other.index) return false - if (name != other.name) return false - if (byteArray != null) { - if (other.byteArray == null) return false - if (!byteArray.contentEquals(other.byteArray)) return false - } else if (other.byteArray != null) return false - - return true - } - - override fun hashCode(): Int { - var result = index.hashCode() - result = 31 * result + (name?.hashCode() ?: 0) - result = 31 * result + (byteArray?.contentHashCode() ?: 0) - return result - } - } - - @Test - fun wrongCredentials() { - assertFailsWith { - PostgresNativeDriver( - host = "wrongHost", - user = "postgres", - database = "postgres", - password = "password" - ) - } - assertFailsWith { - PostgresNativeDriver( - host = "localhost", - user = "postgres", - database = "postgres", - password = "wrongPassword" - ) - } - assertFailsWith { - PostgresNativeDriver( - host = "localhost", - user = "wrongUser", - database = "postgres", - password = "password" - ) - } - } - - @Test - fun copyTest() { - val driver = PostgresNativeDriver( - host = "localhost", - port = 5432, - user = "postgres", - database = "postgres", - password = "password" - ) - assertEquals(0, driver.execute(null, "DROP TABLE IF EXISTS copying;", parameters = 0).value) - assertEquals(0, driver.execute(null, "CREATE TABLE copying(a int primary key);", parameters = 0).value) - driver.execute(-42, "COPY copying FROM STDIN (FORMAT CSV);", 0) - val results = driver.copy("1\n2\n") - assertEquals(2, results) - } - - @Test - fun remoteListenerTest() = runBlocking { - val other = PostgresNativeDriver( - host = "localhost", - port = 5432, - user = "postgres", - database = "postgres", - password = "password", - listenerSupport = ListenerSupport.Remote(this) - ) - - val driver = PostgresNativeDriver( - host = "localhost", - port = 5432, - user = "postgres", - database = "postgres", - password = "password", - listenerSupport = ListenerSupport.Remote(this) - ) - - val results = MutableStateFlow(0) - val listener = object : Query.Listener { - override fun queryResultsChanged() { - results.update { it + 1 } - } - } - driver.addListener(listener, arrayOf("foo", "bar")) - - val dbDelay = 2.seconds - delay(dbDelay) - other.notifyListeners(arrayOf("foo")) - - other.notifyListeners(arrayOf("foo", "bar")) - other.notifyListeners(arrayOf("bar")) - - delay(dbDelay) - - driver.removeListener(listener, arrayOf("foo", "bar")) - driver.notifyListeners(arrayOf("foo")) - driver.notifyListeners(arrayOf("bar")) - - delay(dbDelay) - assertEquals(4, results.value) - - other.close() - driver.close() - } - - @Test - fun localListenerTest() = runTest { - val notifications = MutableSharedFlow() - val notificationList = async { - notifications.take(4).toList() - } - - val driver = PostgresNativeDriver( - host = "localhost", - port = 5432, - user = "postgres", - database = "postgres", - password = "password", - listenerSupport = ListenerSupport.Local( - this, - notifications, - ) { - notifications.emit(it) - } - ) - - val results = MutableStateFlow(0) - val listener = object : Query.Listener { - override fun queryResultsChanged() { - results.update { it + 1 } - } - } - driver.addListener(listener, arrayOf("foo", "bar")) - runCurrent() - driver.notifyListeners(arrayOf("foo")) - runCurrent() - driver.notifyListeners(arrayOf("foo", "bar")) - runCurrent() - driver.notifyListeners(arrayOf("bar")) - runCurrent() - - driver.removeListener(listener, arrayOf("foo", "bar")) - runCurrent() - driver.notifyListeners(arrayOf("foo")) - runCurrent() - driver.notifyListeners(arrayOf("bar")) - runCurrent() - - assertEquals(4, results.value) - assertEquals(listOf("foo", "foo", "bar", "bar"), notificationList.await()) - - driver.close() - } -} diff --git a/postgres-native-sqldelight-driver/src/linuxX64Main/kotlin/app/softwork/sqldelight/postgresdriver/pselectBridge.kt b/postgres-native-sqldelight-driver/src/linuxX64Main/kotlin/app/softwork/sqldelight/postgresdriver/pselectBridge.kt deleted file mode 100644 index 5556c00..0000000 --- a/postgres-native-sqldelight-driver/src/linuxX64Main/kotlin/app/softwork/sqldelight/postgresdriver/pselectBridge.kt +++ /dev/null @@ -1,32 +0,0 @@ -package app.softwork.sqldelight.postgresdriver - -/* - * Copyright (C) 2022 JetBrains s.r.o and contributors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import kotlinx.cinterop.* -import platform.posix.* - -// COPIED FROM KTOR -// License Apache-2.0 -// Changes: Make all Ktor internal classes private -// Reason: https://youtrack.jetbrains.com/issue/KTOR-5035/Remove-check-for-internal-class-in-Select - -internal actual fun pselectBridge( - descriptor: Int, - readSet: CPointer, - writeSet: CPointer, - errorSet: CPointer -): Int = pselect(descriptor, readSet, writeSet, errorSet, null, null) diff --git a/postgres-native-sqldelight-driver/src/macosArm64Main/kotlin/app/softwork/sqldelight/postgresdriver/pselectBridge.kt b/postgres-native-sqldelight-driver/src/macosArm64Main/kotlin/app/softwork/sqldelight/postgresdriver/pselectBridge.kt deleted file mode 100644 index 5556c00..0000000 --- a/postgres-native-sqldelight-driver/src/macosArm64Main/kotlin/app/softwork/sqldelight/postgresdriver/pselectBridge.kt +++ /dev/null @@ -1,32 +0,0 @@ -package app.softwork.sqldelight.postgresdriver - -/* - * Copyright (C) 2022 JetBrains s.r.o and contributors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import kotlinx.cinterop.* -import platform.posix.* - -// COPIED FROM KTOR -// License Apache-2.0 -// Changes: Make all Ktor internal classes private -// Reason: https://youtrack.jetbrains.com/issue/KTOR-5035/Remove-check-for-internal-class-in-Select - -internal actual fun pselectBridge( - descriptor: Int, - readSet: CPointer, - writeSet: CPointer, - errorSet: CPointer -): Int = pselect(descriptor, readSet, writeSet, errorSet, null, null) diff --git a/postgres-native-sqldelight-driver/src/macosX64Main/kotlin/app/softwork/sqldelight/postgresdriver/pselectBridge.kt b/postgres-native-sqldelight-driver/src/macosX64Main/kotlin/app/softwork/sqldelight/postgresdriver/pselectBridge.kt deleted file mode 100644 index 5556c00..0000000 --- a/postgres-native-sqldelight-driver/src/macosX64Main/kotlin/app/softwork/sqldelight/postgresdriver/pselectBridge.kt +++ /dev/null @@ -1,32 +0,0 @@ -package app.softwork.sqldelight.postgresdriver - -/* - * Copyright (C) 2022 JetBrains s.r.o and contributors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import kotlinx.cinterop.* -import platform.posix.* - -// COPIED FROM KTOR -// License Apache-2.0 -// Changes: Make all Ktor internal classes private -// Reason: https://youtrack.jetbrains.com/issue/KTOR-5035/Remove-check-for-internal-class-in-Select - -internal actual fun pselectBridge( - descriptor: Int, - readSet: CPointer, - writeSet: CPointer, - errorSet: CPointer -): Int = pselect(descriptor, readSet, writeSet, errorSet, null, null) diff --git a/postgres-native-sqldelight-driver/src/nativeInterop/cinterop/libpq.def b/postgres-native-sqldelight-driver/src/nativeInterop/cinterop/libpq.def index 7fe6531..fa38fc1 100644 --- a/postgres-native-sqldelight-driver/src/nativeInterop/cinterop/libpq.def +++ b/postgres-native-sqldelight-driver/src/nativeInterop/cinterop/libpq.def @@ -4,5 +4,5 @@ package = libpq #staticLibraries = libpq.a #libraryPaths = /opt/homebrew/opt/libpq/lib -compilerOpts = -I/home/linuxbrew/.linuxbrew/opt/libpq/include -I/opt/homebrew/opt/libpq/include -I/usr/local/opt/libpq/include -linkerOpts = -L/home/linuxbrew/.linuxbrew/opt/libpq/lib -L/opt/homebrew/opt/libpq/lib -L/usr/local/opt/libpq/lib -lpq +compilerOpts = -I/home/linuxbrew/.linuxbrew/opt/libpq/include -I/opt/homebrew/opt/libpq/include -I/usr/local/opt/libpq/include -I/usr/include/postgresql +linkerOpts = -L/home/linuxbrew/.linuxbrew/opt/libpq/lib -L/opt/homebrew/opt/libpq/lib -L/usr/local/opt/libpq/lib -L/usr/lib64 -L/usr/lib -L/usr/lib/x86_64-linux-gnu -lpq diff --git a/settings.gradle.kts b/settings.gradle.kts index f3adc06..6703776 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,36 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } + includeBuild("gradle/build-logic") +} + +plugins { + id("MyRepos") + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" + id("com.gradle.develocity") version "4.0.2" +} + +develocity { + buildScan { + termsOfUseUrl.set("https://gradle.com/terms-of-service") + termsOfUseAgree.set("yes") + val isCI = providers.environmentVariable("CI").isPresent + publishing { + onlyIf { isCI } + } + tag("CI") + } +} + rootProject.name = "postgres-native-sqldelight" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") +enableFeaturePreview("STABLE_CONFIGURATION_CACHE") include(":postgres-native-sqldelight-driver") include(":postgres-native-sqldelight-dialect") + include(":testing") +include(":testing-sqldelight") diff --git a/testing-sqldelight/build.gradle.kts b/testing-sqldelight/build.gradle.kts new file mode 100644 index 0000000..11128a5 --- /dev/null +++ b/testing-sqldelight/build.gradle.kts @@ -0,0 +1,43 @@ +import org.jetbrains.kotlin.konan.target.* + +plugins { + kotlin("multiplatform") + id("app.cash.sqldelight") +} + +kotlin { + compilerOptions { + optIn.add("kotlin.uuid.ExperimentalUuidApi") + } + + when (HostManager.host) { + KonanTarget.LINUX_X64 -> linuxX64() + KonanTarget.MACOS_ARM64 -> macosArm64() + KonanTarget.MACOS_X64 -> macosX64() + else -> error("Not supported") + } + + sourceSets { + commonMain { + dependencies { + implementation(projects.postgresNativeSqldelightDriver) + implementation(libs.sqldelight.coroutines) + } + } + commonTest { + dependencies { + implementation(kotlin("test")) + implementation(libs.coroutines.test) + } + } + } +} + +sqldelight { + databases.register("NativePostgres") { + dialect(projects.postgresNativeSqldelightDialect) + packageName.set("app.softwork.sqldelight.postgresdriver") + deriveSchemaFromMigrations.set(true) + } + linkSqlite.set(false) +} diff --git a/testing/src/commonMain/sqldelight/app/softwork/sqldelight/postgresdriver/1.sqm b/testing-sqldelight/src/commonMain/sqldelight/app/softwork/sqldelight/postgresdriver/1.sqm similarity index 100% rename from testing/src/commonMain/sqldelight/app/softwork/sqldelight/postgresdriver/1.sqm rename to testing-sqldelight/src/commonMain/sqldelight/app/softwork/sqldelight/postgresdriver/1.sqm diff --git a/testing/src/commonMain/sqldelight/app/softwork/sqldelight/postgresdriver/Foo.sq b/testing-sqldelight/src/commonMain/sqldelight/app/softwork/sqldelight/postgresdriver/Foo.sq similarity index 100% rename from testing/src/commonMain/sqldelight/app/softwork/sqldelight/postgresdriver/Foo.sq rename to testing-sqldelight/src/commonMain/sqldelight/app/softwork/sqldelight/postgresdriver/Foo.sq diff --git a/testing/src/commonMain/sqldelight/app/softwork/sqldelight/postgresdriver/Users.sq b/testing-sqldelight/src/commonMain/sqldelight/app/softwork/sqldelight/postgresdriver/Users.sq similarity index 100% rename from testing/src/commonMain/sqldelight/app/softwork/sqldelight/postgresdriver/Users.sq rename to testing-sqldelight/src/commonMain/sqldelight/app/softwork/sqldelight/postgresdriver/Users.sq diff --git a/testing-sqldelight/src/commonTest/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeSqldelightDriverTest.kt b/testing-sqldelight/src/commonTest/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeSqldelightDriverTest.kt new file mode 100644 index 0000000..342377c --- /dev/null +++ b/testing-sqldelight/src/commonTest/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeSqldelightDriverTest.kt @@ -0,0 +1,243 @@ +package app.softwork.sqldelight.postgresdriver + +import app.cash.sqldelight.coroutines.* +import kotlinx.cinterop.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.* +import kotlinx.datetime.* +import kotlin.uuid.* +import platform.posix.* +import kotlin.test.* +import kotlin.time.Duration.Companion.seconds + +@ExperimentalCoroutinesApi +class PostgresNativeSqldelightDriverTest { + private val driver = PostgresNativeDriver( + host = env("POSTGRES_HOSTNAME") ?: "localhost", + port = 5432, + user = env("POSTGRES_USER") ?: "postgres", + database = env("POSTGRES_DB") ?: "postgres", + password = env("POSTGRES_PASSWORD") ?: "password" + ) + + @Test + fun allTypes() { + val queries = NativePostgres(driver).fooQueries + NativePostgres.Schema.migrate(driver, 0, NativePostgres.Schema.version) + assertEquals(emptyList(), queries.get().executeAsList()) + + val foo = Foo( + a = 42, + b = "answer", + date = LocalDate(2020, Month.DECEMBER, 12), + time = LocalTime(12, 42, 0, 0), + timestamp = LocalDateTime(2014, Month.AUGUST, 1, 12, 1, 2, 0), + instant = Instant.fromEpochMilliseconds(10L), + interval = DateTimePeriod(42, 42, 42, 42, 42, 42, 424242000), + uuid = Uuid.NIL + ) + queries.create( + a = foo.a, + b = foo.b, + date = foo.date, + time = foo.time, + timestamp = foo.timestamp, + instant = foo.instant, + interval = foo.interval, + uuid = foo.uuid + ) + assertEquals(foo, queries.get().executeAsOne()) + } + + @Test + fun copyTest() { + val queries = NativePostgres(driver).fooQueries + NativePostgres.Schema.migrate(driver, 0, NativePostgres.Schema.version) + queries.startCopy() + val result = driver.copy( + sequenceOf( + "42,answer,2020-12-12,12:42:00.0000,2014-08-01T12:01:02.0000,1970-01-01T00:00:00.010Z,P45Y6M42DT42H42M42.424242S,00000000-0000-0000-0000-000000000000" + ) + ) + assertEquals(1, result) + val foo = Foo( + a = 42, + b = "answer", + date = LocalDate(2020, Month.DECEMBER, 12), + time = LocalTime(12, 42, 0, 0), + timestamp = LocalDateTime(2014, Month.AUGUST, 1, 12, 1, 2, 0), + instant = Instant.fromEpochMilliseconds(10L), + interval = DateTimePeriod(42, 42, 42, 42, 42, 42, 424242000), + uuid = Uuid.NIL, + ) + assertEquals(foo, queries.get().executeAsOne()) + } + + @Test + fun userTest() { + val queries = NativePostgres(driver).usersQueries + NativePostgres.Schema.migrate(driver, 0, NativePostgres.Schema.version) + val id = queries.insertAndGet("test@test", "test", "bio", "", null).executeAsOne() + assertEquals(1, id) + val id2 = queries.insertAndGet("test2@test", "test2", "bio2", "", null).executeAsOne() + assertEquals(2, id2) + val testUser = queries.selectByUsername("test").executeAsOne() + assertEquals( + SelectByUsername( + "test@test", + "test", + "bio", + "" + ), + testUser + ) + } + + @Test + fun remoteListenerTest() = runTest(timeout = 10.seconds) { + val client = PostgresNativeDriver( + host = env("POSTGRES_HOSTNAME") ?: "localhost", + port = 5432, + user = env("POSTGRES_USER") ?: "postgres", + database = env("POSTGRES_DB") ?: "postgres", + password = env("POSTGRES_PASSWORD") ?: "password", + listenerSupport = ListenerSupport.Remote(backgroundScope) { + it + it + } + ) + + val server = PostgresNativeDriver( + host = env("POSTGRES_HOSTNAME") ?: "localhost", + port = 5432, + user = env("POSTGRES_USER") ?: "postgres", + database = env("POSTGRES_DB") ?: "postgres", + password = env("POSTGRES_PASSWORD") ?: "password", + listenerSupport = ListenerSupport.Remote(backgroundScope) { + it + it + } + ) + + val db = NativePostgres(client) + NativePostgres.Schema.migrate(driver, 0, NativePostgres.Schema.version) + + db.fooQueries.create( + a = 42, + b = "answer", + date = LocalDate(2020, Month.DECEMBER, 12), + time = LocalTime(12, 42, 0, 0), + timestamp = LocalDateTime(2014, Month.AUGUST, 1, 12, 1, 2, 0), + instant = Instant.fromEpochMilliseconds(10L), + interval = DateTimePeriod(42, 42, 42, 42, 42, 42, 424242), + uuid = Uuid.NIL + ) + val userQueries = db.usersQueries + val id = userQueries.insertAndGet("foo", "foo", "foo", "", 42).executeAsOne() + + val users = async { + db.usersQueries.selectByFoo(42) + .asFlow().mapToOne(coroutineContext) + .take(2).toList() + } + withContext(Dispatchers.Default) { + val waitForRemoteNotifications = 2.seconds + delay(waitForRemoteNotifications) + } + runCurrent() + + NativePostgres(server).usersQueries.updateWhereFoo("foo2", 42) + withContext(Dispatchers.Default) { + val waitForRemoteNotifications = 2.seconds + delay(waitForRemoteNotifications) + } + runCurrent() + + assertEquals( + listOf( + Users( + id, + "foo", + "foo", + "foo", + "", + 42 + ), Users( + id, + "foo2", + "foo", + "foo", + "", + 42 + ) + ), users.await() + ) + + client.close() + server.close() + } + + @Test + fun localListenerTest() = runTest(timeout = 10.seconds) { + val client = PostgresNativeDriver( + host = env("POSTGRES_HOSTNAME") ?: "localhost", + port = 5432, + user = env("POSTGRES_USER") ?: "postgres", + database = env("POSTGRES_DB") ?: "postgres", + password = env("POSTGRES_PASSWORD") ?: "password", + listenerSupport = ListenerSupport.Local(backgroundScope) + ) + + val db = NativePostgres(client) + NativePostgres.Schema.migrate(driver, 0, NativePostgres.Schema.version) + + db.fooQueries.create( + a = 42, + b = "answer", + date = LocalDate(2020, Month.DECEMBER, 12), + time = LocalTime(12, 42, 0, 0), + timestamp = LocalDateTime(2014, Month.AUGUST, 1, 12, 1, 2, 0), + instant = Instant.fromEpochMilliseconds(10L), + interval = DateTimePeriod(42, 42, 42, 42, 42, 42, 424242), + uuid = Uuid.NIL + ) + val userQueries = db.usersQueries + val id = userQueries.insertAndGet("foo", "foo", "foo", "", 42).executeAsOne() + + val users = async { + db.usersQueries.selectByFoo(42) + .asFlow().mapToOne(coroutineContext) + .take(2).toList() + } + runCurrent() + + userQueries.updateWhereFoo("foo2", 42) + runCurrent() + + assertEquals( + listOf( + Users( + id, + "foo", + "foo", + "foo", + "", + 42 + ), Users( + id, + "foo2", + "foo", + "foo", + "", + 42 + ) + ), users.await() + ) + + client.close() + } +} + +@OptIn(ExperimentalForeignApi::class) +private fun env(name: String): String? { + return getenv(name)?.toKStringFromUtf8()?.takeUnless { it.isEmpty() } +} diff --git a/testing/build.gradle.kts b/testing/build.gradle.kts index cd1c78a..8d14c01 100644 --- a/testing/build.gradle.kts +++ b/testing/build.gradle.kts @@ -1,41 +1,24 @@ +import org.jetbrains.kotlin.konan.target.* + plugins { kotlin("multiplatform") - id("app.cash.sqldelight") -} - -repositories { - mavenCentral() } kotlin { - - macosArm64() - macosX64() - - linuxX64() - // mingwX64() + when (HostManager.host) { + KonanTarget.LINUX_X64 -> linuxX64() + KonanTarget.MACOS_ARM64 -> macosArm64() + KonanTarget.MACOS_X64 -> macosX64() + else -> error("Not supported") + } sourceSets { - commonMain { - dependencies { - implementation(projects.postgresNativeSqldelightDriver) - implementation("app.cash.sqldelight:coroutines-extensions:2.0.0-alpha04") - } - } commonTest { dependencies { + implementation(projects.postgresNativeSqldelightDriver) implementation(kotlin("test")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") + implementation(libs.coroutines.test) } } } } - -sqldelight { - database("NativePostgres") { - dialect(projects.postgresNativeSqldelightDialect) - packageName = "app.softwork.sqldelight.postgresdriver" - deriveSchemaFromMigrations = true - } - linkSqlite = false -} diff --git a/testing/src/commonTest/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeDriverTest.kt b/testing/src/commonTest/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeDriverTest.kt index 409b9c1..03daa44 100644 --- a/testing/src/commonTest/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeDriverTest.kt +++ b/testing/src/commonTest/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeDriverTest.kt @@ -1,233 +1,323 @@ package app.softwork.sqldelight.postgresdriver -import app.cash.sqldelight.coroutines.* -import kotlinx.coroutines.* +import app.cash.sqldelight.Query +import app.cash.sqldelight.db.QueryResult +import kotlinx.cinterop.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* -import kotlinx.coroutines.test.* -import kotlinx.datetime.* -import kotlinx.uuid.* +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import platform.posix.* import kotlin.test.* import kotlin.time.Duration.Companion.seconds @ExperimentalCoroutinesApi class PostgresNativeDriverTest { private val driver = PostgresNativeDriver( - host = "localhost", + host = env("POSTGRES_HOSTNAME") ?: "localhost", port = 5432, - user = "postgres", - database = "postgres", - password = "password" + user = env("POSTGRES_USER") ?: "postgres", + database = env("POSTGRES_DB") ?: "postgres", + password = env("POSTGRES_PASSWORD") ?: "password" ) @Test - fun allTypes() { - val queries = NativePostgres(driver).fooQueries - NativePostgres.Schema.migrate(driver, 0, NativePostgres.Schema.version) - assertEquals(emptyList(), queries.get().executeAsList()) - - val foo = Foo( - a = 42, - b = "answer", - date = LocalDate(2020, Month.DECEMBER, 12), - time = LocalTime(12, 42, 0, 0), - timestamp = LocalDateTime(2014, Month.AUGUST, 1, 12, 1, 2, 0), - instant = Instant.fromEpochMilliseconds(10L), - interval = 42.seconds, - uuid = UUID.NIL + fun simpleTest() = runTest { + assertEquals(0, driver.execute(null, "DROP TABLE IF EXISTS baz;", parameters = 0).value) + assertEquals( + 0, + driver.execute(null, "CREATE TABLE baz(a INT PRIMARY KEY, foo TEXT, b BYTEA);", parameters = 0).value ) - queries.create( - a = foo.a, - b = foo.b, - date = foo.date, - time = foo.time, - timestamp = foo.timestamp, - instant = foo.instant, - interval = foo.interval, - uuid = foo.uuid + repeat(5) { + val result = driver.execute(null, "INSERT INTO baz VALUES ($it)", parameters = 0) + assertEquals(1, result.value) + } + + val result = driver.execute(null, "INSERT INTO baz VALUES ($1, $2, $3), ($4, $5, $6)", parameters = 6) { + bindLong(0, 5) + bindString(1, "bar 0") + bindBytes(2, byteArrayOf(1.toByte(), 2.toByte())) + + bindLong(3, 6) + bindString(4, "bar 1") + bindBytes(5, byteArrayOf(16.toByte(), 12.toByte())) + }.value + assertEquals(2, result) + val notPrepared = driver.executeQuery(null, "SELECT * FROM baz LIMIT 1;", parameters = 0, mapper = { + assertTrue(it.next().value) + QueryResult.Value( + Simple( + index = it.getLong(0)!!.toInt(), + name = it.getString(1), + byteArray = it.getBytes(2) + ) + ) + }) + assertEquals(Simple(0, null, null), notPrepared.value) + val preparedStatement = driver.executeQuery( + 42, + sql = "SELECT * FROM baz;", + parameters = 0, binders = null, + mapper = { + QueryResult.Value(buildList { + while (it.next().value) { + add( + Simple( + index = it.getLong(0)!!.toInt(), + name = it.getString(1), + byteArray = it.getBytes(2) + ) + ) + } + }) + } + ).value + + assertEquals(7, preparedStatement.size) + assertEquals( + List(5) { + Simple(it, null, null) + } + listOf( + Simple(5, "bar 0", byteArrayOf(1.toByte(), 2.toByte())), + Simple(6, "bar 1", byteArrayOf(16.toByte(), 12.toByte())), + ), + preparedStatement ) - assertEquals(foo, queries.get().executeAsOne()) + + expect(7) { + val cursorList = driver.executeQueryAsFlow( + -99, + "SELECT * FROM baz", + fetchSize = 4, + parameters = 0, + binders = null, + mapper = { + Simple( + index = it.getLong(0)!!.toInt(), + name = it.getString(1), + byteArray = it.getBytes(2) + ) + }) + cursorList.count() + } + + expect(7) { + val cursorList = driver.executeQueryAsFlow( + -5, + "SELECT * FROM baz", + fetchSize = 1, + parameters = 0, + binders = null, + mapper = { + Simple( + index = it.getLong(0)!!.toInt(), + name = it.getString(1), + byteArray = it.getBytes(2) + + ) + }) + cursorList.count() + } + + val cursorFlow = driver.executeQueryAsFlow( + -42, + "SELECT * FROM baz", + fetchSize = 1, + parameters = 0, + binders = null, + mapper = { + Simple( + index = it.getLong(0)!!.toInt(), + name = it.getString(1), + byteArray = it.getBytes(2) + ) + }) + assertEquals(7, cursorFlow.count()) + assertEquals(4, cursorFlow.take(4).count()) + + expect(0) { + val cursorList = driver.executeQueryAsFlow( + -100, + "SELECT * FROM baz WHERE a = -1", + fetchSize = 1, + parameters = 0, + binders = null, + mapper = { + Simple( + index = it.getLong(0)!!.toInt(), + name = it.getString(1), + byteArray = it.getBytes(2) + ) + }) + cursorList.count() + } + } + + private data class Simple(val index: Int, val name: String?, val byteArray: ByteArray?) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + + other as Simple + + if (index != other.index) return false + if (name != other.name) return false + if (byteArray != null) { + if (other.byteArray == null) return false + if (!byteArray.contentEquals(other.byteArray)) return false + } else if (other.byteArray != null) return false + + return true + } + + override fun hashCode(): Int { + var result = index.hashCode() + result = 31 * result + (name?.hashCode() ?: 0) + result = 31 * result + (byteArray?.contentHashCode() ?: 0) + return result + } } @Test - fun copyTest() { - val queries = NativePostgres(driver).fooQueries - NativePostgres.Schema.migrate(driver, 0, NativePostgres.Schema.version) - queries.startCopy() - val result = - driver.copy("42,answer,2020-12-12,12:42:00.0000,2014-08-01T12:01:02.0000,1970-01-01T00:00:00.010Z,PT42S,00000000-0000-0000-0000-000000000000") - assertEquals(1, result) - val foo = Foo( - a = 42, - b = "answer", - date = LocalDate(2020, Month.DECEMBER, 12), - time = LocalTime(12, 42, 0, 0), - timestamp = LocalDateTime(2014, Month.AUGUST, 1, 12, 1, 2, 0), - instant = Instant.fromEpochMilliseconds(10L), - interval = 42.seconds, - uuid = UUID.NIL, - ) - assertEquals(foo, queries.get().executeAsOne()) + fun wrongCredentials() { + assertFailsWith { + PostgresNativeDriver( + host = "wrongHost", + port = 5432, + user = env("POSTGRES_USER") ?: "postgres", + database = env("POSTGRES_DB") ?: "postgres", + password = env("POSTGRES_PASSWORD") ?: "password" + ) + } + assertFailsWith { + PostgresNativeDriver( + host = "wrongHost", + port = 5432, + user = env("POSTGRES_USER") ?: "postgres", + database = env("POSTGRES_DB") ?: "postgres", + password = "wrongPassword" + ) + } + assertFailsWith { + PostgresNativeDriver( + host = "wrongHost", + port = 5432, + user = "wrongUser", + database = env("POSTGRES_DB") ?: "postgres", + password = env("POSTGRES_PASSWORD") ?: "password" + ) + } } @Test - fun userTest() { - val queries = NativePostgres(driver).usersQueries - NativePostgres.Schema.migrate(driver, 0, NativePostgres.Schema.version) - val id = queries.insertAndGet("test@test", "test", "bio", "", null).executeAsOne() - assertEquals(1, id) - val id2 = queries.insertAndGet("test2@test", "test2", "bio2", "", null).executeAsOne() - assertEquals(2, id2) - val testUser = queries.selectByUsername("test").executeAsOne() + fun copyTest() { + assertEquals(0, driver.execute(null, "DROP TABLE IF EXISTS copying;", parameters = 0).value) + assertEquals(0, driver.execute(null, "CREATE TABLE copying(a int primary key);", parameters = 0).value) + driver.execute(-42, "COPY copying FROM STDIN (FORMAT CSV);", 0) + val results = driver.copy(sequenceOf("1\n2\n", "3\n4\n")) + assertEquals(4, results) assertEquals( - SelectByUsername( - "test@test", - "test", - "bio", - "" - ), - testUser + listOf(1, 2, 3, 4), + driver.executeQuery(null, "SELECT * FROM copying", parameters = 0, binders = null, mapper = { + QueryResult.Value(buildList { + while (it.next().value) { + add(it.getLong(0)!!.toInt()) + } + }) + }).value ) } @Test - fun remoteListenerTest() = runTest(dispatchTimeoutMs = 10.seconds.inWholeMilliseconds) { - val client = PostgresNativeDriver( - host = "localhost", + fun remoteListenerTest() = runBlocking { + val other = PostgresNativeDriver( + host = env("POSTGRES_HOSTNAME") ?: "localhost", port = 5432, - user = "postgres", - database = "postgres", - password = "password", - listenerSupport = ListenerSupport.Remote(backgroundScope) { - it + it - } + user = env("POSTGRES_USER") ?: "postgres", + database = env("POSTGRES_DB") ?: "postgres", + password = env("POSTGRES_PASSWORD") ?: "password", + listenerSupport = ListenerSupport.Remote(this) ) - val server = PostgresNativeDriver( - host = "localhost", + val driver = PostgresNativeDriver( + host = env("POSTGRES_HOSTNAME") ?: "localhost", port = 5432, - user = "postgres", - database = "postgres", - password = "password", - listenerSupport = ListenerSupport.Remote(backgroundScope) { - it + it - } + user = env("POSTGRES_USER") ?: "postgres", + database = env("POSTGRES_DB") ?: "postgres", + password = env("POSTGRES_PASSWORD") ?: "password", + listenerSupport = ListenerSupport.Remote(this) ) - val db = NativePostgres(client) - NativePostgres.Schema.migrate(driver, 0, NativePostgres.Schema.version) - - db.fooQueries.create( - a = 42, - b = "answer", - date = LocalDate(2020, Month.DECEMBER, 12), - time = LocalTime(12, 42, 0, 0), - timestamp = LocalDateTime(2014, Month.AUGUST, 1, 12, 1, 2, 0), - instant = Instant.fromEpochMilliseconds(10L), - interval = 42.seconds, - uuid = UUID.NIL - ) - val userQueries = db.usersQueries - val id = userQueries.insertAndGet("foo", "foo", "foo", "", 42).executeAsOne() + val results = MutableStateFlow(0) + val listener = Query.Listener { results.update { it + 1 } } + driver.addListener("foo", "bar", listener = listener) - val users = async { - db.usersQueries.selectByFoo(42) - .asFlow().mapToOne(coroutineContext) - .take(2).toList() - } - withContext(Dispatchers.Default) { - val waitForRemoteNotifications = 2.seconds - delay(waitForRemoteNotifications) - } - runCurrent() + val dbDelay = 2.seconds + delay(dbDelay) + other.notifyListeners("foo") - NativePostgres(server).usersQueries.updateWhereFoo("foo2", 42) - withContext(Dispatchers.Default) { - val waitForRemoteNotifications = 2.seconds - delay(waitForRemoteNotifications) - } - runCurrent() + other.notifyListeners("foo", "bar") + other.notifyListeners("bar") - assertEquals( - listOf( - Users( - id, - "foo", - "foo", - "foo", - "", - 42 - ), Users( - id, - "foo2", - "foo", - "foo", - "", - 42 - ) - ), users.await() - ) + delay(dbDelay) - client.close() - server.close() + driver.removeListener("foo", "bar", listener = listener) + driver.notifyListeners("foo") + driver.notifyListeners("bar") + + delay(dbDelay) + assertEquals(4, results.value) + + other.close() } @Test - fun localListenerTest() = runTest(dispatchTimeoutMs = 10.seconds.inWholeMilliseconds) { - val client = PostgresNativeDriver( - host = "localhost", - port = 5432, - user = "postgres", - database = "postgres", - password = "password", - listenerSupport = ListenerSupport.Local(backgroundScope) - ) + fun localListenerTest() = runTest { + val notifications = MutableSharedFlow() + val notificationList = async { + notifications.take(4).toList() + } - val db = NativePostgres(client) - NativePostgres.Schema.migrate(driver, 0, NativePostgres.Schema.version) - - db.fooQueries.create( - a = 42, - b = "answer", - date = LocalDate(2020, Month.DECEMBER, 12), - time = LocalTime(12, 42, 0, 0), - timestamp = LocalDateTime(2014, Month.AUGUST, 1, 12, 1, 2, 0), - instant = Instant.fromEpochMilliseconds(10L), - interval = 42.seconds, - uuid = UUID.NIL + val driver = PostgresNativeDriver( + host = env("POSTGRES_HOSTNAME") ?: "localhost", + port = 5432, + user = env("POSTGRES_USER") ?: "postgres", + database = env("POSTGRES_DB") ?: "postgres", + password = env("POSTGRES_PASSWORD") ?: "password", + listenerSupport = ListenerSupport.Local( + this, + notifications, + ) { + notifications.emit(it) + } ) - val userQueries = db.usersQueries - val id = userQueries.insertAndGet("foo", "foo", "foo", "", 42).executeAsOne() - val users = async { - db.usersQueries.selectByFoo(42) - .asFlow().mapToOne(coroutineContext) - .take(2).toList() - } + val results = MutableStateFlow(0) + val listener = Query.Listener { results.update { it + 1 } } + driver.addListener("foo", "bar", listener = listener) runCurrent() - - userQueries.updateWhereFoo("foo2", 42) + driver.notifyListeners("foo") + runCurrent() + driver.notifyListeners("foo", "bar") + runCurrent() + driver.notifyListeners("bar") runCurrent() - assertEquals( - listOf( - Users( - id, - "foo", - "foo", - "foo", - "", - 42 - ), Users( - id, - "foo2", - "foo", - "foo", - "", - 42 - ) - ), users.await() - ) + driver.removeListener("foo", "bar", listener = listener) + runCurrent() + driver.notifyListeners("foo") + runCurrent() + driver.notifyListeners("bar") + runCurrent() - client.close() + assertEquals(4, results.value) + assertEquals(listOf("foo", "foo", "bar", "bar"), notificationList.await()) } } + +@OptIn(ExperimentalForeignApi::class) +private fun env(name: String): String? { + return getenv(name)?.toKStringFromUtf8()?.takeUnless { it.isEmpty() } +}