├── .devcontainer ├── Dockerfile ├── devcontainer.json └── docker-compose.yml ├── .github ├── FUNDING.yml ├── dependabot.yml ├── release.yml └── workflows │ ├── CD.yml │ ├── CI.yml │ ├── Docs.yml │ └── dependencies.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── build-logic │ ├── build.gradle.kts │ ├── settings.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ ├── MyRepos.kt │ │ ├── MyRepos.settings.gradle.kts │ │ ├── exclude.gradle.kts │ │ ├── publish.gradle.kts │ │ └── repos.gradle.kts ├── gradle-daemon-jvm.properties ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── postgres-native-sqldelight-dialect ├── api │ └── postgres-native-sqldelight-dialect.api ├── build.gradle.kts └── src │ └── main │ ├── kotlin │ └── app │ │ └── softwork │ │ └── sqldelight │ │ └── postgresdialect │ │ ├── BindParameterMixin.kt │ │ ├── PostgreSqlNative.bnf │ │ └── PostgresNativeDialect.kt │ └── resources │ └── META-INF │ └── services │ └── app.cash.sqldelight.dialect.api.SqlDelightDialect ├── postgres-native-sqldelight-driver ├── api │ └── postgres-native-sqldelight-driver.klib.api ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── app │ │ └── softwork │ │ └── sqldelight │ │ └── postgresdriver │ │ ├── ListenerSupport.kt │ │ ├── NoCursor.kt │ │ ├── PostgresCursor.kt │ │ ├── PostgresNativeDriver.kt │ │ ├── PostgresPreparedStatement.kt │ │ ├── RealCursor.kt │ │ └── ScopedListenerSupport.kt │ └── nativeInterop │ └── cinterop │ └── libpq.def ├── settings.gradle.kts ├── testing-sqldelight ├── build.gradle.kts └── src │ ├── commonMain │ └── sqldelight │ │ └── app │ │ └── softwork │ │ └── sqldelight │ │ └── postgresdriver │ │ ├── 1.sqm │ │ ├── Foo.sq │ │ └── Users.sq │ └── commonTest │ └── kotlin │ └── app │ └── softwork │ └── sqldelight │ └── postgresdriver │ └── PostgresNativeSqldelightDriverTest.kt └── testing ├── build.gradle.kts └── src └── commonTest └── kotlin └── app └── softwork └── sqldelight └── postgresdriver └── PostgresNativeDriverTest.kt /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/java:21 2 | 3 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 4 | && apt-get -y install --no-install-recommends libpq-dev 5 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "dockerComposeFile": "docker-compose.yml", 3 | "service": "app", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "features": { 6 | "ghcr.io/devcontainers/features/sshd:1": {} 7 | }, 8 | 9 | "forwardPorts": [ 10 | 5432 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | app: 5 | container_name: javadev 6 | # https://youtrack.jetbrains.com/issue/KT-36871/Support-Aarch64-Linux-as-a-host-for-the-Kotlin-Native 7 | platform: "linux/amd64" 8 | build: 9 | context: . 10 | dockerfile: Dockerfile 11 | environment: 12 | POSTGRES_HOSTNAME: postgresdb 13 | POSTGRES_DB: postgres 14 | POSTGRES_USER: postgres 15 | POSTGRES_PASSWORD: password 16 | 17 | volumes: 18 | - ../..:/workspaces:cached 19 | 20 | command: sleep infinity 21 | 22 | network_mode: service:db 23 | 24 | db: 25 | container_name: postgresdb 26 | image: postgres:latest 27 | restart: unless-stopped 28 | healthcheck: 29 | test: [ "CMD-SHELL", "pg_isready" ] 30 | interval: 1s 31 | timeout: 5s 32 | retries: 10 33 | environment: 34 | POSTGRES_DB: postgres 35 | POSTGRES_USER: postgres 36 | POSTGRES_PASSWORD: password 37 | ports: 38 | - "5432:5432" 39 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: hfhbd 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | assignees: 8 | - "hfhbd" 9 | - package-ecosystem: "gradle" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | assignees: 14 | - "hfhbd" 15 | rebase-strategy: "disabled" 16 | - package-ecosystem: "devcontainers" 17 | directory: "/" 18 | schedule: 19 | interval: "daily" 20 | assignees: 21 | - "hfhbd" 22 | - package-ecosystem: "docker" 23 | directory: "/.devcontainer" 24 | schedule: 25 | interval: "daily" 26 | assignees: 27 | - "hfhbd" 28 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: Features 4 | labels: 5 | - '*' 6 | exclude: 7 | labels: 8 | - dependencies 9 | - title: Updated Dependencies 10 | labels: 11 | - dependencies 12 | -------------------------------------------------------------------------------- /.github/workflows/CD.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | release: 5 | types: [ created ] 6 | 7 | jobs: 8 | build: 9 | runs-on: macos-latest 10 | 11 | steps: 12 | - name: Set environment for version 13 | run: long="${{ github.ref }}"; version=${long#"refs/tags/v"}; echo "version=${version}" >> $GITHUB_ENV 14 | - uses: actions/checkout@v4 15 | - uses: Homebrew/actions/setup-homebrew@master 16 | id: set-up-homebrew 17 | - uses: actions/setup-java@v4 18 | with: 19 | distribution: 'temurin' 20 | java-version: 21 21 | - uses: gradle/actions/setup-gradle@v4 22 | - run: brew install libpq 23 | env: 24 | HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: true 25 | - name: Publish 26 | run: ./gradlew -Pversion=$version -Dorg.gradle.parallel=false --no-configuration-cache publish closeAndReleaseStagingRepositories 27 | env: 28 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_PRIVATE_KEY }} 29 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} 30 | ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONARTYPE_APIKEY }} 31 | ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONARTYPE_APITOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | permissions: 13 | contents: write 14 | strategy: 15 | matrix: 16 | os: [ 'ubuntu-latest', 'ubuntu-24.04', 'macos-latest' ] 17 | 18 | env: 19 | HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: true 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: Homebrew/actions/setup-homebrew@master 24 | id: set-up-homebrew 25 | - run: brew install libpq 26 | - uses: ikalnytskyi/action-setup-postgres@v7 27 | with: 28 | password: password 29 | - uses: actions/setup-java@v4 30 | with: 31 | distribution: 'temurin' 32 | java-version: 21 33 | - uses: gradle/actions/setup-gradle@v4 34 | - run: ./gradlew assemble 35 | - run: ./gradlew build 36 | -------------------------------------------------------------------------------- /.github/workflows/Docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | release: 5 | types: [ created ] 6 | workflow_dispatch: 7 | 8 | concurrency: 9 | group: "docs" 10 | cancel-in-progress: false 11 | 12 | env: 13 | GRADLE_OPTS: -Dorg.gradle.caching=true 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: read 20 | pages: write 21 | id-token: write 22 | 23 | environment: 24 | name: github-pages 25 | url: ${{ steps.deployment.outputs.page_url }} 26 | 27 | steps: 28 | - uses: actions/configure-pages@v5 29 | - uses: actions/checkout@v4 30 | - uses: Homebrew/actions/setup-homebrew@master 31 | id: set-up-homebrew 32 | - run: brew install libpq 33 | - uses: actions/setup-java@v4 34 | with: 35 | distribution: 'temurin' 36 | java-version: 21 37 | - uses: gradle/actions/setup-gradle@v4 38 | - name: Generate Docs 39 | run: ./gradlew :dokkaHtmlMultiModule --no-configuration-cache 40 | - uses: actions/upload-pages-artifact@v3 41 | with: 42 | path: build/dokka/htmlMultiModule 43 | - name: Deploy to GitHub Pages 44 | id: deployment 45 | uses: actions/deploy-pages@v4 46 | -------------------------------------------------------------------------------- /.github/workflows/dependencies.yml: -------------------------------------------------------------------------------- 1 | name: Dependency review for pull requests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | dependency-submission: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | 15 | env: 16 | GRADLE_OPTS: -Dorg.gradle.caching=true 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-java@v4 21 | with: 22 | distribution: 'temurin' 23 | java-version: 21 24 | - uses: gradle/actions/dependency-submission@v4 25 | 26 | dependency-review: 27 | runs-on: ubuntu-latest 28 | needs: dependency-submission 29 | if: github.event_name == 'pull_request' 30 | 31 | steps: 32 | - uses: actions/dependency-review-action@v4 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | .kotlin 4 | build 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostgreSQL native SQLDelight driver 2 | 3 | A native Postgres driver using libpq. 4 | 5 | You can use the driver with [SQLDelight](https://github.com/cashapp/sqldelight), but this is not required. 6 | 7 | - [Source code](https://github.com/hfhbd/postgres-native-sqldelight) 8 | 9 | > Keep in mind, until now, this is only a single-threaded wrapper over libpq using 1 connection only. There is no 10 | > connection pool nor multithread support (like JDBC or R2DBC). 11 | 12 | ## Install 13 | 14 | You need `libpq` installed and available in your `$PATH`. 15 | 16 | This package is uploaded to MavenCentral and supports macOS and linuxX64. 17 | Windows is currently not supported. 18 | 19 | ````kotlin 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | implementation("app.softwork:postgres-native-sqldelight-driver:LATEST") 26 | } 27 | 28 | // optional SQLDelight setup: 29 | sqldelight { 30 | databases.register("NativePostgres") { 31 | dialect("app.softwork:postgres-native-sqldelight-dialect:LATEST") 32 | } 33 | linkSqlite.set(false) 34 | } 35 | ```` 36 | 37 | ## Usage 38 | 39 | ```kotlin 40 | val driver = PostgresNativeDriver( 41 | host = "localhost", 42 | port = 5432, 43 | user = "postgres", 44 | database = "postgres", 45 | password = "password", 46 | options = null, 47 | listenerSupport = ListenerSupport.Remote(coroutineScope) 48 | ) 49 | ``` 50 | 51 | ### Listeners 52 | 53 | This driver supports local and remote listeners. 54 | Local listeners only notify this client, ideally for testing or using the database with only one client at a time. 55 | Remote listener support uses `NOTIFY` and `LISTEN`, so you can use this to sync multiple clients or with existing 56 | database 57 | triggers. 58 | SQLDelight uses and expects the table name as payload, but you can provide a mapper function. 59 | 60 | ### SQLDelight Support 61 | 62 | Just create the driver and use your database instances in the usual way. 63 | 64 | ### Raw usage 65 | 66 | Beside SQLDelight you could also use this driver with raw queries. 67 | The identifier is used to reuse prepared statements. 68 | 69 | ```kotlin 70 | driver.execute(identifier = null, sql = "INSERT INTO foo VALUES (42)", parameters = 0, binders = null) 71 | ``` 72 | 73 | It also supports a real lazy cursor by using a `Flow`. The `fetchSize` parameter defines how many rows are fetched at 74 | once: 75 | 76 | ```kotlin 77 | val namesFlow: Flow = driver.executeQueryAsFlow( 78 | identifier = null, 79 | sql = "SELECT index, name, bytes FROM foo", 80 | mapper = { cursor -> 81 | Simple( 82 | index = cursor.getLong(0)!!.toInt(), 83 | name = cursor.getString(1), 84 | byteArray = cursor.getBytes(2) 85 | ) 86 | }, 87 | parameters = 0, 88 | fetchSize = 100, 89 | binders = null 90 | ) 91 | ``` 92 | 93 | And for bulk imports, use the `copy` method. You need to enable `COPY` first: 94 | 95 | ```kotlin 96 | driver.execute(514394779, "COPY foo FROM STDIN (FORMAT CSV)", 0) 97 | val rows = driver.copy("1,2,3\n4,5,6\n") 98 | ``` 99 | 100 | ## License 101 | 102 | Apache 2 103 | 104 | ## Contributing 105 | 106 | ### Devcontainers 107 | 108 | Start the devcontainer, that's it. 109 | 110 | ### Local 111 | 112 | #### docker compose 113 | 114 | This is the preferred local option. The `app` service contains the JVM as well as libpq. 115 | 116 | #### Manual 117 | 118 | You need to install `libpq`, eg using Homebrew: https://formulae.brew.sh/formula/libpq#default 119 | 120 | For installation using homebrew, the default path is already added. 121 | 122 | Otherwise, you have to add the compiler flags to 123 | the [libpq.def](postgres-native-sqldelight-driver/src/nativeInterop/cinterop/libpq.def). 124 | The exact flags depend on your config, for example: 125 | 126 | ``` 127 | For compilers to find libpq you may need to set: 128 | export LDFLAGS="-L/home/linuxbrew/.linuxbrew/opt/libpq/lib" 129 | export CPPFLAGS="-I/home/linuxbrew/.linuxbrew/opt/libpq/include" 130 | ``` 131 | 132 | ##### Testing 133 | 134 | If you installed libpq with homebrew, it will install the platform-specific artifact. 135 | 136 | To test other platforms, eg. linux x64 on macOS, you need to install the platform-specific libpq of linux x64 too. 137 | 138 | To start the postgres instance, you can use docker: 139 | 140 | ```sh 141 | docker run -e POSTGRES_PASSWORD=password -p 5432:5432 postgres 142 | ``` 143 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("io.github.gradle-nexus.publish-plugin") 3 | id("org.jetbrains.dokka") 4 | } 5 | 6 | tasks.dokkaHtmlMultiModule { 7 | includes.from("README.md") 8 | } 9 | 10 | nexusPublishing { 11 | this.repositories { 12 | sonatype { 13 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) 14 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.mpp.enableCInteropCommonization=true 3 | 4 | org.gradle.parallel=true 5 | org.gradle.jvmargs=-Xmx2048m 6 | org.gradle.configuration-cache=true 7 | org.gradle.configureondemand=true 8 | 9 | group=app.softwork 10 | -------------------------------------------------------------------------------- /gradle/build-logic/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | dependencies { 6 | implementation(libs.plugins.kotlin.jvm.toDep()) 7 | implementation(libs.plugins.kotlin.serialization.toDep()) 8 | implementation(libs.plugins.grammarKit.toDep()) 9 | implementation(libs.plugins.publish.toDep()) 10 | implementation(libs.plugins.binary.toDep()) 11 | implementation(libs.plugins.sqldelight.toDep()) 12 | implementation(libs.plugins.licensee.toDep()) 13 | implementation(libs.plugins.dokka.toDep()) 14 | } 15 | 16 | fun Provider.toDep() = map { 17 | "${it.pluginId}:${it.pluginId}.gradle.plugin:${it.version}" 18 | } 19 | 20 | kotlin.jvmToolchain(17) 21 | -------------------------------------------------------------------------------- /gradle/build-logic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 3 | repositories { 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | versionCatalogs.register("libs") { 8 | from(files("../libs.versions.toml")) 9 | } 10 | } 11 | 12 | plugins { 13 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 14 | } 15 | 16 | rootProject.name = "build-logic" 17 | -------------------------------------------------------------------------------- /gradle/build-logic/src/main/kotlin/MyRepos.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.artifacts.dsl.RepositoryHandler 2 | import org.gradle.kotlin.dsl.maven 3 | 4 | fun RepositoryHandler.repos() { 5 | mavenCentral() 6 | 7 | maven(url = "https://www.jetbrains.com/intellij-repository/releases") 8 | maven(url = "https://cache-redirector.jetbrains.com/intellij-dependencies") 9 | maven(url = "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-ide-plugin-dependencies/") 10 | maven(url = "https://maven.pkg.jetbrains.space/public/p/ktor/eap") 11 | } 12 | -------------------------------------------------------------------------------- /gradle/build-logic/src/main/kotlin/MyRepos.settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositories { 3 | repos() 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /gradle/build-logic/src/main/kotlin/exclude.gradle.kts: -------------------------------------------------------------------------------- 1 | configurations.configureEach { 2 | exclude(group = "com.jetbrains.rd") 3 | exclude(group = "com.github.jetbrains", module = "jetCheck") 4 | exclude(group = "com.jetbrains.infra") 5 | 6 | exclude(group = "org.roaringbitmap") 7 | 8 | exclude(group = "ai.grazie.spell") 9 | exclude(group = "ai.grazie.model") 10 | exclude(group = "ai.grazie.utils") 11 | exclude(group = "ai.grazie.nlp") 12 | } 13 | -------------------------------------------------------------------------------- /gradle/build-logic/src/main/kotlin/publish.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.publish.maven.* 2 | import org.gradle.api.tasks.bundling.* 3 | import org.gradle.kotlin.dsl.* 4 | import java.util.* 5 | 6 | plugins { 7 | id("maven-publish") 8 | id("signing") 9 | } 10 | 11 | val emptyJar by tasks.registering(Jar::class) { } 12 | 13 | publishing { 14 | publications.configureEach { 15 | this as MavenPublication 16 | if (project.name != "postgres-native-sqldelight-dialect") { 17 | artifact(emptyJar) { 18 | classifier = "javadoc" 19 | } 20 | } 21 | pom { 22 | name.set("app.softwork Postgres Native Driver and SqlDelight Dialect") 23 | description.set("A Postgres native driver including support for SqlDelight") 24 | url.set("https://github.com/hfhbd/SqlDelightNativePostgres") 25 | licenses { 26 | license { 27 | name.set("The Apache License, Version 2.0") 28 | url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") 29 | } 30 | } 31 | developers { 32 | developer { 33 | id.set("hfhbd") 34 | name.set("Philip Wedemann") 35 | email.set("mybztg+mavencentral@icloud.com") 36 | } 37 | } 38 | scm { 39 | connection.set("scm:git://github.com/hfhbd/SqlDelightNativePostgres.git") 40 | developerConnection.set("scm:git://github.com/hfhbd/SqlDelightNativePostgres.git") 41 | url.set("https://github.com/hfhbd/SqlDelightNativePostgres") 42 | } 43 | } 44 | } 45 | } 46 | 47 | signing { 48 | val signingKey: String? by project 49 | val signingPassword: String? by project 50 | signingKey?.let { 51 | useInMemoryPgpKeys(String(Base64.getDecoder().decode(it)).trim(), signingPassword) 52 | sign(publishing.publications) 53 | } 54 | } 55 | 56 | // https://youtrack.jetbrains.com/issue/KT-46466 57 | val signingTasks = tasks.withType() 58 | tasks.withType().configureEach { 59 | dependsOn(signingTasks) 60 | } 61 | 62 | tasks.withType().configureEach { 63 | isPreserveFileTimestamps = false 64 | isReproducibleFileOrder = true 65 | filePermissions {} 66 | dirPermissions {} 67 | } 68 | -------------------------------------------------------------------------------- /gradle/build-logic/src/main/kotlin/repos.gradle.kts: -------------------------------------------------------------------------------- 1 | repositories { 2 | repos() 3 | } 4 | -------------------------------------------------------------------------------- /gradle/gradle-daemon-jvm.properties: -------------------------------------------------------------------------------- 1 | #This file is generated by updateDaemonJvm 2 | toolchainVersion=21 3 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.1.21" 3 | sqldelight = "2.0.2" 4 | idea = "222.4459.24" 5 | coroutines = "1.10.2" 6 | 7 | [libraries] 8 | sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqldelight" } 9 | sqldelight-postgresql-dialect = { module = "app.cash.sqldelight:postgresql-dialect", version.ref = "sqldelight" } 10 | sqldelight-dialect-api = { module = "app.cash.sqldelight:dialect-api", version.ref = "sqldelight" } 11 | sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } 12 | sqldelight-compiler-env = { module = "app.cash.sqldelight:compiler-env", version.ref = "sqldelight" } 13 | 14 | coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } 15 | coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } 16 | ktor-network = { module = "io.ktor:ktor-network", version = "3.1.3" } 17 | datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.6.2" } 18 | serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version = "1.8.1" } 19 | 20 | [plugins] 21 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 22 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 23 | grammarKit = { id = "com.alecstrong.grammar.kit.composer", version = "0.1.12" } 24 | publish = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" } 25 | binary = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.17.0" } 26 | sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } 27 | licensee = { id = "app.cash.licensee", version = "1.12.0" } 28 | dokka = { id = "org.jetbrains.dokka", version = "1.9.20" } 29 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfhbd/postgres-native-sqldelight/81945b73190b0ff2102546d20459d027fa728913/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /postgres-native-sqldelight-dialect/api/postgres-native-sqldelight-dialect.api: -------------------------------------------------------------------------------- 1 | public abstract class app/softwork/sqldelight/postgresdialect/BindParameterMixin : app/cash/sqldelight/dialect/grammar/mixins/BindParameterMixin { 2 | public fun (Lcom/intellij/lang/ASTNode;)V 3 | public fun replaceWith (ZI)Ljava/lang/String; 4 | } 5 | 6 | public class app/softwork/sqldelight/postgresdialect/PostgreSqlNativeParser : com/intellij/lang/LightPsiParser, com/intellij/lang/PsiParser { 7 | public fun ()V 8 | public static fun bind_parameter_real (Lcom/intellij/lang/PsiBuilder;I)Z 9 | public static fun overrides_real (Lcom/intellij/lang/PsiBuilder;I)Z 10 | public fun parse (Lcom/intellij/psi/tree/IElementType;Lcom/intellij/lang/PsiBuilder;)Lcom/intellij/lang/ASTNode; 11 | public fun parseLight (Lcom/intellij/psi/tree/IElementType;Lcom/intellij/lang/PsiBuilder;)V 12 | protected fun parse_root_ (Lcom/intellij/psi/tree/IElementType;Lcom/intellij/lang/PsiBuilder;)Z 13 | } 14 | 15 | public final class app/softwork/sqldelight/postgresdialect/PostgreSqlNativeParserUtil : com/intellij/lang/parser/GeneratedParserUtilBase { 16 | public static final field INSTANCE Lapp/softwork/sqldelight/postgresdialect/PostgreSqlNativeParserUtil; 17 | public static final fun bindParameterExt (Lcom/intellij/lang/PsiBuilder;ILcom/intellij/lang/parser/GeneratedParserUtilBase$Parser;)Z 18 | public final fun getBind_parameter ()Lcom/intellij/lang/parser/GeneratedParserUtilBase$Parser; 19 | public final fun getCreateElement ()Lkotlin/jvm/functions/Function1; 20 | public final fun getOverrides ()Lcom/intellij/lang/parser/GeneratedParserUtilBase$Parser; 21 | public final fun overrideSqlParser ()V 22 | public static final fun overridesExt (Lcom/intellij/lang/PsiBuilder;ILcom/intellij/lang/parser/GeneratedParserUtilBase$Parser;)Z 23 | public final fun reset ()V 24 | public final fun setBind_parameter (Lcom/intellij/lang/parser/GeneratedParserUtilBase$Parser;)V 25 | public final fun setCreateElement (Lkotlin/jvm/functions/Function1;)V 26 | public final fun setOverrides (Lcom/intellij/lang/parser/GeneratedParserUtilBase$Parser;)V 27 | } 28 | 29 | public final class app/softwork/sqldelight/postgresdialect/PostgresNativeDialect : app/cash/sqldelight/dialect/api/SqlDelightDialect { 30 | public fun ()V 31 | public fun getAllowsReferenceCycles ()Z 32 | public fun getAsyncRuntimeTypes ()Lapp/cash/sqldelight/dialect/api/RuntimeTypes; 33 | public fun getConnectionManager ()Lapp/cash/sqldelight/dialect/api/ConnectionManager; 34 | public fun getIcon ()Ljavax/swing/Icon; 35 | public fun getMigrationStrategy ()Lapp/cash/sqldelight/dialect/api/SqlGeneratorStrategy; 36 | public fun getRuntimeTypes ()Lapp/cash/sqldelight/dialect/api/RuntimeTypes; 37 | public fun isSqlite ()Z 38 | public fun migrationSquasher (Lapp/cash/sqldelight/dialect/api/MigrationSquasher;)Lapp/cash/sqldelight/dialect/api/MigrationSquasher; 39 | public fun setup ()V 40 | public fun typeResolver (Lapp/cash/sqldelight/dialect/api/TypeResolver;)Lapp/cash/sqldelight/dialect/api/TypeResolver; 41 | } 42 | 43 | public abstract interface class app/softwork/sqldelight/postgresdialect/psi/PostgreSqlNativeBindParameter : com/alecstrong/sql/psi/core/psi/SqlBindParameter { 44 | } 45 | 46 | public abstract interface class app/softwork/sqldelight/postgresdialect/psi/PostgreSqlNativeOverrides : com/alecstrong/sql/psi/core/psi/SqlCompositeElement { 47 | public abstract fun getBindParameter ()Lapp/softwork/sqldelight/postgresdialect/psi/PostgreSqlNativeBindParameter; 48 | } 49 | 50 | public abstract interface class app/softwork/sqldelight/postgresdialect/psi/PostgreSqlNativeTypes { 51 | public static final field BIND_PARAMETER Lcom/intellij/psi/tree/IElementType; 52 | public static final field DEFAULT Lcom/intellij/psi/tree/IElementType; 53 | public static final field OVERRIDES Lcom/intellij/psi/tree/IElementType; 54 | } 55 | 56 | public class app/softwork/sqldelight/postgresdialect/psi/PostgreSqlNativeTypes$Factory { 57 | public fun ()V 58 | public static fun createElement (Lcom/intellij/lang/ASTNode;)Lcom/intellij/psi/PsiElement; 59 | } 60 | 61 | public class app/softwork/sqldelight/postgresdialect/psi/PostgreSqlNativeVisitor : com/intellij/psi/PsiElementVisitor { 62 | public fun ()V 63 | public fun visitBindParameter (Lapp/softwork/sqldelight/postgresdialect/psi/PostgreSqlNativeBindParameter;)V 64 | public fun visitOverrides (Lapp/softwork/sqldelight/postgresdialect/psi/PostgreSqlNativeOverrides;)V 65 | public fun visitSqlBindParameter (Lcom/alecstrong/sql/psi/core/psi/SqlBindParameter;)V 66 | public fun visitSqlCompositeElement (Lcom/alecstrong/sql/psi/core/psi/SqlCompositeElement;)V 67 | } 68 | 69 | public class app/softwork/sqldelight/postgresdialect/psi/impl/PostgreSqlNativeBindParameterImpl : app/softwork/sqldelight/postgresdialect/BindParameterMixin, app/softwork/sqldelight/postgresdialect/psi/PostgreSqlNativeBindParameter { 70 | public fun (Lcom/intellij/lang/ASTNode;)V 71 | public fun accept (Lapp/softwork/sqldelight/postgresdialect/psi/PostgreSqlNativeVisitor;)V 72 | public fun accept (Lcom/intellij/psi/PsiElementVisitor;)V 73 | } 74 | 75 | public class app/softwork/sqldelight/postgresdialect/psi/impl/PostgreSqlNativeOverridesImpl : com/alecstrong/sql/psi/core/psi/SqlCompositeElementImpl, app/softwork/sqldelight/postgresdialect/psi/PostgreSqlNativeOverrides { 76 | public fun (Lcom/intellij/lang/ASTNode;)V 77 | public fun accept (Lapp/softwork/sqldelight/postgresdialect/psi/PostgreSqlNativeVisitor;)V 78 | public fun accept (Lcom/intellij/psi/PsiElementVisitor;)V 79 | public fun getBindParameter ()Lapp/softwork/sqldelight/postgresdialect/psi/PostgreSqlNativeBindParameter; 80 | } 81 | 82 | -------------------------------------------------------------------------------- /postgres-native-sqldelight-dialect/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | id("com.alecstrong.grammar.kit.composer") 4 | id("org.jetbrains.kotlinx.binary-compatibility-validator") 5 | id("app.cash.licensee") 6 | id("repos") 7 | id("publish") 8 | id("exclude") 9 | } 10 | 11 | java { 12 | withJavadocJar() 13 | withSourcesJar() 14 | } 15 | 16 | grammarKit { 17 | intellijRelease.set(libs.versions.idea) 18 | } 19 | 20 | dependencies { 21 | api(libs.sqldelight.postgresql.dialect) 22 | 23 | api(libs.sqldelight.dialect.api) 24 | 25 | compileOnly(libs.sqldelight.compiler.env) 26 | 27 | testImplementation(kotlin("test")) 28 | testImplementation(libs.sqldelight.compiler.env) 29 | } 30 | 31 | kotlin { 32 | jvmToolchain(17) 33 | 34 | target.compilations.configureEach { 35 | kotlinOptions.allWarningsAsErrors = true 36 | } 37 | explicitApi() 38 | sourceSets { 39 | configureEach { 40 | languageSettings.progressiveMode = true 41 | } 42 | } 43 | } 44 | 45 | licensee { 46 | allow("Apache-2.0") 47 | allow("MIT") 48 | allowUrl("https://jdbc.postgresql.org/about/license.html") 49 | } 50 | 51 | publishing { 52 | publications.register("maven") { 53 | from(components["java"]) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /postgres-native-sqldelight-dialect/src/main/kotlin/app/softwork/sqldelight/postgresdialect/BindParameterMixin.kt: -------------------------------------------------------------------------------- 1 | package app.softwork.sqldelight.postgresdialect 2 | 3 | import app.cash.sqldelight.dialect.grammar.mixins.BindParameterMixin 4 | import com.intellij.lang.* 5 | 6 | public abstract class BindParameterMixin(node: ASTNode) : BindParameterMixin(node) { 7 | override fun replaceWith(isAsync: Boolean, index: Int): String = "$$index" 8 | } 9 | -------------------------------------------------------------------------------- /postgres-native-sqldelight-dialect/src/main/kotlin/app/softwork/sqldelight/postgresdialect/PostgreSqlNative.bnf: -------------------------------------------------------------------------------- 1 | { 2 | // Specify the parent parser. 3 | overrides="com.alecstrong.sql.psi.core.SqlParser" 4 | elementTypeClass = "com.alecstrong.sql.psi.core.SqlElementType" 5 | 6 | implements="com.alecstrong.sql.psi.core.psi.SqlCompositeElement" 7 | extends="com.alecstrong.sql.psi.core.psi.SqlCompositeElementImpl" 8 | psiClassPrefix = "PostgreSqlNative" 9 | 10 | parserImports=[ 11 | "static com.alecstrong.sql.psi.core.psi.SqlTypes.DEFAULT" 12 | ] 13 | } 14 | overrides ::= bind_parameter 15 | 16 | bind_parameter ::= DEFAULT | ( '?' | ':' {identifier} ) { 17 | mixin = "app.softwork.sqldelight.postgresdialect.BindParameterMixin" 18 | extends = "com.alecstrong.sql.psi.core.psi.impl.SqlBindParameterImpl" 19 | implements = "com.alecstrong.sql.psi.core.psi.SqlBindParameter" 20 | override = true 21 | } 22 | -------------------------------------------------------------------------------- /postgres-native-sqldelight-dialect/src/main/kotlin/app/softwork/sqldelight/postgresdialect/PostgresNativeDialect.kt: -------------------------------------------------------------------------------- 1 | package app.softwork.sqldelight.postgresdialect 2 | 3 | import app.cash.sqldelight.dialect.api.* 4 | import app.cash.sqldelight.dialects.postgresql.* 5 | import app.cash.sqldelight.dialects.postgresql.grammar.* 6 | import app.cash.sqldelight.dialects.postgresql.grammar.psi.* 7 | import com.alecstrong.sql.psi.core.* 8 | import com.alecstrong.sql.psi.core.psi.* 9 | import com.squareup.kotlinpoet.* 10 | 11 | public class PostgresNativeDialect : SqlDelightDialect by PostgreSqlDialect() { 12 | 13 | override fun setup() { 14 | SqlParserUtil.reset() 15 | 16 | PostgreSqlParserUtil.reset() 17 | PostgreSqlParserUtil.overrideSqlParser() 18 | 19 | PostgreSqlNativeParserUtil.reset() 20 | PostgreSqlNativeParserUtil.overrideSqlParser() 21 | } 22 | 23 | override val runtimeTypes: RuntimeTypes = RuntimeTypes( 24 | cursorType = ClassName("app.softwork.sqldelight.postgresdriver", "PostgresCursor"), 25 | preparedStatementType = ClassName("app.softwork.sqldelight.postgresdriver", "PostgresPreparedStatement") 26 | ) 27 | 28 | override val asyncRuntimeTypes: RuntimeTypes 29 | get() = error("Async native driver is not yet supported") 30 | 31 | override fun typeResolver(parentResolver: TypeResolver): TypeResolver = PostgresNativeTypeResolver(parentResolver) 32 | } 33 | 34 | private class PostgresNativeTypeResolver(parentResolver: TypeResolver) : TypeResolver by PostgreSqlTypeResolver(parentResolver) { 35 | override fun definitionType(typeName: SqlTypeName): IntermediateType = with(typeName) { 36 | check(this is PostgreSqlTypeName) 37 | val type = IntermediateType( 38 | when { 39 | smallIntDataType != null -> PostgreSqlType.SMALL_INT 40 | intDataType != null -> PostgreSqlType.INTEGER 41 | bigIntDataType != null -> PostgreSqlType.BIG_INT 42 | approximateNumericDataType != null -> PrimitiveType.REAL 43 | stringDataType != null -> PrimitiveType.TEXT 44 | uuidDataType != null -> PostgreSqlType.UUID 45 | smallSerialDataType != null -> PostgreSqlType.SMALL_INT 46 | serialDataType != null -> PostgreSqlType.INTEGER 47 | bigSerialDataType != null -> PostgreSqlType.BIG_INT 48 | dateDataType != null -> { 49 | when (dateDataType!!.firstChild.text) { 50 | "DATE" -> PostgreSqlType.DATE 51 | "TIME" -> PostgreSqlType.TIME 52 | "TIMESTAMP" -> if (dateDataType!!.node.getChildren(null) 53 | .any { it.text == "WITH" } 54 | ) PostgreSqlType.TIMESTAMP_TIMEZONE else PostgreSqlType.TIMESTAMP 55 | "TIMESTAMPTZ" -> PostgreSqlType.TIMESTAMP_TIMEZONE 56 | "INTERVAL" -> PostgreSqlType.INTERVAL 57 | else -> throw IllegalArgumentException("Unknown date type ${dateDataType!!.text}") 58 | } 59 | } 60 | jsonDataType != null -> PrimitiveType.TEXT 61 | booleanDataType != null -> PrimitiveType.BOOLEAN 62 | blobDataType != null -> PrimitiveType.BLOB 63 | else -> throw IllegalArgumentException("Unknown kotlin type for sql type ${this.text}") 64 | } 65 | ) 66 | return type 67 | } 68 | } 69 | 70 | private enum class PostgreSqlType(override val javaType: TypeName): DialectType { 71 | SMALL_INT(SHORT) { 72 | override fun decode(value: CodeBlock) = CodeBlock.of("%L.toShort()", value) 73 | 74 | override fun encode(value: CodeBlock) = CodeBlock.of("%L.toLong()", value) 75 | }, 76 | INTEGER(INT) { 77 | override fun decode(value: CodeBlock) = CodeBlock.of("%L.toInt()", value) 78 | 79 | override fun encode(value: CodeBlock) = CodeBlock.of("%L.toLong()", value) 80 | }, 81 | BIG_INT(LONG), 82 | DATE(ClassName("kotlinx.datetime", "LocalDate")), 83 | TIME(ClassName("kotlinx.datetime", "LocalTime")), 84 | TIMESTAMP(ClassName("kotlinx.datetime", "LocalDateTime")), 85 | TIMESTAMP_TIMEZONE(ClassName("kotlinx.datetime", "Instant")), 86 | INTERVAL(ClassName("kotlinx.datetime", "DateTimePeriod")), 87 | UUID(ClassName("kotlin.uuid", "Uuid")); 88 | 89 | override fun prepareStatementBinder(columnIndex: CodeBlock, value: CodeBlock): CodeBlock { 90 | return CodeBlock.builder() 91 | .add( 92 | when (this) { 93 | SMALL_INT, INTEGER, BIG_INT -> "bindLong" 94 | DATE -> "bindDate" 95 | TIME -> "bindTime" 96 | TIMESTAMP -> "bindLocalTimestamp" 97 | TIMESTAMP_TIMEZONE -> "bindTimestamp" 98 | INTERVAL -> "bindInterval" 99 | UUID -> "bindUuid" 100 | } 101 | ) 102 | .add("(%L, %L)\n", columnIndex, value) 103 | .build() 104 | } 105 | 106 | override fun cursorGetter(columnIndex: Int, cursorName: String): CodeBlock { 107 | return CodeBlock.of( 108 | when (this) { 109 | SMALL_INT, INTEGER, BIG_INT -> "$cursorName.getLong($columnIndex)" 110 | DATE -> "$cursorName.getDate($columnIndex)" 111 | TIME -> "$cursorName.getTime($columnIndex)" 112 | TIMESTAMP -> "$cursorName.getLocalTimestamp($columnIndex)" 113 | TIMESTAMP_TIMEZONE -> "$cursorName.getTimestamp($columnIndex)" 114 | INTERVAL -> "$cursorName.getInterval($columnIndex)" 115 | UUID -> "$cursorName.getUuid($columnIndex)" 116 | } 117 | ) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /postgres-native-sqldelight-dialect/src/main/resources/META-INF/services/app.cash.sqldelight.dialect.api.SqlDelightDialect: -------------------------------------------------------------------------------- 1 | app.softwork.sqldelight.postgresdialect.PostgresNativeDialect 2 | -------------------------------------------------------------------------------- /postgres-native-sqldelight-driver/api/postgres-native-sqldelight-driver.klib.api: -------------------------------------------------------------------------------- 1 | // Klib ABI Dump 2 | // Targets: [linuxArm64, linuxX64, macosArm64, macosX64] 3 | // Rendering settings: 4 | // - Signature version: 2 5 | // - Show manifest properties: true 6 | // - Show declarations: true 7 | 8 | // Library unique name: 9 | sealed interface app.softwork.sqldelight.postgresdriver/ListenerSupport { // app.softwork.sqldelight.postgresdriver/ListenerSupport|null[0] 10 | final class Local : app.softwork.sqldelight.postgresdriver/ScopedListenerSupport { // app.softwork.sqldelight.postgresdriver/ListenerSupport.Local|null[0] 11 | 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] 12 | 13 | final val notificationScope // app.softwork.sqldelight.postgresdriver/ListenerSupport.Local.notificationScope|{}notificationScope[0] 14 | final fun (): kotlinx.coroutines/CoroutineScope // app.softwork.sqldelight.postgresdriver/ListenerSupport.Local.notificationScope.|(){}[0] 15 | } 16 | 17 | final class Remote : app.softwork.sqldelight.postgresdriver/ScopedListenerSupport { // app.softwork.sqldelight.postgresdriver/ListenerSupport.Remote|null[0] 18 | constructor (kotlinx.coroutines/CoroutineScope, kotlin/Function1 = ...) // app.softwork.sqldelight.postgresdriver/ListenerSupport.Remote.|(kotlinx.coroutines.CoroutineScope;kotlin.Function1){}[0] 19 | 20 | final val notificationScope // app.softwork.sqldelight.postgresdriver/ListenerSupport.Remote.notificationScope|{}notificationScope[0] 21 | final fun (): kotlinx.coroutines/CoroutineScope // app.softwork.sqldelight.postgresdriver/ListenerSupport.Remote.notificationScope.|(){}[0] 22 | } 23 | 24 | final object Companion { // app.softwork.sqldelight.postgresdriver/ListenerSupport.Companion|null[0] 25 | final fun Local(kotlinx.coroutines/CoroutineScope): app.softwork.sqldelight.postgresdriver/ListenerSupport.Local // app.softwork.sqldelight.postgresdriver/ListenerSupport.Companion.Local|Local(kotlinx.coroutines.CoroutineScope){}[0] 26 | } 27 | 28 | final object None : app.softwork.sqldelight.postgresdriver/ListenerSupport // app.softwork.sqldelight.postgresdriver/ListenerSupport.None|null[0] 29 | } 30 | 31 | final class app.softwork.sqldelight.postgresdriver/PostgresNativeDriver : app.cash.sqldelight.db/SqlDriver { // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver|null[0] 32 | constructor (kotlinx.cinterop/CPointer, app.softwork.sqldelight.postgresdriver/ListenerSupport) // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.|(kotlinx.cinterop.CPointer;app.softwork.sqldelight.postgresdriver.ListenerSupport){}[0] 33 | 34 | 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] 35 | 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] 36 | 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] 37 | final fun close() // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.close|close(){}[0] 38 | final fun copy(kotlin.sequences/Sequence): kotlin/Long // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.copy|copy(kotlin.sequences.Sequence){}[0] 39 | final fun currentTransaction(): app.cash.sqldelight/Transacter.Transaction? // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.currentTransaction|currentTransaction(){}[0] 40 | 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] 41 | final fun newTransaction(): app.cash.sqldelight.db/QueryResult.Value // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.newTransaction|newTransaction(){}[0] 42 | final fun notifyListeners(kotlin/Array...) // app.softwork.sqldelight.postgresdriver/PostgresNativeDriver.notifyListeners|notifyListeners(kotlin.Array...){}[0] 43 | 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] 44 | } 45 | 46 | final class app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement : app.cash.sqldelight.db/SqlPreparedStatement { // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement|null[0] 47 | final fun bindBoolean(kotlin/Int, kotlin/Boolean?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindBoolean|bindBoolean(kotlin.Int;kotlin.Boolean?){}[0] 48 | final fun bindBytes(kotlin/Int, kotlin/ByteArray?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindBytes|bindBytes(kotlin.Int;kotlin.ByteArray?){}[0] 49 | final fun bindDate(kotlin/Int, kotlinx.datetime/LocalDate?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindDate|bindDate(kotlin.Int;kotlinx.datetime.LocalDate?){}[0] 50 | final fun bindDouble(kotlin/Int, kotlin/Double?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindDouble|bindDouble(kotlin.Int;kotlin.Double?){}[0] 51 | final fun bindInterval(kotlin/Int, kotlinx.datetime/DateTimePeriod?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindInterval|bindInterval(kotlin.Int;kotlinx.datetime.DateTimePeriod?){}[0] 52 | final fun bindLocalTimestamp(kotlin/Int, kotlinx.datetime/LocalDateTime?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindLocalTimestamp|bindLocalTimestamp(kotlin.Int;kotlinx.datetime.LocalDateTime?){}[0] 53 | final fun bindLong(kotlin/Int, kotlin/Long?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindLong|bindLong(kotlin.Int;kotlin.Long?){}[0] 54 | final fun bindString(kotlin/Int, kotlin/String?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindString|bindString(kotlin.Int;kotlin.String?){}[0] 55 | final fun bindTime(kotlin/Int, kotlinx.datetime/LocalTime?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindTime|bindTime(kotlin.Int;kotlinx.datetime.LocalTime?){}[0] 56 | final fun bindTimestamp(kotlin/Int, kotlinx.datetime/Instant?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindTimestamp|bindTimestamp(kotlin.Int;kotlinx.datetime.Instant?){}[0] 57 | final fun bindUuid(kotlin/Int, kotlin.uuid/Uuid?) // app.softwork.sqldelight.postgresdriver/PostgresPreparedStatement.bindUuid|bindUuid(kotlin.Int;kotlin.uuid.Uuid?){}[0] 58 | } 59 | 60 | sealed class app.softwork.sqldelight.postgresdriver/PostgresCursor : app.cash.sqldelight.db/SqlCursor { // app.softwork.sqldelight.postgresdriver/PostgresCursor|null[0] 61 | final fun getDate(kotlin/Int): kotlinx.datetime/LocalDate? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getDate|getDate(kotlin.Int){}[0] 62 | final fun getInterval(kotlin/Int): kotlinx.datetime/DateTimePeriod? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getInterval|getInterval(kotlin.Int){}[0] 63 | final fun getLocalTimestamp(kotlin/Int): kotlinx.datetime/LocalDateTime? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getLocalTimestamp|getLocalTimestamp(kotlin.Int){}[0] 64 | final fun getTime(kotlin/Int): kotlinx.datetime/LocalTime? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getTime|getTime(kotlin.Int){}[0] 65 | final fun getTimestamp(kotlin/Int): kotlinx.datetime/Instant? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getTimestamp|getTimestamp(kotlin.Int){}[0] 66 | final fun getUuid(kotlin/Int): kotlin.uuid/Uuid? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getUuid|getUuid(kotlin.Int){}[0] 67 | open fun getBoolean(kotlin/Int): kotlin/Boolean? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getBoolean|getBoolean(kotlin.Int){}[0] 68 | open fun getBytes(kotlin/Int): kotlin/ByteArray? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getBytes|getBytes(kotlin.Int){}[0] 69 | open fun getDouble(kotlin/Int): kotlin/Double? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getDouble|getDouble(kotlin.Int){}[0] 70 | open fun getLong(kotlin/Int): kotlin/Long? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getLong|getLong(kotlin.Int){}[0] 71 | open fun getString(kotlin/Int): kotlin/String? // app.softwork.sqldelight.postgresdriver/PostgresCursor.getString|getString(kotlin.Int){}[0] 72 | } 73 | 74 | 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] 75 | -------------------------------------------------------------------------------- /postgres-native-sqldelight-driver/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.plugin.mpp.* 2 | 3 | plugins { 4 | kotlin("multiplatform") 5 | id("org.jetbrains.kotlinx.binary-compatibility-validator") 6 | id("app.cash.licensee") 7 | id("repos") 8 | id("publish") 9 | id("org.jetbrains.dokka") 10 | } 11 | 12 | kotlin { 13 | explicitApi() 14 | 15 | compilerOptions { 16 | progressiveMode.set(true) 17 | optIn.add("kotlin.uuid.ExperimentalUuidApi") 18 | } 19 | 20 | fun KotlinNativeTarget.config() { 21 | compilations.named("main") { 22 | cinterops { 23 | register("libpq") { 24 | defFile(project.file("src/nativeInterop/cinterop/libpq.def")) 25 | } 26 | } 27 | } 28 | } 29 | 30 | macosArm64 { config() } 31 | macosX64 { config() } 32 | linuxX64 { config() } 33 | linuxArm64 { config() } 34 | // mingwX64 { config() } 35 | 36 | sourceSets { 37 | commonMain { 38 | dependencies { 39 | implementation(libs.ktor.network) 40 | api(libs.coroutines.core) 41 | api(libs.sqldelight.runtime) 42 | api(libs.datetime) 43 | } 44 | } 45 | commonTest { 46 | dependencies { 47 | implementation(kotlin("test")) 48 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2") 49 | } 50 | } 51 | } 52 | } 53 | 54 | licensee { 55 | allow("Apache-2.0") 56 | } 57 | 58 | tasks.dokkaHtmlPartial { 59 | dokkaSourceSets.configureEach { 60 | externalDocumentationLink("https://cashapp.github.io/sqldelight/2.0.0/2.x/") 61 | externalDocumentationLink( 62 | url = "https://kotlinlang.org/api/kotlinx-datetime/", 63 | packageListUrl = "https://kotlinlang.org/api/kotlinx-datetime/kotlinx-datetime/package-list", 64 | ) 65 | externalDocumentationLink("https://uuid.softwork.app") 66 | externalDocumentationLink("https://kotlinlang.org/api/kotlinx.coroutines/") 67 | } 68 | } 69 | 70 | apiValidation { 71 | klib { 72 | enabled = true 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/ListenerSupport.kt: -------------------------------------------------------------------------------- 1 | package app.softwork.sqldelight.postgresdriver 2 | 3 | import io.ktor.network.selector.* 4 | import kotlinx.cinterop.* 5 | import kotlinx.coroutines.* 6 | import kotlinx.coroutines.flow.* 7 | import libpq.* 8 | import kotlin.time.* 9 | 10 | public sealed interface ListenerSupport { 11 | 12 | public companion object { 13 | public fun Local(notificationScope: CoroutineScope): Local { 14 | val notifications = MutableSharedFlow() 15 | return Local(notificationScope, notifications) { notifications.emit(it) } 16 | } 17 | } 18 | 19 | public class Local( 20 | notificationScope: CoroutineScope, 21 | internal val notifications: Flow, 22 | internal val notify: suspend (String) -> Unit 23 | ) : ScopedListenerSupport { 24 | override val notificationScope: CoroutineScope = notificationScope + Job() 25 | } 26 | 27 | public class Remote( 28 | notificationScope: CoroutineScope, 29 | internal val notificationName: (String) -> String = { it } 30 | ) : ScopedListenerSupport { 31 | override val notificationScope: CoroutineScope = notificationScope + Job() 32 | 33 | @ExperimentalForeignApi 34 | internal fun remoteListener(conn: CPointer): Flow = channelFlow { 35 | val selector = SelectorManager() 36 | 37 | try { 38 | val socket = PQsocket(conn) 39 | check(socket >= 0) { 40 | "Error while connecting to the PostgreSql socket" 41 | } 42 | val selectable = object : Selectable { 43 | override val descriptor: Int = socket 44 | } 45 | 46 | while (isActive) { 47 | selector.select(selectable, SelectInterest.READ) 48 | PQconsumeInput(conn) 49 | var notification: PGnotify? = null 50 | while (PQnotifies(conn)?.pointed?.also { notification = it } != null) { 51 | notification?.let { 52 | val tableName = it.relname!!.toKString() 53 | PQfreemem(it.ptr) 54 | send(tableName) 55 | } 56 | } 57 | } 58 | } finally { 59 | selector.close() 60 | } 61 | }.shareIn(notificationScope, SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO)) 62 | } 63 | 64 | public object None : ListenerSupport 65 | } 66 | -------------------------------------------------------------------------------- /postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/NoCursor.kt: -------------------------------------------------------------------------------- 1 | package app.softwork.sqldelight.postgresdriver 2 | 3 | import app.cash.sqldelight.db.* 4 | import kotlinx.cinterop.* 5 | import libpq.* 6 | 7 | @ExperimentalForeignApi 8 | internal class NoCursor( 9 | result: CPointer 10 | ) : PostgresCursor(result), Closeable { 11 | override fun close() { 12 | result.clear() 13 | } 14 | 15 | private val maxRowIndex = PQntuples(result) - 1 16 | override var currentRowIndex = -1 17 | 18 | override fun next(): QueryResult.Value { 19 | return if (currentRowIndex < maxRowIndex) { 20 | currentRowIndex += 1 21 | QueryResult.Value(true) 22 | } else { 23 | QueryResult.Value(false) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/PostgresCursor.kt: -------------------------------------------------------------------------------- 1 | package app.softwork.sqldelight.postgresdriver 2 | 3 | import app.cash.sqldelight.db.* 4 | import kotlinx.cinterop.* 5 | import kotlinx.datetime.* 6 | import libpq.* 7 | import kotlin.uuid.* 8 | 9 | @OptIn(ExperimentalForeignApi::class) 10 | public sealed class PostgresCursor( 11 | internal var result: CPointer 12 | ) : SqlCursor { 13 | internal abstract val currentRowIndex: Int 14 | 15 | override fun getBoolean(index: Int): Boolean? = getString(index)?.toBoolean() 16 | 17 | override fun getBytes(index: Int): ByteArray? { 18 | val isNull = PQgetisnull(result, tup_num = currentRowIndex, field_num = index) == 1 19 | return if (isNull) { 20 | null 21 | } else { 22 | val bytes = PQgetvalue(result, tup_num = currentRowIndex, field_num = index)!! 23 | val length = PQgetlength(result, tup_num = currentRowIndex, field_num = index) 24 | bytes.fromHex(length) 25 | } 26 | } 27 | 28 | private inline fun Int.fromHex(): Int = if (this in 48..57) { 29 | this - 48 30 | } else { 31 | this - 87 32 | } 33 | 34 | // because "normal" CPointer.toByteArray() functions does not support hex (2 Bytes) bytes 35 | private fun CPointer.fromHex(length: Int): ByteArray { 36 | val array = ByteArray((length - 2) / 2) 37 | var index = 0 38 | for (i in 2 until length step 2) { 39 | val first = this[i].toInt().fromHex() 40 | val second = this[i + 1].toInt().fromHex() 41 | val octet = first.shl(4).or(second) 42 | array[index] = octet.toByte() 43 | index++ 44 | } 45 | return array 46 | } 47 | 48 | override fun getDouble(index: Int): Double? = getString(index)?.toDouble() 49 | 50 | override fun getLong(index: Int): Long? = getString(index)?.toLong() 51 | 52 | override fun getString(index: Int): String? { 53 | val isNull = PQgetisnull(result, tup_num = currentRowIndex, field_num = index) == 1 54 | return if (isNull) { 55 | null 56 | } else { 57 | val value = PQgetvalue(result, tup_num = currentRowIndex, field_num = index) 58 | value!!.toKString() 59 | } 60 | } 61 | 62 | public fun getDate(index: Int): LocalDate? = getString(index)?.toLocalDate() 63 | public fun getTime(index: Int): LocalTime? = getString(index)?.toLocalTime() 64 | public fun getLocalTimestamp(index: Int): LocalDateTime? = getString(index)?.replace(" ", "T")?.toLocalDateTime() 65 | public fun getTimestamp(index: Int): Instant? = getString(index)?.let { 66 | Instant.parse(it.replace(" ", "T")) 67 | } 68 | 69 | public fun getInterval(index: Int): DateTimePeriod? = getString(index)?.let { DateTimePeriod.parse(it) } 70 | public fun getUuid(index: Int): Uuid? = getString(index)?.let { Uuid.parse(it) } 71 | } 72 | -------------------------------------------------------------------------------- /postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeDriver.kt: -------------------------------------------------------------------------------- 1 | package app.softwork.sqldelight.postgresdriver 2 | 3 | import app.cash.sqldelight.Query 4 | import app.cash.sqldelight.Transacter 5 | import app.cash.sqldelight.db.* 6 | import kotlinx.cinterop.* 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.Job 9 | import kotlinx.coroutines.cancel 10 | import kotlinx.coroutines.flow.* 11 | import kotlinx.coroutines.launch 12 | import libpq.* 13 | 14 | @OptIn(ExperimentalForeignApi::class) 15 | public class PostgresNativeDriver( 16 | private val conn: CPointer, 17 | private val listenerSupport: ListenerSupport 18 | ) : SqlDriver { 19 | private var transaction: Transacter.Transaction? = null 20 | 21 | private val notifications: Flow 22 | 23 | init { 24 | require(PQstatus(conn) == ConnStatusType.CONNECTION_OK) { 25 | conn.error() 26 | } 27 | setDateOutputs() 28 | 29 | notifications = when (listenerSupport) { 30 | is ListenerSupport.Local -> listenerSupport.notifications 31 | is ListenerSupport.Remote -> listenerSupport.remoteListener(conn) 32 | is ListenerSupport.None -> emptyFlow() 33 | } 34 | } 35 | 36 | private fun setDateOutputs() { 37 | execute(null, "SET intervalstyle = 'iso_8601';", 0) 38 | execute(null, "SET datestyle = 'ISO';", 0) 39 | } 40 | 41 | private val listeners = mutableMapOf() 42 | 43 | private fun CoroutineScope.listen(queryKeys: List, action: suspend (String) -> Unit) = 44 | launch { 45 | notifications.filter { 46 | it in queryKeys 47 | }.collect { 48 | action(it) 49 | } 50 | } 51 | 52 | override fun addListener(vararg queryKeys: String, listener: Query.Listener) { 53 | when (listenerSupport) { 54 | ListenerSupport.None -> return 55 | is ListenerSupport.Local -> { 56 | listeners[listener] = listenerSupport.notificationScope.listen(queryKeys.toList()) { 57 | listener.queryResultsChanged() 58 | } 59 | } 60 | 61 | is ListenerSupport.Remote -> { 62 | val queryKeysRenamed = queryKeys.map { 63 | listenerSupport.notificationName(it) 64 | } 65 | listeners[listener] = listenerSupport.notificationScope.listen(queryKeysRenamed) { 66 | listener.queryResultsChanged() 67 | } 68 | for (queryKey in queryKeysRenamed) { 69 | execute(null, "LISTEN ${conn.escaped(queryKey)}", parameters = 0) 70 | } 71 | } 72 | } 73 | } 74 | 75 | override fun notifyListeners(vararg queryKeys: String) { 76 | when (listenerSupport) { 77 | is ListenerSupport.Local -> { 78 | listenerSupport.notificationScope.launch { 79 | for (queryKey in queryKeys) { 80 | listenerSupport.notify(queryKey) 81 | } 82 | } 83 | } 84 | 85 | is ListenerSupport.Remote -> { 86 | for (queryKey in queryKeys) { 87 | val name = listenerSupport.notificationName(queryKey) 88 | execute(null, "NOTIFY ${conn.escaped(name)}", parameters = 0) 89 | } 90 | } 91 | 92 | ListenerSupport.None -> return 93 | } 94 | } 95 | 96 | override fun removeListener(vararg queryKeys: String, listener: Query.Listener) { 97 | val queryListeners = listeners[listener] 98 | if (queryListeners != null) { 99 | if (listenerSupport is ListenerSupport.Remote) { 100 | for (queryKey in queryKeys) { 101 | val name = listenerSupport.notificationName(queryKey) 102 | execute(null, "UNLISTEN ${conn.escaped(name)}", parameters = 0) 103 | } 104 | } 105 | queryListeners.cancel() 106 | listeners.remove(listener) 107 | } 108 | } 109 | 110 | override fun currentTransaction(): Transacter.Transaction? = transaction 111 | 112 | override fun execute( 113 | identifier: Int?, 114 | sql: String, 115 | parameters: Int, 116 | binders: (SqlPreparedStatement.() -> Unit)? 117 | ): QueryResult.Value { 118 | val preparedStatement = if (parameters != 0) PostgresPreparedStatement(parameters).apply { 119 | if (binders != null) { 120 | binders() 121 | } 122 | } else null 123 | val result = if (identifier != null) { 124 | if (!preparedStatementExists(identifier)) { 125 | PQprepare( 126 | conn, 127 | stmtName = identifier.toString(), 128 | query = sql, 129 | nParams = parameters, 130 | paramTypes = preparedStatement?.types?.refTo(0) 131 | ).check(conn).clear() 132 | } 133 | memScoped { 134 | PQexecPrepared( 135 | conn, 136 | stmtName = identifier.toString(), 137 | nParams = parameters, 138 | paramValues = preparedStatement?.values(this), 139 | paramFormats = preparedStatement?.formats?.refTo(0), 140 | paramLengths = preparedStatement?.lengths?.refTo(0), 141 | resultFormat = TEXT_RESULT_FORMAT 142 | ) 143 | } 144 | } else { 145 | memScoped { 146 | PQexecParams( 147 | conn, 148 | command = sql, 149 | nParams = parameters, 150 | paramValues = preparedStatement?.values(this), 151 | paramFormats = preparedStatement?.formats?.refTo(0), 152 | paramLengths = preparedStatement?.lengths?.refTo(0), 153 | resultFormat = TEXT_RESULT_FORMAT, 154 | paramTypes = preparedStatement?.types?.refTo(0) 155 | ) 156 | } 157 | }.check(conn) 158 | 159 | return QueryResult.Value(value = result.rows) 160 | } 161 | 162 | private val CPointer.rows: Long 163 | get() { 164 | val rows = PQcmdTuples(this)!!.toKString() 165 | clear() 166 | return rows.toLongOrNull() ?: 0 167 | } 168 | 169 | private fun preparedStatementExists(identifier: Int): Boolean { 170 | val result = 171 | executeQuery( 172 | null, 173 | "SELECT name FROM pg_prepared_statements WHERE name = $1", 174 | parameters = 1, 175 | binders = { 176 | bindString(0, identifier.toString()) 177 | }, 178 | mapper = { 179 | it.next().map { next -> 180 | if (next) { 181 | it.getString(0) 182 | } else null 183 | } 184 | } 185 | ) 186 | return result.value != null 187 | } 188 | 189 | private fun Int.escapeNegative(): String = if (this < 0) "_${toString().substring(1)}" else toString() 190 | 191 | private fun preparedStatement( 192 | parameters: Int, 193 | binders: (PostgresPreparedStatement.() -> Unit)? 194 | ): PostgresPreparedStatement? = if (parameters != 0) { 195 | PostgresPreparedStatement(parameters).apply { 196 | if (binders != null) { 197 | binders() 198 | } 199 | } 200 | } else null 201 | 202 | private fun checkPreparedStatement( 203 | identifier: Int, 204 | sql: String, 205 | parameters: Int, 206 | preparedStatement: PostgresPreparedStatement? 207 | ) { 208 | if (!preparedStatementExists(identifier)) { 209 | PQprepare( 210 | conn, 211 | stmtName = identifier.toString(), 212 | query = sql, 213 | nParams = parameters, 214 | paramTypes = preparedStatement?.types?.refTo(0) 215 | ).check(conn).clear() 216 | } 217 | } 218 | 219 | override fun executeQuery( 220 | identifier: Int?, 221 | sql: String, 222 | mapper: (SqlCursor) -> QueryResult, 223 | parameters: Int, 224 | binders: (SqlPreparedStatement.() -> Unit)? 225 | ): QueryResult { 226 | val preparedStatement = preparedStatement(parameters, binders) 227 | val result = if (identifier != null) { 228 | checkPreparedStatement(identifier, sql, parameters, preparedStatement) 229 | memScoped { 230 | PQexecPrepared( 231 | conn, 232 | stmtName = identifier.toString(), 233 | nParams = parameters, 234 | paramValues = preparedStatement?.values(this), 235 | paramLengths = preparedStatement?.lengths?.refTo(0), 236 | paramFormats = preparedStatement?.formats?.refTo(0), 237 | resultFormat = TEXT_RESULT_FORMAT 238 | ) 239 | } 240 | } else { 241 | memScoped { 242 | PQexecParams( 243 | conn, 244 | command = sql, 245 | nParams = parameters, 246 | paramValues = preparedStatement?.values(this), 247 | paramLengths = preparedStatement?.lengths?.refTo(0), 248 | paramFormats = preparedStatement?.formats?.refTo(0), 249 | paramTypes = preparedStatement?.types?.refTo(0), 250 | resultFormat = TEXT_RESULT_FORMAT 251 | ) 252 | } 253 | }.check(conn) 254 | 255 | return NoCursor(result).use(mapper) 256 | } 257 | 258 | internal companion object { 259 | const val TEXT_RESULT_FORMAT = 0 260 | const val BINARY_RESULT_FORMAT = 1 261 | } 262 | 263 | override fun close() { 264 | PQfinish(conn) 265 | if (listenerSupport is ScopedListenerSupport) { 266 | listenerSupport.notificationScope.cancel() 267 | } 268 | } 269 | 270 | override fun newTransaction(): QueryResult.Value { 271 | conn.exec("BEGIN") 272 | return QueryResult.Value(Transaction(transaction)) 273 | } 274 | 275 | private inner class Transaction( 276 | override val enclosingTransaction: Transacter.Transaction? 277 | ) : Transacter.Transaction() { 278 | override fun endTransaction(successful: Boolean): QueryResult.Value { 279 | if (enclosingTransaction == null) { 280 | if (successful) { 281 | conn.exec("END") 282 | } else { 283 | conn.exec("ROLLBACK") 284 | } 285 | } 286 | transaction = enclosingTransaction 287 | return QueryResult.Unit 288 | } 289 | } 290 | 291 | // Custom functions 292 | 293 | /** 294 | * Each element of stdin can be up to 2 GB. 295 | */ 296 | public fun copy(stdin: Sequence): Long { 297 | for (std in stdin) { 298 | val status = PQputCopyData(conn, std, std.encodeToByteArray().size) 299 | check(status == 1) { 300 | conn.error() 301 | } 302 | } 303 | val end = PQputCopyEnd(conn, null) 304 | check(end == 1) { 305 | conn.error() 306 | } 307 | val result = PQgetResult(conn).check(conn) 308 | return result.rows 309 | } 310 | 311 | public fun executeQueryAsFlow( 312 | identifier: Int?, 313 | sql: String, 314 | mapper: suspend (PostgresCursor) -> R, 315 | parameters: Int, 316 | fetchSize: Int = 10, 317 | binders: (PostgresPreparedStatement.() -> Unit)? 318 | ): Flow = flow { 319 | val (result, cursorName) = prepareQuery(identifier, sql, parameters, binders) 320 | val cursor = RealCursor(result, cursorName, conn, fetchSize) 321 | try { 322 | while (cursor.next().value) { 323 | emit(mapper(cursor)) 324 | } 325 | } finally { 326 | cursor.close() 327 | } 328 | } 329 | 330 | private fun prepareQuery( 331 | identifier: Int?, 332 | sql: String, 333 | parameters: Int, 334 | binders: (PostgresPreparedStatement.() -> Unit)? 335 | ): Pair, String> { 336 | val cursorName = if (identifier == null) "myCursor" else "cursor${identifier.escapeNegative()}" 337 | val cursor = "DECLARE $cursorName CURSOR FOR" 338 | 339 | val preparedStatement = preparedStatement(parameters, binders) 340 | return if (identifier != null) { 341 | checkPreparedStatement(identifier, "$cursor $sql", parameters, preparedStatement) 342 | conn.exec("BEGIN") 343 | memScoped { 344 | PQexecPrepared( 345 | conn, 346 | stmtName = identifier.toString(), 347 | nParams = parameters, 348 | paramValues = preparedStatement?.values(this), 349 | paramLengths = preparedStatement?.lengths?.refTo(0), 350 | paramFormats = preparedStatement?.formats?.refTo(0), 351 | resultFormat = TEXT_RESULT_FORMAT 352 | ) 353 | } 354 | } else { 355 | conn.exec("BEGIN") 356 | memScoped { 357 | PQexecParams( 358 | conn, 359 | command = "$cursor $sql", 360 | nParams = parameters, 361 | paramValues = preparedStatement?.values(this), 362 | paramLengths = preparedStatement?.lengths?.refTo(0), 363 | paramFormats = preparedStatement?.formats?.refTo(0), 364 | paramTypes = preparedStatement?.types?.refTo(0), 365 | resultFormat = TEXT_RESULT_FORMAT 366 | ) 367 | } 368 | }.check(conn) to cursorName 369 | } 370 | } 371 | 372 | @ExperimentalForeignApi 373 | private fun CPointer?.error(): String { 374 | val errorMessage = PQerrorMessage(this)!!.toKString() 375 | PQfinish(this) 376 | return errorMessage 377 | } 378 | 379 | @ExperimentalForeignApi 380 | internal fun CPointer?.clear() { 381 | PQclear(this) 382 | } 383 | 384 | @ExperimentalForeignApi 385 | internal fun CPointer.exec(sql: String) { 386 | val result = PQexec(this, sql) 387 | result.check(this) 388 | result.clear() 389 | } 390 | 391 | @ExperimentalForeignApi 392 | internal fun CPointer?.check(conn: CPointer): CPointer { 393 | val status = PQresultStatus(this) 394 | check(status == PGRES_TUPLES_OK || status == PGRES_COMMAND_OK || status == PGRES_COPY_IN) { 395 | conn.error() 396 | } 397 | return this!! 398 | } 399 | 400 | @ExperimentalForeignApi 401 | private fun CPointer.escaped(value: String): String { 402 | val cString = PQescapeIdentifier(this, value, value.length.convert()) 403 | val escaped = cString!!.toKString() 404 | PQfreemem(cString) 405 | return escaped 406 | } 407 | 408 | @OptIn(ExperimentalForeignApi::class) 409 | public fun PostgresNativeDriver( 410 | host: String, 411 | database: String, 412 | user: String, 413 | password: String, 414 | port: Int = 5432, 415 | options: String? = null, 416 | listenerSupport: ListenerSupport = ListenerSupport.None 417 | ): PostgresNativeDriver { 418 | val conn = PQsetdbLogin( 419 | pghost = host, 420 | pgport = port.toString(), 421 | pgtty = null, 422 | dbName = database, 423 | login = user, 424 | pwd = password, 425 | pgoptions = options 426 | ) 427 | val status = PQstatus(conn) 428 | if (status == ConnStatusType.CONNECTION_BAD) { 429 | throw IllegalArgumentException(conn.error()) 430 | } 431 | require(status == ConnStatusType.CONNECTION_OK) { 432 | conn.error() 433 | } 434 | return PostgresNativeDriver(conn!!, listenerSupport = listenerSupport) 435 | } 436 | 437 | private fun QueryResult.map(action: (T) -> R): QueryResult = when (this) { 438 | is QueryResult.Value -> QueryResult.Value(action(value)) 439 | is QueryResult.AsyncValue -> QueryResult.AsyncValue { 440 | action(await()) 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/PostgresPreparedStatement.kt: -------------------------------------------------------------------------------- 1 | package app.softwork.sqldelight.postgresdriver 2 | 3 | import app.cash.sqldelight.db.* 4 | import kotlinx.cinterop.* 5 | import kotlinx.datetime.* 6 | import kotlin.uuid.* 7 | 8 | public class PostgresPreparedStatement internal constructor(private val parameters: Int) : SqlPreparedStatement { 9 | @ExperimentalForeignApi 10 | internal fun values(scope: AutofreeScope): CValuesRef> = createValues(parameters) { 11 | value = when (val value = _values[it]) { 12 | null -> null 13 | is Data.Bytes -> value.bytes.refTo(0).getPointer(scope) 14 | is Data.Text -> value.text.cstr.getPointer(scope) 15 | } 16 | } 17 | 18 | private sealed interface Data { 19 | value class Bytes(val bytes: ByteArray) : Data 20 | value class Text(val text: String) : Data 21 | } 22 | 23 | private val _values = arrayOfNulls(parameters) 24 | internal val lengths = IntArray(parameters) 25 | internal val formats = IntArray(parameters) 26 | internal val types = UIntArray(parameters) 27 | 28 | private fun bind(index: Int, value: String?, oid: UInt) { 29 | lengths[index] = if (value != null) { 30 | _values[index] = Data.Text(value) 31 | value.length 32 | } else 0 33 | formats[index] = PostgresNativeDriver.TEXT_RESULT_FORMAT 34 | types[index] = oid 35 | } 36 | 37 | override fun bindBoolean(index: Int, boolean: Boolean?) { 38 | bind(index, boolean?.toString(), boolOid) 39 | } 40 | 41 | override fun bindBytes(index: Int, bytes: ByteArray?) { 42 | lengths[index] = if (bytes != null && bytes.isNotEmpty()) { 43 | _values[index] = Data.Bytes(bytes) 44 | bytes.size 45 | } else 0 46 | formats[index] = PostgresNativeDriver.BINARY_RESULT_FORMAT 47 | types[index] = byteaOid 48 | } 49 | 50 | override fun bindDouble(index: Int, double: Double?) { 51 | bind(index, double?.toString(), doubleOid) 52 | } 53 | 54 | override fun bindLong(index: Int, long: Long?) { 55 | bind(index, long?.toString(), longOid) 56 | } 57 | 58 | override fun bindString(index: Int, string: String?) { 59 | bind(index, string, textOid) 60 | } 61 | 62 | public fun bindDate(index: Int, value: LocalDate?) { 63 | bind(index, value?.toString(), dateOid) 64 | } 65 | 66 | 67 | public fun bindTime(index: Int, value: LocalTime?) { 68 | bind(index, value?.toString(), timeOid) 69 | } 70 | 71 | public fun bindLocalTimestamp(index: Int, value: LocalDateTime?) { 72 | bind(index, value?.toString(), timestampOid) 73 | } 74 | 75 | public fun bindTimestamp(index: Int, value: Instant?) { 76 | bind(index, value?.toString(), timestampTzOid) 77 | } 78 | 79 | public fun bindInterval(index: Int, value: DateTimePeriod?) { 80 | bind(index, value?.toString(), intervalOid) 81 | } 82 | 83 | public fun bindUuid(index: Int, value: Uuid?) { 84 | bind(index, value?.toString(), uuidOid) 85 | } 86 | 87 | private companion object { 88 | // Hardcoded, because not provided in libpq-fe.h for unknown reasons... 89 | // select * from pg_type; 90 | private const val boolOid = 16u 91 | private const val byteaOid = 17u 92 | private const val longOid = 20u 93 | private const val textOid = 25u 94 | private const val doubleOid = 701u 95 | 96 | private const val dateOid = 1082u 97 | private const val timeOid = 1083u 98 | private const val intervalOid = 1186u 99 | private const val timestampOid = 1114u 100 | private const val timestampTzOid = 1184u 101 | private const val uuidOid = 2950u 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/RealCursor.kt: -------------------------------------------------------------------------------- 1 | package app.softwork.sqldelight.postgresdriver 2 | 3 | import app.cash.sqldelight.db.* 4 | import kotlinx.cinterop.* 5 | import libpq.* 6 | 7 | /** 8 | * Must be used inside a transaction! 9 | */ 10 | @ExperimentalForeignApi 11 | internal class RealCursor( 12 | result: CPointer, 13 | private val name: String, 14 | private val conn: CPointer, 15 | private val fetchSize: Int 16 | ) : PostgresCursor(result), Closeable { 17 | override fun close() { 18 | result.clear() 19 | conn.exec("CLOSE $name") 20 | conn.exec("END") 21 | } 22 | 23 | override var currentRowIndex = -1 24 | private var maxRowIndex = -1 25 | 26 | override fun next(): QueryResult.Value { 27 | if (currentRowIndex == maxRowIndex) { 28 | currentRowIndex = -1 29 | } 30 | if (currentRowIndex == -1) { 31 | result = PQexec(conn, "FETCH $fetchSize IN $name").check(conn) 32 | maxRowIndex = PQntuples(result) - 1 33 | } 34 | return if (currentRowIndex < maxRowIndex) { 35 | currentRowIndex += 1 36 | QueryResult.Value(true) 37 | } else QueryResult.Value(false) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /postgres-native-sqldelight-driver/src/commonMain/kotlin/app/softwork/sqldelight/postgresdriver/ScopedListenerSupport.kt: -------------------------------------------------------------------------------- 1 | package app.softwork.sqldelight.postgresdriver 2 | 3 | import kotlinx.coroutines.* 4 | 5 | internal sealed interface ScopedListenerSupport : ListenerSupport { 6 | val notificationScope: CoroutineScope 7 | } 8 | -------------------------------------------------------------------------------- /postgres-native-sqldelight-driver/src/nativeInterop/cinterop/libpq.def: -------------------------------------------------------------------------------- 1 | headers = libpq-fe.h 2 | headerFilter = * 3 | package = libpq 4 | #staticLibraries = libpq.a 5 | #libraryPaths = /opt/homebrew/opt/libpq/lib 6 | 7 | compilerOpts = -I/home/linuxbrew/.linuxbrew/opt/libpq/include -I/opt/homebrew/opt/libpq/include -I/usr/local/opt/libpq/include -I/usr/include/postgresql 8 | 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 9 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | includeBuild("gradle/build-logic") 7 | } 8 | 9 | plugins { 10 | id("MyRepos") 11 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 12 | id("com.gradle.develocity") version "4.0.2" 13 | } 14 | 15 | develocity { 16 | buildScan { 17 | termsOfUseUrl.set("https://gradle.com/terms-of-service") 18 | termsOfUseAgree.set("yes") 19 | val isCI = providers.environmentVariable("CI").isPresent 20 | publishing { 21 | onlyIf { isCI } 22 | } 23 | tag("CI") 24 | } 25 | } 26 | 27 | rootProject.name = "postgres-native-sqldelight" 28 | 29 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 30 | enableFeaturePreview("STABLE_CONFIGURATION_CACHE") 31 | 32 | include(":postgres-native-sqldelight-driver") 33 | include(":postgres-native-sqldelight-dialect") 34 | 35 | include(":testing") 36 | include(":testing-sqldelight") 37 | -------------------------------------------------------------------------------- /testing-sqldelight/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.konan.target.* 2 | 3 | plugins { 4 | kotlin("multiplatform") 5 | id("app.cash.sqldelight") 6 | } 7 | 8 | kotlin { 9 | compilerOptions { 10 | optIn.add("kotlin.uuid.ExperimentalUuidApi") 11 | } 12 | 13 | when (HostManager.host) { 14 | KonanTarget.LINUX_X64 -> linuxX64() 15 | KonanTarget.MACOS_ARM64 -> macosArm64() 16 | KonanTarget.MACOS_X64 -> macosX64() 17 | else -> error("Not supported") 18 | } 19 | 20 | sourceSets { 21 | commonMain { 22 | dependencies { 23 | implementation(projects.postgresNativeSqldelightDriver) 24 | implementation(libs.sqldelight.coroutines) 25 | } 26 | } 27 | commonTest { 28 | dependencies { 29 | implementation(kotlin("test")) 30 | implementation(libs.coroutines.test) 31 | } 32 | } 33 | } 34 | } 35 | 36 | sqldelight { 37 | databases.register("NativePostgres") { 38 | dialect(projects.postgresNativeSqldelightDialect) 39 | packageName.set("app.softwork.sqldelight.postgresdriver") 40 | deriveSchemaFromMigrations.set(true) 41 | } 42 | linkSqlite.set(false) 43 | } 44 | -------------------------------------------------------------------------------- /testing-sqldelight/src/commonMain/sqldelight/app/softwork/sqldelight/postgresdriver/1.sqm: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS foo; 2 | DROP TABLE IF EXISTS users; 3 | 4 | CREATE TABLE foo( 5 | a INT PRIMARY KEY, 6 | b TEXT NOT NULL, 7 | date DATE NOT NULL, 8 | time TIME NOT NULL, 9 | timestamp TIMESTAMP NOT NULL, 10 | instant TIMESTAMPTZ NOT NULL, 11 | interval INTERVAL NOT NULL, 12 | uuid UUID NOT NULL 13 | ); 14 | 15 | CREATE TABLE IF NOT EXISTS users( 16 | id BIGSERIAL PRIMARY KEY, 17 | email VARCHAR(200) NOT NULL UNIQUE, 18 | username VARCHAR(100) NOT NULL UNIQUE, 19 | bio VARCHAR(1000) NOT NULL DEFAULT '', 20 | image VARCHAR(255) NOT NULL DEFAULT '', 21 | fooID INT NULL 22 | ); 23 | -------------------------------------------------------------------------------- /testing-sqldelight/src/commonMain/sqldelight/app/softwork/sqldelight/postgresdriver/Foo.sq: -------------------------------------------------------------------------------- 1 | create: 2 | INSERT INTO foo VALUES (?, ?, ?, ?, ?, ?, ?, ?); 3 | 4 | get: 5 | SELECT * FROM foo; 6 | 7 | startCopy: 8 | COPY foo FROM STDIN (FORMAT CSV); 9 | -------------------------------------------------------------------------------- /testing-sqldelight/src/commonMain/sqldelight/app/softwork/sqldelight/postgresdriver/Users.sq: -------------------------------------------------------------------------------- 1 | insertAndGet: 2 | INSERT INTO users(email, username, bio, image, fooID) 3 | VALUES (?, ?, ?, ?, ?) 4 | RETURNING id; 5 | 6 | selectByUsername: 7 | SELECT email, username, bio, image 8 | FROM users 9 | WHERE username = :username; 10 | 11 | selectByFoo: 12 | SELECT * FROM users WHERE fooID = :fooID; 13 | 14 | updateWhereFoo: 15 | UPDATE users 16 | SET email = :newEmail 17 | WHERE fooID = :fooID; 18 | -------------------------------------------------------------------------------- /testing-sqldelight/src/commonTest/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeSqldelightDriverTest.kt: -------------------------------------------------------------------------------- 1 | package app.softwork.sqldelight.postgresdriver 2 | 3 | import app.cash.sqldelight.coroutines.* 4 | import kotlinx.cinterop.* 5 | import kotlinx.coroutines.* 6 | import kotlinx.coroutines.flow.* 7 | import kotlinx.coroutines.test.* 8 | import kotlinx.datetime.* 9 | import kotlin.uuid.* 10 | import platform.posix.* 11 | import kotlin.test.* 12 | import kotlin.time.Duration.Companion.seconds 13 | 14 | @ExperimentalCoroutinesApi 15 | class PostgresNativeSqldelightDriverTest { 16 | private val driver = PostgresNativeDriver( 17 | host = env("POSTGRES_HOSTNAME") ?: "localhost", 18 | port = 5432, 19 | user = env("POSTGRES_USER") ?: "postgres", 20 | database = env("POSTGRES_DB") ?: "postgres", 21 | password = env("POSTGRES_PASSWORD") ?: "password" 22 | ) 23 | 24 | @Test 25 | fun allTypes() { 26 | val queries = NativePostgres(driver).fooQueries 27 | NativePostgres.Schema.migrate(driver, 0, NativePostgres.Schema.version) 28 | assertEquals(emptyList(), queries.get().executeAsList()) 29 | 30 | val foo = Foo( 31 | a = 42, 32 | b = "answer", 33 | date = LocalDate(2020, Month.DECEMBER, 12), 34 | time = LocalTime(12, 42, 0, 0), 35 | timestamp = LocalDateTime(2014, Month.AUGUST, 1, 12, 1, 2, 0), 36 | instant = Instant.fromEpochMilliseconds(10L), 37 | interval = DateTimePeriod(42, 42, 42, 42, 42, 42, 424242000), 38 | uuid = Uuid.NIL 39 | ) 40 | queries.create( 41 | a = foo.a, 42 | b = foo.b, 43 | date = foo.date, 44 | time = foo.time, 45 | timestamp = foo.timestamp, 46 | instant = foo.instant, 47 | interval = foo.interval, 48 | uuid = foo.uuid 49 | ) 50 | assertEquals(foo, queries.get().executeAsOne()) 51 | } 52 | 53 | @Test 54 | fun copyTest() { 55 | val queries = NativePostgres(driver).fooQueries 56 | NativePostgres.Schema.migrate(driver, 0, NativePostgres.Schema.version) 57 | queries.startCopy() 58 | val result = driver.copy( 59 | sequenceOf( 60 | "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" 61 | ) 62 | ) 63 | assertEquals(1, result) 64 | val foo = Foo( 65 | a = 42, 66 | b = "answer", 67 | date = LocalDate(2020, Month.DECEMBER, 12), 68 | time = LocalTime(12, 42, 0, 0), 69 | timestamp = LocalDateTime(2014, Month.AUGUST, 1, 12, 1, 2, 0), 70 | instant = Instant.fromEpochMilliseconds(10L), 71 | interval = DateTimePeriod(42, 42, 42, 42, 42, 42, 424242000), 72 | uuid = Uuid.NIL, 73 | ) 74 | assertEquals(foo, queries.get().executeAsOne()) 75 | } 76 | 77 | @Test 78 | fun userTest() { 79 | val queries = NativePostgres(driver).usersQueries 80 | NativePostgres.Schema.migrate(driver, 0, NativePostgres.Schema.version) 81 | val id = queries.insertAndGet("test@test", "test", "bio", "", null).executeAsOne() 82 | assertEquals(1, id) 83 | val id2 = queries.insertAndGet("test2@test", "test2", "bio2", "", null).executeAsOne() 84 | assertEquals(2, id2) 85 | val testUser = queries.selectByUsername("test").executeAsOne() 86 | assertEquals( 87 | SelectByUsername( 88 | "test@test", 89 | "test", 90 | "bio", 91 | "" 92 | ), 93 | testUser 94 | ) 95 | } 96 | 97 | @Test 98 | fun remoteListenerTest() = runTest(timeout = 10.seconds) { 99 | val client = PostgresNativeDriver( 100 | host = env("POSTGRES_HOSTNAME") ?: "localhost", 101 | port = 5432, 102 | user = env("POSTGRES_USER") ?: "postgres", 103 | database = env("POSTGRES_DB") ?: "postgres", 104 | password = env("POSTGRES_PASSWORD") ?: "password", 105 | listenerSupport = ListenerSupport.Remote(backgroundScope) { 106 | it + it 107 | } 108 | ) 109 | 110 | val server = PostgresNativeDriver( 111 | host = env("POSTGRES_HOSTNAME") ?: "localhost", 112 | port = 5432, 113 | user = env("POSTGRES_USER") ?: "postgres", 114 | database = env("POSTGRES_DB") ?: "postgres", 115 | password = env("POSTGRES_PASSWORD") ?: "password", 116 | listenerSupport = ListenerSupport.Remote(backgroundScope) { 117 | it + it 118 | } 119 | ) 120 | 121 | val db = NativePostgres(client) 122 | NativePostgres.Schema.migrate(driver, 0, NativePostgres.Schema.version) 123 | 124 | db.fooQueries.create( 125 | a = 42, 126 | b = "answer", 127 | date = LocalDate(2020, Month.DECEMBER, 12), 128 | time = LocalTime(12, 42, 0, 0), 129 | timestamp = LocalDateTime(2014, Month.AUGUST, 1, 12, 1, 2, 0), 130 | instant = Instant.fromEpochMilliseconds(10L), 131 | interval = DateTimePeriod(42, 42, 42, 42, 42, 42, 424242), 132 | uuid = Uuid.NIL 133 | ) 134 | val userQueries = db.usersQueries 135 | val id = userQueries.insertAndGet("foo", "foo", "foo", "", 42).executeAsOne() 136 | 137 | val users = async { 138 | db.usersQueries.selectByFoo(42) 139 | .asFlow().mapToOne(coroutineContext) 140 | .take(2).toList() 141 | } 142 | withContext(Dispatchers.Default) { 143 | val waitForRemoteNotifications = 2.seconds 144 | delay(waitForRemoteNotifications) 145 | } 146 | runCurrent() 147 | 148 | NativePostgres(server).usersQueries.updateWhereFoo("foo2", 42) 149 | withContext(Dispatchers.Default) { 150 | val waitForRemoteNotifications = 2.seconds 151 | delay(waitForRemoteNotifications) 152 | } 153 | runCurrent() 154 | 155 | assertEquals( 156 | listOf( 157 | Users( 158 | id, 159 | "foo", 160 | "foo", 161 | "foo", 162 | "", 163 | 42 164 | ), Users( 165 | id, 166 | "foo2", 167 | "foo", 168 | "foo", 169 | "", 170 | 42 171 | ) 172 | ), users.await() 173 | ) 174 | 175 | client.close() 176 | server.close() 177 | } 178 | 179 | @Test 180 | fun localListenerTest() = runTest(timeout = 10.seconds) { 181 | val client = PostgresNativeDriver( 182 | host = env("POSTGRES_HOSTNAME") ?: "localhost", 183 | port = 5432, 184 | user = env("POSTGRES_USER") ?: "postgres", 185 | database = env("POSTGRES_DB") ?: "postgres", 186 | password = env("POSTGRES_PASSWORD") ?: "password", 187 | listenerSupport = ListenerSupport.Local(backgroundScope) 188 | ) 189 | 190 | val db = NativePostgres(client) 191 | NativePostgres.Schema.migrate(driver, 0, NativePostgres.Schema.version) 192 | 193 | db.fooQueries.create( 194 | a = 42, 195 | b = "answer", 196 | date = LocalDate(2020, Month.DECEMBER, 12), 197 | time = LocalTime(12, 42, 0, 0), 198 | timestamp = LocalDateTime(2014, Month.AUGUST, 1, 12, 1, 2, 0), 199 | instant = Instant.fromEpochMilliseconds(10L), 200 | interval = DateTimePeriod(42, 42, 42, 42, 42, 42, 424242), 201 | uuid = Uuid.NIL 202 | ) 203 | val userQueries = db.usersQueries 204 | val id = userQueries.insertAndGet("foo", "foo", "foo", "", 42).executeAsOne() 205 | 206 | val users = async { 207 | db.usersQueries.selectByFoo(42) 208 | .asFlow().mapToOne(coroutineContext) 209 | .take(2).toList() 210 | } 211 | runCurrent() 212 | 213 | userQueries.updateWhereFoo("foo2", 42) 214 | runCurrent() 215 | 216 | assertEquals( 217 | listOf( 218 | Users( 219 | id, 220 | "foo", 221 | "foo", 222 | "foo", 223 | "", 224 | 42 225 | ), Users( 226 | id, 227 | "foo2", 228 | "foo", 229 | "foo", 230 | "", 231 | 42 232 | ) 233 | ), users.await() 234 | ) 235 | 236 | client.close() 237 | } 238 | } 239 | 240 | @OptIn(ExperimentalForeignApi::class) 241 | private fun env(name: String): String? { 242 | return getenv(name)?.toKStringFromUtf8()?.takeUnless { it.isEmpty() } 243 | } 244 | -------------------------------------------------------------------------------- /testing/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.konan.target.* 2 | 3 | plugins { 4 | kotlin("multiplatform") 5 | } 6 | 7 | kotlin { 8 | when (HostManager.host) { 9 | KonanTarget.LINUX_X64 -> linuxX64() 10 | KonanTarget.MACOS_ARM64 -> macosArm64() 11 | KonanTarget.MACOS_X64 -> macosX64() 12 | else -> error("Not supported") 13 | } 14 | 15 | sourceSets { 16 | commonTest { 17 | dependencies { 18 | implementation(projects.postgresNativeSqldelightDriver) 19 | implementation(kotlin("test")) 20 | implementation(libs.coroutines.test) 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testing/src/commonTest/kotlin/app/softwork/sqldelight/postgresdriver/PostgresNativeDriverTest.kt: -------------------------------------------------------------------------------- 1 | package app.softwork.sqldelight.postgresdriver 2 | 3 | import app.cash.sqldelight.Query 4 | import app.cash.sqldelight.db.QueryResult 5 | import kotlinx.cinterop.* 6 | import kotlinx.coroutines.ExperimentalCoroutinesApi 7 | import kotlinx.coroutines.async 8 | import kotlinx.coroutines.delay 9 | import kotlinx.coroutines.flow.* 10 | import kotlinx.coroutines.runBlocking 11 | import kotlinx.coroutines.test.runCurrent 12 | import kotlinx.coroutines.test.runTest 13 | import platform.posix.* 14 | import kotlin.test.* 15 | import kotlin.time.Duration.Companion.seconds 16 | 17 | @ExperimentalCoroutinesApi 18 | class PostgresNativeDriverTest { 19 | private val driver = PostgresNativeDriver( 20 | host = env("POSTGRES_HOSTNAME") ?: "localhost", 21 | port = 5432, 22 | user = env("POSTGRES_USER") ?: "postgres", 23 | database = env("POSTGRES_DB") ?: "postgres", 24 | password = env("POSTGRES_PASSWORD") ?: "password" 25 | ) 26 | 27 | @Test 28 | fun simpleTest() = runTest { 29 | assertEquals(0, driver.execute(null, "DROP TABLE IF EXISTS baz;", parameters = 0).value) 30 | assertEquals( 31 | 0, 32 | driver.execute(null, "CREATE TABLE baz(a INT PRIMARY KEY, foo TEXT, b BYTEA);", parameters = 0).value 33 | ) 34 | repeat(5) { 35 | val result = driver.execute(null, "INSERT INTO baz VALUES ($it)", parameters = 0) 36 | assertEquals(1, result.value) 37 | } 38 | 39 | val result = driver.execute(null, "INSERT INTO baz VALUES ($1, $2, $3), ($4, $5, $6)", parameters = 6) { 40 | bindLong(0, 5) 41 | bindString(1, "bar 0") 42 | bindBytes(2, byteArrayOf(1.toByte(), 2.toByte())) 43 | 44 | bindLong(3, 6) 45 | bindString(4, "bar 1") 46 | bindBytes(5, byteArrayOf(16.toByte(), 12.toByte())) 47 | }.value 48 | assertEquals(2, result) 49 | val notPrepared = driver.executeQuery(null, "SELECT * FROM baz LIMIT 1;", parameters = 0, mapper = { 50 | assertTrue(it.next().value) 51 | QueryResult.Value( 52 | Simple( 53 | index = it.getLong(0)!!.toInt(), 54 | name = it.getString(1), 55 | byteArray = it.getBytes(2) 56 | ) 57 | ) 58 | }) 59 | assertEquals(Simple(0, null, null), notPrepared.value) 60 | val preparedStatement = driver.executeQuery( 61 | 42, 62 | sql = "SELECT * FROM baz;", 63 | parameters = 0, binders = null, 64 | mapper = { 65 | QueryResult.Value(buildList { 66 | while (it.next().value) { 67 | add( 68 | Simple( 69 | index = it.getLong(0)!!.toInt(), 70 | name = it.getString(1), 71 | byteArray = it.getBytes(2) 72 | ) 73 | ) 74 | } 75 | }) 76 | } 77 | ).value 78 | 79 | assertEquals(7, preparedStatement.size) 80 | assertEquals( 81 | List(5) { 82 | Simple(it, null, null) 83 | } + listOf( 84 | Simple(5, "bar 0", byteArrayOf(1.toByte(), 2.toByte())), 85 | Simple(6, "bar 1", byteArrayOf(16.toByte(), 12.toByte())), 86 | ), 87 | preparedStatement 88 | ) 89 | 90 | expect(7) { 91 | val cursorList = driver.executeQueryAsFlow( 92 | -99, 93 | "SELECT * FROM baz", 94 | fetchSize = 4, 95 | parameters = 0, 96 | binders = null, 97 | mapper = { 98 | Simple( 99 | index = it.getLong(0)!!.toInt(), 100 | name = it.getString(1), 101 | byteArray = it.getBytes(2) 102 | ) 103 | }) 104 | cursorList.count() 105 | } 106 | 107 | expect(7) { 108 | val cursorList = driver.executeQueryAsFlow( 109 | -5, 110 | "SELECT * FROM baz", 111 | fetchSize = 1, 112 | parameters = 0, 113 | binders = null, 114 | mapper = { 115 | Simple( 116 | index = it.getLong(0)!!.toInt(), 117 | name = it.getString(1), 118 | byteArray = it.getBytes(2) 119 | 120 | ) 121 | }) 122 | cursorList.count() 123 | } 124 | 125 | val cursorFlow = driver.executeQueryAsFlow( 126 | -42, 127 | "SELECT * FROM baz", 128 | fetchSize = 1, 129 | parameters = 0, 130 | binders = null, 131 | mapper = { 132 | Simple( 133 | index = it.getLong(0)!!.toInt(), 134 | name = it.getString(1), 135 | byteArray = it.getBytes(2) 136 | ) 137 | }) 138 | assertEquals(7, cursorFlow.count()) 139 | assertEquals(4, cursorFlow.take(4).count()) 140 | 141 | expect(0) { 142 | val cursorList = driver.executeQueryAsFlow( 143 | -100, 144 | "SELECT * FROM baz WHERE a = -1", 145 | fetchSize = 1, 146 | parameters = 0, 147 | binders = null, 148 | mapper = { 149 | Simple( 150 | index = it.getLong(0)!!.toInt(), 151 | name = it.getString(1), 152 | byteArray = it.getBytes(2) 153 | ) 154 | }) 155 | cursorList.count() 156 | } 157 | } 158 | 159 | private data class Simple(val index: Int, val name: String?, val byteArray: ByteArray?) { 160 | override fun equals(other: Any?): Boolean { 161 | if (this === other) return true 162 | 163 | other as Simple 164 | 165 | if (index != other.index) return false 166 | if (name != other.name) return false 167 | if (byteArray != null) { 168 | if (other.byteArray == null) return false 169 | if (!byteArray.contentEquals(other.byteArray)) return false 170 | } else if (other.byteArray != null) return false 171 | 172 | return true 173 | } 174 | 175 | override fun hashCode(): Int { 176 | var result = index.hashCode() 177 | result = 31 * result + (name?.hashCode() ?: 0) 178 | result = 31 * result + (byteArray?.contentHashCode() ?: 0) 179 | return result 180 | } 181 | } 182 | 183 | @Test 184 | fun wrongCredentials() { 185 | assertFailsWith { 186 | PostgresNativeDriver( 187 | host = "wrongHost", 188 | port = 5432, 189 | user = env("POSTGRES_USER") ?: "postgres", 190 | database = env("POSTGRES_DB") ?: "postgres", 191 | password = env("POSTGRES_PASSWORD") ?: "password" 192 | ) 193 | } 194 | assertFailsWith { 195 | PostgresNativeDriver( 196 | host = "wrongHost", 197 | port = 5432, 198 | user = env("POSTGRES_USER") ?: "postgres", 199 | database = env("POSTGRES_DB") ?: "postgres", 200 | password = "wrongPassword" 201 | ) 202 | } 203 | assertFailsWith { 204 | PostgresNativeDriver( 205 | host = "wrongHost", 206 | port = 5432, 207 | user = "wrongUser", 208 | database = env("POSTGRES_DB") ?: "postgres", 209 | password = env("POSTGRES_PASSWORD") ?: "password" 210 | ) 211 | } 212 | } 213 | 214 | @Test 215 | fun copyTest() { 216 | assertEquals(0, driver.execute(null, "DROP TABLE IF EXISTS copying;", parameters = 0).value) 217 | assertEquals(0, driver.execute(null, "CREATE TABLE copying(a int primary key);", parameters = 0).value) 218 | driver.execute(-42, "COPY copying FROM STDIN (FORMAT CSV);", 0) 219 | val results = driver.copy(sequenceOf("1\n2\n", "3\n4\n")) 220 | assertEquals(4, results) 221 | assertEquals( 222 | listOf(1, 2, 3, 4), 223 | driver.executeQuery(null, "SELECT * FROM copying", parameters = 0, binders = null, mapper = { 224 | QueryResult.Value(buildList { 225 | while (it.next().value) { 226 | add(it.getLong(0)!!.toInt()) 227 | } 228 | }) 229 | }).value 230 | ) 231 | } 232 | 233 | @Test 234 | fun remoteListenerTest() = runBlocking { 235 | val other = PostgresNativeDriver( 236 | host = env("POSTGRES_HOSTNAME") ?: "localhost", 237 | port = 5432, 238 | user = env("POSTGRES_USER") ?: "postgres", 239 | database = env("POSTGRES_DB") ?: "postgres", 240 | password = env("POSTGRES_PASSWORD") ?: "password", 241 | listenerSupport = ListenerSupport.Remote(this) 242 | ) 243 | 244 | val driver = PostgresNativeDriver( 245 | host = env("POSTGRES_HOSTNAME") ?: "localhost", 246 | port = 5432, 247 | user = env("POSTGRES_USER") ?: "postgres", 248 | database = env("POSTGRES_DB") ?: "postgres", 249 | password = env("POSTGRES_PASSWORD") ?: "password", 250 | listenerSupport = ListenerSupport.Remote(this) 251 | ) 252 | 253 | val results = MutableStateFlow(0) 254 | val listener = Query.Listener { results.update { it + 1 } } 255 | driver.addListener("foo", "bar", listener = listener) 256 | 257 | val dbDelay = 2.seconds 258 | delay(dbDelay) 259 | other.notifyListeners("foo") 260 | 261 | other.notifyListeners("foo", "bar") 262 | other.notifyListeners("bar") 263 | 264 | delay(dbDelay) 265 | 266 | driver.removeListener("foo", "bar", listener = listener) 267 | driver.notifyListeners("foo") 268 | driver.notifyListeners("bar") 269 | 270 | delay(dbDelay) 271 | assertEquals(4, results.value) 272 | 273 | other.close() 274 | } 275 | 276 | @Test 277 | fun localListenerTest() = runTest { 278 | val notifications = MutableSharedFlow() 279 | val notificationList = async { 280 | notifications.take(4).toList() 281 | } 282 | 283 | val driver = PostgresNativeDriver( 284 | host = env("POSTGRES_HOSTNAME") ?: "localhost", 285 | port = 5432, 286 | user = env("POSTGRES_USER") ?: "postgres", 287 | database = env("POSTGRES_DB") ?: "postgres", 288 | password = env("POSTGRES_PASSWORD") ?: "password", 289 | listenerSupport = ListenerSupport.Local( 290 | this, 291 | notifications, 292 | ) { 293 | notifications.emit(it) 294 | } 295 | ) 296 | 297 | val results = MutableStateFlow(0) 298 | val listener = Query.Listener { results.update { it + 1 } } 299 | driver.addListener("foo", "bar", listener = listener) 300 | runCurrent() 301 | driver.notifyListeners("foo") 302 | runCurrent() 303 | driver.notifyListeners("foo", "bar") 304 | runCurrent() 305 | driver.notifyListeners("bar") 306 | runCurrent() 307 | 308 | driver.removeListener("foo", "bar", listener = listener) 309 | runCurrent() 310 | driver.notifyListeners("foo") 311 | runCurrent() 312 | driver.notifyListeners("bar") 313 | runCurrent() 314 | 315 | assertEquals(4, results.value) 316 | assertEquals(listOf("foo", "foo", "bar", "bar"), notificationList.await()) 317 | } 318 | } 319 | 320 | @OptIn(ExperimentalForeignApi::class) 321 | private fun env(name: String): String? { 322 | return getenv(name)?.toKStringFromUtf8()?.takeUnless { it.isEmpty() } 323 | } 324 | --------------------------------------------------------------------------------