├── docs └── .git_preserve_dir ├── .gitignore ├── .github └── workflows │ ├── compilers.json │ ├── docs.yml │ └── ci.yml ├── tests └── main.d ├── dub.sdl ├── LICENSE.txt ├── source └── vibe │ └── db │ └── postgresql │ ├── cancellation.d │ ├── query.d │ └── package.d ├── example └── example.d ├── README.md └── example_pipelining └── pipelining.d /docs/.git_preserve_dir: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dub/ 2 | vibe-d-postgresql 3 | dub.selections.json 4 | *-test-* 5 | -------------------------------------------------------------------------------- /.github/workflows/compilers.json: -------------------------------------------------------------------------------- 1 | [ 2 | "dmd-master", 3 | "dmd-latest", 4 | "dmd-beta", 5 | "dmd-2.105.3", 6 | "ldc-master", 7 | "ldc-latest", 8 | "ldc-beta", 9 | "ldc-1.35.0" 10 | ] -------------------------------------------------------------------------------- /tests/main.d: -------------------------------------------------------------------------------- 1 | import db = vibe.db.postgresql; 2 | 3 | version(all) 4 | { 5 | import std.getopt; 6 | import std.experimental.logger; 7 | 8 | bool debugEnabled = false; 9 | string connString; 10 | 11 | void readOpts(string[] args) 12 | { 13 | auto helpInformation = getopt( 14 | args, 15 | "debug", &debugEnabled, 16 | "conninfo", &connString 17 | ); 18 | } 19 | 20 | int main(string[] args) 21 | { 22 | readOpts(args); 23 | if(!debugEnabled) 24 | globalLogLevel = LogLevel.warning; 25 | 26 | db.__integration_test(connString); 27 | 28 | return 0; 29 | } 30 | 31 | shared static this() 32 | { 33 | globalLogLevel = LogLevel.trace; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "vibe-d-postgresql" 2 | description "PostgreSQL support for Vibe.d" 3 | authors "Denis Feklushkin " 4 | license "MIT" 5 | copyright "Copyright © 2016" 6 | targetType "sourceLibrary" 7 | 8 | dependency "dpq2" version="~>1.3.0-alpha.5" 9 | dependency "vibe-core" version=">=1.22.4" 10 | 11 | configuration "release_app" { 12 | buildType "release" 13 | } 14 | 15 | subPackage { 16 | name "integration_tests" 17 | sourcePaths "tests" 18 | targetType "executable" 19 | buildType "unittest" 20 | versions "IntegrationTest" 21 | dependency "vibe-d-postgresql" version="*" 22 | } 23 | 24 | subPackage { 25 | name "example" 26 | sourcePaths "example" 27 | targetType "executable" 28 | dependency "vibe-d" version="*" 29 | dependency "vibe-d-postgresql" version="*" 30 | } 31 | 32 | subPackage { 33 | name "example_pipelining" 34 | sourcePaths "example_pipelining" 35 | targetType "executable" 36 | dependency "vibe-d" version="*" 37 | dependency "vibe-d-postgresql" version="*" 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Denis Feklushkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /source/vibe/db/postgresql/cancellation.d: -------------------------------------------------------------------------------- 1 | /// 2 | module vibe.db.postgresql.cancellation; 3 | 4 | import vibe.db.postgresql: Connection, createReadSocketEvent, PostgresClientTimeoutException; 5 | import dpq2.cancellation; 6 | import dpq2.socket_stuff: duplicateSocket; 7 | import derelict.pq.pq; 8 | import core.time: Duration; 9 | 10 | /// 11 | package void cancelRequest(Connection conn, Duration timeout) 12 | { 13 | auto c = new Cancellation(conn); 14 | c.start; 15 | auto event = createReadSocketEvent(c.socket.duplicateSocket); 16 | 17 | while(true) 18 | { 19 | if(c.status == CONNECTION_BAD) 20 | throw new CancellationException(c.errorMessage); 21 | 22 | const r = c.poll; 23 | 24 | if(r == PGRES_POLLING_OK) 25 | break; 26 | else if(r == PGRES_POLLING_FAILED) 27 | throw new CancellationException(c.errorMessage); 28 | else if(r == PGRES_POLLING_READING) 29 | { 30 | // On success cancellation socket will be closed without any 31 | // data receive and wait() will return false. So there is no 32 | // point in checking whether wait() was executed successfully 33 | event.wait(timeout); 34 | } 35 | 36 | continue; 37 | } 38 | } 39 | 40 | /// 41 | class CancellationTimeoutException : CancellationException 42 | { 43 | this(string file = __FILE__, size_t line = __LINE__) 44 | { 45 | super("Exceeded cancellation time limit", file, line); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # Author: Harrison Ford (@hatf0) 2 | # This CI file has been heavily based off of my work in the Mir project. 3 | 4 | name: Build Documentation 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | workflow_dispatch: 11 | 12 | # Only allow for one job from each actor to run at a time, and cancel any jobs currently in progress. 13 | concurrency: 14 | group: docs-${{ github.actor }}-${{ github.head_ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | build_docs: 19 | name: Build documentation 20 | runs-on: ubuntu-22.04 21 | permissions: 22 | # Allow this action and this action only to write to the repo itself 23 | contents: write 24 | steps: 25 | - name: Checkout repo 26 | uses: actions/checkout@v4 27 | - name: Setup D compiler 28 | uses: dlang-community/setup-dlang@v1.3.0 29 | with: 30 | compiler: dmd-latest 31 | - name: Cache dub dependencies 32 | uses: actions/cache@v4 33 | with: 34 | path: ~/.dub/packages 35 | key: docs-build-${{ hashFiles('**/dub.sdl', '**/dub.json') }} 36 | restore-keys: | 37 | docs-build- 38 | # Not sure if all of these dependencies are needed 39 | - name: Install dependencies 40 | run: sudo apt-get -y update && sudo apt-get -y install libpq-dev libevent-dev 41 | - name: Build documentation 42 | run: | 43 | dub build --build=ddox 44 | shell: bash 45 | - name: Deploy to GitHub Pages 46 | uses: JamesIves/github-pages-deploy-action@8817a56e5bfec6e2b08345c81f4d422db53a2cdc 47 | with: 48 | branch: gh-pages 49 | folder: docs -------------------------------------------------------------------------------- /example/example.d: -------------------------------------------------------------------------------- 1 | module vibe.db.postgresql.example; 2 | 3 | import std.getopt; 4 | import vibe.d; 5 | import vibe.db.postgresql; 6 | 7 | PostgresClient client; 8 | 9 | void performDbRequest() 10 | { 11 | immutable result = client.pickConnection( 12 | (scope LockedConnection conn) 13 | { 14 | return conn.exec( 15 | "SELECT 123 as first_num, 567 as second_num, 'abc'::text as third_text "~ 16 | "UNION ALL "~ 17 | "SELECT 890, 233, 'fgh'::text as third_text", 18 | ValueFormat.BINARY 19 | ); 20 | } 21 | ); 22 | 23 | assert(result[0]["second_num"].as!int == 567); 24 | assert(result[1]["third_text"].as!string == "fgh"); 25 | 26 | foreach (val; rangify(result[0])) 27 | logInfo("Found entry: %s", val.as!Bson.toJson); 28 | } 29 | 30 | void main(string[] args) 31 | { 32 | string connString; 33 | getopt(args, "conninfo", &connString); 34 | 35 | void initConnectionDg(Connection conn) 36 | { 37 | // D uses UTF8, but Postgres settings may differ. If you want to 38 | // use text strings it is recommended to force set up UTF8 encoding 39 | conn.exec(`set client_encoding to 'UTF8'`); 40 | 41 | // Canceling a statement execution due to a timeout implies 42 | // re-initialization of the connection. Therefore, it is 43 | // recommended to additionally set a smaller statement 44 | // execution time limit on the server side so that server can 45 | // quickly interrupt statement processing on its own 46 | // initiative without connection re-initialization. 47 | conn.exec(`set statement_timeout to '15 s'`); 48 | } 49 | 50 | // params: conninfo string, maximum number of connections in 51 | // the connection pool and connection initialization delegate 52 | client = new PostgresClient(connString, 4, &initConnectionDg); 53 | 54 | // This function can be invoked in parallel from different Vibe.d processes or threads 55 | performDbRequest(); 56 | 57 | logInfo("Done!"); 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PostgreSQL support for Vibe.d 2 | ==== 3 | 4 | [API documentation](https://denizzzka.github.io/vibe.d.db.postgresql/) 5 | 6 | _Please help us to make documentation better!_ 7 | 8 | Example: 9 | ```D 10 | module vibe.db.postgresql.example; 11 | 12 | import std.getopt; 13 | import vibe.d; 14 | import vibe.db.postgresql; 15 | 16 | PostgresClient client; 17 | 18 | void performDbRequest() 19 | { 20 | immutable result = client.pickConnection( 21 | (scope LockedConnection conn) 22 | { 23 | return conn.exec( 24 | "SELECT 123 as first_num, 567 as second_num, 'abc'::text as third_text "~ 25 | "UNION ALL "~ 26 | "SELECT 890, 233, 'fgh'::text as third_text", 27 | ValueFormat.BINARY 28 | ); 29 | } 30 | ); 31 | 32 | assert(result[0]["second_num"].as!int == 567); 33 | assert(result[1]["third_text"].as!string == "fgh"); 34 | 35 | foreach (val; rangify(result[0])) 36 | logInfo("Found entry: %s", val.as!Bson.toJson); 37 | } 38 | 39 | void main(string[] args) 40 | { 41 | string connString; 42 | getopt(args, "conninfo", &connString); 43 | 44 | void initConnectionDg(Connection conn) 45 | { 46 | // D uses UTF8, but Postgres settings may differ. If you want to 47 | // use text strings it is recommended to force set up UTF8 encoding 48 | conn.exec(`set client_encoding to 'UTF8'`); 49 | 50 | // Canceling a statement execution due to a timeout implies 51 | // re-initialization of the connection. Therefore, it is 52 | // recommended to additionally set a smaller statement 53 | // execution time limit on the server side so that server can 54 | // quickly interrupt statement processing on its own 55 | // initiative without connection re-initialization. 56 | conn.exec(`set statement_timeout to '15 s'`); 57 | } 58 | 59 | // params: conninfo string, maximum number of connections in 60 | // the connection pool and connection initialization delegate 61 | client = new PostgresClient(connString, 4, &initConnectionDg); 62 | 63 | // This function can be invoked in parallel from different Vibe.d processes or threads 64 | performDbRequest(); 65 | 66 | logInfo("Done!"); 67 | } 68 | ``` 69 | 70 | Output: 71 | ``` 72 | [main(----) INF] Found entry: 123 73 | [main(----) INF] Found entry: 567 74 | [main(----) INF] Found entry: "abc" 75 | [main(----) INF] Done! 76 | ``` 77 | -------------------------------------------------------------------------------- /example_pipelining/pipelining.d: -------------------------------------------------------------------------------- 1 | module vibe.db.postgresql.pipelining; 2 | 3 | import std.getopt; 4 | import std.exception; 5 | import vibe.d; 6 | import vibe.db.postgresql; 7 | 8 | PostgresClient client; 9 | 10 | void main(string[] args) 11 | { 12 | enforce(PQlibVersion() >= 14_0000); 13 | 14 | string connString; 15 | getopt(args, "conninfo", &connString); 16 | 17 | void initConnectionDg(Connection conn) 18 | { 19 | conn.exec(`set client_encoding to 'UTF8'`); 20 | conn.exec(`set statement_timeout to '15 s'`); 21 | } 22 | 23 | client = new PostgresClient(connString, 1, &initConnectionDg); 24 | 25 | foreach(_; 0 .. 10) 26 | { 27 | assertThrown( 28 | client.pickConnection!void((conn) => requestsInPipeline(conn, true)) 29 | ); 30 | 31 | client.pickConnection!void((conn) => requestsInPipeline(conn, false)); 32 | } 33 | 34 | logInfo("Done!"); 35 | } 36 | 37 | void requestsInPipeline(scope LockedConnection conn, in bool shouldFail) 38 | { 39 | assert(conn.pipelineStatus == PGpipelineStatus.PQ_PIPELINE_OFF, conn.pipelineStatus.to!string); 40 | assert(conn.transactionStatus == PQTRANS_IDLE, conn.transactionStatus.to!string); 41 | 42 | conn.enterPipelineMode; 43 | scope(exit) 44 | { 45 | // Remove remaining results 46 | while(conn.pipelineStatus == PGpipelineStatus.PQ_PIPELINE_ABORTED) 47 | conn.getResult(conn.pollingTimeout); 48 | 49 | conn.exitPipelineMode; 50 | 51 | // Finish possible opened transaction 52 | if(conn.transactionStatus != PQTRANS_IDLE) 53 | conn.exec("ROLLBACK"); // not pipelined 54 | } 55 | 56 | conn.sendQuery("BEGIN READ ONLY"); 57 | 58 | conn.sendQuery("select id, 'test string' as txt from generate_series(1, 500) AS id"); 59 | 60 | { 61 | QueryParams p; 62 | p.sqlCommand = 63 | "SELECT 123 as a, 456 as b, 789 as c, $1 as d "~ 64 | "UNION ALL "~ 65 | "SELECT 0, 1, 2, $2" ~ (shouldFail ? "'wrong field'" : ""); 66 | p.argsVariadic(-5, -7); 67 | conn.sendQueryParams(p); 68 | } 69 | 70 | // Record command for server to flush its own buffer into client 71 | conn.sendFlushRequest; 72 | 73 | // Client buffer flushing, server receives it and starts processing 74 | conn.flush(); 75 | 76 | { 77 | auto p = QueryParams(sqlCommand: "SELECT $1 as single"); 78 | p.argsVariadic(31337); 79 | conn.sendQueryParams(p); 80 | } 81 | 82 | conn.sendQuery("COMMIT"); 83 | 84 | conn.pipelineSync; 85 | 86 | auto processNextResult(in ConnStatusType expectedStatus) 87 | { 88 | auto r = conn.getResult(conn.requestTimeout); 89 | enforce(r.status == expectedStatus, "status="~r.statusString); 90 | 91 | if(expectedStatus != PGRES_PIPELINE_SYNC) 92 | { 93 | // Read result delimiter 94 | enforce(conn.getResult(conn.requestTimeout) is null); 95 | } 96 | 97 | return r; 98 | } 99 | 100 | auto processNextAnswerRowByRow() 101 | { 102 | auto r = conn.getResult(conn.requestTimeout); 103 | 104 | enforce(r !is null, "Unexpected delimiter"); 105 | 106 | if(r.status == PGRES_TUPLES_OK) 107 | { 108 | assert(r.getAnswer.length == 0, "End of table expected"); 109 | 110 | enforce(conn.getResult(conn.requestTimeout) is null, "Result delimiter expected"); 111 | 112 | return null; 113 | } 114 | 115 | enforce(r.status == PGRES_SINGLE_TUPLE, "status="~r.statusString); 116 | 117 | return r.getAnswer; 118 | } 119 | 120 | processNextResult(PGRES_COMMAND_OK); // BEGIN 121 | 122 | // Single row read 123 | { 124 | conn.setSingleRowModeEx(); 125 | 126 | while(auto r = processNextAnswerRowByRow()) 127 | { 128 | auto row = r.oneRow; 129 | auto id = row["id"].as!int; 130 | assert(row["txt"].as!string == "test string"); 131 | } 132 | } 133 | 134 | auto r2 = processNextResult(PGRES_TUPLES_OK).getAnswer; 135 | auto r3 = processNextResult(PGRES_TUPLES_OK).getAnswer; 136 | processNextResult(PGRES_COMMAND_OK); // COMMIT 137 | processNextResult(PGRES_PIPELINE_SYNC); // End of pipeline 138 | 139 | assert(r2[1]["d"].as!int == -7); 140 | assert(r3.oneCell.as!int == 31337); 141 | } 142 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Author: Harrison Ford (@hatf0) 2 | # This CI file has been heavily based off of my work in the Mir project. 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | workflow_dispatch: 14 | # allow this workflow to be triggered manually 15 | 16 | # Only allow for one job from each actor to run at a time, and cancel any jobs currently in progress. 17 | concurrency: 18 | group: gh-actions-${{ github.actor }}-${{ github.head_ref }} 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | setup: 23 | name: 'Load job configuration' 24 | runs-on: ubuntu-22.04 25 | outputs: 26 | compilers: ${{ steps.load-config.outputs.compilers }} 27 | steps: 28 | - uses: actions/checkout@v4 29 | # This step checks if we want to skip CI entirely, then outputs the compilers to be used for 30 | # each job. A little overkill, as we don't intend to support multiple platforms, but I digress. 31 | - id: load-config 32 | uses: actions/github-script@9ac08808f993958e9de277fe43a64532a609130e 33 | with: 34 | script: | 35 | const base_compiler_config = require("./.github/workflows/compilers.json"); 36 | let compilers = []; 37 | const {owner, repo} = context.repo; 38 | let commit_sha = context.sha; 39 | if (context.eventName == "pull_request") 40 | { 41 | commit_sha = context.payload.pull_request.head.sha; 42 | } 43 | 44 | const commit = await github.rest.git.getCommit({ 45 | owner, 46 | repo, 47 | commit_sha 48 | }); 49 | const head_commit_message = commit.data.message; 50 | 51 | if (!head_commit_message.startsWith("[skip-ci]")) 52 | { 53 | compilers = base_compiler_config; 54 | } 55 | core.setOutput("compilers", JSON.stringify(compilers)); 56 | 57 | ci: 58 | name: '[ci] ${{ matrix.dc }}/${{ matrix.build }}' 59 | runs-on: ubuntu-22.04 60 | needs: setup 61 | # Only run if the setup phase explicitly defined compilers to be used 62 | if: ${{ fromJSON(needs.setup.outputs.compilers) != '' && fromJSON(needs.setup.outputs.compilers) != '[]' }} 63 | # Beta / master versions of any compiler are allowed to fail 64 | continue-on-error: ${{ contains(matrix.dc, 'beta') || contains(matrix.dc, 'master') }} 65 | env: 66 | BUILD: ${{ matrix.build }} 67 | CONN_STRING: "host=localhost port=5432 dbname=postgres password=postgres user=postgres" 68 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 69 | services: 70 | postgres: 71 | image: postgres 72 | env: 73 | POSTGRES_PASSWORD: postgres 74 | options: >- 75 | --health-cmd pg_isready 76 | --health-interval 10s 77 | --health-timeout 5s 78 | --health-retries 5 79 | ports: 80 | - 5432:5432 81 | strategy: 82 | fail-fast: false 83 | matrix: 84 | build: [unittest, release] 85 | dc: ${{ fromJSON(needs.setup.outputs.compilers) }} 86 | include: 87 | - build: unittest-cov 88 | dc: dmd-latest 89 | steps: 90 | - name: Checkout repo 91 | uses: actions/checkout@v4 92 | - name: Setup D compiler 93 | uses: dlang-community/setup-dlang@v1.3.0 94 | with: 95 | compiler: ${{ matrix.dc }} 96 | - name: Install dependencies 97 | run: sudo apt-get -y update && sudo apt-get -y install libpq-dev libevent-dev libcurl4-gnutls-dev postgresql 98 | - name: Cache dub dependencies 99 | uses: actions/cache@v4 100 | with: 101 | path: ~/.dub/packages 102 | key: ubuntu-latest-build-${{ hashFiles('**/dub.sdl', '**/dub.json') }} 103 | restore-keys: | 104 | ubuntu-latest-build- 105 | - name: Install dub dependencies 106 | run: | 107 | dub fetch dscanner 108 | dub fetch doveralls 109 | shell: bash 110 | - name: Build / test 111 | if: matrix.build != 'unittest-cov' 112 | run: | 113 | dub test --build=$BUILD 114 | dub run :integration_tests --build=$BUILD -- --conninfo="$CONN_STRING" --debug=true 115 | shell: bash 116 | - name: Build / test with coverage 117 | if: matrix.build == 'unittest-cov' 118 | run: | 119 | dub test --build=$BUILD 120 | dub run :integration_tests --build=$BUILD -- --conninfo="$CONN_STRING" --debug=true 121 | dub run :example --build=release -- --conninfo="$CONN_STRING" 122 | dub run :example_pipelining -- --conninfo="$CONN_STRING" 123 | dub run doveralls 124 | shell: bash 125 | - name: Upload coverage data 126 | if: matrix.build == 'unittest-cov' 127 | uses: codecov/codecov-action@v2 128 | -------------------------------------------------------------------------------- /source/vibe/db/postgresql/query.d: -------------------------------------------------------------------------------- 1 | /// 2 | module vibe.db.postgresql.query; 3 | 4 | import vibe.db.postgresql; 5 | 6 | mixin template Queries() 7 | { 8 | /// Perform SQL query to DB 9 | /// All values are returned in textual 10 | /// form. This means that the dpq2.conv.to_d_types.as template will likely 11 | /// not work for anything but strings. 12 | /// Try to use exec(string, ValueFormat.BINARY) or execParams(QueryParams) instead, even if now parameters are present. 13 | override immutable(Answer) exec(string sqlCommand) 14 | { 15 | return exec(sqlCommand, ValueFormat.TEXT); 16 | } 17 | 18 | /// 19 | immutable(Answer) exec( 20 | string sqlCommand, 21 | ValueFormat resultFormat 22 | ) 23 | { 24 | QueryParams p; 25 | p.resultFormat = resultFormat; 26 | p.sqlCommand = sqlCommand; 27 | 28 | return execParams(p); 29 | } 30 | 31 | /// Perform SQL query to DB 32 | override immutable(Answer) execParams(scope const ref QueryParams params) 33 | { 34 | auto res = runStatementBlockingManner({ sendQueryParams(params); }); 35 | 36 | return res.getAnswer; 37 | } 38 | 39 | /// Submits a command to the server without waiting for the result 40 | override void sendQuery(string SQLcmd) 41 | { 42 | auto p = QueryParams(sqlCommand: SQLcmd); 43 | sendQueryParams(p); 44 | } 45 | 46 | /// Row-by-row version of exec 47 | /// 48 | /// Delegate called for each received row. 49 | /// 50 | /// More info: https://www.postgresql.org/docs/current/libpq-single-row-mode.html 51 | /// 52 | void execStatementRbR( 53 | string sqlCommand, 54 | void delegate(immutable(Row)) answerRowProcessDg, 55 | ValueFormat resultFormat = ValueFormat.BINARY 56 | ) 57 | { 58 | QueryParams p; 59 | p.resultFormat = resultFormat; 60 | p.sqlCommand = sqlCommand; 61 | 62 | execStatementRbR(p, answerRowProcessDg); 63 | } 64 | 65 | /// Row-by-row version of execParams 66 | /// 67 | /// Delegate called for each received row. 68 | /// 69 | /// More info: https://www.postgresql.org/docs/current/libpq-single-row-mode.html 70 | /// 71 | void execStatementRbR(scope const ref QueryParams params, void delegate(immutable(Row)) answerRowProcessDg) 72 | { 73 | runStatementWithRowByRowResult( 74 | { sendQueryParams(params); }, 75 | answerRowProcessDg 76 | ); 77 | } 78 | 79 | /// Submits a request to create a prepared statement with the given parameters, and waits for completion. 80 | /// 81 | /// Throws an exception if preparing failed. 82 | void prepareEx( 83 | string statementName, 84 | string sqlStatement, 85 | Oid[] oids = null 86 | ) 87 | { 88 | auto r = runStatementBlockingManner( 89 | {sendPrepare(statementName, sqlStatement, oids);} 90 | ); 91 | 92 | if(r.status != PGRES_COMMAND_OK) 93 | throw new ResponseException(r, __FILE__, __LINE__); 94 | } 95 | 96 | /// Submits a request to execute a prepared statement with given parameters, and waits for completion. 97 | override immutable(Answer) execPrepared(scope const ref QueryParams params) 98 | { 99 | auto res = runStatementBlockingManner({ sendQueryPrepared(params); }); 100 | 101 | return res.getAnswer; 102 | } 103 | 104 | /// Row-by-row version of execPrepared 105 | /// 106 | /// Delegate called for each received row. 107 | /// 108 | /// More info: https://www.postgresql.org/docs/current/libpq-single-row-mode.html 109 | /// 110 | void execPreparedRbR(scope const ref QueryParams params, void delegate(immutable(Row)) answerRowProcessDg) 111 | { 112 | runStatementWithRowByRowResult( 113 | { sendQueryPrepared(params); }, 114 | answerRowProcessDg 115 | ); 116 | } 117 | 118 | /// Submits a request to obtain information about the specified prepared statement, and waits for completion. 119 | override immutable(Answer) describePrepared(string preparedStatementName) 120 | { 121 | auto res = runStatementBlockingManner({ sendDescribePrepared(preparedStatementName); }); 122 | 123 | return res.getAnswer; 124 | } 125 | 126 | override void cancel() 127 | { 128 | import vibe.db.postgresql.cancellation; 129 | 130 | cancelRequest(this, requestTimeout); 131 | } 132 | 133 | deprecated("please use exec(sqlCommand, ValueFormat.BINARY) instead. execStatement() will be removed by 2027") 134 | immutable(Answer) execStatement( 135 | string sqlCommand, 136 | ValueFormat resultFormat = ValueFormat.BINARY 137 | ) 138 | { 139 | return exec(sqlCommand, resultFormat); 140 | } 141 | 142 | deprecated("please use execParams() instead. execStatement() will be removed by 2027") 143 | immutable(Answer) execStatement(scope const ref QueryParams params) 144 | { 145 | return execParams(params); 146 | } 147 | 148 | deprecated("please use prepareEx() instead. prepareStatement() will be removed by 2027") 149 | void prepareStatement( 150 | string statementName, 151 | string sqlStatement, 152 | Oid[] oids = null 153 | ) 154 | { 155 | prepareEx(statementName, sqlStatement, oids); 156 | } 157 | 158 | deprecated("please use execPrepared() instead. execPreparedStatement() will be removed by 2027") 159 | immutable(Answer) execPreparedStatement(scope const ref QueryParams params) 160 | { 161 | return execPrepared(params); 162 | } 163 | 164 | deprecated("please use describePrepared() instead. describePreparedStatement() will be removed by 2027") 165 | immutable(Answer) describePreparedStatement(string preparedStatementName) 166 | { 167 | return describePrepared(preparedStatementName); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /source/vibe/db/postgresql/package.d: -------------------------------------------------------------------------------- 1 | /// PostgreSQL database client implementation. 2 | module vibe.db.postgresql; 3 | 4 | import vibe.db.postgresql.query; 5 | 6 | public import dpq2: ValueFormat; 7 | public import dpq2.exception: Dpq2Exception; 8 | public import dpq2.result; 9 | public import dpq2.connection: ConnectionException, connStringCheck, ConnectionStart; 10 | public import dpq2.args; 11 | public import derelict.pq.pq; 12 | 13 | import vibe.core.core; 14 | import vibe.core.connectionpool: ConnectionPool, VibeLockedConnection = LockedConnection; 15 | import vibe.core.log; 16 | import core.time; 17 | import std.exception: assertThrown, enforce; 18 | import std.conv: to; 19 | 20 | /// 21 | struct ClientSettings 22 | { 23 | string connString; /// PostgreSQL connection string 24 | void delegate(Connection) afterStartConnectOrReset; /// called after connection established 25 | } 26 | 27 | /// A Postgres client with connection pooling. 28 | class PostgresClient 29 | { 30 | private ConnectionPool!Connection pool; 31 | 32 | /// afterStartConnectOrReset is called after connection established 33 | this( 34 | string connString, 35 | uint connNum, 36 | void delegate(Connection) afterStartConnectOrReset = null 37 | ) 38 | { 39 | immutable cs = ClientSettings( 40 | connString, 41 | afterStartConnectOrReset 42 | ); 43 | 44 | this(&createConnection, cs, connNum); 45 | } 46 | 47 | /// Useful for external Connection implementation 48 | this 49 | ( 50 | Connection delegate(in ClientSettings) @safe connFactory, 51 | immutable ClientSettings cs, 52 | uint connNum, 53 | ) 54 | { 55 | cs.connString.connStringCheck; 56 | 57 | this( 58 | () @safe { return connFactory(cs); }, 59 | connNum 60 | ); 61 | } 62 | 63 | /// Useful for external Connection implementation 64 | /// 65 | /// Not cares about checking of connection string 66 | this(Connection delegate() @safe connFactory, uint connNum) 67 | { 68 | enforce(PQisthreadsafe() == 1); 69 | 70 | pool = new ConnectionPool!Connection( 71 | connFactory, 72 | connNum 73 | ); 74 | } 75 | 76 | /// Get connection from the pool. 77 | /// 78 | /// Do not forgot to call .reset() for connection if ConnectionException 79 | /// was caught while using LockedConnection! 80 | LockedConnection lockConnection() 81 | { 82 | logDebugV("get connection from the pool"); 83 | 84 | return pool.lockConnection(); 85 | } 86 | 87 | /// Use connection from the pool. 88 | /// 89 | /// Same as lockConnection but automatically maintains initiation of 90 | /// reestablishing of connection by calling .reset() 91 | /// 92 | /// Returns: Value returned by delegate or void 93 | T pickConnection(T)(scope T delegate(scope LockedConnection conn) dg) 94 | { 95 | logDebugV("get connection from the pool"); 96 | scope conn = pool.lockConnection(); 97 | 98 | try 99 | return dg(conn); 100 | catch(ConnectionException e) 101 | { 102 | conn.reset(); // also may throw ConnectionException and this is normal behaviour 103 | 104 | throw e; 105 | } 106 | } 107 | 108 | /// 109 | private Connection createConnection(in ClientSettings cs) @safe 110 | { 111 | return new Connection(cs); 112 | } 113 | } 114 | 115 | /// 116 | alias LockedConnection = VibeLockedConnection!Connection; 117 | 118 | /** 119 | * dpq2.Connection adopted for using with Vibe.d 120 | */ 121 | class Connection : dpq2.Connection 122 | { 123 | shared static immutable Duration pollingTimeout = dur!"seconds"(10); /// Timeout for use in polling loops etc 124 | Duration requestTimeout = dur!"seconds"(30); /// 125 | 126 | private const ClientSettings settings; 127 | private FileDescriptorEvent event; 128 | 129 | /// 130 | this(const ref ClientSettings settings) @trusted 131 | { 132 | this.settings = settings; 133 | 134 | super(settings.connString); 135 | event = this.posixSocketDuplicate.createReadSocketEvent; 136 | 137 | import std.conv: to; 138 | logDebugV("creating new connection, delegate isNull="~(settings.afterStartConnectOrReset is null).to!string); 139 | 140 | if(settings.afterStartConnectOrReset !is null) 141 | settings.afterStartConnectOrReset(this); 142 | 143 | if(exec(`show client_encoding`).oneCell.as!string != "UTF8") 144 | { 145 | logWarn("client_encoding is not set to UTF8\nIt will be forced now to UTF8, but this behavior may be changed in the 2027." 146 | ~` Please add appropriate setting call exec("set client_encoding to 'UTF8'")` 147 | ~` into your connection factory or afterStartConnectOrReset delegate!`); 148 | 149 | exec("set client_encoding to 'UTF8'"); 150 | } 151 | } 152 | 153 | /// Blocks while connection will be established or exception thrown 154 | void reset() 155 | { 156 | super.resetStart; 157 | 158 | while(true) 159 | { 160 | if(status() == CONNECTION_BAD) 161 | throw new ConnectionException(this); 162 | 163 | if(resetPoll() != PGRES_POLLING_OK) 164 | { 165 | event.wait(pollingTimeout); 166 | continue; 167 | } 168 | 169 | break; 170 | } 171 | 172 | if(settings.afterStartConnectOrReset !is null) 173 | settings.afterStartConnectOrReset(this); 174 | } 175 | 176 | /// Select single-row mode for the currently-executing query 177 | void setSingleRowModeEx() 178 | { 179 | if(setSingleRowMode() != 1) 180 | throw new ConnectionException("PQsetSingleRowMode failed"); 181 | } 182 | 183 | 184 | /// 185 | immutable(Result) getResult(in Duration timeout) 186 | { 187 | // Pipeline methods may provide result without having to maintain a busy flag 188 | if(isBusy) 189 | waitEndOfReadAndConsume(timeout); 190 | 191 | return super.getResult(); 192 | } 193 | 194 | private void waitEndOfReadAndConsume(in Duration timeout) 195 | { 196 | do 197 | { 198 | if(!event.wait(timeout)) 199 | throw new PostgresClientTimeoutException(__FILE__, __LINE__); 200 | 201 | consumeInput(); 202 | } 203 | while (this.isBusy); // wait until PQgetresult won't block anymore 204 | } 205 | 206 | private void doQuery(void delegate() doesQueryAndCollectsResults) 207 | { 208 | // Try to get usable connection and send SQL command 209 | while(true) 210 | { 211 | if(status() == CONNECTION_BAD) 212 | throw new ConnectionException(this, __FILE__, __LINE__); 213 | 214 | if(poll() != PGRES_POLLING_OK) 215 | { 216 | waitEndOfReadAndConsume(pollingTimeout); 217 | continue; 218 | } 219 | else 220 | { 221 | break; 222 | } 223 | } 224 | 225 | logDebugV("doesQuery() call"); 226 | doesQueryAndCollectsResults(); 227 | } 228 | 229 | private immutable(Result) runStatementBlockingManner(void delegate() sendsStatementDg) 230 | { 231 | immutable(Result)[] res; 232 | 233 | runStatementBlockingMannerWithMultipleResults(sendsStatementDg, (r){ res ~= r; }, false); 234 | 235 | enforce(res.length == 1, "Simple query without row-by-row mode can return only one Result instance, not "~res.length.to!string); 236 | 237 | return res[0]; 238 | } 239 | 240 | private void runStatementBlockingMannerWithMultipleResults(void delegate() sendsStatementDg, void delegate(immutable(Result)) processResult, bool isRowByRowMode) 241 | { 242 | logDebugV(__FUNCTION__); 243 | immutable(Result)[] res; 244 | 245 | doQuery(() 246 | { 247 | sendsStatementDg(); 248 | 249 | if(isRowByRowMode) 250 | setSingleRowModeEx(); 251 | 252 | scope(failure) 253 | { 254 | if(isRowByRowMode) 255 | while(super.getResult() !is null){} // autoclean of results queue 256 | } 257 | 258 | scope(exit) 259 | { 260 | logDebugV("consumeInput()"); 261 | consumeInput(); // TODO: redundant call (also called in waitEndOfReadAndConsume) - can be moved into catch block? 262 | 263 | while(true) 264 | { 265 | auto r = super.getResult(); 266 | 267 | /* 268 | I am trying to check connection status with PostgreSQL server 269 | with PQstatus and it always always return CONNECTION_OK even 270 | when the cable to the server is unplugged. 271 | – user1972556 (stackoverflow.com) 272 | 273 | ...the idea of testing connections is fairly silly, since the 274 | connection might die between when you test it and when you run 275 | your "real" query. Don't test connections, just use them, and 276 | if they fail be prepared to retry everything since you opened 277 | the transaction. – Craig Ringer Jan 14 '13 at 2:59 278 | */ 279 | if(status == CONNECTION_BAD) 280 | throw new ConnectionException(this, __FILE__, __LINE__); 281 | 282 | if(r is null) break; 283 | 284 | processResult(r); 285 | } 286 | } 287 | 288 | try 289 | { 290 | waitEndOfReadAndConsume(requestTimeout); 291 | } 292 | catch(PostgresClientTimeoutException e) 293 | { 294 | logDebugV("Exceeded Posgres query time limit"); 295 | reset(); 296 | throw(e); 297 | } 298 | } 299 | ); 300 | } 301 | 302 | mixin Queries; 303 | 304 | private void runStatementWithRowByRowResult(void delegate() sendsStatementDg, void delegate(immutable(Row)) answerRowProcessDg) 305 | { 306 | runStatementBlockingMannerWithMultipleResults( 307 | sendsStatementDg, 308 | (r) 309 | { 310 | auto answer = r.getAnswer; 311 | 312 | enforce(answer.length <= 1, `0 or 1 rows can be received, not `~answer.length.to!string); 313 | 314 | if(answer.length == 1) 315 | { 316 | enforce(r.status == PGRES_SINGLE_TUPLE, `Wrong result status: `~r.status.to!string); 317 | 318 | answerRowProcessDg(answer[0]); 319 | } 320 | }, 321 | true 322 | ); 323 | } 324 | 325 | /** 326 | * Non blocking method to wait for next notification. 327 | * 328 | * Params: 329 | * timeout = maximal duration to wait for the new Notify to be received 330 | * 331 | * Returns: New Notify or null when no other notification is available or timeout occurs. 332 | * Throws: ConnectionException on connection failure 333 | */ 334 | Notify waitForNotify(in Duration timeout = Duration.max) 335 | { 336 | // try read available 337 | auto ntf = getNextNotify(); 338 | if (ntf !is null) return ntf; 339 | 340 | // wait for next one 341 | try waitEndOfReadAndConsume(timeout); 342 | catch (PostgresClientTimeoutException) return null; 343 | return getNextNotify(); 344 | } 345 | } 346 | 347 | package auto createReadSocketEvent(T)(T newSocket) 348 | { 349 | version(Posix) 350 | { 351 | import core.sys.posix.fcntl; 352 | import std.socket; 353 | assert((fcntl(cast(socket_t) newSocket, F_GETFL, 0) & O_NONBLOCK), "Socket assumed to be non-blocking already"); 354 | } 355 | 356 | // vibe-core right now supports only read trigger event 357 | // it also closes the socket on scope exit, thus a socket duplication here 358 | return createFileDescriptorEvent(newSocket, FileDescriptorEvent.Trigger.read); 359 | } 360 | 361 | /// 362 | class PostgresClientTimeoutException : Dpq2Exception 363 | { 364 | this(string file = __FILE__, size_t line = __LINE__) 365 | { 366 | this("Exceeded query time limit", file, line); 367 | } 368 | 369 | this(string msg, string file = __FILE__, size_t line = __LINE__) 370 | { 371 | super(msg, file, line); 372 | } 373 | } 374 | 375 | unittest 376 | { 377 | bool raised = false; 378 | 379 | try 380 | { 381 | auto client = new PostgresClient("wrong connect string", 2); 382 | } 383 | catch(ConnectionException e) 384 | raised = true; 385 | 386 | assert(raised); 387 | } 388 | 389 | version(IntegrationTest) void __integration_test(string connString) 390 | { 391 | setLogLevel = LogLevel.debugV; 392 | 393 | auto client = new PostgresClient(connString, 3); 394 | 395 | auto conn = client.lockConnection; 396 | { 397 | auto res = conn.exec( 398 | "SELECT 123::integer, 567::integer, 'asd fgh'::text", 399 | ValueFormat.BINARY 400 | ); 401 | 402 | assert(res.getAnswer[0][1].as!PGinteger == 567); 403 | } 404 | 405 | { 406 | // Row-by-row result receiving 407 | int[] res; 408 | 409 | conn.execStatementRbR( 410 | `SELECT generate_series(0, 3) as i, pg_sleep(0.2)`, 411 | (immutable(Row) r) 412 | { 413 | res ~= r[0].as!int; 414 | } 415 | ); 416 | 417 | assert(res.length == 4); 418 | } 419 | 420 | { 421 | // Row-by-row result receiving: error while receiving 422 | size_t rowCounter; 423 | 424 | QueryParams p; 425 | p.sqlCommand = 426 | `SELECT 1.0 / (generate_series(1, 100000) % 80000)`; // division by zero error at generate_series=80000 427 | 428 | bool assertThrown; 429 | 430 | try 431 | conn.execStatementRbR(p, 432 | (immutable(Row) r) 433 | { 434 | rowCounter++; 435 | } 436 | ); 437 | catch(ResponseException) // catches ERROR: division by zero 438 | assertThrown = true; 439 | 440 | assert(assertThrown); 441 | assert(rowCounter > 0); 442 | } 443 | 444 | { 445 | QueryParams p; 446 | p.sqlCommand = `SELECT 123`; 447 | 448 | auto res = conn.execParams(p); 449 | 450 | assert(res.length == 1); 451 | assert(res[0][0].as!int == 123); 452 | } 453 | 454 | { 455 | conn.prepareEx("stmnt_name", "SELECT 123::integer UNION SELECT 456::integer"); 456 | 457 | bool throwFlag = false; 458 | 459 | try 460 | conn.prepareEx("wrong_stmnt", "WRONG SQL STATEMENT"); 461 | catch(ResponseException) 462 | throwFlag = true; 463 | 464 | assert(throwFlag); 465 | } 466 | 467 | { 468 | import dpq2.oids: OidType; 469 | 470 | auto a = conn.describePrepared("stmnt_name"); 471 | 472 | assert(a.nParams == 0); 473 | assert(a.OID(0) == OidType.Int4); 474 | } 475 | 476 | { 477 | QueryParams p; 478 | p.preparedStatementName = "stmnt_name"; 479 | 480 | auto r = conn.execPrepared(p); 481 | 482 | assert(r.getAnswer[0][0].as!PGinteger == 123); 483 | } 484 | 485 | { 486 | QueryParams p; 487 | p.preparedStatementName = "stmnt_name"; 488 | 489 | int[] res; 490 | 491 | conn.execPreparedRbR( 492 | p, 493 | (immutable(Row) r) 494 | { 495 | res ~= r.oneCell.as!int; 496 | } 497 | ); 498 | 499 | assert(res.length == 2, res.to!string); 500 | assert(res[0] == 123); 501 | assert(res[1] == 456); 502 | } 503 | 504 | { 505 | // Fibers test 506 | import vibe.core.concurrency; 507 | 508 | auto future0 = async({ 509 | client.pickConnection( 510 | (scope c) 511 | { 512 | immutable answer = c.exec("SELECT 'New connection 0'"); 513 | } 514 | ); 515 | 516 | return 1; 517 | }); 518 | 519 | auto future1 = async({ 520 | client.pickConnection( 521 | (scope c) 522 | { 523 | immutable answer = c.exec("SELECT 'New connection 1'"); 524 | } 525 | ); 526 | 527 | return 1; 528 | }); 529 | 530 | immutable answer = conn.exec("SELECT 'Old connection'", ValueFormat.BINARY); 531 | 532 | assert(future0 == 1); 533 | assert(future1 == 1); 534 | assert(answer.length == 1); 535 | } 536 | 537 | { 538 | assert(conn.escapeIdentifier("abc") == "\"abc\""); 539 | } 540 | 541 | { 542 | import core.time : msecs; 543 | import vibe.core.concurrency : async; 544 | 545 | struct NTF {string name; string extra;} 546 | 547 | auto futureNtf = async({ 548 | Notify pgNtf; 549 | 550 | client.pickConnection( 551 | (scope c) 552 | { 553 | c.exec("LISTEN foo"); 554 | pgNtf = c.waitForNotify(); 555 | } 556 | ); 557 | 558 | assert(pgNtf !is null); 559 | return NTF(pgNtf.name.idup, pgNtf.extra.idup); 560 | }); 561 | 562 | sleep(10.msecs); 563 | conn.exec("NOTIFY foo, 'bar'"); 564 | 565 | assert(futureNtf.name == "foo"); 566 | assert(futureNtf.extra == "bar"); 567 | } 568 | 569 | { 570 | // Request cancellation test 571 | import vibe.core.concurrency: async; 572 | import vibe.db.postgresql.cancellation: cancelRequest; 573 | 574 | QueryParams p; 575 | p.sqlCommand = `SELECT pg_sleep_for('1 minute')`; 576 | 577 | auto future = async({ 578 | try 579 | conn.execParams(p); 580 | catch(ResponseException e) 581 | return e.msg; //ERROR: canceling statement due to user request 582 | 583 | return null; 584 | }); 585 | 586 | conn.cancel(); 587 | 588 | assert(future.getResult !is null); 589 | } 590 | 591 | { 592 | // Timeouts test 593 | conn.exec(`set statement_timeout to '5s'`); 594 | 595 | QueryParams p; 596 | p.sqlCommand = `SELECT pg_sleep_for('1 minute')`; 597 | 598 | assertThrown!ResponseException( 599 | conn.execParams(p) 600 | ); 601 | 602 | // Internal statement timeout check 603 | conn.requestTimeout = 5.seconds; 604 | 605 | conn.reset(); 606 | 607 | assertThrown!PostgresClientTimeoutException( 608 | conn.execParams(p) 609 | ); 610 | } 611 | } 612 | --------------------------------------------------------------------------------