├── .github └── workflows │ ├── ci.yml │ ├── compilers.json │ └── docs.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── .git_preserve_dir └── _config.yml ├── dub.json ├── example └── example.d ├── integration_tests └── integration_tests.d └── src └── dpq2 ├── args.d ├── connection.d ├── conv ├── arrays.d ├── from_bson.d ├── from_d_types.d ├── geometric.d ├── inet.d ├── jsonb.d ├── native_tests.d ├── numeric.d ├── time.d ├── to_bson.d ├── to_d_types.d └── to_variant.d ├── dynloader.d ├── exception.d ├── oids.d ├── package.d ├── query.d ├── query_gen.d ├── result.d └── value.d /.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 }}-${{ matrix.stat_type }}' 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 | STAT_TYPE: ${{ matrix.stat_type }} 68 | CONN_STRING: "host=localhost port=5432 dbname=postgres password=postgres user=postgres" 69 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 70 | services: 71 | postgres: 72 | image: postgres 73 | env: 74 | POSTGRES_PASSWORD: postgres 75 | options: >- 76 | --health-cmd pg_isready 77 | --health-interval 10s 78 | --health-timeout 5s 79 | --health-retries 5 80 | ports: 81 | - 5432:5432 82 | strategy: 83 | fail-fast: false 84 | matrix: 85 | build: [unittest, release] 86 | stat_type: [static, dynamic, dynamic-unmanaged] 87 | dc: ${{ fromJSON(needs.setup.outputs.compilers) }} 88 | include: 89 | - build: tests_and_cov 90 | dc: dmd-latest 91 | stat_type: none 92 | steps: 93 | - name: Checkout repo 94 | uses: actions/checkout@v4 95 | - name: Setup D compiler 96 | uses: dlang-community/setup-dlang@v1.3.0 97 | with: 98 | compiler: ${{ matrix.dc }} 99 | - name: Install dependencies 100 | run: sudo apt-get -y update && sudo apt-get -y install libpq-dev libevent-dev libcurl4-gnutls-dev postgresql 101 | - name: Cache dub dependencies 102 | uses: actions/cache@v4 103 | with: 104 | path: ~/.dub/packages 105 | key: ubuntu-latest-build-${{ hashFiles('**/dub.sdl', '**/dub.json') }} 106 | restore-keys: | 107 | ubuntu-latest-build- 108 | - name: Install dub dependencies 109 | run: | 110 | dub fetch dscanner 111 | dub fetch doveralls 112 | shell: bash 113 | - name: Build / test 114 | if: matrix.build != 'tests_and_cov' 115 | run: | 116 | dub run dpq2:integration_tests --build=$BUILD --config=$STAT_TYPE -- --conninfo="$CONN_STRING" 117 | shell: bash 118 | - name: Build / test with coverage 119 | if: matrix.build == 'tests_and_cov' 120 | run: | 121 | dub run dpq2:integration_tests --build=unittest-cov -- --conninfo="$CONN_STRING" 122 | dub run dpq2:integration_tests --build=cov -- --conninfo="$CONN_STRING" 123 | dub run dpq2:example --build=release -- --conninfo="$CONN_STRING" 124 | dub run doveralls 125 | shell: bash 126 | - name: Upload coverage data 127 | if: matrix.build == 'tests_and_cov' 128 | uses: codecov/codecov-action@v2 129 | -------------------------------------------------------------------------------- /.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 | ] -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | .dub 3 | dub.selections.json 4 | *_integration_* 5 | *.lst 6 | docs 7 | docs.json 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dpq2 2 | [![Coverage Status](https://coveralls.io/repos/denizzzka/dpq2/badge.svg?branch=master)](https://coveralls.io/r/denizzzka/dpq2) 3 | [![codecov.io](https://codecov.io/github/denizzzka/dpq2/coverage.svg?branch=master)](https://codecov.io/github/denizzzka/dpq2) 4 | 5 | An interface to PostgreSQL for the D programming language. 6 | 7 | > It adds only tiny overhead to the original low level library libpq but make convenient use PostgreSQL from D. 8 | 9 | [API documentation](https://denizzzka.github.io/dpq2) 10 | 11 | _Please help us to make documentation better!_ 12 | 13 | ## Features 14 | 15 | * Text string arguments support 16 | * Binary arguments support (including multi-dimensional arrays) 17 | * Both text and binary formats of query result support 18 | * Immutable query result for simplify multithreading 19 | * Async queries support 20 | * Reading of the text query results to native D text types 21 | * Representation of binary arguments and binary query results as native D types 22 | * Text types 23 | * Integer and decimal types 24 | * Money type (into money.currency, https://github.com/dlang-community/d-money) 25 | * Some data and time types 26 | * JSON type (stored into vibe.data.json.Json) 27 | * JSONB type (ditto) 28 | * Geometric types 29 | * Conversion of values to BSON (into vibe.data.bson.Bson) 30 | * Access to PostgreSQL's multidimensional arrays 31 | * LISTEN/NOTIFY support 32 | * Bulk data upload to table from string data ([SQL COPY](https://www.postgresql.org/docs/current/sql-copy.html)) 33 | * Simple SQL query builder 34 | 35 | ## Building 36 | > Bindings for libpq can be static or dynamic. The static bindings are generated by default. 37 | 38 | * On Linux, you may install `libpq-dev` for dynamic linking e.g. `sudo apt-get install libpq-dev` 39 | 40 | ## Example 41 | ```D 42 | #!/usr/bin/env rdmd 43 | 44 | import dpq2; 45 | import std.getopt; 46 | import std.stdio: writeln; 47 | import std.typecons: Nullable; 48 | import vibe.data.bson; 49 | 50 | void main(string[] args) 51 | { 52 | string connInfo; 53 | getopt(args, "conninfo", &connInfo); 54 | 55 | Connection conn = new Connection(connInfo); 56 | 57 | // Only text query result can be obtained by this call: 58 | auto answer = conn.exec( 59 | "SELECT now()::timestamp as current_time, 'abc'::text as field_name, "~ 60 | "123 as field_3, 456.78 as field_4, '{\"JSON field name\": 123.456}'::json" 61 | ); 62 | 63 | writeln( "Text query result by name: ", answer[0]["current_time"].as!PGtext ); 64 | writeln( "Text query result by index: ", answer[0][3].as!PGtext ); 65 | 66 | // It is possible to read values of unknown type using BSON: 67 | auto firstRow = answer[0]; 68 | foreach(cell; rangify(firstRow)) 69 | { 70 | writeln("bson: ", cell.as!Bson); 71 | } 72 | 73 | // Binary arguments query with binary result: 74 | QueryParams p; 75 | p.sqlCommand = "SELECT "~ 76 | "$1::double precision as double_field, "~ 77 | "$2::text, "~ 78 | "$3::text as null_field, "~ 79 | "array['first', 'second', NULL]::text[] as array_field, "~ 80 | "$4::integer[] as multi_array, "~ 81 | "'{\"float_value\": 123.456,\"text_str\": \"text string\"}'::json as json_value"; 82 | 83 | p.argsVariadic( 84 | -1234.56789012345, 85 | "first line\nsecond line", 86 | Nullable!string.init, 87 | [[1, 2, 3], [4, 5, 6]] 88 | ); 89 | 90 | auto r = conn.execParams(p); 91 | scope(exit) destroy(r); 92 | 93 | writeln( "0: ", r[0]["double_field"].as!PGdouble_precision ); 94 | writeln( "1: ", r.oneRow[1].as!PGtext ); // .oneRow additionally checks that here is only one row was returned 95 | writeln( "2.1 isNull: ", r[0][2].isNull ); 96 | writeln( "2.2 isNULL: ", r[0].isNULL(2) ); 97 | writeln( "3.1: ", r[0][3].asArray[0].as!PGtext ); 98 | writeln( "3.2: ", r[0][3].asArray[1].as!PGtext ); 99 | writeln( "3.3: ", r[0]["array_field"].asArray[2].isNull ); 100 | writeln( "3.4: ", r[0]["array_field"].asArray.isNULL(2) ); 101 | writeln( "4.1: ", r[0]["multi_array"].asArray.getValue(1, 2).as!PGinteger ); 102 | writeln( "4.2: ", r[0]["multi_array"].as!(int[][]) ); 103 | writeln( "5.1 Json: ", r[0]["json_value"].as!Json); 104 | writeln( "5.2 Bson: ", r[0]["json_value"].as!Bson); 105 | 106 | // It is possible to read values of unknown type using BSON: 107 | for(auto column = 0; column < r.columnCount; column++) 108 | { 109 | writeln("column name: '"~r.columnName(column)~"', bson: ", r[0][column].as!Bson); 110 | } 111 | 112 | // It is possible to upload CSV data ultra-fast: 113 | conn.exec("CREATE TEMP TABLE test_dpq2_copy (v1 TEXT, v2 INT)"); 114 | 115 | // Init the COPY command. This sets the connection in a COPY receive 116 | // mode until putCopyEnd() is called. Copy CSV data, because it's standard, 117 | // ultra fast, and readable: 118 | conn.exec("COPY test_dpq2_copy FROM STDIN WITH (FORMAT csv)"); 119 | 120 | // Write 2 lines of CSV, including text that contains the delimiter. 121 | // Postgresql handles it well: 122 | string data = "\"This, right here, is a test\",8\nWow! it works,13\n"; 123 | conn.putCopyData(data); 124 | 125 | // Write 2 more lines 126 | data = "Horray!,3456\nSuper fast!,325\n"; 127 | conn.putCopyData(data); 128 | 129 | // Signal that the COPY is finished. Let Postgresql finalize the command 130 | // and return any errors with the data. 131 | conn.putCopyEnd(); 132 | } 133 | ``` 134 | 135 | Compile and run: 136 | ``` 137 | Running ./dpq2_example --conninfo=user=postgres 138 | 2018-12-09T10:08:07.862:package.d:__lambda1:19 DerelictPQ loading... 139 | 2018-12-09T10:08:07.863:package.d:__lambda1:26 ...DerelictPQ loading finished 140 | Text query result by name: 2018-12-09 10:08:07.868141 141 | Text query result by index: 456.78 142 | bson: "2018-12-09 10:08:07.868141" 143 | bson: "abc" 144 | bson: "123" 145 | bson: "456.78" 146 | bson: {"JSON field name":123.456} 147 | 0: -1234.57 148 | 1: first line 149 | second line 150 | 2.1 isNull: true 151 | 2.2 isNULL: true 152 | 3.1: first 153 | 3.2: second 154 | 3.3: true 155 | 3.4: true 156 | 4.1: 6 157 | 4.2: [[1, 2, 3], [4, 5, 6]] 158 | 5.1 Json: {"text_str":"text string","float_value":123.456} 159 | 5.2 Bson: {"text_str":"text string","float_value":123.456} 160 | column name: 'double_field', bson: -1234.56789012345 161 | column name: 'text', bson: "first line\nsecond line" 162 | column name: 'null_field', bson: null 163 | column name: 'array_field', bson: ["first","second",null] 164 | column name: 'multi_array', bson: [[1,2,3],[4,5,6]] 165 | column name: 'json_value', bson: {"text_str":"text string","float_value":123.456} 166 | ``` 167 | 168 | ## Using dynamic version of libpq 169 | Is provided two ways to load `libpq` dynamically: 170 | 171 | * Automatic load and unload (`dynamic` build config option) 172 | * Manual load (and unload, if need) (`dynamic-unmanaged`) 173 | 174 | To load automatically it is necessary to allocate `ConnectionFactory`. 175 | This class is only available then `dynamic` config is used. 176 | Only one instance of `ConnectionFactory` is allowed. 177 | It is possible to specify filepath to a library/libraries what you want to use, otherwise default will be used: 178 | ```D 179 | // Argument is a string containing one or more comma-separated 180 | // shared library names 181 | auto connFactory = new immutable ConnectionFactory("path/to/libpq.dll"); 182 | ``` 183 | 184 | Then you can create connection by calling `createConnection` method: 185 | ```D 186 | Connection conn = connFactory.createConnection(params); 187 | ``` 188 | And then this connection can be used as usual. 189 | 190 | When all objects related to `libpq` (including `ConnectionFactory`) is destroyed library will be unloaded automatically. 191 | 192 | To load `libpq` manually it is necessary to use build config `dynamic-unmanaged`. 193 | Manual dynamic `libpq` loading example: 194 | ```D 195 | import derelict.pq.pq: DerelictPQ; 196 | import core.memory: GC; 197 | 198 | DerelictPQ.load(); 199 | 200 | auto conn = new Connection(connInfo); 201 | /* Skipped rest of useful SQL processing */ 202 | conn.destroy(); // Ensure that all related to libpq objects are destroyed 203 | 204 | GC.collect(); // Forced removal of references to libpq before library unload 205 | DerelictPQ.unload(); 206 | ``` 207 | In this case is not need to use `ConnectionFactory` - just create `Connection` by the same way as for `static` config. 208 | -------------------------------------------------------------------------------- /docs/.git_preserve_dir: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denizzzka/dpq2/d618398135b909a64205d35149104661d87f5052/docs/.git_preserve_dir -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dpq2", 3 | "description": "Medium-level binding to the PostgreSQL database", 4 | "homepage": "https://github.com/denizzzka/dpq2", 5 | "license": "Boost", 6 | "authors": [ 7 | "Denis Feklushkin", "Anton Gushcha" 8 | ], 9 | "targetPath": "bin", 10 | "dependencies": { 11 | "derelict-pq": "~>4.0.0", 12 | "vibe-serialization": "~>1.0.4", 13 | "money": "~>3.0.2" 14 | }, 15 | "targetType": "sourceLibrary", 16 | "libs-windows": ["ws2_32"], 17 | "-ddoxTool": "scod", 18 | "configurations": [ 19 | { 20 | "name": "static", 21 | "versions": ["Dpq2_Static"], 22 | "subConfigurations": { 23 | "derelict-pq": "derelict-pq-static" 24 | }, 25 | "libs": ["pq"] 26 | }, 27 | { 28 | "name": "dynamic-unmanaged", 29 | "versions": ["Dpq2_Static"], 30 | "subConfigurations": { 31 | "derelict-pq": "derelict-pq-dynamic" 32 | } 33 | }, 34 | { 35 | "name": "dynamic", 36 | "versions": ["Dpq2_Dynamic"], 37 | "subConfigurations": { 38 | "derelict-pq": "derelict-pq-dynamic" 39 | } 40 | } 41 | ], 42 | "subPackages": [ 43 | { 44 | "name": "integration_tests", 45 | "targetType": "executable", 46 | "dflags-dmd": ["-preview=in"], 47 | "dependencies": 48 | { 49 | "dpq2": { "version": "*", "dflags-dmd": ["-preview=in"] }, 50 | "vibe-serialization": { "version": "*", "dflags-dmd": ["-preview=in"] }, 51 | "vibe-core": { "version": "*", "dflags-dmd": ["-preview=in"] }, 52 | "gfm:math": "~>8.0.6" 53 | }, 54 | "configurations": [ 55 | { 56 | "name": "dynamic", 57 | "subConfigurations": { 58 | "dpq2": "dynamic" 59 | } 60 | }, 61 | { 62 | "name": "dynamic-unmanaged", 63 | "versions": ["Test_Dynamic_Unmanaged"], 64 | "subConfigurations": { 65 | "derelict-pq": "derelict-pq-dynamic", 66 | "dpq2": "dynamic-unmanaged" 67 | } 68 | }, 69 | { 70 | "name": "static", 71 | "subConfigurations": { 72 | "dpq2": "static" 73 | } 74 | } 75 | ], 76 | "sourcePaths": [ "integration_tests" ], 77 | "versions": ["integration_tests"] 78 | }, 79 | { 80 | "name": "example", 81 | "targetType": "executable", 82 | "dflags": ["-preview=in"], 83 | "dependencies": 84 | { 85 | "dpq2": { "version": "*", "dflags": ["-preview=in"] }, 86 | "vibe-serialization": { "version": "*", "dflags": ["-preview=in"] }, 87 | }, 88 | "sourcePaths": [ "example" ] 89 | } 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /example/example.d: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rdmd 2 | 3 | import dpq2; 4 | import std.getopt; 5 | import std.stdio: writeln; 6 | import std.typecons: Nullable; 7 | import vibe.data.bson; 8 | 9 | void main(string[] args) 10 | { 11 | string connInfo; 12 | getopt(args, "conninfo", &connInfo); 13 | 14 | Connection conn = new Connection(connInfo); 15 | 16 | // Only text query result can be obtained by this call: 17 | auto answer = conn.exec( 18 | "SELECT now()::timestamp as current_time, 'abc'::text as field_name, "~ 19 | "123 as field_3, 456.78 as field_4, '{\"JSON field name\": 123.456}'::json" 20 | ); 21 | 22 | writeln( "Text query result by name: ", answer[0]["current_time"].as!PGtext ); 23 | writeln( "Text query result by index: ", answer[0][3].as!PGtext ); 24 | 25 | // It is possible to read values of unknown type using BSON: 26 | auto firstRow = answer[0]; 27 | foreach(cell; rangify(firstRow)) 28 | { 29 | writeln("bson: ", cell.as!Bson); 30 | } 31 | 32 | // Binary arguments query with binary result: 33 | QueryParams p; 34 | p.sqlCommand = "SELECT "~ 35 | "$1::double precision as double_field, "~ 36 | "$2::text, "~ 37 | "$3::text as null_field, "~ 38 | "array['first', 'second', NULL]::text[] as array_field, "~ 39 | "$4::integer[] as multi_array, "~ 40 | "'{\"float_value\": 123.456,\"text_str\": \"text string\"}'::json as json_value"; 41 | 42 | p.argsVariadic( 43 | -1234.56789012345, 44 | "first line\nsecond line", 45 | Nullable!string.init, 46 | [[1, 2, 3], [4, 5, 6]] 47 | ); 48 | 49 | auto r = conn.execParams(p); 50 | scope(exit) destroy(r); 51 | 52 | writeln( "0: ", r[0]["double_field"].as!PGdouble_precision ); 53 | writeln( "1: ", r.oneRow[1].as!PGtext ); // .oneRow additionally checks that here is only one row was returned 54 | writeln( "2.1 isNull: ", r[0][2].isNull ); 55 | writeln( "2.2 isNULL: ", r[0].isNULL(2) ); 56 | writeln( "3.1: ", r[0][3].asArray[0].as!PGtext ); 57 | writeln( "3.2: ", r[0][3].asArray[1].as!PGtext ); 58 | writeln( "3.3: ", r[0]["array_field"].asArray[2].isNull ); 59 | writeln( "3.4: ", r[0]["array_field"].asArray.isNULL(2) ); 60 | writeln( "4.1: ", r[0]["multi_array"].asArray.getValue(1, 2).as!PGinteger ); 61 | writeln( "4.2: ", r[0]["multi_array"].as!(int[][]) ); 62 | writeln( "5.1 Json: ", r[0]["json_value"].as!Json); 63 | writeln( "5.2 Bson: ", r[0]["json_value"].as!Bson); 64 | 65 | // It is possible to read values of unknown type using BSON: 66 | for(auto column = 0; column < r.columnCount; column++) 67 | { 68 | writeln("column name: '"~r.columnName(column)~"', bson: ", r[0][column].as!Bson); 69 | } 70 | 71 | // It is possible to upload CSV data ultra-fast: 72 | conn.exec("CREATE TEMP TABLE test_dpq2_copy (v1 TEXT, v2 INT)"); 73 | 74 | // Init the COPY command. This sets the connection in a COPY receive 75 | // mode until putCopyEnd() is called. Copy CSV data, because it's standard, 76 | // ultra fast, and readable: 77 | conn.exec("COPY test_dpq2_copy FROM STDIN WITH (FORMAT csv)"); 78 | 79 | // Write 2 lines of CSV, including text that contains the delimiter. 80 | // Postgresql handles it well: 81 | string data = "\"This, right here, is a test\",8\nWow! it works,13\n"; 82 | conn.putCopyData(data); 83 | 84 | // Write 2 more lines 85 | data = "Horray!,3456\nSuper fast!,325\n"; 86 | conn.putCopyData(data); 87 | 88 | // Signal that the COPY is finished. Let Postgresql finalize the command 89 | // and return any errors with the data. 90 | conn.putCopyEnd(); 91 | } 92 | -------------------------------------------------------------------------------- /integration_tests/integration_tests.d: -------------------------------------------------------------------------------- 1 | @trusted: 2 | 3 | import std.getopt; 4 | 5 | import dpq2; 6 | import dynld = dpq2.dynloader; 7 | import conn = dpq2.connection: _integration_test; 8 | import query = dpq2.query: _integration_test; 9 | import query_gen = dpq2.query_gen: _integration_test; 10 | import result = dpq2.result: _integration_test; 11 | import native = dpq2.conv.native_tests: _integration_test; 12 | import bson = dpq2.conv.to_bson: _integration_test; 13 | 14 | int main(string[] args) 15 | { 16 | version(Dpq2_Dynamic) 17 | { 18 | dynld._integration_test(); 19 | dynld._initTestsConnectionFactory(); 20 | } 21 | else version(Test_Dynamic_Unmanaged) 22 | { 23 | import derelict.pq.pq; 24 | 25 | DerelictPQ.load(); 26 | } 27 | 28 | string conninfo; 29 | getopt( args, "conninfo", &conninfo ); 30 | 31 | conn._integration_test( conninfo ); 32 | query._integration_test( conninfo ); 33 | query_gen._integration_test( conninfo ); 34 | result._integration_test( conninfo ); 35 | native._integration_test( conninfo ); 36 | bson._integration_test( conninfo ); 37 | 38 | return 0; 39 | } 40 | -------------------------------------------------------------------------------- /src/dpq2/args.d: -------------------------------------------------------------------------------- 1 | /// Dealing with query arguments 2 | module dpq2.args; 3 | 4 | @safe: 5 | 6 | public import dpq2.conv.from_d_types; 7 | public import dpq2.conv.from_bson; 8 | 9 | import dpq2.value; 10 | import dpq2.oids: Oid; 11 | import std.conv: to; 12 | import std.string: toStringz; 13 | 14 | /// Query parameters 15 | struct QueryParams 16 | { 17 | string sqlCommand; /// SQL command 18 | ValueFormat resultFormat = ValueFormat.BINARY; /// Result value format 19 | private Value[] _args; // SQL command arguments 20 | 21 | /// SQL command arguments 22 | @property void args(Value[] vargs) 23 | { 24 | _args = vargs; 25 | } 26 | 27 | /// ditto 28 | @property ref inout (Value[]) args() inout pure return 29 | { 30 | return _args; 31 | } 32 | 33 | /// Fills out arguments from array 34 | /// 35 | /// Useful for simple text-only query params 36 | /// Postgres infers a data type for the parameter in the same way it would do for an untyped literal string. 37 | @property void argsFromArray(in string[] arr) 38 | { 39 | _args.length = arr.length; 40 | 41 | foreach(i, ref a; _args) 42 | a = toValue(arr[i], ValueFormat.TEXT); 43 | } 44 | 45 | /// Fills out arguments from variadic arguments 46 | void argsVariadic(Targs ...)(Targs t_args) 47 | { 48 | _args.length = t_args.length; 49 | 50 | static foreach(i, T; Targs) 51 | { 52 | _args[i] = toValue!T(t_args[i]); 53 | } 54 | } 55 | 56 | /// Access to prepared statement name 57 | /// 58 | /// Use it to prepare queries 59 | // FIXME: it is need to check in debug mode what sqlCommand is used in "SQL command" or "prepared statement" mode 60 | @property string preparedStatementName() const { return sqlCommand; } 61 | /// ditto 62 | @property void preparedStatementName(string s){ sqlCommand = s; } 63 | } 64 | 65 | unittest 66 | { 67 | QueryParams qp; 68 | qp.argsVariadic(123, "asd", true); 69 | 70 | assert(qp.args[0] == 123.toValue); 71 | assert(qp.args[1] == "asd".toValue); 72 | assert(qp.args[2] == true.toValue); 73 | } 74 | 75 | /// Used as parameters by PQexecParams-like functions 76 | package struct InternalQueryParams 77 | { 78 | private 79 | { 80 | const(string)* sqlCommand; 81 | Oid[] oids; 82 | int[] formats; 83 | int[] lengths; 84 | const(ubyte)*[] values; 85 | } 86 | 87 | ValueFormat resultFormat; 88 | 89 | this(const QueryParams* qp) pure 90 | { 91 | sqlCommand = &qp.sqlCommand; 92 | resultFormat = qp.resultFormat; 93 | 94 | oids = new Oid[qp.args.length]; 95 | formats = new int[qp.args.length]; 96 | lengths = new int[qp.args.length]; 97 | values = new const(ubyte)* [qp.args.length]; 98 | 99 | for(int i = 0; i < qp.args.length; ++i) 100 | { 101 | oids[i] = qp.args[i].oidType; 102 | formats[i] = qp.args[i].format; 103 | 104 | if(!qp.args[i].isNull) 105 | { 106 | lengths[i] = qp.args[i].data.length.to!int; 107 | 108 | immutable ubyte[] zeroLengthArg = [123]; // fake value, isn't used as argument 109 | 110 | if(qp.args[i].data.length == 0) 111 | values[i] = &zeroLengthArg[0]; 112 | else 113 | values[i] = &qp.args[i].data[0]; 114 | } 115 | } 116 | } 117 | 118 | /// Values used by PQexecParams-like functions 119 | const(char)* command() pure const 120 | { 121 | return cast(const(char)*) (*sqlCommand).toStringz; 122 | } 123 | 124 | /// ditto 125 | const(char)* stmtName() pure const 126 | { 127 | return command(); 128 | } 129 | 130 | /// ditto 131 | int nParams() pure const 132 | { 133 | return values.length.to!int; 134 | } 135 | 136 | /// ditto 137 | const(Oid)* paramTypes() pure 138 | { 139 | if(oids.length == 0) 140 | return null; 141 | else 142 | return &oids[0]; 143 | } 144 | 145 | /// ditto 146 | const(ubyte*)* paramValues() pure 147 | { 148 | if(values.length == 0) 149 | return null; 150 | else 151 | return &values[0]; 152 | } 153 | 154 | /// ditto 155 | const(int)* paramLengths() pure 156 | { 157 | if(lengths.length == 0) 158 | return null; 159 | else 160 | return &lengths[0]; 161 | } 162 | 163 | /// ditto 164 | const(int)* paramFormats() pure 165 | { 166 | if(formats.length == 0) 167 | return null; 168 | else 169 | return &formats[0]; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/dpq2/conv/arrays.d: -------------------------------------------------------------------------------- 1 | /++ 2 | Module to handle PostgreSQL array types 3 | +/ 4 | module dpq2.conv.arrays; 5 | 6 | import dpq2.oids : OidType; 7 | import dpq2.value; 8 | 9 | import std.traits : isArray, isAssociativeArray, isSomeString; 10 | import std.range : ElementType; 11 | import std.typecons : Nullable; 12 | import std.exception: assertThrown; 13 | 14 | @safe: 15 | 16 | template isStaticArrayString(T) 17 | { 18 | import std.traits : isStaticArray; 19 | static if(isStaticArray!T) 20 | enum isStaticArrayString = isSomeString!(typeof(T.init[])); 21 | else 22 | enum isStaticArrayString = false; 23 | } 24 | 25 | static assert(isStaticArrayString!(char[2])); 26 | static assert(!isStaticArrayString!string); 27 | static assert(!isStaticArrayString!(ubyte[2])); 28 | 29 | // From array to Value: 30 | 31 | template isArrayType(T) 32 | { 33 | import dpq2.conv.geometric : isValidPolygon; 34 | import std.traits : Unqual; 35 | 36 | enum isArrayType = isArray!T && !isAssociativeArray!T && !isValidPolygon!T && !is(Unqual!(ElementType!T) == ubyte) && !isSomeString!T 37 | && !isStaticArrayString!T; 38 | } 39 | 40 | static assert(isArrayType!(int[])); 41 | static assert(!isArrayType!(int[string])); 42 | static assert(!isArrayType!(ubyte[])); 43 | static assert(!isArrayType!(string)); 44 | static assert(!isArrayType!(char[2])); 45 | 46 | /// Write array element into buffer 47 | private void writeArrayElement(R, T)(ref R output, T item, ref int counter) 48 | { 49 | import std.array : Appender; 50 | import std.bitmanip : nativeToBigEndian; 51 | import std.format : format; 52 | 53 | static if (is(T == ArrayElementType!T)) 54 | { 55 | import dpq2.conv.from_d_types : toValue; 56 | 57 | static immutable ubyte[] nullVal = [255,255,255,255]; //special length value to indicate null value in array 58 | auto v = item.toValue; // TODO: Direct serialization to buffer would be more effective 59 | 60 | if (v.isNull) 61 | output ~= nullVal; 62 | else 63 | { 64 | auto l = v._data.length; 65 | 66 | if(!(l < uint.max)) 67 | throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH, 68 | format!"Array item size can't be larger than %s"(uint.max-1)); // -1 because uint.max is a NULL special value 69 | 70 | output ~= (cast(uint)l).nativeToBigEndian[]; // write item length 71 | output ~= v._data; 72 | } 73 | 74 | counter++; 75 | } 76 | else 77 | { 78 | foreach (i; item) 79 | writeArrayElement(output, i, counter); 80 | } 81 | } 82 | 83 | /// Converts dynamic or static array of supported types to the coresponding PG array type value 84 | Value toValue(T)(auto ref T v) 85 | if (isArrayType!T) 86 | { 87 | import dpq2.oids : detectOidTypeFromNative, oidConvTo; 88 | import std.array : Appender; 89 | import std.bitmanip : nativeToBigEndian; 90 | import std.traits : isStaticArray; 91 | 92 | alias ET = ArrayElementType!T; 93 | enum dimensions = arrayDimensions!T; 94 | enum elemOid = detectOidTypeFromNative!ET; 95 | auto arrOid = oidConvTo!("array")(elemOid); //TODO: check in CT for supported array types 96 | 97 | // check for null element 98 | static if (__traits(compiles, v[0] is null) || is(ET == Nullable!R,R)) 99 | { 100 | bool hasNull = false; 101 | foreach (vv; v) 102 | { 103 | static if (is(ET == Nullable!R,R)) hasNull = vv.isNull; 104 | else hasNull = vv is null; 105 | 106 | if (hasNull) break; 107 | } 108 | } 109 | else bool hasNull = false; 110 | 111 | auto buffer = Appender!(immutable(ubyte)[])(); 112 | 113 | // write header 114 | buffer ~= dimensions.nativeToBigEndian[]; // write number of dimensions 115 | buffer ~= (hasNull ? 1 : 0).nativeToBigEndian[]; // write null element flag 116 | buffer ~= (cast(int)elemOid).nativeToBigEndian[]; // write elements Oid 117 | const size_t[dimensions] dlen = getDimensionsLengths(v); 118 | 119 | static foreach (d; 0..dimensions) 120 | { 121 | buffer ~= (cast(uint)dlen[d]).nativeToBigEndian[]; // write number of dimensions 122 | buffer ~= 1.nativeToBigEndian[]; // write left bound index (PG indexes from 1 implicitly) 123 | } 124 | 125 | //write data 126 | int elemCount; 127 | foreach (i; v) writeArrayElement(buffer, i, elemCount); 128 | 129 | // Array consistency check 130 | // Can be triggered if non-symmetric multidimensional dynamic array is used 131 | { 132 | size_t mustBeElementsCount = 1; 133 | 134 | foreach(dim; dlen) 135 | mustBeElementsCount *= dim; 136 | 137 | if(elemCount != mustBeElementsCount) 138 | throw new ValueConvException(ConvExceptionType.DIMENSION_MISMATCH, 139 | "Native array dimensions isn't fit to Postgres array type"); 140 | } 141 | 142 | return Value(buffer.data, arrOid); 143 | } 144 | 145 | @system unittest 146 | { 147 | import dpq2.conv.to_d_types : as; 148 | import dpq2.result : asArray; 149 | 150 | { 151 | int[3][2][1] arr = [[[1,2,3], [4,5,6]]]; 152 | 153 | assert(arr[0][0][2] == 3); 154 | assert(arr[0][1][2] == 6); 155 | 156 | auto v = arr.toValue(); 157 | assert(v.oidType == OidType.Int4Array); 158 | 159 | auto varr = v.asArray; 160 | assert(varr.length == 6); 161 | assert(varr.getValue(0,0,2).as!int == 3); 162 | assert(varr.getValue(0,1,2).as!int == 6); 163 | } 164 | 165 | { 166 | int[][][] arr = [[[1,2,3], [4,5,6]]]; 167 | 168 | assert(arr[0][0][2] == 3); 169 | assert(arr[0][1][2] == 6); 170 | 171 | auto v = arr.toValue(); 172 | assert(v.oidType == OidType.Int4Array); 173 | 174 | auto varr = v.asArray; 175 | assert(varr.length == 6); 176 | assert(varr.getValue(0,0,2).as!int == 3); 177 | assert(varr.getValue(0,1,2).as!int == 6); 178 | } 179 | 180 | { 181 | string[] arr = ["foo", "bar", "baz"]; 182 | 183 | auto v = arr.toValue(); 184 | assert(v.oidType == OidType.TextArray); 185 | 186 | auto varr = v.asArray; 187 | assert(varr.length == 3); 188 | assert(varr[0].as!string == "foo"); 189 | assert(varr[1].as!string == "bar"); 190 | assert(varr[2].as!string == "baz"); 191 | } 192 | 193 | { 194 | string[] arr = ["foo", null, "baz"]; 195 | 196 | auto v = arr.toValue(); 197 | assert(v.oidType == OidType.TextArray); 198 | 199 | auto varr = v.asArray; 200 | assert(varr.length == 3); 201 | assert(varr[0].as!string == "foo"); 202 | assert(varr[1].as!string == ""); 203 | assert(varr[2].as!string == "baz"); 204 | } 205 | 206 | { 207 | string[] arr; 208 | 209 | auto v = arr.toValue(); 210 | assert(v.oidType == OidType.TextArray); 211 | assert(!v.isNull); 212 | 213 | auto varr = v.asArray; 214 | assert(varr.length == 0); 215 | } 216 | 217 | { 218 | Nullable!string[] arr = [Nullable!string("foo"), Nullable!string.init, Nullable!string("baz")]; 219 | 220 | auto v = arr.toValue(); 221 | assert(v.oidType == OidType.TextArray); 222 | 223 | auto varr = v.asArray; 224 | assert(varr.length == 3); 225 | assert(varr[0].as!string == "foo"); 226 | assert(varr[1].isNull); 227 | assert(varr[2].as!string == "baz"); 228 | } 229 | } 230 | 231 | // Corrupt array test 232 | unittest 233 | { 234 | alias TA = int[][2][]; 235 | 236 | TA arr = [[[1,2,3], [4,5]]]; // dimensions is not equal 237 | assertThrown!ValueConvException(arr.toValue); 238 | } 239 | 240 | package: 241 | 242 | template ArrayElementType(T) 243 | { 244 | import std.traits : isSomeString; 245 | 246 | static if (!isArrayType!T) 247 | alias ArrayElementType = T; 248 | else 249 | alias ArrayElementType = ArrayElementType!(ElementType!T); 250 | } 251 | 252 | unittest 253 | { 254 | static assert(is(ArrayElementType!(int[][][]) == int)); 255 | static assert(is(ArrayElementType!(int[]) == int)); 256 | static assert(is(ArrayElementType!(int) == int)); 257 | static assert(is(ArrayElementType!(string[][][]) == string)); 258 | static assert(is(ArrayElementType!(bool[]) == bool)); 259 | } 260 | 261 | template arrayDimensions(T) 262 | if (isArray!T) 263 | { 264 | static if (is(ElementType!T == ArrayElementType!T)) 265 | enum int arrayDimensions = 1; 266 | else 267 | enum int arrayDimensions = 1 + arrayDimensions!(ElementType!T); 268 | } 269 | 270 | unittest 271 | { 272 | static assert(arrayDimensions!(bool[]) == 1); 273 | static assert(arrayDimensions!(int[][]) == 2); 274 | static assert(arrayDimensions!(int[][][]) == 3); 275 | static assert(arrayDimensions!(int[][][][]) == 4); 276 | } 277 | 278 | template arrayDimensionType(T, size_t dimNum, size_t currDimNum = 0) 279 | if (isArray!T) 280 | { 281 | alias CurrT = ElementType!T; 282 | 283 | static if (currDimNum < dimNum) 284 | alias arrayDimensionType = arrayDimensionType!(CurrT, dimNum, currDimNum + 1); 285 | else 286 | alias arrayDimensionType = CurrT; 287 | } 288 | 289 | unittest 290 | { 291 | static assert(is(arrayDimensionType!(bool[2][3], 0) == bool[2])); 292 | static assert(is(arrayDimensionType!(bool[][3], 0) == bool[])); 293 | static assert(is(arrayDimensionType!(bool[3][], 0) == bool[3])); 294 | static assert(is(arrayDimensionType!(bool[2][][4], 0) == bool[2][])); 295 | static assert(is(arrayDimensionType!(bool[3][], 1) == bool)); 296 | } 297 | 298 | auto getDimensionsLengths(T)(T v) 299 | if (isArrayType!T) 300 | { 301 | enum dimNum = arrayDimensions!T; 302 | size_t[dimNum] ret = -1; 303 | 304 | calcDimensionsLengths(v, ret, 0); 305 | 306 | return ret; 307 | } 308 | 309 | private void calcDimensionsLengths(T, Ret)(T arr, ref Ret ret, int currDimNum) 310 | if (isArray!T) 311 | { 312 | import std.format : format; 313 | 314 | if(!(arr.length < uint.max)) 315 | throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH, 316 | format!"Array dimension length can't be larger or equal %s"(uint.max)); 317 | 318 | ret[currDimNum] = arr.length; 319 | 320 | static if(isArrayType!(ElementType!T)) 321 | { 322 | currDimNum++; 323 | 324 | if(currDimNum < ret.length) 325 | if(arr.length > 0) 326 | calcDimensionsLengths(arr[0], ret, currDimNum); 327 | } 328 | } 329 | 330 | unittest 331 | { 332 | alias T = int[][2][]; 333 | 334 | T arr = [[[1,2,3], [4,5,6]]]; 335 | 336 | auto ret = getDimensionsLengths(arr); 337 | 338 | assert(ret[0] == 1); 339 | assert(ret[1] == 2); 340 | assert(ret[2] == 3); 341 | } 342 | 343 | // From Value to array: 344 | 345 | import dpq2.result: ArrayProperties; 346 | 347 | /// Convert Value to native array type 348 | T binaryValueAs(T)(in Value v) @trusted 349 | if(isArrayType!T) 350 | { 351 | int idx; 352 | return v.valueToArrayRow!(T, 0)(ArrayProperties(v), idx); 353 | } 354 | 355 | private T valueToArrayRow(T, int currDimension)(in Value v, ArrayProperties arrayProperties, ref int elemIdx) @system 356 | { 357 | import std.traits: isStaticArray; 358 | import std.conv: to; 359 | 360 | T res; 361 | 362 | // Postgres interprets empty arrays as zero-dimensional arrays 363 | if(arrayProperties.dimsSize.length == 0) 364 | arrayProperties.dimsSize ~= 0; // adds one zero-size dimension 365 | 366 | static if(isStaticArray!T) 367 | { 368 | if(T.length != arrayProperties.dimsSize[currDimension]) 369 | throw new ValueConvException(ConvExceptionType.DIMENSION_MISMATCH, 370 | "Result array dimension "~currDimension.to!string~" mismatch" 371 | ); 372 | } 373 | else 374 | res.length = arrayProperties.dimsSize[currDimension]; 375 | 376 | foreach(size_t i, ref elem; res) 377 | { 378 | import dpq2.result; 379 | 380 | alias ElemType = typeof(elem); 381 | 382 | static if(isArrayType!ElemType) 383 | elem = v.valueToArrayRow!(ElemType, currDimension + 1)(arrayProperties, elemIdx); 384 | else 385 | { 386 | elem = v.asArray.getValueByFlatIndex(elemIdx).as!ElemType; 387 | elemIdx++; 388 | } 389 | } 390 | 391 | return res; 392 | } 393 | 394 | // Array test 395 | @system unittest 396 | { 397 | alias TA = int[][2][]; 398 | 399 | TA arr = [[[1,2,3], [4,5,6]]]; 400 | Value v = arr.toValue; 401 | 402 | TA r = v.binaryValueAs!TA; 403 | 404 | assert(r == arr); 405 | } 406 | 407 | // Dimension mismatch test 408 | @system unittest 409 | { 410 | alias TA = int[][2][]; 411 | alias R = int[][2][3]; // array dimensions mismatch 412 | 413 | TA arr = [[[1,2,3], [4,5,6]]]; 414 | Value v = arr.toValue; 415 | 416 | assertThrown!ValueConvException(v.binaryValueAs!R); 417 | } 418 | -------------------------------------------------------------------------------- /src/dpq2/conv/from_bson.d: -------------------------------------------------------------------------------- 1 | /// 2 | module dpq2.conv.from_bson; 3 | 4 | import dpq2.value; 5 | import dpq2.oids; 6 | import dpq2.result: ArrayProperties, ArrayHeader_net, Dim_net; 7 | import dpq2.conv.from_d_types; 8 | import dpq2.conv.to_d_types; 9 | import vibe.data.bson; 10 | import std.bitmanip: nativeToBigEndian; 11 | import std.conv: to; 12 | 13 | /// Default type will be used for NULL value and for array without detected type 14 | Value bsonToValue(Bson v, OidType defaultType = OidType.Undefined) 15 | { 16 | if(v.type == Bson.Type.array) 17 | return bsonArrayToValue(v, defaultType); 18 | else 19 | return bsonValueToValue(v, defaultType); 20 | } 21 | 22 | private: 23 | 24 | Value bsonValueToValue(Bson v, OidType defaultType) 25 | { 26 | Value ret; 27 | 28 | with(Bson.Type) 29 | switch(v.type) 30 | { 31 | case null_: 32 | ret = Value(ValueFormat.BINARY, defaultType); 33 | break; 34 | 35 | case Bson.Type.object: 36 | ret = v.toJson.toString.toValue; 37 | ret.oidType = OidType.Json; 38 | break; 39 | 40 | case bool_: 41 | ret = v.get!bool.toValue; 42 | break; 43 | 44 | case int_: 45 | ret = v.get!int.toValue; 46 | break; 47 | 48 | case long_: 49 | ret = v.get!long.toValue; 50 | break; 51 | 52 | case double_: 53 | ret = v.get!double.toValue; 54 | break; 55 | 56 | case Bson.Type.string: 57 | ret = v.get!(immutable(char)[]).toValue; 58 | break; 59 | 60 | default: 61 | throw new ValueConvException( 62 | ConvExceptionType.NOT_IMPLEMENTED, 63 | "Format "~v.typeInternal.to!(immutable(char)[])~" doesn't supported by Bson to Value converter", 64 | __FILE__, __LINE__ 65 | ); 66 | } 67 | 68 | return ret; 69 | } 70 | 71 | //TODO: remove when vibe/data/bson.d removes deprecated types names 72 | /// Bson.Type without deprecated items 73 | package enum BsonTypeWODeprecated : ubyte 74 | { 75 | end = 0x00, /// End marker - should never occur explicitly 76 | double_ = 0x01, /// A 64-bit floating point value 77 | string = 0x02, /// A UTF-8 string 78 | object = 0x03, /// An object aka. dictionary of string to Bson 79 | array = 0x04, /// An array of BSON values 80 | binData = 0x05, /// Raw binary data (ubyte[]) 81 | undefined = 0x06, /// Deprecated 82 | objectID = 0x07, /// BSON Object ID (96-bit) 83 | bool_ = 0x08, /// Boolean value 84 | date = 0x09, /// Date value (UTC) 85 | null_ = 0x0A, /// Null value 86 | regex = 0x0B, /// Regular expression 87 | dbRef = 0x0C, /// Deprecated 88 | code = 0x0D, /// JaveScript code 89 | symbol = 0x0E, /// Symbol/variable name 90 | codeWScope = 0x0F, /// JavaScript code with scope 91 | int_ = 0x10, /// 32-bit integer 92 | timestamp = 0x11, /// Timestamp value 93 | long_ = 0x12, /// 64-bit integer 94 | minKey = 0xff, /// Internal value 95 | maxKey = 0x7f, /// Internal value 96 | } 97 | 98 | package BsonTypeWODeprecated typeInternal(in Bson v) 99 | { 100 | return cast(BsonTypeWODeprecated) v.type; 101 | } 102 | 103 | unittest 104 | { 105 | { 106 | Value v1 = bsonToValue(Bson(123)); 107 | Value v2 = (123).toValue; 108 | 109 | assert(v1.as!int == v2.as!int); 110 | } 111 | 112 | { 113 | Value v1 = bsonToValue(Bson("Test string")); 114 | Value v2 = ("Test string").toValue; 115 | 116 | assert(v1.as!string == v2.as!string); 117 | } 118 | 119 | { 120 | Value t = bsonToValue(Bson(true)); 121 | Value f = bsonToValue(Bson(false)); 122 | 123 | assert(t.as!bool == true); 124 | assert(f.as!bool == false); 125 | } 126 | } 127 | 128 | Value bsonArrayToValue(ref Bson bsonArr, OidType defaultType) 129 | { 130 | ubyte[] nullValue() pure 131 | { 132 | ubyte[] ret = [0xff, 0xff, 0xff, 0xff]; //NULL magic number 133 | return ret; 134 | } 135 | 136 | ubyte[] rawValue(Value v) pure 137 | { 138 | if(v.isNull) 139 | { 140 | return nullValue(); 141 | } 142 | else 143 | { 144 | return v._data.length.to!uint.nativeToBigEndian ~ v._data; 145 | } 146 | } 147 | 148 | ArrayProperties ap; 149 | ubyte[] rawValues; 150 | 151 | void recursive(ref Bson bsonArr, int dimension) 152 | { 153 | if(dimension == ap.dimsSize.length) 154 | { 155 | ap.dimsSize ~= bsonArr.length.to!int; 156 | } 157 | else 158 | { 159 | if(ap.dimsSize[dimension] != bsonArr.length) 160 | throw new ValueConvException(ConvExceptionType.NOT_ARRAY, "Jagged arrays are unsupported", __FILE__, __LINE__); 161 | } 162 | 163 | foreach(bElem; bsonArr) 164 | { 165 | ap.nElems++; 166 | 167 | switch(bElem.type) 168 | { 169 | case Bson.Type.array: 170 | recursive(bElem, dimension + 1); 171 | break; 172 | 173 | case Bson.Type.null_: 174 | rawValues ~= nullValue(); 175 | break; 176 | 177 | default: 178 | Value v = bsonValueToValue(bElem, OidType.Undefined); 179 | 180 | if(ap.OID == OidType.Undefined) 181 | { 182 | ap.OID = v.oidType; 183 | } 184 | else 185 | { 186 | if(ap.OID != v.oidType) 187 | throw new ValueConvException( 188 | ConvExceptionType.NOT_ARRAY, 189 | "Bson (which used for creating "~ap.OID.to!string~" array) also contains value of type "~v.oidType.to!string, 190 | __FILE__, __LINE__ 191 | ); 192 | } 193 | 194 | rawValues ~= rawValue(v); 195 | } 196 | } 197 | } 198 | 199 | recursive(bsonArr, 0); 200 | 201 | if(ap.OID == OidType.Undefined) ap.OID = defaultType.oidConvTo!"element"; 202 | 203 | ArrayHeader_net h; 204 | h.ndims = nativeToBigEndian(ap.dimsSize.length.to!int); 205 | h.OID = nativeToBigEndian(ap.OID.to!Oid); 206 | 207 | ubyte[] ret; 208 | ret ~= (cast(ubyte*) &h)[0 .. h.sizeof]; 209 | 210 | foreach(i; 0 .. ap.dimsSize.length) 211 | { 212 | Dim_net dim; 213 | dim.dim_size = nativeToBigEndian(ap.dimsSize[i]); 214 | dim.lbound = nativeToBigEndian!int(1); 215 | 216 | ret ~= (cast(ubyte*) &dim)[0 .. dim.sizeof]; 217 | } 218 | 219 | ret ~= rawValues; 220 | 221 | return Value(cast(immutable) ret, ap.OID.oidConvTo!"array", false, ValueFormat.BINARY); 222 | } 223 | 224 | unittest 225 | { 226 | import dpq2.conv.to_bson; 227 | 228 | { 229 | Bson bsonArray = Bson( 230 | [Bson(123), Bson(155), Bson(null), Bson(0), Bson(null)] 231 | ); 232 | 233 | Value v = bsonToValue(bsonArray); 234 | 235 | assert(v.isSupportedArray); 236 | assert(v.as!Bson == bsonArray); 237 | } 238 | 239 | { 240 | Bson bsonArray = Bson([ 241 | Bson([Bson(123), Bson(155), Bson(null)]), 242 | Bson([Bson(0), Bson(null), Bson(155)]) 243 | ]); 244 | 245 | Value v = bsonToValue(bsonArray); 246 | 247 | assert(v.isSupportedArray); 248 | assert(v.as!Bson == bsonArray); 249 | } 250 | 251 | { 252 | Bson bsonArray = Bson([ 253 | Bson([Bson(123), Bson(155)]), 254 | Bson([Bson(0)]) 255 | ]); 256 | 257 | bool exceptionFlag = false; 258 | 259 | try 260 | bsonToValue(bsonArray); 261 | catch(ValueConvException e) 262 | { 263 | if(e.type == ConvExceptionType.NOT_ARRAY) 264 | exceptionFlag = true; 265 | } 266 | 267 | assert(exceptionFlag); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/dpq2/conv/from_d_types.d: -------------------------------------------------------------------------------- 1 | /// 2 | module dpq2.conv.from_d_types; 3 | 4 | @safe: 5 | 6 | public import dpq2.conv.arrays : isArrayType, toValue, isStaticArrayString; 7 | public import dpq2.conv.geometric : isGeometricType, toValue; 8 | public import dpq2.conv.inet: toValue, vibe2pg; 9 | import dpq2.conv.time : POSTGRES_EPOCH_DATE, TimeStamp, TimeStampUTC, TimeOfDayWithTZ, Interval; 10 | import dpq2.oids : detectOidTypeFromNative, oidConvTo, OidType; 11 | import dpq2.value : Value, ValueFormat; 12 | 13 | import std.bitmanip: nativeToBigEndian, BitArray, append; 14 | import std.datetime.date: Date, DateTime, TimeOfDay; 15 | import std.datetime.systime: SysTime; 16 | import std.datetime.timezone: LocalTime, TimeZone, UTC; 17 | import std.traits: isImplicitlyConvertible, isNumeric, isInstanceOf, OriginalType, Unqual, isSomeString; 18 | import std.typecons : Nullable; 19 | import std.uuid: UUID; 20 | import vibe.data.json: Json; 21 | import money: currency; 22 | 23 | /// Converts Nullable!T to Value 24 | Value toValue(T)(T v) 25 | if (is(T == Nullable!R, R) && !(isArrayType!(typeof(v.get))) && !isGeometricType!(typeof(v.get))) 26 | { 27 | if (v.isNull) 28 | return Value(ValueFormat.BINARY, detectOidTypeFromNative!T); 29 | else 30 | return toValue(v.get); 31 | } 32 | 33 | /// ditto 34 | Value toValue(T)(T v) 35 | if (is(T == Nullable!R, R) && (isArrayType!(typeof(v.get)))) 36 | { 37 | import dpq2.conv.arrays : arrToValue = toValue; // deprecation import workaround 38 | import std.range : ElementType; 39 | 40 | if (v.isNull) 41 | return Value(ValueFormat.BINARY, detectOidTypeFromNative!(ElementType!(typeof(v.get))).oidConvTo!"array"); 42 | else 43 | return arrToValue(v.get); 44 | } 45 | 46 | /// ditto 47 | Value toValue(T)(T v) 48 | if (is(T == Nullable!R, R) && isGeometricType!(typeof(v.get))) 49 | { 50 | import dpq2.conv.geometric : geoToValue = toValue; // deprecation import workaround 51 | 52 | if (v.isNull) 53 | return Value(ValueFormat.BINARY, detectOidTypeFromNative!T); 54 | else 55 | return geoToValue(v.get); 56 | } 57 | 58 | /// 59 | Value toValue(T)(T v) 60 | if(isNumeric!(T)) 61 | { 62 | return Value(v.nativeToBigEndian.dup, detectOidTypeFromNative!T, false, ValueFormat.BINARY); 63 | } 64 | 65 | /// Convert money.currency to PG value 66 | /// 67 | /// Caution: here is no check of fractional precision while conversion! 68 | /// See also: PostgreSQL's "lc_monetary" description and "money" package description 69 | Value toValue(T)(T v) 70 | if(isInstanceOf!(currency, T) && T.amount.sizeof == 8) 71 | { 72 | return Value(v.amount.nativeToBigEndian.dup, OidType.Money, false, ValueFormat.BINARY); 73 | } 74 | 75 | unittest 76 | { 77 | import dpq2.conv.to_d_types: PGTestMoney; 78 | 79 | const pgtm = PGTestMoney(-123.45); 80 | 81 | Value v = pgtm.toValue; 82 | 83 | assert(v.oidType == OidType.Money); 84 | assert(v.as!PGTestMoney == pgtm); 85 | } 86 | 87 | /// Convert std.bitmanip.BitArray to PG value 88 | Value toValue(T)(T v) @trusted 89 | if(is(Unqual!T == BitArray)) 90 | { 91 | import std.array : appender; 92 | import core.bitop : bitswap; 93 | 94 | size_t len = v.length / 8 + (v.length % 8 ? 1 : 0); 95 | auto data = cast(size_t[])v; 96 | auto buffer = appender!(const ubyte[])(); 97 | buffer.append!uint(cast(uint)v.length); 98 | foreach (d; data[0 .. v.dim]) 99 | { 100 | //FIXME: DMD Issue 19693 101 | version(DigitalMars) 102 | auto ntb = nativeToBigEndian(softBitswap(d)); 103 | else 104 | auto ntb = nativeToBigEndian(bitswap(d)); 105 | foreach (b; ntb[0 .. len]) 106 | { 107 | buffer.append!ubyte(b); 108 | } 109 | 110 | } 111 | return Value(buffer.data.dup, detectOidTypeFromNative!T, false, ValueFormat.BINARY); 112 | } 113 | 114 | /// Reverses the order of bits - needed because of dmd Issue 19693 115 | /// https://issues.dlang.org/show_bug.cgi?id=19693 116 | package N softBitswap(N)(N x) pure 117 | if (is(N == uint) || is(N == ulong)) 118 | { 119 | import core.bitop : bswap; 120 | // swap 1-bit pairs: 121 | enum mask1 = cast(N) 0x5555_5555_5555_5555L; 122 | x = ((x >> 1) & mask1) | ((x & mask1) << 1); 123 | // swap 2-bit pairs: 124 | enum mask2 = cast(N) 0x3333_3333_3333_3333L; 125 | x = ((x >> 2) & mask2) | ((x & mask2) << 2); 126 | // swap 4-bit pairs: 127 | enum mask4 = cast(N) 0x0F0F_0F0F_0F0F_0F0FL; 128 | x = ((x >> 4) & mask4) | ((x & mask4) << 4); 129 | 130 | // reverse the order of all bytes: 131 | x = bswap(x); 132 | 133 | return x; 134 | } 135 | 136 | @trusted unittest 137 | { 138 | import std.bitmanip : BitArray; 139 | 140 | auto varbit = BitArray([1,0,1,1,0]); 141 | 142 | Value v = varbit.toValue; 143 | 144 | assert(v.oidType == OidType.VariableBitString); 145 | assert(v.as!BitArray == varbit); 146 | 147 | // test softBitswap 148 | assert (softBitswap!uint( 0x8000_0100 ) == 0x0080_0001); 149 | foreach (i; 0 .. 32) 150 | assert (softBitswap!uint(1 << i) == 1 << 32 - i - 1); 151 | 152 | assert (softBitswap!ulong( 0b1000000000000000000000010000000000000000100000000000000000000001) 153 | == 0b1000000000000000000000010000000000000000100000000000000000000001); 154 | assert (softBitswap!ulong( 0b1110000000000000000000010000000000000000100000000000000000000001) 155 | == 0b1000000000000000000000010000000000000000100000000000000000000111); 156 | foreach (i; 0 .. 64) 157 | assert (softBitswap!ulong(1UL << i) == 1UL << 64 - i - 1); 158 | 159 | } 160 | 161 | /** 162 | Converts types implicitly convertible to string to PG Value. 163 | Note that if string is null it is written as an empty string. 164 | If NULL is a desired DB value, Nullable!string can be used instead. 165 | */ 166 | Value toValue(T)(T v, ValueFormat valueFormat = ValueFormat.BINARY) @trusted 167 | if(isSomeString!T || isStaticArrayString!T) 168 | { 169 | static if(is(T == string)) 170 | { 171 | import std.string : representation; 172 | 173 | static assert(isImplicitlyConvertible!(T, string)); 174 | auto buf = (cast(string) v).representation; 175 | 176 | if(valueFormat == ValueFormat.TEXT) buf ~= 0; // for prepareArgs only 177 | 178 | return Value(buf, OidType.Text, false, valueFormat); 179 | } 180 | else 181 | { 182 | // convert to a string 183 | import std.conv : to; 184 | return toValue(v.to!string, valueFormat); 185 | } 186 | } 187 | 188 | /// Constructs Value from array of bytes 189 | Value toValue(T)(T v) 190 | if(is(T : immutable(ubyte)[])) 191 | { 192 | return Value(v, detectOidTypeFromNative!(ubyte[]), false, ValueFormat.BINARY); 193 | } 194 | 195 | /// Constructs Value from boolean 196 | Value toValue(T : bool)(T v) @trusted 197 | if (!is(T == Nullable!R, R)) 198 | { 199 | immutable ubyte[] buf = [ v ? 1 : 0 ]; 200 | 201 | return Value(buf, detectOidTypeFromNative!T, false, ValueFormat.BINARY); 202 | } 203 | 204 | /// Constructs Value from Date 205 | Value toValue(T)(T v) 206 | if (is(Unqual!T == Date)) 207 | { 208 | import std.conv: to; 209 | import dpq2.value; 210 | import dpq2.conv.time: POSTGRES_EPOCH_JDATE; 211 | 212 | long mj_day = v.modJulianDay; 213 | 214 | // max days isn't checked because Phobos Date days value always fits into Postgres Date 215 | if (mj_day < -POSTGRES_EPOCH_JDATE) 216 | throw new ValueConvException( 217 | ConvExceptionType.DATE_VALUE_OVERFLOW, 218 | "Date value doesn't fit into Postgres binary Date", 219 | __FILE__, __LINE__ 220 | ); 221 | 222 | enum mj_pg_epoch = POSTGRES_EPOCH_DATE.modJulianDay; 223 | long days = mj_day - mj_pg_epoch; 224 | 225 | return Value(nativeToBigEndian(days.to!int).dup, OidType.Date, false); 226 | } 227 | 228 | private long convTimeOfDayToPG(in TimeOfDay v) pure 229 | { 230 | return ((60L * v.hour + v.minute) * 60 + v.second) * 1_000_000; 231 | } 232 | 233 | /// Constructs Value from TimeOfDay 234 | Value toValue(T)(T v) 235 | if (is(Unqual!T == TimeOfDay)) 236 | { 237 | return Value(v.convTimeOfDayToPG.nativeToBigEndian.dup, OidType.Time); 238 | } 239 | 240 | /// Constructs Value from TimeOfDay 241 | Value toValue(T)(T v) 242 | if (is(Unqual!T == TimeOfDayWithTZ)) 243 | { 244 | const buf = v.time.convTimeOfDayToPG.nativeToBigEndian ~ v.tzSec.nativeToBigEndian; 245 | assert(buf.length == 12); 246 | 247 | return Value(buf.dup, OidType.TimeWithZone); 248 | } 249 | 250 | /// Constructs Value from Interval 251 | Value toValue(T)(T v) 252 | if (is(Unqual!T == Interval)) 253 | { 254 | const buf = v.usecs.nativeToBigEndian ~ v.days.nativeToBigEndian ~ v.months.nativeToBigEndian; 255 | assert(buf.length == 16); 256 | 257 | return Value(buf.dup, OidType.TimeInterval); 258 | } 259 | 260 | /// Constructs Value from TimeStamp or from TimeStampUTC 261 | Value toValue(T)(T v) 262 | if (is(Unqual!T == TimeStamp) || is(Unqual!T == TimeStampUTC)) 263 | { 264 | long us; /// microseconds 265 | 266 | if(v.isLater) // infinity 267 | us = us.max; 268 | else if(v.isEarlier) // -infinity 269 | us = us.min; 270 | else 271 | { 272 | enum mj_pg_epoch = POSTGRES_EPOCH_DATE.modJulianDay; 273 | long j = modJulianDayForIntYear(v.date.year, v.date.month, v.date.day) - mj_pg_epoch; 274 | us = (((j * 24 + v.time.hour) * 60 + v.time.minute) * 60 + v.time.second) * 1_000_000 + v.fracSec.total!"usecs"; 275 | } 276 | 277 | return Value( 278 | nativeToBigEndian(us).dup, 279 | is(Unqual!T == TimeStamp) ? OidType.TimeStamp : OidType.TimeStampWithZone, 280 | false 281 | ); 282 | } 283 | 284 | private auto modJulianDayForIntYear(const int year, const ubyte month, const short day) pure 285 | { 286 | // Wikipedia magic: 287 | 288 | const a = (14 - month) / 12; 289 | const y = year + 4800 - a; 290 | const m = month + a * 12 - 3; 291 | 292 | const jd = day + (m*153+2)/5 + y*365 + y/4 - y/100 + y/400 - 32045; 293 | 294 | return jd - 2_400_001; 295 | } 296 | unittest 297 | { 298 | assert(modJulianDayForIntYear(1858, 11, 17) == 0); 299 | assert(modJulianDayForIntYear(2010, 8, 24) == 55_432); 300 | assert(modJulianDayForIntYear(1999, 7, 6) == 51_365); 301 | } 302 | 303 | /++ 304 | Constructs Value from DateTime 305 | It uses Timestamp without TZ as a resulting PG type 306 | +/ 307 | Value toValue(T)(T v) 308 | if (is(Unqual!T == DateTime)) 309 | { 310 | return TimeStamp(v).toValue; 311 | } 312 | 313 | /++ 314 | Constructs Value from SysTime 315 | Note that SysTime has a precision in hnsecs and PG TimeStamp in usecs. 316 | It means that PG value will have 10 times lower precision. 317 | And as both types are using long for internal storage it also means that PG TimeStamp can store greater range of values than SysTime. 318 | +/ 319 | Value toValue(T)(T v) 320 | if (is(Unqual!T == SysTime)) 321 | { 322 | import dpq2.value: ValueConvException, ConvExceptionType; 323 | import core.time; 324 | import std.conv: to; 325 | 326 | long usecs; 327 | int hnsecs; 328 | v.fracSecs.split!("usecs", "hnsecs")(usecs, hnsecs); 329 | 330 | if(hnsecs) 331 | throw new ValueConvException( 332 | ConvExceptionType.TOO_PRECISE, 333 | "fracSecs have 1 microsecond resolution but contains "~v.fracSecs.to!string 334 | ); 335 | 336 | long us = (v - SysTime(POSTGRES_EPOCH_DATE, UTC())).total!"usecs"; 337 | 338 | return Value(nativeToBigEndian(us).dup, OidType.TimeStampWithZone, false); 339 | } 340 | 341 | /// Constructs Value from UUID 342 | Value toValue(T)(T v) 343 | if (is(Unqual!T == UUID)) 344 | { 345 | return Value(v.data.dup, OidType.UUID); 346 | } 347 | 348 | /// Constructs Value from Json 349 | Value toValue(T)(T v) 350 | if (is(Unqual!T == Json)) 351 | { 352 | auto r = toValue(v.toString); 353 | r.oidType = OidType.Json; 354 | 355 | return r; 356 | } 357 | 358 | Value toRecordValue(Value[] elements) 359 | { 360 | import std.array : appender; 361 | auto buffer = appender!(ubyte[])(); 362 | buffer ~= nativeToBigEndian!int(cast(int)elements.length)[]; 363 | foreach (element; elements) 364 | { 365 | buffer ~= nativeToBigEndian!int(element.oidType)[]; 366 | if (element.isNull) { 367 | buffer ~= nativeToBigEndian!int(-1)[]; 368 | } else { 369 | buffer ~= nativeToBigEndian!int(cast(int)element.data.length)[]; 370 | buffer ~= element.data; 371 | } 372 | } 373 | 374 | return Value(buffer.data.idup, OidType.Record); 375 | } 376 | 377 | version(unittest) 378 | import dpq2.conv.to_d_types : as, deserializeRecord; 379 | 380 | unittest 381 | { 382 | import std.stdio; 383 | Value[] vals = [toValue(17.34), toValue(Nullable!long(17)), toValue(Nullable!long.init)]; 384 | Value v = vals.toRecordValue; 385 | assert(deserializeRecord(v) == vals); 386 | } 387 | 388 | unittest 389 | { 390 | Value v = toValue(cast(short) 123); 391 | 392 | assert(v.oidType == OidType.Int2); 393 | assert(v.as!short == 123); 394 | } 395 | 396 | unittest 397 | { 398 | Value v = toValue(-123.456); 399 | 400 | assert(v.oidType == OidType.Float8); 401 | assert(v.as!double == -123.456); 402 | } 403 | 404 | unittest 405 | { 406 | Value v = toValue("Test string"); 407 | 408 | assert(v.oidType == OidType.Text); 409 | assert(v.as!string == "Test string"); 410 | } 411 | 412 | // string Null values 413 | @system unittest 414 | { 415 | { 416 | import core.exception: AssertError; 417 | import std.exception: assertThrown; 418 | 419 | auto v = Nullable!string.init.toValue; 420 | assert(v.oidType == OidType.Text); 421 | assert(v.isNull); 422 | 423 | assertThrown!AssertError(v.as!string); 424 | assert(v.as!(Nullable!string).isNull); 425 | } 426 | 427 | { 428 | string s; 429 | auto v = s.toValue; 430 | assert(v.oidType == OidType.Text); 431 | assert(!v.isNull); 432 | } 433 | } 434 | 435 | unittest 436 | { 437 | immutable ubyte[] buf = [0, 1, 2, 3, 4, 5]; 438 | Value v = toValue(buf); 439 | 440 | assert(v.oidType == OidType.ByteArray); 441 | assert(v.as!(const ubyte[]) == buf); 442 | } 443 | 444 | unittest 445 | { 446 | Value t = toValue(true); 447 | Value f = toValue(false); 448 | 449 | assert(t.as!bool == true); 450 | assert(f.as!bool == false); 451 | } 452 | 453 | unittest 454 | { 455 | Value v = toValue(Nullable!long(1)); 456 | Value nv = toValue(Nullable!bool.init); 457 | 458 | assert(!v.isNull); 459 | assert(v.oidType == OidType.Int8); 460 | assert(v.as!long == 1); 461 | 462 | assert(nv.isNull); 463 | assert(nv.oidType == OidType.Bool); 464 | } 465 | 466 | unittest 467 | { 468 | import std.datetime : DateTime; 469 | 470 | Value v = toValue(Nullable!TimeStamp(TimeStamp(DateTime(2017, 1, 2)))); 471 | 472 | assert(!v.isNull); 473 | assert(v.oidType == OidType.TimeStamp); 474 | } 475 | 476 | unittest 477 | { 478 | // Date: '2018-1-15' 479 | auto d = Date(2018, 1, 15); 480 | auto v = toValue(d); 481 | 482 | assert(v.oidType == OidType.Date); 483 | assert(v.as!Date == d); 484 | } 485 | 486 | unittest 487 | { 488 | auto d = immutable Date(2018, 1, 15); 489 | auto v = toValue(d); 490 | 491 | assert(v.oidType == OidType.Date); 492 | assert(v.as!Date == d); 493 | } 494 | 495 | unittest 496 | { 497 | // Date: '2000-1-1' 498 | auto d = Date(2000, 1, 1); 499 | auto v = toValue(d); 500 | 501 | assert(v.oidType == OidType.Date); 502 | assert(v.as!Date == d); 503 | } 504 | 505 | unittest 506 | { 507 | // Date: '0010-2-20' 508 | auto d = Date(10, 2, 20); 509 | auto v = toValue(d); 510 | 511 | assert(v.oidType == OidType.Date); 512 | assert(v.as!Date == d); 513 | } 514 | 515 | unittest 516 | { 517 | // Date: max (always fits into Postgres Date) 518 | auto d = Date.max; 519 | auto v = toValue(d); 520 | 521 | assert(v.oidType == OidType.Date); 522 | assert(v.as!Date == d); 523 | } 524 | 525 | unittest 526 | { 527 | // Date: min (overflow) 528 | import std.exception: assertThrown; 529 | import dpq2.value: ValueConvException; 530 | 531 | auto d = Date.min; 532 | assertThrown!ValueConvException(d.toValue); 533 | } 534 | 535 | unittest 536 | { 537 | // DateTime 538 | auto d = const DateTime(2018, 2, 20, 1, 2, 3); 539 | auto v = toValue(d); 540 | 541 | assert(v.oidType == OidType.TimeStamp); 542 | assert(v.as!DateTime == d); 543 | } 544 | 545 | unittest 546 | { 547 | // Nullable!DateTime 548 | import std.typecons : nullable; 549 | auto d = nullable(DateTime(2018, 2, 20, 1, 2, 3)); 550 | auto v = toValue(d); 551 | 552 | assert(v.oidType == OidType.TimeStamp); 553 | assert(v.as!(Nullable!DateTime) == d); 554 | 555 | d.nullify(); 556 | v = toValue(d); 557 | assert(v.oidType == OidType.TimeStamp); 558 | assert(v.as!(Nullable!DateTime).isNull); 559 | } 560 | 561 | unittest 562 | { 563 | // TimeOfDay: '14:29:17' 564 | auto tod = TimeOfDay(14, 29, 17); 565 | auto v = toValue(tod); 566 | 567 | assert(v.oidType == OidType.Time); 568 | assert(v.as!TimeOfDay == tod); 569 | } 570 | 571 | unittest 572 | { 573 | auto t = TimeOfDayWithTZ( 574 | TimeOfDay(14, 29, 17), 575 | -3600 * 7 // Negative means TZ == +07 576 | ); 577 | 578 | auto v = toValue(t); 579 | 580 | assert(v.oidType == OidType.TimeWithZone); 581 | assert(v.as!TimeOfDayWithTZ == t); 582 | } 583 | 584 | unittest 585 | { 586 | auto t = Interval( 587 | -123, 588 | -456, 589 | -789 590 | ); 591 | 592 | auto v = toValue(t); 593 | 594 | assert(v.oidType == OidType.TimeInterval); 595 | assert(v.as!Interval == t); 596 | } 597 | 598 | unittest 599 | { 600 | // SysTime: '2017-11-13T14:29:17.075678Z' 601 | auto t = SysTime.fromISOExtString("2017-11-13T14:29:17.075678Z"); 602 | auto v = toValue(t); 603 | 604 | assert(v.oidType == OidType.TimeStampWithZone); 605 | assert(v.as!SysTime == t); 606 | } 607 | 608 | unittest 609 | { 610 | import core.time: dur; 611 | import std.exception: assertThrown; 612 | import dpq2.value: ValueConvException; 613 | 614 | auto t = SysTime.fromISOExtString("2017-11-13T14:29:17.075678Z"); 615 | t += dur!"hnsecs"(1); 616 | 617 | // TOO_PRECISE 618 | assertThrown!ValueConvException(t.toValue); 619 | } 620 | 621 | unittest 622 | { 623 | import core.time : usecs; 624 | import std.datetime.date : DateTime; 625 | 626 | // TimeStamp: '2017-11-13 14:29:17.075678' 627 | auto t = TimeStamp(DateTime(2017, 11, 13, 14, 29, 17), 75_678.usecs); 628 | auto v = toValue(t); 629 | 630 | assert(v.oidType == OidType.TimeStamp); 631 | assert(v.as!TimeStamp == t); 632 | } 633 | 634 | unittest 635 | { 636 | auto j = Json(["foo":Json("bar")]); 637 | auto v = j.toValue; 638 | 639 | assert(v.oidType == OidType.Json); 640 | assert(v.as!Json == j); 641 | 642 | auto nj = Nullable!Json(j); 643 | auto nv = nj.toValue; 644 | assert(nv.oidType == OidType.Json); 645 | assert(!nv.as!(Nullable!Json).isNull); 646 | assert(nv.as!(Nullable!Json).get == j); 647 | } 648 | 649 | unittest 650 | { 651 | import dpq2.conv.to_d_types : as; 652 | char[2] arr; 653 | auto v = arr.toValue(); 654 | assert(v.oidType == OidType.Text); 655 | assert(!v.isNull); 656 | 657 | auto varr = v.as!string; 658 | assert(varr.length == 2); 659 | } 660 | -------------------------------------------------------------------------------- /src/dpq2/conv/geometric.d: -------------------------------------------------------------------------------- 1 | /// 2 | module dpq2.conv.geometric; 3 | 4 | import dpq2.oids: OidType; 5 | import dpq2.value: ConvExceptionType, throwTypeComplaint, Value, ValueConvException, ValueFormat; 6 | import dpq2.conv.to_d_types: checkValue; 7 | import std.bitmanip: bigEndianToNative, nativeToBigEndian; 8 | import std.traits; 9 | import std.typecons : Nullable; 10 | import std.range.primitives: ElementType; 11 | 12 | @safe: 13 | 14 | private template GetRvalueOfMember(T, string memberName) 15 | { 16 | mixin("alias MemberType = typeof(T."~memberName~");"); 17 | 18 | static if(is(MemberType == function)) 19 | alias R = ReturnType!(MemberType); 20 | else 21 | alias R = MemberType; 22 | 23 | alias GetRvalueOfMember = R; 24 | } 25 | 26 | template isGeometricType(T) if(!is(T == Nullable!N, N)) 27 | { 28 | enum isGeometricType = 29 | isValidPointType!T 30 | || isValidLineType!T 31 | || isValidPathType!T 32 | || isValidPolygon!T 33 | || isValidCircleType!T 34 | || isValidLineSegmentType!T 35 | || isValidBoxType!T; 36 | } 37 | 38 | /// Checks that type have "x" and "y" members of returning type "double" 39 | template isValidPointType(T) 40 | { 41 | static if (is(T == Nullable!R, R)) enum isValidPointType = false; 42 | else static if(__traits(compiles, typeof(T.x)) && __traits(compiles, typeof(T.y))) 43 | { 44 | enum isValidPointType = 45 | is(GetRvalueOfMember!(T, "x") == double) && 46 | is(GetRvalueOfMember!(T, "y") == double); 47 | } 48 | else 49 | enum isValidPointType = false; 50 | } 51 | 52 | unittest 53 | { 54 | { 55 | struct PT {double x; double y;} 56 | assert(isValidPointType!PT); 57 | } 58 | 59 | { 60 | struct InvalidPT {double x;} 61 | assert(!isValidPointType!InvalidPT); 62 | } 63 | } 64 | 65 | /// Checks that type have "min" and "max" members of suitable returning type of point 66 | template isValidBoxType(T) 67 | { 68 | static if (is(T == Nullable!R, R)) enum isValidBoxType = false; 69 | else static if(__traits(compiles, typeof(T.min)) && __traits(compiles, typeof(T.max))) 70 | { 71 | enum isValidBoxType = 72 | isValidPointType!(GetRvalueOfMember!(T, "min")) && 73 | isValidPointType!(GetRvalueOfMember!(T, "max")); 74 | } 75 | else 76 | enum isValidBoxType = false; 77 | } 78 | 79 | template isValidLineType(T) 80 | { 81 | enum isValidLineType = is(T == Line); 82 | } 83 | 84 | template isValidPathType(T) 85 | { 86 | enum isValidPathType = isInstanceOf!(Path, T); 87 | } 88 | 89 | template isValidCircleType(T) 90 | { 91 | enum isValidCircleType = isInstanceOf!(Circle, T); 92 | } 93 | 94 | /// 95 | template isValidLineSegmentType(T) 96 | { 97 | static if (is(T == Nullable!R, R)) enum isValidLineSegmentType = false; 98 | else static if(__traits(compiles, typeof(T.start)) && __traits(compiles, typeof(T.end))) 99 | { 100 | enum isValidLineSegmentType = 101 | isValidPointType!(GetRvalueOfMember!(T, "start")) && 102 | isValidPointType!(GetRvalueOfMember!(T, "end")); 103 | } 104 | else 105 | enum isValidLineSegmentType = false; 106 | } 107 | 108 | /// 109 | template isValidPolygon(T) 110 | { 111 | static if (is(T == Nullable!R, R)) 112 | enum isValidPolygon = false; 113 | else 114 | enum isValidPolygon = isArray!T && isValidPointType!(ElementType!T); 115 | } 116 | 117 | unittest 118 | { 119 | struct PT {double x; double y;} 120 | assert(isValidPolygon!(PT[])); 121 | assert(!isValidPolygon!(PT)); 122 | } 123 | 124 | private auto serializePoint(Vec2Ddouble, T)(Vec2Ddouble point, T target) 125 | if(isValidPointType!Vec2Ddouble) 126 | { 127 | import std.algorithm : copy; 128 | 129 | auto rem = point.x.nativeToBigEndian[0 .. $].copy(target); 130 | rem = point.y.nativeToBigEndian[0 .. $].copy(rem); 131 | 132 | return rem; 133 | } 134 | 135 | Value toValue(Vec2Ddouble)(Vec2Ddouble pt) 136 | if(isValidPointType!Vec2Ddouble) 137 | { 138 | ubyte[] data = new ubyte[16]; 139 | pt.serializePoint(data); 140 | 141 | return createValue(data, OidType.Point); 142 | } 143 | 144 | private auto serializeBox(Box, T)(Box box, T target) 145 | { 146 | auto rem = box.max.serializePoint(target); 147 | rem = box.min.serializePoint(rem); 148 | 149 | return rem; 150 | } 151 | 152 | Value toValue(Box)(Box box) 153 | if(isValidBoxType!Box) 154 | { 155 | ubyte[] data = new ubyte[32]; 156 | box.serializeBox(data); 157 | 158 | return createValue(data, OidType.Box); 159 | } 160 | 161 | /// Infinite line - {A,B,C} (Ax + By + C = 0) 162 | struct Line 163 | { 164 | double a; /// 165 | double b; /// 166 | double c; /// 167 | } 168 | 169 | /// 170 | struct Path(Point) 171 | if(isValidPointType!Point) 172 | { 173 | bool isClosed; /// 174 | Point[] points; /// 175 | } 176 | 177 | /// 178 | struct Circle(Point) 179 | if(isValidPointType!Point) 180 | { 181 | Point center; /// 182 | double radius; /// 183 | } 184 | 185 | Value toValue(T)(T line) 186 | if(isValidLineType!T) 187 | { 188 | import std.algorithm : copy; 189 | 190 | ubyte[] data = new ubyte[24]; 191 | 192 | auto rem = line.a.nativeToBigEndian[0 .. $].copy(data); 193 | rem = line.b.nativeToBigEndian[0 .. $].copy(rem); 194 | rem = line.c.nativeToBigEndian[0 .. $].copy(rem); 195 | 196 | return createValue(data, OidType.Line); 197 | } 198 | 199 | Value toValue(LineSegment)(LineSegment lseg) 200 | if(isValidLineSegmentType!LineSegment) 201 | { 202 | ubyte[] data = new ubyte[32]; 203 | 204 | auto rem = lseg.start.serializePoint(data); 205 | rem = lseg.end.serializePoint(rem); 206 | 207 | return createValue(data, OidType.LineSegment); 208 | } 209 | 210 | Value toValue(T)(T path) 211 | if(isValidPathType!T) 212 | { 213 | import std.algorithm : copy; 214 | 215 | if(path.points.length < 1) 216 | throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH, 217 | "At least one point is needed for Path", __FILE__, __LINE__); 218 | 219 | ubyte[] data = new ubyte[path.points.length * 16 + 5]; 220 | 221 | ubyte isClosed = path.isClosed ? 1 : 0; 222 | auto rem = [isClosed].copy(data); 223 | rem = (cast(int) path.points.length).nativeToBigEndian[0 .. $].copy(rem); 224 | 225 | foreach (ref p; path.points) 226 | { 227 | rem = p.serializePoint(rem); 228 | } 229 | 230 | return createValue(data, OidType.Path); 231 | } 232 | 233 | Value toValue(Polygon)(Polygon poly) 234 | if(isValidPolygon!Polygon) 235 | { 236 | import std.algorithm : copy; 237 | 238 | if(poly.length < 1) 239 | throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH, 240 | "At least one point is needed for Polygon", __FILE__, __LINE__); 241 | 242 | ubyte[] data = new ubyte[poly.length * 16 + 4]; 243 | auto rem = (cast(int)poly.length).nativeToBigEndian[0 .. $].copy(data); 244 | 245 | foreach (ref p; poly) 246 | rem = p.serializePoint(rem); 247 | 248 | return createValue(data, OidType.Polygon); 249 | } 250 | 251 | Value toValue(T)(T c) 252 | if(isValidCircleType!T) 253 | { 254 | import std.algorithm : copy; 255 | 256 | ubyte[] data = new ubyte[24]; 257 | auto rem = c.center.serializePoint(data); 258 | c.radius.nativeToBigEndian[0 .. $].copy(rem); 259 | 260 | return createValue(data, OidType.Circle); 261 | } 262 | 263 | /// Caller must ensure that reference to the data will not be passed to elsewhere 264 | private Value createValue(const ubyte[] data, OidType oid) pure @trusted 265 | { 266 | return Value(cast(immutable) data, oid); 267 | } 268 | 269 | private alias AE = ValueConvException; 270 | private alias ET = ConvExceptionType; 271 | 272 | /// Convert to Point 273 | Vec2Ddouble binaryValueAs(Vec2Ddouble)(in Value v) 274 | if(isValidPointType!Vec2Ddouble) 275 | { 276 | v.checkValue(OidType.Point, 16, "Point"); 277 | 278 | return pointFromBytes!Vec2Ddouble(v.data[0..16]); 279 | } 280 | 281 | private Vec2Ddouble pointFromBytes(Vec2Ddouble)(in ubyte[16] data) pure 282 | if(isValidPointType!Vec2Ddouble) 283 | { 284 | return Vec2Ddouble(data[0..8].bigEndianToNative!double, data[8..16].bigEndianToNative!double); 285 | } 286 | 287 | T binaryValueAs(T)(in Value v) 288 | if (is(T == Line)) 289 | { 290 | v.checkValue(OidType.Line, 24, "Line"); 291 | 292 | return Line((v.data[0..8].bigEndianToNative!double), v.data[8..16].bigEndianToNative!double, v.data[16..24].bigEndianToNative!double); 293 | } 294 | 295 | LineSegment binaryValueAs(LineSegment)(in Value v) 296 | if(isValidLineSegmentType!LineSegment) 297 | { 298 | v.checkValue(OidType.LineSegment, 32, "LineSegment"); 299 | 300 | alias Point = ReturnType!(LineSegment.start); 301 | 302 | auto start = v.data[0..16].pointFromBytes!Point; 303 | auto end = v.data[16..32].pointFromBytes!Point; 304 | 305 | return LineSegment(start, end); 306 | } 307 | 308 | Box binaryValueAs(Box)(in Value v) 309 | if(isValidBoxType!Box) 310 | { 311 | v.checkValue(OidType.Box, 32, "Box"); 312 | 313 | alias Point = typeof(Box.min); 314 | 315 | Box res; 316 | res.max = v.data[0..16].pointFromBytes!Point; 317 | res.min = v.data[16..32].pointFromBytes!Point; 318 | 319 | return res; 320 | } 321 | 322 | T binaryValueAs(T)(in Value v) 323 | if(isInstanceOf!(Path, T)) 324 | { 325 | import std.array : uninitializedArray; 326 | 327 | if(!(v.oidType == OidType.Path)) 328 | throwTypeComplaint(v.oidType, "Path", __FILE__, __LINE__); 329 | 330 | if(!((v.data.length - 5) % 16 == 0)) 331 | throw new AE(ET.SIZE_MISMATCH, 332 | "Value length isn't equal to Postgres Path size", __FILE__, __LINE__); 333 | 334 | T res; 335 | res.isClosed = v.data[0..1].bigEndianToNative!byte == 1; 336 | int len = v.data[1..5].bigEndianToNative!int; 337 | 338 | if (len != (v.data.length - 5)/16) 339 | throw new AE(ET.SIZE_MISMATCH, "Path points number mismatch", __FILE__, __LINE__); 340 | 341 | alias Point = typeof(T.points[0]); 342 | 343 | res.points = uninitializedArray!(Point[])(len); 344 | for (int i=0; i a.isNull ? "null" : a.to!string).joiner(", "), "]").text; 80 | else return val.to!string; 81 | } 82 | 83 | // test string to native conversion 84 | params.sqlCommand = format("SELECT %s::%s as d_type_test_value", pgValue is null ? "NULL" : pgValue, pgType); 85 | params.args = null; 86 | auto answer = conn.execParams(params); 87 | immutable Value v = answer[0][0]; 88 | 89 | auto result = v.as!T; 90 | 91 | enum disabledForStdVariant = ( 92 | is(T == Nullable!string[]) || // Variant haven't heuristics to understand what array elements can contain NULLs 93 | is(T == Nullable!(int[])) || // Same reason, but here is all values are Nullable and thus incompatible for comparison with original values 94 | is(T == SysTime) || is(T == Nullable!SysTime) || // Can't be supported by toVariant because TimeStampWithZone converted to PGtimestamptz 95 | is(T == LineSegment) || // Impossible to support: LineSegment struct must be provided by user 96 | is(T == PGTestMoney) || // ditto 97 | is(T == BitArray) || //TODO: Format of the column (VariableBitString) doesn't supported by Value to Variant converter 98 | is(T == Nullable!BitArray) || // ditto 99 | is(T == Point) || // Impossible to support: LineSegment struct must be provided by user 100 | is(T == Nullable!Point) || // ditto 101 | is(T == Box) || // ditto 102 | is(T == TestPath) || // ditto 103 | is(T == Polygon) || // ditto 104 | is(T == TestCircle) // ditto 105 | ); 106 | 107 | static if(!disabledForStdVariant) 108 | { 109 | static if (is(T == Nullable!R, R)) 110 | auto stdVariantResult = v.as!(Variant, true); 111 | else 112 | auto stdVariantResult = v.as!(Variant, false); 113 | } 114 | 115 | string formatMsg(string varType) 116 | { 117 | return format( 118 | "PG to %s conv: received unexpected value\nreceived pgType=%s\nexpected nativeType=%s\nsent pgValue=%s\nexpected nativeValue=%s\nresult=%s", 119 | varType, v.oidType, typeid(T), pgValue, formatValue(nativeValue), formatValue(result) 120 | ); 121 | } 122 | 123 | static if(isArrayType!T) 124 | const bool assertResult = compareArraysWithCareAboutNullables(result, nativeValue); 125 | else 126 | { 127 | const bool assertResult = result == nativeValue; 128 | 129 | //Variant: 130 | static if(!disabledForStdVariant) 131 | { 132 | // Ignores "json as string" test case with Json sent natively as string 133 | if(!(is(T == string) && v.oidType == OidType.Json)) 134 | { 135 | assert(stdVariantResult == nativeValue, formatMsg("std.variant.Variant (type: %s)".format(stdVariantResult.type))); 136 | } 137 | } 138 | } 139 | 140 | assert(assertResult, formatMsg("native")); 141 | 142 | { 143 | // test binary to text conversion 144 | params.sqlCommand = "SELECT $1::text"; 145 | params.args = [toValue(nativeValue)]; 146 | 147 | auto answer2 = conn.execParams(params); 148 | auto v2 = answer2[0][0]; 149 | 150 | string textResult = v2.isNull 151 | ? "NULL" 152 | : v2.as!string.strip(' '); 153 | 154 | pgValue = pgValue.strip('\''); 155 | 156 | // Special cases: 157 | static if(is(T == PGbytea)) 158 | pgValue = `\x442072756c65730021`; // Server formats its reply slightly different from the passed argument 159 | 160 | static if(is(T == Json)) 161 | { 162 | // Reformatting by same way in the hope that the data will be sorted same in both cases 163 | pgValue = pgValue.parseJsonString.toString; 164 | textResult = textResult.parseJsonString.toString; 165 | } 166 | 167 | assert(textResult == pgValue, 168 | format("Native to PG conv: received unexpected value\nreceived pgType=%s\nsent nativeType=%s\nsent nativeValue=%s\nexpected pgValue=%s\nresult=%s\nexpectedRepresentation=%s\nreceivedRepresentation=%s", 169 | v.oidType, typeid(T), formatValue(nativeValue), pgValue, textResult, pgValue.representation, textResult.representation) 170 | ); 171 | } 172 | } 173 | 174 | alias C = testIt; // "C" means "case" 175 | 176 | import dpq2.conv.to_d_types: PGTestMoney; 177 | 178 | C!PGboolean(true, "boolean", "true"); 179 | C!PGboolean(false, "boolean", "false"); 180 | C!(Nullable!PGboolean)(Nullable!PGboolean.init, "boolean", "NULL"); 181 | C!(Nullable!PGboolean)(Nullable!PGboolean(true), "boolean", "true"); 182 | C!PGsmallint(-32_761, "smallint", "-32761"); 183 | C!PGinteger(-2_147_483_646, "integer", "-2147483646"); 184 | C!PGbigint(-9_223_372_036_854_775_806, "bigint", "-9223372036854775806"); 185 | C!PGTestMoney(PGTestMoney(-123.45), "money", "'-$123.45'"); 186 | C!PGreal(-12.3456f, "real", "-12.3456"); 187 | C!PGdouble_precision(-1234.56789012345, "double precision", "-1234.56789012345"); 188 | C!PGtext("first line\nsecond line", "text", "'first line\nsecond line'"); 189 | C!PGtext("12345 ", "char(6)", "'12345'"); 190 | C!PGtext("12345", "varchar(6)", "'12345'"); 191 | C!(Nullable!PGtext)(Nullable!PGtext.init, "text", "NULL"); 192 | C!PGbytea([0x44, 0x20, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x00, 0x21], 193 | "bytea", r"E'\\x44 20 72 75 6c 65 73 00 21'"); // "D rules\x00!" (ASCII) 194 | C!PGuuid(UUID("8b9ab33a-96e9-499b-9c36-aad1fe86d640"), "uuid", "'8b9ab33a-96e9-499b-9c36-aad1fe86d640'"); 195 | C!(Nullable!PGuuid)(Nullable!UUID(UUID("8b9ab33a-96e9-499b-9c36-aad1fe86d640")), "uuid", "'8b9ab33a-96e9-499b-9c36-aad1fe86d640'"); 196 | C!PGvarbit(BitArray([1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1]), "varbit", "'101011010110101'"); 197 | C!PGvarbit(BitArray([0, 0, 1, 0, 1]), "varbit", "'00101'"); 198 | C!PGvarbit(BitArray([1, 0, 1, 0, 0]), "varbit", "'10100'"); 199 | C!(Nullable!PGvarbit)(Nullable!PGvarbit.init, "varbit", "NULL"); 200 | 201 | // numeric testing 202 | C!PGnumeric("NaN", "numeric", "'NaN'"); 203 | 204 | const string[] numericTests = [ 205 | "42", 206 | "-42", 207 | "0", 208 | "0.0146328", 209 | "0.0007", 210 | "0.007", 211 | "0.07", 212 | "0.7", 213 | "7", 214 | "70", 215 | "700", 216 | "7000", 217 | "70000", 218 | 219 | "7.0", 220 | "70.0", 221 | "700.0", 222 | "7000.0", 223 | "70000.000", 224 | 225 | "2354877787627192443", 226 | "2354877787627192443.0", 227 | "2354877787627192443.00000", 228 | "-2354877787627192443.00000" 229 | ]; 230 | 231 | foreach(i, s; numericTests) 232 | C!PGnumeric(s, "numeric", s); 233 | 234 | // date and time testing 235 | C!PGdate(Date(2016, 01, 8), "date", "'2016-01-08'"); 236 | { 237 | import std.exception : assertThrown; 238 | 239 | assertThrown!ValueConvException( 240 | C!PGdate(Date(0001, 01, 8), "date", "'5874897-12-31'") 241 | ); 242 | } 243 | C!PGtime_without_time_zone(TimeOfDay(12, 34, 56), "time without time zone", "'12:34:56'"); 244 | C!PGtime_with_time_zone(PGtime_with_time_zone(TimeOfDay(12, 34, 56), 3600 * 5), "time with time zone", "'12:34:56-05'"); 245 | C!PGinterval(PGinterval(-123), "interval", "'-00:00:00.000123'"); 246 | C!PGinterval(PGinterval(7200_000_000 + 123), "interval", "'02:00:00.000123'"); 247 | C!PGinterval(PGinterval(0, 2, 13), "interval", "'1 year 1 mon 2 days'"); 248 | C!PGinterval(PGinterval(0, 0, -1), "interval", "'-1 mons'"); 249 | C!PGinterval(PGinterval(0, -2, 1), "interval", "'1 mon -2 days'"); 250 | C!PGinterval(PGinterval(-123, -2, -1), "interval", "'-1 mons -2 days -00:00:00.000123'"); 251 | C!PGinterval(PGinterval(-(7200_000_000 + 123), 2, 177999999 * 12 + 3), "interval", "'177999999 years 3 mons 2 days -02:00:00.000123'"); 252 | C!PGtimestamp(PGtimestamp(DateTime(1997, 12, 17, 7, 37, 16), dur!"usecs"(12)), "timestamp without time zone", "'1997-12-17 07:37:16.000012'"); 253 | C!PGtimestamptz(PGtimestamptz(DateTime(1997, 12, 17, 5, 37, 16), dur!"usecs"(12)), "timestamp with time zone", "'1997-12-17 07:37:16.000012+02'"); 254 | C!PGtimestamp(PGtimestamp.earlier, "timestamp", "'-infinity'"); 255 | C!PGtimestamp(PGtimestamp.later, "timestamp", "'infinity'"); 256 | C!PGtimestamp(PGtimestamp.min, "timestamp", `'4713-01-01 00:00:00 BC'`); 257 | C!PGtimestamp(PGtimestamp.max, "timestamp", `'294276-12-31 23:59:59.999999'`); 258 | 259 | // SysTime testing 260 | auto testTZ = new immutable SimpleTimeZone(2.dur!"hours"); // custom TZ 261 | C!SysTime(SysTime(DateTime(1997, 12, 17, 7, 37, 16), dur!"usecs"(12), testTZ), "timestamptz", "'1997-12-17 07:37:16.000012+02'"); 262 | C!(Nullable!SysTime)(Nullable!SysTime(SysTime(DateTime(1997, 12, 17, 7, 37, 16), dur!"usecs"(12), testTZ)), "timestamptz", "'1997-12-17 07:37:16.000012+02'"); 263 | 264 | // inet 265 | const testInetAddr1 = InetAddress(new InternetAddress("127.0.0.1", InternetAddress.PORT_ANY), 9); 266 | C!InetAddress(testInetAddr1, "inet", `'127.0.0.1/9'`); 267 | const testInetAddr2 = InetAddress(new InternetAddress("127.0.0.1", InternetAddress.PORT_ANY)); 268 | C!InetAddress(testInetAddr2, "inet", `'127.0.0.1/32'`); 269 | const testInetAddr3 = VibeNetworkAddress(new InternetAddress("127.0.0.1", InternetAddress.PORT_ANY)).vibe2pg; 270 | C!InetAddress(testInetAddr3, "inet", `'127.0.0.1/32'`); 271 | 272 | // inet6 273 | const testInet6Addr1 = InetAddress(new Internet6Address("2::1", InternetAddress.PORT_ANY)); 274 | C!InetAddress(testInet6Addr1, "inet", `'2::1/128'`); 275 | const testInet6Addr2 = InetAddress(new Internet6Address("2001:0:130F::9C0:876A:130B", InternetAddress.PORT_ANY),24); 276 | C!InetAddress(testInet6Addr2, "inet", `'2001:0:130f::9c0:876a:130b/24'`); 277 | const testInet6Addr3 = VibeNetworkAddress(new Internet6Address("2001:0:130F::9C0:876A:130B", InternetAddress.PORT_ANY)).vibe2pg; 278 | C!InetAddress(testInet6Addr3, "inet", `'2001:0:130f::9c0:876a:130b/128'`); 279 | 280 | // cidr 281 | const testCidrAddr1 = CidrAddress(new InternetAddress("192.168.0.0", InternetAddress.PORT_ANY), 25); 282 | C!CidrAddress(testCidrAddr1, "cidr", `'192.168.0.0/25'`); 283 | const testCidrAddr2 = CidrAddress(new Internet6Address("::", InternetAddress.PORT_ANY), 64); 284 | C!CidrAddress(testCidrAddr2, "cidr", `'::/64'`); 285 | 286 | // json 287 | C!PGjson(Json(["float_value": Json(123.456), "text_str": Json("text string")]), "json", `'{"float_value": 123.456,"text_str": "text string"}'`); 288 | C!(Nullable!PGjson)(Nullable!Json(Json(["foo": Json("bar")])), "json", `'{"foo":"bar"}'`); 289 | 290 | // json as string 291 | C!string(`{"float_value": 123.456}`, "json", `'{"float_value": 123.456}'`); 292 | 293 | // jsonb 294 | C!PGjson(Json(["float_value": Json(123.456), "text_str": Json("text string"), "abc": Json(["key": Json("value")])]), "jsonb", 295 | `'{"float_value": 123.456, "text_str": "text string", "abc": {"key": "value"}}'`); 296 | 297 | // Geometric 298 | C!Point(Point(1,2), "point", "'(1,2)'"); 299 | C!PGline(Line(1,2,3), "line", "'{1,2,3}'"); 300 | C!LineSegment(LineSegment(Point(1,2), Point(3,4)), "lseg", "'[(1,2),(3,4)]'"); 301 | C!Box(Box(Point(1,2),Point(3,4)), "box", "'(3,4),(1,2)'"); // PG handles box ordered as upper right first and lower left next 302 | C!TestPath(TestPath(true, [Point(1,1), Point(2,2), Point(3,3)]), "path", "'((1,1),(2,2),(3,3))'"); 303 | C!TestPath(TestPath(false, [Point(1,1), Point(2,2), Point(3,3)]), "path", "'[(1,1),(2,2),(3,3)]'"); 304 | C!Polygon(([Point(1,1), Point(2,2), Point(3,3)]), "polygon", "'((1,1),(2,2),(3,3))'"); 305 | C!TestCircle(TestCircle(Point(1,2), 10), "circle", "'<(1,2),10>'"); 306 | C!(Nullable!Point)(Nullable!Point(Point(1,2)), "point", "'(1,2)'"); 307 | 308 | //Arrays 309 | C!(int[][])([[1,2],[3,4]], "int[]", "'{{1,2},{3,4}}'"); 310 | C!(int[])([], "int[]", "'{}'"); // empty array test 311 | C!((Nullable!string)[])([Nullable!string("foo"), Nullable!string.init], "text[]", "'{foo,NULL}'"); 312 | C!(string[])(["foo","bar", "baz"], "text[]", "'{foo,bar,baz}'"); 313 | C!(PGjson[])([Json(["foo": Json(42)])], "json[]", `'{"{\"foo\":42}"}'`); 314 | C!(PGuuid[])([UUID("8b9ab33a-96e9-499b-9c36-aad1fe86d640")], "uuid[]", "'{8b9ab33a-96e9-499b-9c36-aad1fe86d640}'"); 315 | C!(PGline[])([Line(1,2,3), Line(4,5,6)], "line[]", `'{"{1,2,3}","{4,5,6}"}'`); 316 | C!(PGtimestamp[])([PGtimestamp(DateTime(1997, 12, 17, 7, 37, 16), dur!"usecs"(12))], "timestamp[]", `'{"1997-12-17 07:37:16.000012"}'`); 317 | C!(InetAddress[])([testInetAddr1, testInet6Addr2], "inet[]", `'{127.0.0.1/9,2001:0:130f::9c0:876a:130b/24}'`); 318 | C!(Nullable!(int[]))(Nullable!(int[]).init, "int[]", "NULL"); 319 | C!(Nullable!(int[]))(Nullable!(int[])([1,2,3]), "int[]", "'{1,2,3}'"); 320 | } 321 | 322 | // test round-trip compound types 323 | { 324 | conn.exec("CREATE TYPE test_type AS (x int, y int)"); 325 | scope(exit) conn.exec("DROP TYPE test_type"); 326 | 327 | params.sqlCommand = "SELECT 'test_type'::regtype::oid"; 328 | OidType oid = cast(OidType)conn.execParams(params)[0][0].as!Oid; 329 | 330 | Value input = Value(toRecordValue([17.toValue, Nullable!int.init.toValue]).data, oid); 331 | 332 | params.sqlCommand = "SELECT $1::text"; 333 | params.args = [input]; 334 | Value v = conn.execParams(params)[0][0]; 335 | assert(v.as!string == `(17,)`, v.as!string); 336 | params.sqlCommand = "SELECT $1"; 337 | v = conn.execParams(params)[0][0]; 338 | assert(v.oidType == oid && v.data == input.data); 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /src/dpq2/conv/numeric.d: -------------------------------------------------------------------------------- 1 | /* 2 | * PostgreSQL numeric format 3 | * 4 | * Copyright: © 2014 DSoftOut 5 | * Authors: NCrashed 6 | */ 7 | module dpq2.conv.numeric; 8 | 9 | private pure // inner representation from libpq sources 10 | { 11 | alias NumericDigit = ushort; 12 | enum DEC_DIGITS = 4; 13 | enum NUMERIC_NEG = 0x4000; 14 | enum NUMERIC_NAN = 0xC000; 15 | 16 | struct NumericVar 17 | { 18 | int weight; 19 | int sign; 20 | int dscale; 21 | NumericDigit[] digits; 22 | } 23 | 24 | string numeric_out(in NumericVar num) 25 | { 26 | string str; 27 | 28 | if(num.sign == NUMERIC_NAN) 29 | { 30 | return "NaN"; 31 | } 32 | 33 | str = get_str_from_var(num); 34 | 35 | return str; 36 | } 37 | 38 | /* 39 | * get_str_from_var() - 40 | * 41 | * Convert a var to text representation (guts of numeric_out). 42 | * The var is displayed to the number of digits indicated by its dscale. 43 | * Returns a palloc'd string. 44 | */ 45 | string get_str_from_var(in NumericVar var) 46 | { 47 | int dscale; 48 | ubyte[] str; 49 | ubyte* cp; 50 | ubyte* endcp; 51 | int i; 52 | int d; 53 | NumericDigit dig; 54 | 55 | static if(DEC_DIGITS > 1) 56 | { 57 | NumericDigit d1; 58 | } 59 | 60 | dscale = var.dscale; 61 | 62 | /* 63 | * Allocate space for the result. 64 | * 65 | * i is set to the # of decimal digits before decimal point. dscale is the 66 | * # of decimal digits we will print after decimal point. We may generate 67 | * as many as DEC_DIGITS-1 excess digits at the end, and in addition we 68 | * need room for sign, decimal point, null terminator. 69 | */ 70 | i = (var.weight + 1) * DEC_DIGITS; 71 | if (i <= 0) 72 | i = 1; 73 | 74 | str = new ubyte[i + dscale + DEC_DIGITS + 2]; 75 | cp = str.ptr; 76 | 77 | /* 78 | * Output a dash for negative values 79 | */ 80 | if (var.sign == NUMERIC_NEG) 81 | *cp++ = '-'; 82 | 83 | /* 84 | * Output all digits before the decimal point 85 | */ 86 | if (var.weight < 0) 87 | { 88 | d = var.weight + 1; 89 | *cp++ = '0'; 90 | } 91 | else 92 | { 93 | for (d = 0; d <= var.weight; d++) 94 | { 95 | dig = (d < var.digits.length) ? var.digits[d] : 0; 96 | /* In the first digit, suppress extra leading decimal zeroes */ 97 | static if(DEC_DIGITS == 4) 98 | { 99 | bool putit = (d > 0); 100 | 101 | d1 = dig / 1000; 102 | dig -= d1 * 1000; 103 | putit |= (d1 > 0); 104 | if (putit) 105 | *cp++ = cast(char)(d1 + '0'); 106 | d1 = dig / 100; 107 | dig -= d1 * 100; 108 | putit |= (d1 > 0); 109 | if (putit) 110 | *cp++ = cast(char)(d1 + '0'); 111 | d1 = dig / 10; 112 | dig -= d1 * 10; 113 | putit |= (d1 > 0); 114 | if (putit) 115 | *cp++ = cast(char)(d1 + '0'); 116 | *cp++ = cast(char)(dig + '0'); 117 | } 118 | else static if(DEC_DIGITS == 2) 119 | { 120 | d1 = dig / 10; 121 | dig -= d1 * 10; 122 | if (d1 > 0 || d > 0) 123 | *cp++ = cast(char)(d1 + '0'); 124 | *cp++ = cast(char)(dig + '0'); 125 | } 126 | else static if(DEC_DIGITS == 1) 127 | { 128 | *cp++ = cast(char)(dig + '0'); 129 | } 130 | else pragma(error, "unsupported NBASE"); 131 | } 132 | } 133 | 134 | /* 135 | * If requested, output a decimal point and all the digits that follow it. 136 | * We initially put out a multiple of DEC_DIGITS digits, then truncate if 137 | * needed. 138 | */ 139 | if (dscale > 0) 140 | { 141 | *cp++ = '.'; 142 | endcp = cp + dscale; 143 | for (i = 0; i < dscale; d++, i += DEC_DIGITS) 144 | { 145 | dig = (d >= 0 && d < var.digits.length) ? var.digits[d] : 0; 146 | static if(DEC_DIGITS == 4) 147 | { 148 | d1 = dig / 1000; 149 | dig -= d1 * 1000; 150 | *cp++ = cast(char)(d1 + '0'); 151 | d1 = dig / 100; 152 | dig -= d1 * 100; 153 | *cp++ = cast(char)(d1 + '0'); 154 | d1 = dig / 10; 155 | dig -= d1 * 10; 156 | *cp++ = cast(char)(d1 + '0'); 157 | *cp++ = cast(char)(dig + '0'); 158 | } 159 | else static if(DEC_DIGITS == 2) 160 | { 161 | d1 = dig / 10; 162 | dig -= d1 * 10; 163 | *cp++ = cast(char)(d1 + '0'); 164 | *cp++ = cast(char)(dig + '0'); 165 | } 166 | else static if(DEC_DIGITS == 1) 167 | { 168 | *cp++ = cast(char)(dig + '0'); 169 | } 170 | else pragma(error, "unsupported NBASE"); 171 | } 172 | cp = endcp; 173 | } 174 | 175 | /* 176 | * terminate the string and return it 177 | */ 178 | *cp = '\0'; 179 | 180 | return (cast(char*) str).fromStringz; 181 | } 182 | } 183 | 184 | import std.conv: to; 185 | import std.string: fromStringz; 186 | import std.bitmanip: bigEndianToNative; 187 | 188 | package string rawValueToNumeric(in ubyte[] v) pure 189 | { 190 | import dpq2.result: ValueConvException, ConvExceptionType; 191 | 192 | struct NumericVar_net // network byte order 193 | { 194 | ubyte[2] num; // num of digits 195 | ubyte[2] weight; 196 | ubyte[2] sign; 197 | ubyte[2] dscale; 198 | } 199 | 200 | if(!(v.length >= NumericVar_net.sizeof)) 201 | throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH, 202 | "Value length ("~to!string(v.length)~") less than it is possible for numeric type", 203 | __FILE__, __LINE__); 204 | 205 | NumericVar_net* h = cast(NumericVar_net*) v.ptr; 206 | 207 | NumericVar res; 208 | res.weight = bigEndianToNative!short(h.weight); 209 | res.sign = bigEndianToNative!ushort(h.sign); 210 | res.dscale = bigEndianToNative!ushort(h.dscale); 211 | 212 | auto len = (v.length - NumericVar_net.sizeof) / NumericDigit.sizeof; 213 | 214 | res.digits = new NumericDigit[len]; 215 | 216 | size_t offset = NumericVar_net.sizeof; 217 | foreach(i; 0 .. len) 218 | { 219 | res.digits[i] = bigEndianToNative!NumericDigit( 220 | (&(v[offset]))[0..NumericDigit.sizeof] 221 | ); 222 | offset += NumericDigit.sizeof; 223 | } 224 | 225 | return numeric_out(res); 226 | } 227 | -------------------------------------------------------------------------------- /src/dpq2/conv/time.d: -------------------------------------------------------------------------------- 1 | /** 2 | * PostgreSQL time types binary format. 3 | * 4 | * Copyright: © 2014 DSoftOut 5 | * Authors: NCrashed 6 | */ 7 | module dpq2.conv.time; 8 | 9 | @safe: 10 | 11 | import dpq2.result; 12 | import dpq2.oids: OidType; 13 | import dpq2.conv.to_d_types: checkValue; 14 | 15 | import core.time; 16 | import std.datetime.date : Date, DateTime, TimeOfDay; 17 | import std.datetime.systime: SysTime; 18 | import std.datetime.timezone: LocalTime, TimeZone, UTC; 19 | import std.bitmanip: bigEndianToNative, nativeToBigEndian; 20 | import std.math; 21 | import std.conv: to; 22 | 23 | /++ 24 | Returns value timestamp with time zone as SysTime 25 | 26 | Note that SysTime has a precision in hnsecs and PG TimeStamp in usecs. 27 | It means that PG value will have 10 times lower precision. 28 | And as both types are using long for internal storage it also means that PG TimeStamp can store greater range of values than SysTime. 29 | 30 | Because of these differences, it can happen that database value will not fit to the SysTime range of values. 31 | +/ 32 | SysTime binaryValueAs(T)(in Value v) @trusted 33 | if( is( T == SysTime ) ) 34 | { 35 | v.checkValue(OidType.TimeStampWithZone, long.sizeof, "timestamp with time zone"); 36 | 37 | auto t = rawTimeStamp2nativeTime!TimeStampUTC(bigEndianToNative!long(v.data.ptr[0..long.sizeof])); 38 | return SysTime(t.dateTime, t.fracSec, UTC()); 39 | } 40 | 41 | pure: 42 | 43 | /// Returns value data as native Date 44 | Date binaryValueAs(T)(in Value v) @trusted 45 | if( is( T == Date ) ) 46 | { 47 | v.checkValue(OidType.Date, uint.sizeof, "date type"); 48 | 49 | int jd = bigEndianToNative!uint(v.data.ptr[0..uint.sizeof]); 50 | int year, month, day; 51 | j2date(jd, year, month, day); 52 | 53 | // TODO: support PG Date like TTimeStamp manner and remove this check 54 | if(year > short.max) 55 | throw new ValueConvException(ConvExceptionType.DATE_VALUE_OVERFLOW, 56 | "Year "~year.to!string~" is bigger than supported by std.datetime.Date", __FILE__, __LINE__); 57 | 58 | return Date(year, month, day); 59 | } 60 | 61 | /// Returns value time without time zone as native TimeOfDay 62 | TimeOfDay binaryValueAs(T)(in Value v) @trusted 63 | if( is( T == TimeOfDay ) ) 64 | { 65 | v.checkValue(OidType.Time, TimeADT.sizeof, "time without time zone"); 66 | 67 | return time2tm(bigEndianToNative!TimeADT(v.data.ptr[0..TimeADT.sizeof])); 68 | } 69 | 70 | /// Returns value timestamp without time zone as TimeStamp 71 | TimeStamp binaryValueAs(T)(in Value v) @trusted 72 | if( is( T == TimeStamp ) ) 73 | { 74 | v.checkValue(OidType.TimeStamp, long.sizeof, "timestamp without time zone"); 75 | 76 | return rawTimeStamp2nativeTime!TimeStamp( 77 | bigEndianToNative!long(v.data.ptr[0..long.sizeof]) 78 | ); 79 | } 80 | 81 | /// Returns value timestamp with time zone as TimeStampUTC 82 | TimeStampUTC binaryValueAs(T)(in Value v) @trusted 83 | if( is( T == TimeStampUTC ) ) 84 | { 85 | v.checkValue(OidType.TimeStampWithZone, long.sizeof, "timestamp with time zone"); 86 | 87 | return rawTimeStamp2nativeTime!TimeStampUTC( 88 | bigEndianToNative!long(v.data.ptr[0..long.sizeof]) 89 | ); 90 | } 91 | 92 | /// Returns value timestamp without time zone as DateTime (it drops the fracSecs from the database value) 93 | DateTime binaryValueAs(T)(in Value v) @trusted 94 | if( is( T == DateTime ) ) 95 | { 96 | return v.binaryValueAs!TimeStamp.dateTime; 97 | } 98 | 99 | /// 100 | enum InfinityState : byte 101 | { 102 | NONE = 0, /// 103 | INFINITY_MIN = -1, /// 104 | INFINITY_MAX = 1, /// 105 | } 106 | 107 | /// 108 | struct PgDate 109 | { 110 | int year; /// 111 | ubyte month; /// 112 | ubyte day; /// 113 | 114 | /// '-infinity', earlier than all other dates 115 | static PgDate earlier() pure { return PgDate(int.min, 0, 0); } 116 | 117 | /// 'infinity', later than all other dates 118 | static PgDate later() pure { return PgDate(int.max, 0, 0); } 119 | 120 | bool isEarlier() const pure { return year == earlier.year; } /// '-infinity' 121 | bool isLater() const pure { return year == later.year; } /// 'infinity' 122 | } 123 | 124 | /// 125 | static toPgDate(Date d) pure 126 | { 127 | return PgDate(d.year, d.month, d.day); 128 | } 129 | 130 | /++ 131 | Structure to represent PostgreSQL Timestamp with/without time zone 132 | +/ 133 | struct TTimeStamp(bool isWithTZ) 134 | { 135 | /** 136 | * Date and time of TimeStamp 137 | * 138 | * If value is '-infinity' or '+infinity' it will be equal PgDate.min or PgDate.max 139 | */ 140 | PgDate date; 141 | TimeOfDay time; /// 142 | Duration fracSec; /// fractional seconds, 1 microsecond resolution 143 | 144 | /// 145 | this(DateTime dt, Duration fractionalSeconds = Duration.zero) pure 146 | { 147 | this(dt.date.toPgDate, dt.timeOfDay, fractionalSeconds); 148 | } 149 | 150 | /// 151 | this(PgDate d, TimeOfDay t = TimeOfDay(), Duration fractionalSeconds = Duration.zero) pure 152 | { 153 | date = d; 154 | time = t; 155 | fracSec = fractionalSeconds; 156 | } 157 | 158 | /// 159 | void throwIfNotFitsToDate() const 160 | { 161 | if(date.year > short.max) 162 | throw new ValueConvException(ConvExceptionType.DATE_VALUE_OVERFLOW, 163 | "Year "~date.year.to!string~" is bigger than supported by std.datetime", __FILE__, __LINE__); 164 | } 165 | 166 | /// 167 | DateTime dateTime() const pure 168 | { 169 | if(infinity != InfinityState.NONE) 170 | throw new ValueConvException(ConvExceptionType.DATE_VALUE_OVERFLOW, 171 | "TTimeStamp value is "~infinity.to!string, __FILE__, __LINE__); 172 | 173 | throwIfNotFitsToDate(); 174 | 175 | return DateTime(Date(date.year, date.month, date.day), time); 176 | } 177 | 178 | invariant() 179 | { 180 | assert(fracSec < 1.seconds, "fracSec can't be more than 1 second but contains "~fracSec.to!string); 181 | assert(fracSec >= Duration.zero, "fracSec is negative: "~fracSec.to!string); 182 | assert(!fracSec.isNegative, "fracSec is negative"); 183 | } 184 | 185 | bool isEarlier() const pure { return date.isEarlier; } /// '-infinity' 186 | bool isLater() const pure { return date.isLater; } /// 'infinity' 187 | 188 | /// Returns infinity state 189 | InfinityState infinity() const pure 190 | { 191 | with(InfinityState) 192 | { 193 | if(isEarlier) return INFINITY_MIN; 194 | if(isLater) return INFINITY_MAX; 195 | 196 | return NONE; 197 | } 198 | } 199 | 200 | unittest 201 | { 202 | assert(TTimeStamp.min == TTimeStamp.min); 203 | assert(TTimeStamp.max == TTimeStamp.max); 204 | assert(TTimeStamp.min != TTimeStamp.max); 205 | 206 | assert(TTimeStamp.earlier != TTimeStamp.later); 207 | assert(TTimeStamp.min != TTimeStamp.earlier); 208 | assert(TTimeStamp.max != TTimeStamp.later); 209 | 210 | assert(TTimeStamp.min.infinity == InfinityState.NONE); 211 | assert(TTimeStamp.max.infinity == InfinityState.NONE); 212 | assert(TTimeStamp.earlier.infinity == InfinityState.INFINITY_MIN); 213 | assert(TTimeStamp.later.infinity == InfinityState.INFINITY_MAX); 214 | } 215 | 216 | /// Returns the TimeStamp farthest in the past which is representable by TimeStamp. 217 | static immutable(TTimeStamp) min() 218 | { 219 | /* 220 | Postgres low value is 4713 BC but here is used -4712 because 221 | "Date uses the Proleptic Gregorian Calendar, so it assumes the 222 | Gregorian leap year calculations for its entire length. As per 223 | ISO 8601, it treats 1 B.C. as year 0, i.e. 1 B.C. is 0, 2 B.C. 224 | is -1, etc." (Phobos docs). But Postgres isn't uses ISO 8601 225 | for date calculation. 226 | */ 227 | return TTimeStamp(PgDate(-4712, 1, 1), TimeOfDay.min, Duration.zero); 228 | } 229 | 230 | /// Returns the TimeStamp farthest in the future which is representable by TimeStamp. 231 | static immutable(TTimeStamp) max() 232 | { 233 | enum maxFract = 1.seconds - 1.usecs; 234 | 235 | return TTimeStamp(PgDate(294276, 12, 31), TimeOfDay(23, 59, 59), maxFract); 236 | } 237 | 238 | /// '-infinity', earlier than all other time stamps 239 | static immutable(TTimeStamp) earlier() pure { return TTimeStamp(PgDate.earlier); } 240 | 241 | /// 'infinity', later than all other time stamps 242 | static immutable(TTimeStamp) later() pure { return TTimeStamp(PgDate.later); } 243 | 244 | /// 245 | string toString() const 246 | { 247 | import std.format; 248 | 249 | return format("%04d-%02d-%02d %s %s", date.year, date.month, date.day, time, fracSec.toString); 250 | } 251 | } 252 | 253 | alias TimeStamp = TTimeStamp!false; /// Unknown TZ timestamp 254 | alias TimeStampUTC = TTimeStamp!true; /// Assumed that this is UTC timestamp 255 | 256 | unittest 257 | { 258 | auto t = TimeStamp(DateTime(2017, 11, 13, 14, 29, 17), 75_678.usecs); 259 | assert(t.dateTime.hour == 14); 260 | } 261 | 262 | unittest 263 | { 264 | auto dt = DateTime(2017, 11, 13, 14, 29, 17); 265 | auto t = TimeStamp(dt, 75_678.usecs); 266 | 267 | assert(t.dateTime == dt); // test the implicit conversion to DateTime 268 | } 269 | 270 | unittest 271 | { 272 | auto t = TimeStampUTC( 273 | DateTime(2017, 11, 13, 14, 29, 17), 274 | 75_678.usecs 275 | ); 276 | 277 | assert(t.dateTime.hour == 14); 278 | assert(t.fracSec == 75_678.usecs); 279 | } 280 | 281 | unittest 282 | { 283 | import std.exception : assertThrown; 284 | 285 | auto e = TimeStampUTC.earlier; 286 | auto l = TimeStampUTC.later; 287 | 288 | assertThrown!ValueConvException(e.dateTime.hour == 14); 289 | assertThrown!ValueConvException(l.dateTime.hour == 14); 290 | } 291 | 292 | /// Oid tests 293 | unittest 294 | { 295 | assert(detectOidTypeFromNative!TimeStamp == OidType.TimeStamp); 296 | assert(detectOidTypeFromNative!TimeStampUTC == OidType.TimeStampWithZone); 297 | assert(detectOidTypeFromNative!SysTime == OidType.TimeStampWithZone); 298 | assert(detectOidTypeFromNative!Date == OidType.Date); 299 | assert(detectOidTypeFromNative!TimeOfDay == OidType.Time); 300 | } 301 | 302 | /// 303 | struct TimeOfDayWithTZ 304 | { 305 | TimeOfDay time; /// 306 | TimeTZ tzSec; /// Time zone offset from UTC in seconds with east of UTC being negative 307 | } 308 | 309 | /// Returns value time with time zone as TimeOfDayWithTZ 310 | TimeOfDayWithTZ binaryValueAs(T)(in Value v) @trusted 311 | if( is( T == TimeOfDayWithTZ ) ) 312 | { 313 | enum recSize = TimeADT.sizeof + TimeTZ.sizeof; 314 | static assert(recSize == 12); 315 | 316 | v.checkValue(OidType.TimeWithZone, recSize, "time with time zone"); 317 | 318 | return TimeOfDayWithTZ( 319 | time2tm(bigEndianToNative!TimeADT(v.data.ptr[0 .. TimeADT.sizeof])), 320 | bigEndianToNative!TimeTZ(v.data.ptr[TimeADT.sizeof .. recSize]) 321 | ); 322 | } 323 | 324 | /// 325 | struct Interval 326 | { 327 | long usecs; /// All time units less than days 328 | int days; /// Days, after time for alignment. Sign is ignored by PG server if usecs == 0 329 | int months; /// Ditto, after time for alignment. Sign is ignored by PG server if usecs == 0 and days == 0 330 | } 331 | 332 | /// Returns value time with time zone as Interval 333 | Interval binaryValueAs(T)(in Value v) @trusted 334 | if( is( T == Interval ) ) 335 | { 336 | v.checkValue(OidType.TimeInterval, long.sizeof * 2, "interval"); 337 | 338 | return Interval( 339 | bigEndianToNative!long(v.data.ptr[0 .. 8]), 340 | bigEndianToNative!int(v.data.ptr[8 .. 12]), 341 | bigEndianToNative!int(v.data.ptr[12 .. 16]) 342 | ); 343 | } 344 | 345 | package enum POSTGRES_EPOCH_DATE = Date(2000, 1, 1); 346 | package enum POSTGRES_EPOCH_JDATE = POSTGRES_EPOCH_DATE.julianDay; 347 | static assert(POSTGRES_EPOCH_JDATE == 2_451_545); // value from Postgres code 348 | 349 | private: 350 | 351 | T rawTimeStamp2nativeTime(T)(long raw) 352 | if(is(T == TimeStamp) || is(T == TimeStampUTC)) 353 | { 354 | import core.stdc.time: time_t; 355 | 356 | if(raw == long.max) return T.later; // infinity 357 | if(raw == long.min) return T.earlier; // -infinity 358 | 359 | pg_tm tm; 360 | fsec_t ts; 361 | 362 | if(timestamp2tm(raw, tm, ts) < 0) 363 | throw new ValueConvException( 364 | ConvExceptionType.OUT_OF_RANGE, "Timestamp is out of range", 365 | ); 366 | 367 | TimeStamp ret = raw_pg_tm2nativeTime(tm, ts); 368 | 369 | static if(is(T == TimeStamp)) 370 | return ret; 371 | else 372 | return TimeStampUTC(ret.dateTime, ret.fracSec); 373 | } 374 | 375 | TimeStamp raw_pg_tm2nativeTime(pg_tm tm, fsec_t ts) 376 | { 377 | return TimeStamp( 378 | PgDate( 379 | tm.tm_year, 380 | cast(ubyte) tm.tm_mon, 381 | cast(ubyte) tm.tm_mday 382 | ), 383 | TimeOfDay( 384 | tm.tm_hour, 385 | tm.tm_min, 386 | tm.tm_sec 387 | ), 388 | ts.dur!"usecs" 389 | ); 390 | } 391 | 392 | // Here is used names from the original Postgresql source 393 | 394 | void j2date(int jd, out int year, out int month, out int day) 395 | { 396 | enum MONTHS_PER_YEAR = 12; 397 | 398 | jd += POSTGRES_EPOCH_JDATE; 399 | 400 | uint julian = jd + 32044; 401 | uint quad = julian / 146097; 402 | uint extra = (julian - quad * 146097) * 4 + 3; 403 | julian += 60 + quad * 3 + extra / 146097; 404 | quad = julian / 1461; 405 | julian -= quad * 1461; 406 | int y = julian * 4 / 1461; 407 | julian = ((y != 0) ? ((julian + 305) % 365) : ((julian + 306) % 366)) 408 | + 123; 409 | year = (y+ quad * 4) - 4800; 410 | quad = julian * 2141 / 65536; 411 | day = julian - 7834 * quad / 256; 412 | month = (quad + 10) % MONTHS_PER_YEAR + 1; 413 | } 414 | 415 | private alias long Timestamp; 416 | private alias long TimestampTz; 417 | private alias long TimeADT; 418 | private alias int TimeTZ; 419 | private alias long TimeOffset; 420 | private alias int fsec_t; /* fractional seconds (in microseconds) */ 421 | 422 | void TMODULO(ref long t, ref long q, double u) 423 | { 424 | q = cast(long)(t / u); 425 | if (q != 0) t -= q * cast(long)u; 426 | } 427 | 428 | TimeOfDay time2tm(TimeADT time) 429 | { 430 | immutable long USECS_PER_HOUR = 3600000000; 431 | immutable long USECS_PER_MINUTE = 60000000; 432 | immutable long USECS_PER_SEC = 1000000; 433 | 434 | int tm_hour = cast(int)(time / USECS_PER_HOUR); 435 | time -= tm_hour * USECS_PER_HOUR; 436 | int tm_min = cast(int)(time / USECS_PER_MINUTE); 437 | time -= tm_min * USECS_PER_MINUTE; 438 | int tm_sec = cast(int)(time / USECS_PER_SEC); 439 | time -= tm_sec * USECS_PER_SEC; 440 | 441 | return TimeOfDay(tm_hour, tm_min, tm_sec); 442 | } 443 | 444 | struct pg_tm 445 | { 446 | int tm_sec; 447 | int tm_min; 448 | int tm_hour; 449 | int tm_mday; 450 | int tm_mon; /* origin 0, not 1 */ 451 | int tm_year; /* relative to 1900 */ 452 | int tm_wday; 453 | int tm_yday; 454 | int tm_isdst; 455 | long tm_gmtoff; 456 | string tm_zone; 457 | } 458 | 459 | alias pg_time_t = long; 460 | 461 | enum USECS_PER_DAY = 86_400_000_000UL; 462 | enum USECS_PER_HOUR = 3_600_000_000UL; 463 | enum USECS_PER_MINUTE = 60_000_000UL; 464 | enum USECS_PER_SEC = 1_000_000UL; 465 | 466 | /** 467 | * timestamp2tm() - Convert timestamp data type to POSIX time structure. 468 | * 469 | * Note that year is _not_ 1900-based, but is an explicit full value. 470 | * Also, month is one-based, _not_ zero-based. 471 | * Returns: 472 | * 0 on success 473 | * -1 on out of range 474 | * 475 | * If attimezone is null, the global timezone (including possibly brute forced 476 | * timezone) will be used. 477 | */ 478 | int timestamp2tm(Timestamp dt, out pg_tm tm, out fsec_t fsec) 479 | { 480 | Timestamp date; 481 | Timestamp time; 482 | pg_time_t utime; 483 | 484 | time = dt; 485 | TMODULO(time, date, USECS_PER_DAY); 486 | 487 | if (time < 0) 488 | { 489 | time += USECS_PER_DAY; 490 | date -= 1; 491 | } 492 | 493 | j2date(cast(int) date, tm.tm_year, tm.tm_mon, tm.tm_mday); 494 | dt2time(time, tm.tm_hour, tm.tm_min, tm.tm_sec, fsec); 495 | 496 | return 0; 497 | } 498 | 499 | void dt2time(Timestamp jd, out int hour, out int min, out int sec, out fsec_t fsec) 500 | { 501 | TimeOffset time; 502 | 503 | time = jd; 504 | hour = cast(int)(time / USECS_PER_HOUR); 505 | time -= hour * USECS_PER_HOUR; 506 | min = cast(int)(time / USECS_PER_MINUTE); 507 | time -= min * USECS_PER_MINUTE; 508 | sec = cast(int)(time / USECS_PER_SEC); 509 | fsec = cast(int)(time - sec*USECS_PER_SEC); 510 | } 511 | -------------------------------------------------------------------------------- /src/dpq2/conv/to_bson.d: -------------------------------------------------------------------------------- 1 | /// 2 | module dpq2.conv.to_bson; 3 | 4 | import dpq2.value; 5 | import dpq2.oids: OidType; 6 | import dpq2.result: ArrayProperties; 7 | import dpq2.conv.to_d_types; 8 | import dpq2.conv.numeric: rawValueToNumeric; 9 | import dpq2.conv.time: TimeStampUTC; 10 | import vibe.data.bson; 11 | import std.uuid; 12 | import std.datetime: SysTime, dur, TimeZone, UTC; 13 | import std.bitmanip: bigEndianToNative, BitArray; 14 | import std.conv: to; 15 | 16 | /// 17 | Bson as(T)(in Value v) 18 | if(is(T == Bson)) 19 | { 20 | if(v.isNull) 21 | { 22 | return Bson(null); 23 | } 24 | else 25 | { 26 | if(v.isSupportedArray && ValueFormat.BINARY) 27 | return arrayValueToBson(v); 28 | else 29 | return rawValueToBson(v); 30 | } 31 | } 32 | 33 | private: 34 | 35 | Bson arrayValueToBson(in Value cell) 36 | { 37 | const ap = ArrayProperties(cell); 38 | 39 | // empty array 40 | if(ap.dimsSize.length == 0) return Bson.emptyArray; 41 | 42 | size_t curr_offset = ap.dataOffset; 43 | 44 | Bson recursive(size_t dimNum) 45 | { 46 | const dimSize = ap.dimsSize[dimNum]; 47 | Bson[] res = new Bson[dimSize]; 48 | 49 | foreach(elemNum; 0..dimSize) 50 | { 51 | if(dimNum < ap.dimsSize.length - 1) 52 | { 53 | res[elemNum] = recursive(dimNum + 1); 54 | } 55 | else 56 | { 57 | ubyte[int.sizeof] size_net; // network byte order 58 | size_net[] = cell.data[ curr_offset .. curr_offset + size_net.sizeof ]; 59 | uint size = bigEndianToNative!uint( size_net ); 60 | 61 | curr_offset += size_net.sizeof; 62 | 63 | Bson b; 64 | if(size == size.max) // NULL magic number 65 | { 66 | b = Bson(null); 67 | size = 0; 68 | } 69 | else 70 | { 71 | auto v = Value(cast(ubyte[]) cell.data[curr_offset .. curr_offset + size], ap.OID, false); 72 | b = v.as!Bson; 73 | } 74 | 75 | curr_offset += size; 76 | res[elemNum] = b; 77 | } 78 | } 79 | 80 | return Bson(res); 81 | } 82 | 83 | return recursive(0); 84 | } 85 | 86 | Bson rawValueToBson(in Value v) 87 | { 88 | if(v.format == ValueFormat.TEXT) 89 | { 90 | immutable text = v.valueAsString; 91 | 92 | if(v.oidType == OidType.Json) 93 | { 94 | return Bson(text.parseJsonString); 95 | } 96 | 97 | return Bson(text); 98 | } 99 | 100 | Bson res; 101 | 102 | with(OidType) 103 | with(Bson.Type) 104 | switch(v.oidType) 105 | { 106 | case OidType.Bool: 107 | bool n = v.tunnelForBinaryValueAsCalls!PGboolean; 108 | res = Bson(n); 109 | break; 110 | 111 | case Int2: 112 | auto n = v.tunnelForBinaryValueAsCalls!PGsmallint.to!int; 113 | res = Bson(n); 114 | break; 115 | 116 | case Int4: 117 | int n = v.tunnelForBinaryValueAsCalls!PGinteger; 118 | res = Bson(n); 119 | break; 120 | 121 | case Int8: 122 | long n = v.tunnelForBinaryValueAsCalls!PGbigint; 123 | res = Bson(n); 124 | break; 125 | 126 | case Float8: 127 | double n = v.tunnelForBinaryValueAsCalls!PGdouble_precision; 128 | res = Bson(n); 129 | break; 130 | 131 | case Numeric: 132 | res = Bson(rawValueToNumeric(v.data)); 133 | break; 134 | 135 | case Text: 136 | case FixedString: 137 | case VariableString: 138 | res = Bson(v.valueAsString); 139 | break; 140 | 141 | case ByteArray: 142 | auto b = BsonBinData(BsonBinData.Type.userDefined, v.data.idup); 143 | res = Bson(b); 144 | break; 145 | 146 | case UUID: 147 | // See: https://github.com/vibe-d/vibe.d/issues/2161 148 | // res = Bson(v.tunnelForBinaryValueAsCalls!PGuuid); 149 | res = serializeToBson(v.tunnelForBinaryValueAsCalls!PGuuid); 150 | break; 151 | 152 | case TimeStampWithZone: 153 | auto ts = v.tunnelForBinaryValueAsCalls!TimeStampUTC; 154 | auto time = BsonDate(SysTime(ts.dateTime, UTC())); 155 | long usecs = ts.fracSec.total!"usecs"; 156 | res = Bson(["time": Bson(time), "usecs": Bson(usecs)]); 157 | break; 158 | 159 | case Json: 160 | case Jsonb: 161 | vibe.data.json.Json json = v.tunnelForBinaryValueAsCalls!PGjson; 162 | res = Bson(json); 163 | break; 164 | 165 | default: 166 | throw new ValueConvException( 167 | ConvExceptionType.NOT_IMPLEMENTED, 168 | "Format of the column ("~to!(immutable(char)[])(v.oidType)~") doesn't supported by Value to Bson converter", 169 | __FILE__, __LINE__ 170 | ); 171 | } 172 | 173 | return res; 174 | } 175 | 176 | version (integration_tests) 177 | public void _integration_test( string connParam ) 178 | { 179 | import dpq2.connection: Connection, createTestConn; 180 | import dpq2.args: QueryParams; 181 | import std.uuid; 182 | import std.datetime: SysTime, DateTime, UTC; 183 | 184 | auto conn = createTestConn(connParam); 185 | 186 | // text answer tests 187 | { 188 | auto a = conn.exec( 189 | "SELECT 123::int8 as int_num_value,"~ 190 | "'text string'::text as text_value,"~ 191 | "'123.456'::json as json_numeric_value,"~ 192 | "'\"json_value_string\"'::json as json_text_value" 193 | ); 194 | 195 | auto r = a[0]; // first row 196 | 197 | assert(r["int_num_value"].as!Bson == Bson("123")); 198 | assert(r["text_value"].as!Bson == Bson("text string")); 199 | assert(r["json_numeric_value"].as!Bson == Bson(123.456)); 200 | assert(r["json_text_value"].as!Bson == Bson("json_value_string")); 201 | } 202 | 203 | // binary answer tests 204 | QueryParams params; 205 | params.resultFormat = ValueFormat.BINARY; 206 | 207 | { 208 | void testIt(Bson bsonValue, string pgType, string pgValue) 209 | { 210 | params.sqlCommand = "SELECT "~pgValue~"::"~pgType~" as bson_test_value"; 211 | auto answer = conn.execParams(params); 212 | 213 | immutable Value v = answer[0][0]; 214 | Bson bsonRes = v.as!Bson; 215 | 216 | import dpq2.conv.from_bson: typeInternal; 217 | 218 | if(v.isNull || !v.isSupportedArray) // standalone 219 | { 220 | if(pgType == "numeric") pgType = "string"; // bypass for numeric values represented as strings 221 | 222 | assert(bsonRes == bsonValue, "Received unexpected value\nreceived bsonType="~bsonValue.typeInternal.to!string~"\nexpected nativeType="~pgType~ 223 | "\nsent pgValue="~pgValue~"\nexpected bsonValue="~to!string(bsonValue)~"\nresult="~to!string(bsonRes)); 224 | } 225 | else // arrays 226 | { 227 | assert(bsonRes.type == Bson.Type.array && bsonRes.toString == bsonValue.toString, 228 | "pgType="~pgType~" pgValue="~pgValue~" bsonValue="~to!string(bsonValue)); 229 | } 230 | } 231 | 232 | alias C = testIt; // "C" means "case" 233 | 234 | C(Bson(null), "text", "null"); 235 | C(Bson(null), "integer", "null"); 236 | C(Bson(true), "boolean", "true"); 237 | C(Bson(false), "boolean", "false"); 238 | C(Bson(-32_761), "smallint", "-32761"); 239 | C(Bson(-2_147_483_646), "integer", "-2147483646"); 240 | C(Bson(-9_223_372_036_854_775_806), "bigint", "-9223372036854775806"); 241 | C(Bson(-1234.56789012345), "double precision", "-1234.56789012345"); 242 | C(Bson("first line\nsecond line"), "text", "'first line\nsecond line'"); 243 | C(Bson("12345 "), "char(6)", "'12345'"); 244 | C(Bson("-487778762.918209326"), "numeric", "-487778762.918209326"); 245 | 246 | C(Bson(BsonBinData( 247 | BsonBinData.Type.userDefined, 248 | [0x44, 0x20, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x00, 0x21] 249 | )), 250 | "bytea", r"E'\\x44 20 72 75 6c 65 73 00 21'"); // "D rules\x00!" (ASCII) 251 | 252 | C(Bson(UUID("8b9ab33a-96e9-499b-9c36-aad1fe86d640")), 253 | "uuid", "'8b9ab33a-96e9-499b-9c36-aad1fe86d640'"); 254 | C(serializeToBson(UUID("8b9ab33a-96e9-499b-9c36-aad1fe86d640")), 255 | "uuid", "'8b9ab33a-96e9-499b-9c36-aad1fe86d640'"); 256 | 257 | C(Bson([ 258 | Bson([Bson([Bson("1")]),Bson([Bson("22")]),Bson([Bson("333")])]), 259 | Bson([Bson([Bson("4")]),Bson([Bson(null)]),Bson([Bson("6")])]) 260 | ]), "text[]", "'{{{1},{22},{333}},{{4},{null},{6}}}'"); 261 | 262 | C(Bson.emptyArray, "text[]", "'{}'"); 263 | 264 | C(Bson(["time": Bson(BsonDate(SysTime(DateTime(1997, 12, 17, 7, 37, 16), UTC()))), "usecs": Bson(cast(long) 12)]), "timestamp with time zone", "'1997-12-17 07:37:16.000012 UTC'"); 265 | 266 | C(Bson(Json(["float_value": Json(123.456), "text_str": Json("text string")])), "json", "'{\"float_value\": 123.456,\"text_str\": \"text string\"}'"); 267 | 268 | C(Bson(Json(["float_value": Json(123.456), "text_str": Json("text string")])), "jsonb", "'{\"float_value\": 123.456,\"text_str\": \"text string\"}'"); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/dpq2/conv/to_d_types.d: -------------------------------------------------------------------------------- 1 | /// 2 | module dpq2.conv.to_d_types; 3 | 4 | @safe: 5 | 6 | import dpq2.value; 7 | import dpq2.oids: OidType, isNativeInteger, isNativeFloat; 8 | import dpq2.connection: Connection; 9 | import dpq2.query: QueryParams; 10 | import dpq2.result: msg_NOT_BINARY; 11 | import dpq2.conv.from_d_types; 12 | import dpq2.conv.numeric: rawValueToNumeric; 13 | import dpq2.conv.time: binaryValueAs, TimeStamp, TimeStampUTC, TimeOfDayWithTZ, Interval; 14 | import dpq2.conv.geometric: binaryValueAs, Line; 15 | import dpq2.conv.inet: binaryValueAs, InetAddress, CidrAddress; 16 | import dpq2.conv.arrays : binaryValueAs; 17 | 18 | import vibe.data.json: Json, parseJsonString; 19 | import vibe.data.bson: Bson; 20 | import std.traits; 21 | import std.uuid; 22 | import std.datetime; 23 | import std.traits: isScalarType; 24 | import std.variant: Variant; 25 | import std.typecons : Nullable; 26 | import std.bitmanip: bigEndianToNative, BitArray; 27 | import std.conv: to; 28 | version (unittest) import std.exception : assertThrown; 29 | 30 | // Supported PostgreSQL binary types 31 | alias PGboolean = bool; /// boolean 32 | alias PGsmallint = short; /// smallint 33 | alias PGinteger = int; /// integer 34 | alias PGbigint = long; /// bigint 35 | alias PGreal = float; /// real 36 | alias PGdouble_precision = double; /// double precision 37 | alias PGtext = string; /// text 38 | alias PGnumeric = string; /// numeric represented as string 39 | alias PGbytea = immutable(ubyte)[]; /// bytea 40 | alias PGuuid = UUID; /// UUID 41 | alias PGdate = Date; /// Date (no time of day) 42 | alias PGtime_without_time_zone = TimeOfDay; /// Time of day (no date) 43 | alias PGtime_with_time_zone = TimeOfDayWithTZ; /// Time of day with TZ(no date) 44 | alias PGtimestamp = TimeStamp; /// Both date and time without time zone 45 | alias PGtimestamptz = TimeStampUTC; /// Both date and time stored in UTC time zone 46 | alias PGinterval = Interval; /// Interval 47 | alias PGjson = Json; /// json or jsonb 48 | alias PGline = Line; /// Line (geometric type) 49 | alias PGvarbit = BitArray; /// BitArray 50 | 51 | private alias VF = ValueFormat; 52 | private alias AE = ValueConvException; 53 | private alias ET = ConvExceptionType; 54 | 55 | /** 56 | Returns cell value as a Variant type. 57 | */ 58 | T as(T : Variant, bool isNullablePayload = true)(in Value v) 59 | { 60 | import dpq2.conv.to_variant; 61 | 62 | return v.toVariant!isNullablePayload; 63 | } 64 | 65 | /** 66 | Returns cell value as a Nullable type using the underlying type conversion after null check. 67 | */ 68 | T as(T : Nullable!R, R)(in Value v) 69 | { 70 | if (v.isNull) 71 | return T.init; 72 | else 73 | return T(v.as!R); 74 | } 75 | 76 | /** 77 | Returns cell value as a native string based type from text or binary formatted field. 78 | Throws: AssertError if the db value is NULL. 79 | */ 80 | T as(T)(in Value v) pure @trusted 81 | if(is(T : const(char)[]) && !is(T == Nullable!R, R)) 82 | { 83 | if(v.format == VF.BINARY) 84 | { 85 | if(!( 86 | v.oidType == OidType.Text || 87 | v.oidType == OidType.FixedString || 88 | v.oidType == OidType.VariableString || 89 | v.oidType == OidType.Numeric || 90 | v.oidType == OidType.Json || 91 | v.oidType == OidType.Jsonb || 92 | v.oidType == OidType.Name 93 | )) 94 | throwTypeComplaint(v.oidType, "Text, FixedString, VariableString, Name, Numeric, Json or Jsonb", __FILE__, __LINE__); 95 | } 96 | 97 | if(v.format == VF.BINARY && v.oidType == OidType.Numeric) 98 | return rawValueToNumeric(v.data); // special case for 'numeric' which represented in dpq2 as string 99 | else 100 | return v.valueAsString; 101 | } 102 | 103 | @system unittest 104 | { 105 | import core.exception: AssertError; 106 | 107 | auto v = Value(ValueFormat.BINARY, OidType.Text); 108 | 109 | assert(v.isNull); 110 | assertThrown!AssertError(v.as!string == ""); 111 | assert(v.as!(Nullable!string).isNull == true); 112 | 113 | assert(v.as!Variant.get!(Nullable!string).isNull == true); 114 | } 115 | 116 | /** 117 | Returns value as D type value from binary formatted field. 118 | Throws: AssertError if the db value is NULL. 119 | */ 120 | T as(T)(in Value v) 121 | if(!is(T : const(char)[]) && !is(T == Bson) && !is(T == Variant) && !is(T == Nullable!R,R)) 122 | { 123 | if(!(v.format == VF.BINARY)) 124 | throw new AE(ET.NOT_BINARY, 125 | msg_NOT_BINARY, __FILE__, __LINE__); 126 | 127 | return binaryValueAs!T(v); 128 | } 129 | 130 | @system unittest 131 | { 132 | auto v = Value([1], OidType.Int4, false, ValueFormat.TEXT); 133 | assertThrown!AE(v.as!int); 134 | } 135 | 136 | Value[] deserializeRecord(in Value v) 137 | { 138 | if(!(v.oidType == OidType.Record)) 139 | throwTypeComplaint(v.oidType, "record", __FILE__, __LINE__); 140 | 141 | if(!(v.data.length >= uint.sizeof)) 142 | throw new AE(ET.SIZE_MISMATCH, 143 | "Value length isn't enough to hold a size", __FILE__, __LINE__); 144 | 145 | immutable(ubyte)[] data = v.data; 146 | uint entries = bigEndianToNative!uint(v.data[0 .. uint.sizeof]); 147 | data = data[uint.sizeof .. $]; 148 | 149 | Value[] ret = new Value[entries]; 150 | 151 | foreach (ref res; ret) { 152 | if (!(data.length >= 2*int.sizeof)) 153 | throw new AE(ET.SIZE_MISMATCH, 154 | "Value length isn't enough to hold an oid and a size", __FILE__, __LINE__); 155 | OidType oidType = cast(OidType)bigEndianToNative!int(data[0 .. int.sizeof]); 156 | data = data[int.sizeof .. $]; 157 | int size = bigEndianToNative!int(data[0 .. int.sizeof]); 158 | data = data[int.sizeof .. $]; 159 | 160 | if (size == -1) 161 | { 162 | res = Value(null, oidType, true); 163 | continue; 164 | } 165 | assert(size >= 0); 166 | if (!(data.length >= size)) 167 | throw new AE(ET.SIZE_MISMATCH, 168 | "Value length isn't enough to hold object body", __FILE__, __LINE__); 169 | immutable(ubyte)[] resData = data[0 .. size]; 170 | data = data[size .. $]; 171 | res = Value(resData.idup, oidType); 172 | } 173 | 174 | return ret; 175 | } 176 | 177 | package: 178 | 179 | /* 180 | * Something was broken in DMD64 D Compiler v2.079.0-rc.1 so I made this "tunnel" 181 | * TODO: remove it and replace by direct binaryValueAs calls 182 | */ 183 | auto tunnelForBinaryValueAsCalls(T)(in Value v) 184 | { 185 | return binaryValueAs!T(v); 186 | } 187 | 188 | char[] valueAsString(in Value v) pure 189 | { 190 | return (cast(const(char[])) v.data).to!(char[]); 191 | } 192 | 193 | /// Returns value as bytes from binary formatted field 194 | T binaryValueAs(T)(in Value v) 195 | if(is(T : const ubyte[])) 196 | { 197 | if(!(v.oidType == OidType.ByteArray)) 198 | throwTypeComplaint(v.oidType, "immutable ubyte[]", __FILE__, __LINE__); 199 | 200 | return v.data; 201 | } 202 | 203 | @system unittest 204 | { 205 | auto v = Value([1], OidType.Bool); 206 | assertThrown!ValueConvException(v.binaryValueAs!(const ubyte[])); 207 | } 208 | 209 | /// Returns cell value as native integer or decimal values 210 | /// 211 | /// Postgres type "numeric" is oversized and not supported by now 212 | T binaryValueAs(T)(in Value v) 213 | if( isNumeric!(T) ) 214 | { 215 | static if(isIntegral!(T)) 216 | if(!isNativeInteger(v.oidType)) 217 | throwTypeComplaint(v.oidType, "integral types", __FILE__, __LINE__); 218 | 219 | static if(isFloatingPoint!(T)) 220 | if(!isNativeFloat(v.oidType)) 221 | throwTypeComplaint(v.oidType, "floating point types", __FILE__, __LINE__); 222 | 223 | if(!(v.data.length == T.sizeof)) 224 | throw new AE(ET.SIZE_MISMATCH, 225 | to!string(v.oidType)~" length ("~to!string(v.data.length)~") isn't equal to native D type "~ 226 | to!string(typeid(T))~" size ("~to!string(T.sizeof)~")", 227 | __FILE__, __LINE__); 228 | 229 | ubyte[T.sizeof] s = v.data[0..T.sizeof]; 230 | return bigEndianToNative!(T)(s); 231 | } 232 | 233 | @system unittest 234 | { 235 | auto v = Value([1], OidType.Bool); 236 | assertThrown!ValueConvException(v.binaryValueAs!int); 237 | assertThrown!ValueConvException(v.binaryValueAs!float); 238 | 239 | v = Value([1], OidType.Int4); 240 | assertThrown!ValueConvException(v.binaryValueAs!int); 241 | } 242 | 243 | package void checkValue( 244 | in Value v, 245 | in OidType enforceOid, 246 | in size_t enforceSize, 247 | in string typeName 248 | ) pure @safe 249 | { 250 | if(!(v.oidType == enforceOid)) 251 | throwTypeComplaint(v.oidType, typeName); 252 | 253 | if(!(v.data.length == enforceSize)) 254 | throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH, 255 | `Value length isn't equal to Postgres `~typeName~` size`); 256 | } 257 | 258 | /// Returns UUID as native UUID value 259 | UUID binaryValueAs(T)(in Value v) 260 | if( is( T == UUID ) ) 261 | { 262 | v.checkValue(OidType.UUID, 16, "UUID"); 263 | 264 | UUID r; 265 | r.data = v.data; 266 | return r; 267 | } 268 | 269 | @system unittest 270 | { 271 | auto v = Value([1], OidType.Int4); 272 | assertThrown!ValueConvException(v.binaryValueAs!UUID); 273 | 274 | v = Value([1], OidType.UUID); 275 | assertThrown!ValueConvException(v.binaryValueAs!UUID); 276 | } 277 | 278 | /// Returns boolean as native bool value 279 | bool binaryValueAs(T : bool)(in Value v) 280 | if (!is(T == Nullable!R, R)) 281 | { 282 | v.checkValue(OidType.Bool, 1, "bool"); 283 | 284 | return v.data[0] != 0; 285 | } 286 | 287 | @system unittest 288 | { 289 | auto v = Value([1], OidType.Int4); 290 | assertThrown!ValueConvException(v.binaryValueAs!bool); 291 | 292 | v = Value([1,2], OidType.Bool); 293 | assertThrown!ValueConvException(v.binaryValueAs!bool); 294 | } 295 | 296 | /// Returns Vibe.d's Json 297 | Json binaryValueAs(T)(in Value v) @trusted 298 | if( is( T == Json ) ) 299 | { 300 | import dpq2.conv.jsonb: jsonbValueToJson; 301 | 302 | Json res; 303 | 304 | switch(v.oidType) 305 | { 306 | case OidType.Json: 307 | // represent value as text and parse it into Json 308 | string t = v.valueAsString; 309 | res = parseJsonString(t); 310 | break; 311 | 312 | case OidType.Jsonb: 313 | res = v.jsonbValueToJson; 314 | break; 315 | 316 | default: 317 | throwTypeComplaint(v.oidType, "json or jsonb", __FILE__, __LINE__); 318 | } 319 | 320 | return res; 321 | } 322 | 323 | @system unittest 324 | { 325 | auto v = Value([1], OidType.Int4); 326 | assertThrown!ValueConvException(v.binaryValueAs!Json); 327 | } 328 | 329 | import money: currency, roundingMode; 330 | 331 | /// Returns money type 332 | /// 333 | /// Caution: here is no check of fractional precision while conversion! 334 | /// See also: PostgreSQL's "lc_monetary" description and "money" package description 335 | T binaryValueAs(T)(in Value v) @trusted 336 | if( isInstanceOf!(currency, T) && T.amount.sizeof == 8 ) 337 | { 338 | import std.format: format; 339 | 340 | if(v.data.length != T.amount.sizeof) 341 | throw new AE( 342 | ET.SIZE_MISMATCH, 343 | format( 344 | "%s length (%d) isn't equal to D money type %s size (%d)", 345 | v.oidType.to!string, 346 | v.data.length, 347 | typeid(T).to!string, 348 | T.amount.sizeof 349 | ) 350 | ); 351 | 352 | T r; 353 | 354 | r.amount = v.data[0 .. T.amount.sizeof].bigEndianToNative!long; 355 | 356 | return r; 357 | } 358 | 359 | package alias PGTestMoney = currency!("TEST_CURR", 2); //TODO: roundingMode.UNNECESSARY 360 | 361 | unittest 362 | { 363 | auto v = Value([1], OidType.Money); 364 | assertThrown!ValueConvException(v.binaryValueAs!PGTestMoney); 365 | } 366 | 367 | T binaryValueAs(T)(in Value v) @trusted 368 | if( is(T == BitArray) ) 369 | { 370 | import core.bitop : bitswap; 371 | import std.bitmanip; 372 | import std.format: format; 373 | import std.range : chunks; 374 | 375 | if(v.data.length < int.sizeof) 376 | throw new AE( 377 | ET.SIZE_MISMATCH, 378 | format( 379 | "%s length (%d) is less than minimum int type size (%d)", 380 | v.oidType.to!string, 381 | v.data.length, 382 | int.sizeof 383 | ) 384 | ); 385 | 386 | auto data = v.data[]; 387 | size_t len = data.read!int; 388 | size_t[] newData; 389 | foreach (ch; data.chunks(size_t.sizeof)) 390 | { 391 | ubyte[size_t.sizeof] tmpData; 392 | tmpData[0 .. ch.length] = ch[]; 393 | 394 | //FIXME: DMD Issue 19693 395 | version(DigitalMars) 396 | auto re = softBitswap(bigEndianToNative!size_t(tmpData)); 397 | else 398 | auto re = bitswap(bigEndianToNative!size_t(tmpData)); 399 | newData ~= re; 400 | } 401 | return T(newData, len); 402 | } 403 | 404 | unittest 405 | { 406 | auto v = Value([1], OidType.VariableBitString); 407 | assertThrown!ValueConvException(v.binaryValueAs!BitArray); 408 | } 409 | -------------------------------------------------------------------------------- /src/dpq2/conv/to_variant.d: -------------------------------------------------------------------------------- 1 | /// 2 | module dpq2.conv.to_variant; 3 | 4 | import dpq2.value; 5 | import dpq2.oids: OidType; 6 | import dpq2.result: ArrayProperties; 7 | import dpq2.conv.inet: InetAddress, CidrAddress; 8 | import dpq2.conv.to_d_types; 9 | import dpq2.conv.numeric: rawValueToNumeric; 10 | import dpq2.conv.time: TimeStampUTC; 11 | static import geom = dpq2.conv.geometric; 12 | import std.bitmanip: bigEndianToNative, BitArray; 13 | import std.datetime: SysTime, dur, TimeZone, UTC; 14 | import std.conv: to; 15 | import std.typecons: Nullable; 16 | import std.uuid; 17 | import std.variant: Variant; 18 | import vibe.data.json: VibeJson = Json; 19 | 20 | /// 21 | Variant toVariant(bool isNullablePayload = true)(in Value v) @safe 22 | { 23 | auto getNative(T)() 24 | if(!is(T == Variant)) 25 | { 26 | static if(isNullablePayload) 27 | { 28 | Nullable!T ret; 29 | 30 | if (v.isNull) 31 | return ret; 32 | 33 | ret = v.as!T; 34 | 35 | return ret; 36 | } 37 | else 38 | { 39 | return v.as!T; 40 | } 41 | } 42 | 43 | Variant retVariant(T)() @trusted 44 | { 45 | return Variant(getNative!T); 46 | } 47 | 48 | if(v.format == ValueFormat.TEXT) 49 | return retVariant!string; 50 | 51 | template retArray__(NativeT) 52 | { 53 | static if(isNullablePayload) 54 | alias arrType = Nullable!NativeT[]; 55 | else 56 | alias arrType = NativeT[]; 57 | 58 | alias retArray__ = retVariant!arrType; 59 | } 60 | 61 | with(OidType) 62 | switch(v.oidType) 63 | { 64 | case Bool: return retVariant!PGboolean; 65 | case BoolArray: return retArray__!PGboolean; 66 | 67 | case Int2: return retVariant!short; 68 | case Int2Array: return retArray__!short; 69 | 70 | case Int4: return retVariant!int; 71 | case Int4Array: return retArray__!int; 72 | 73 | case Int8: return retVariant!long; 74 | case Int8Array: return retArray__!long; 75 | 76 | case Float4: return retVariant!float; 77 | case Float4Array: return retArray__!float; 78 | 79 | case Float8: return retVariant!double; 80 | case Float8Array: return retArray__!double; 81 | 82 | case Numeric: 83 | case Text: 84 | case FixedString: 85 | case VariableString: 86 | return retVariant!string; 87 | 88 | case NumericArray: 89 | case TextArray: 90 | case FixedStringArray: 91 | case VariableStringArray: 92 | return retArray__!string; 93 | 94 | case ByteArray: return retVariant!PGbytea; 95 | 96 | case UUID: return retVariant!PGuuid; 97 | case UUIDArray: return retArray__!PGuuid; 98 | 99 | case Date: return retVariant!PGdate; 100 | case DateArray: return retArray__!PGdate; 101 | 102 | case HostAddress: return retVariant!InetAddress; 103 | case HostAddressArray: return retArray__!InetAddress; 104 | 105 | case NetworkAddress: return retVariant!CidrAddress; 106 | case NetworkAddressArray: return retArray__!CidrAddress; 107 | 108 | case Time: return retVariant!PGtime_without_time_zone; 109 | case TimeArray: return retArray__!PGtime_without_time_zone; 110 | 111 | case TimeWithZone: return retVariant!PGtime_with_time_zone; 112 | case TimeWithZoneArray: return retArray__!PGtime_with_time_zone; 113 | 114 | case TimeStamp: return retVariant!PGtimestamp; 115 | case TimeStampArray: return retArray__!PGtimestamp; 116 | 117 | case TimeStampWithZone: return retVariant!PGtimestamptz; 118 | case TimeStampWithZoneArray: return retArray__!PGtimestamptz; 119 | 120 | case TimeInterval: return retVariant!PGinterval; 121 | 122 | case Json: 123 | case Jsonb: 124 | return retVariant!VibeJson; 125 | 126 | case JsonArray: 127 | case JsonbArray: 128 | return retArray__!VibeJson; 129 | 130 | case Line: return retVariant!(geom.Line); 131 | case LineArray: return retArray__!(geom.Line); 132 | 133 | default: 134 | throw new ValueConvException( 135 | ConvExceptionType.NOT_IMPLEMENTED, 136 | "Format of the column ("~to!(immutable(char)[])(v.oidType)~") doesn't supported by Value to Variant converter", 137 | __FILE__, __LINE__ 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/dpq2/dynloader.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Supporting of dynamic configuration of libpq 3 | */ 4 | module dpq2.dynloader; 5 | 6 | version(Dpq2_Dynamic): 7 | 8 | import dpq2.connection: Connection; 9 | import core.sync.mutex: Mutex; 10 | import dpq2.exception: Dpq2Exception; 11 | 12 | class ConnectionFactory 13 | { 14 | private __gshared Mutex mutex; 15 | private __gshared bool instanced; 16 | private ReferenceCounter cnt; 17 | 18 | shared static this() 19 | { 20 | mutex = new Mutex(); 21 | } 22 | 23 | //mixin ctorBody; //FIXME: https://issues.dlang.org/show_bug.cgi?id=22627 24 | immutable mixin ctorBody; 25 | 26 | // If ctor throws dtor will be called. This is behaviour of current D design. 27 | // https://issues.dlang.org/show_bug.cgi?id=704 28 | private immutable bool isSucessfulConstructed; 29 | 30 | private mixin template ctorBody() 31 | { 32 | this() { this(""); } 33 | 34 | /** 35 | Params: 36 | path = A string containing one or more comma-separated shared 37 | library names. 38 | */ 39 | this(string path) 40 | { 41 | import std.exception: enforce; 42 | 43 | mutex.lock(); 44 | scope(success) instanced = true; 45 | scope(exit) mutex.unlock(); 46 | 47 | enforce!Dpq2Exception(!instanced, "Already instanced"); 48 | 49 | cnt = ReferenceCounter(path); 50 | assert(ReferenceCounter.instances == 1); 51 | 52 | isSucessfulConstructed = true; 53 | } 54 | } 55 | 56 | ~this() 57 | { 58 | mutex.lock(); 59 | scope(exit) mutex.unlock(); 60 | 61 | if(isSucessfulConstructed) 62 | { 63 | assert(instanced); 64 | 65 | cnt.__custom_dtor(); 66 | } 67 | 68 | instanced = false; 69 | } 70 | 71 | /// This method is need to forbid attempts to create connection without properly loaded libpq 72 | /// Accepts same parameters as Connection ctors in static configuration 73 | Connection createConnection(T...)(T args) const 74 | { 75 | mutex.lock(); 76 | scope(exit) mutex.unlock(); 77 | 78 | assert(instanced); 79 | 80 | return new Connection(args); 81 | } 82 | 83 | void connStringCheck(string connString) const 84 | { 85 | mutex.lock(); 86 | scope(exit) mutex.unlock(); 87 | 88 | assert(instanced); 89 | 90 | import dpq2.connection; 91 | 92 | _connStringCheck(connString); 93 | } 94 | } 95 | 96 | package struct ReferenceCounter 97 | { 98 | import core.atomic; 99 | import derelict.pq.pq: DerelictPQ; 100 | debug import std.experimental.logger; 101 | import std.stdio: writeln; 102 | 103 | debug(dpq2_verbose) invariant() 104 | { 105 | mutex.lock(); 106 | scope(exit) mutex.unlock(); 107 | 108 | import std.stdio; 109 | writeln("Instances ", instances); 110 | } 111 | 112 | private __gshared Mutex mutex; 113 | private __gshared ptrdiff_t instances; 114 | 115 | shared static this() 116 | { 117 | mutex = new Mutex(); 118 | } 119 | 120 | this() @disable; 121 | this(this) @disable; 122 | 123 | /// Used only by connection factory 124 | this(string path) 125 | { 126 | mutex.lock(); 127 | scope(exit) mutex.unlock(); 128 | 129 | assert(instances == 0); 130 | 131 | debug trace("DerelictPQ loading..."); 132 | DerelictPQ.load(path); 133 | debug trace("...DerelictPQ loading finished"); 134 | 135 | instances++; 136 | } 137 | 138 | /// Used by all other objects 139 | this(bool) 140 | { 141 | mutex.lock(); 142 | scope(exit) mutex.unlock(); 143 | 144 | assert(instances > 0); 145 | 146 | instances++; 147 | } 148 | 149 | // TODO: here is must be a destructor, but: 150 | // "This is bug or not? (immutable class containing struct with dtor)" 151 | // https://forum.dlang.org/post/spim8c$108b$1@digitalmars.com 152 | // https://issues.dlang.org/show_bug.cgi?id=13628 153 | void __custom_dtor() const 154 | { 155 | mutex.lock(); 156 | scope(exit) mutex.unlock(); 157 | 158 | assert(instances > 0); 159 | 160 | instances--; 161 | 162 | if(instances == 0) 163 | { 164 | //TODO: replace writeln by trace? 165 | debug trace("DerelictPQ unloading..."); 166 | DerelictPQ.unload(); 167 | debug trace("...DerelictPQ unloading finished"); 168 | } 169 | } 170 | } 171 | 172 | version (integration_tests): 173 | 174 | void _integration_test() 175 | { 176 | import std.exception : assertThrown; 177 | 178 | // Some testing: 179 | { 180 | auto f = new immutable ConnectionFactory(); 181 | assert(ConnectionFactory.instanced); 182 | assert(ReferenceCounter.instances == 1); 183 | f.destroy; 184 | } 185 | 186 | assert(ConnectionFactory.instanced == false); 187 | assert(ReferenceCounter.instances == 0); 188 | 189 | { 190 | auto f = new immutable ConnectionFactory(); 191 | 192 | // Only one instance of ConnectionFactory is allowed 193 | assertThrown!Dpq2Exception(new immutable ConnectionFactory()); 194 | 195 | assert(ConnectionFactory.instanced); 196 | assert(ReferenceCounter.instances == 1); 197 | 198 | f.destroy; 199 | } 200 | 201 | assert(!ConnectionFactory.instanced); 202 | assert(ReferenceCounter.instances == 0); 203 | 204 | { 205 | import derelict.util.exception: SharedLibLoadException; 206 | 207 | assertThrown!SharedLibLoadException( 208 | new immutable ConnectionFactory(`wrong/path/to/libpq.dll`) 209 | ); 210 | } 211 | 212 | assert(!ConnectionFactory.instanced); 213 | assert(ReferenceCounter.instances == 0); 214 | } 215 | 216 | // Used only by integration tests facility: 217 | private shared ConnectionFactory _connFactory; 218 | 219 | //TODO: Remove cast and immutable, https://issues.dlang.org/show_bug.cgi?id=22627 220 | package immutable(ConnectionFactory) connFactory() { return cast(immutable) _connFactory; } 221 | 222 | void _initTestsConnectionFactory() 223 | { 224 | //TODO: ditto 225 | _connFactory = cast(shared) new immutable ConnectionFactory; 226 | 227 | assert(ConnectionFactory.instanced); 228 | assert(ReferenceCounter.instances == 1); 229 | } 230 | -------------------------------------------------------------------------------- /src/dpq2/exception.d: -------------------------------------------------------------------------------- 1 | /// 2 | module dpq2.exception; 3 | 4 | /// Base for all dpq2 exceptions classes 5 | class Dpq2Exception : Exception 6 | { 7 | this(string msg, string file = __FILE__, size_t line = __LINE__) pure @safe 8 | { 9 | super(msg, file, line); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/dpq2/oids.d: -------------------------------------------------------------------------------- 1 | /** 2 | * PostgreSQL major types oids. 3 | * 4 | * Copyright: © 2014 DSoftOut 5 | * Authors: NCrashed 6 | */ 7 | 8 | module dpq2.oids; 9 | 10 | @safe: 11 | 12 | package OidType oid2oidType(Oid oid) pure 13 | { 14 | static assert(Oid.sizeof == OidType.sizeof); 15 | 16 | return cast(OidType)(oid); 17 | } 18 | 19 | /** 20 | * Convert between array Oid and element Oid or vice versa 21 | * 22 | * Params: 23 | * s = "array" or "element" 24 | * type = source object type 25 | */ 26 | OidType oidConvTo(string s)(OidType type) 27 | { 28 | foreach(ref a; appropriateArrOid) 29 | { 30 | static if(s == "array") 31 | { 32 | if(a.value == type) 33 | return a.array; 34 | } 35 | else 36 | static if(s == "element") 37 | { 38 | if(a.array == type) 39 | return a.value; 40 | } 41 | else 42 | static assert(false, "Wrong oidConvTo type "~s); 43 | } 44 | 45 | import dpq2.value: ValueConvException, ConvExceptionType; 46 | import std.conv: to; 47 | 48 | throw new ValueConvException( 49 | ConvExceptionType.NOT_IMPLEMENTED, 50 | "Conv to "~s~" for type "~type.to!string~" isn't defined", 51 | __FILE__, __LINE__ 52 | ); 53 | } 54 | 55 | /// Checks if Oid type can be mapped to native D integer 56 | bool isNativeInteger(OidType t) pure 57 | { 58 | with(OidType) 59 | switch(t) 60 | { 61 | case Int8: 62 | case Int2: 63 | case Int4: 64 | case Oid: 65 | return true; 66 | default: 67 | break; 68 | } 69 | 70 | return false; 71 | } 72 | 73 | /// Checks if Oid type can be mapped to native D float 74 | bool isNativeFloat(OidType t) pure 75 | { 76 | with(OidType) 77 | switch(t) 78 | { 79 | case Float4: 80 | case Float8: 81 | return true; 82 | default: 83 | break; 84 | } 85 | 86 | return false; 87 | } 88 | 89 | package: 90 | 91 | private struct AppropriateArrOid 92 | { 93 | OidType value; 94 | OidType array; 95 | } 96 | 97 | private static immutable AppropriateArrOid[] appropriateArrOid; 98 | 99 | shared static this() 100 | { 101 | alias A = AppropriateArrOid; 102 | 103 | with(OidType) 104 | { 105 | immutable AppropriateArrOid[] a = 106 | [ 107 | A(Text, TextArray), 108 | A(Name, NameArray), 109 | A(Bool, BoolArray), 110 | A(Int2, Int2Array), 111 | A(Int4, Int4Array), 112 | A(Int8, Int8Array), 113 | A(Float4, Float4Array), 114 | A(Float8, Float8Array), 115 | A(Date, DateArray), 116 | A(Time, TimeArray), 117 | A(TimeWithZone, TimeWithZoneArray), 118 | A(TimeStampWithZone, TimeStampWithZoneArray), 119 | A(TimeStamp, TimeStampArray), 120 | A(Line, LineArray), 121 | A(Json, JsonArray), 122 | A(NetworkAddress, NetworkAddressArray), 123 | A(HostAddress, HostAddressArray), 124 | A(UUID, UUIDArray) 125 | ]; 126 | 127 | appropriateArrOid = a; 128 | } 129 | } 130 | 131 | import derelict.pq.pq: Oid; 132 | 133 | bool isSupportedArray(OidType t) pure nothrow @nogc 134 | { 135 | with(OidType) 136 | switch(t) 137 | { 138 | case BoolArray: 139 | case ByteArrayArray: 140 | case CharArray: 141 | case Int2Array: 142 | case Int4Array: 143 | case TextArray: 144 | case NameArray: 145 | case Int8Array: 146 | case Float4Array: 147 | case Float8Array: 148 | case TimeStampArray: 149 | case TimeStampWithZoneArray: 150 | case DateArray: 151 | case TimeArray: 152 | case TimeWithZoneArray: 153 | case NumericArray: 154 | case NetworkAddressArray: 155 | case HostAddressArray: 156 | case UUIDArray: 157 | case LineArray: 158 | case JsonArray: 159 | case JsonbArray: 160 | case RecordArray: 161 | return true; 162 | default: 163 | break; 164 | } 165 | 166 | return false; 167 | } 168 | 169 | OidType detectOidTypeFromNative(T)() 170 | { 171 | import std.typecons : Nullable; 172 | 173 | static if(is(T == Nullable!R,R)) 174 | return detectOidTypeNotCareAboutNullable!(typeof(T.get)); 175 | else 176 | return detectOidTypeNotCareAboutNullable!T; 177 | } 178 | 179 | private OidType detectOidTypeNotCareAboutNullable(T)() 180 | { 181 | import std.bitmanip : BitArray; 182 | import std.datetime.date : StdDate = Date, TimeOfDay, DateTime; 183 | import std.datetime.systime : SysTime; 184 | import std.traits : Unqual, isSomeString; 185 | import std.uuid : StdUUID = UUID; 186 | static import dpq2.conv.geometric; 187 | static import dpq2.conv.time; 188 | import dpq2.conv.inet: InetAddress, CidrAddress; 189 | import vibe.data.json : VibeJson = Json; 190 | 191 | alias UT = Unqual!T; 192 | 193 | with(OidType) 194 | { 195 | static if(isSomeString!UT){ return Text; } else 196 | static if(is(UT == ubyte[])){ return ByteArray; } else 197 | static if(is(UT == bool)){ return Bool; } else 198 | static if(is(UT == short)){ return Int2; } else 199 | static if(is(UT == int)){ return Int4; } else 200 | static if(is(UT == long)){ return Int8; } else 201 | static if(is(UT == float)){ return Float4; } else 202 | static if(is(UT == double)){ return Float8; } else 203 | static if(is(UT == StdDate)){ return Date; } else 204 | static if(is(UT == TimeOfDay)){ return Time; } else 205 | static if(is(UT == dpq2.conv.time.TimeOfDayWithTZ)){ return TimeWithZone; } else 206 | static if(is(UT == DateTime)){ return TimeStamp; } else 207 | static if(is(UT == SysTime)){ return TimeStampWithZone; } else 208 | static if(is(UT == dpq2.conv.time.TimeStamp)){ return TimeStamp; } else 209 | static if(is(UT == dpq2.conv.time.TimeStampUTC)){ return TimeStampWithZone; } else 210 | static if(is(UT == VibeJson)){ return Json; } else 211 | static if(is(UT == InetAddress)){ return HostAddress; } else 212 | static if(is(UT == CidrAddress)){ return NetworkAddress; } else 213 | static if(is(UT == StdUUID)){ return UUID; } else 214 | static if(is(UT == BitArray)){ return VariableBitString; } else 215 | static if(dpq2.conv.geometric.isValidPointType!UT){ return Point; } else 216 | static if(dpq2.conv.geometric.isValidLineType!UT){ return Line; } else 217 | static if(dpq2.conv.geometric.isValidPathType!UT){ return Path; } else 218 | static if(dpq2.conv.geometric.isValidPolygon!UT){ return Polygon; } else 219 | static if(dpq2.conv.geometric.isValidCircleType!UT){ return Circle; } else 220 | static if(dpq2.conv.geometric.isValidLineSegmentType!UT){ return LineSegment; } else 221 | static if(dpq2.conv.geometric.isValidBoxType!UT){ return Box; } else 222 | 223 | static assert(false, "Unsupported D type: "~T.stringof); 224 | } 225 | } 226 | 227 | /// Enum of Oid types defined in PG 228 | public enum OidType : Oid 229 | { 230 | Undefined = 0, /// 231 | 232 | Bool = 16, /// 233 | ByteArray = 17, /// 234 | Char = 18, /// 235 | Name = 19, /// 236 | Int8 = 20, /// 237 | Int2 = 21, /// 238 | Int2Vector = 22, /// 239 | Int4 = 23, /// 240 | RegProc = 24, /// 241 | Text = 25, /// 242 | Oid = 26, /// 243 | Tid = 27, /// 244 | Xid = 28, /// 245 | Cid = 29, /// 246 | OidVector = 30, /// 247 | 248 | AccessControlList = 1033, /// 249 | TypeCatalog = 71, /// 250 | AttributeCatalog = 75, /// 251 | ProcCatalog = 81, /// 252 | ClassCatalog = 83, /// 253 | 254 | Json = 114, /// 255 | Jsonb = 3802, /// 256 | Xml = 142, /// 257 | NodeTree = 194, /// 258 | StorageManager = 210, /// 259 | 260 | Point = 600, /// 261 | LineSegment = 601, /// 262 | Path = 602, /// 263 | Box = 603, /// 264 | Polygon = 604, /// 265 | Line = 628, /// 266 | 267 | Float4 = 700, /// 268 | Float8 = 701, /// 269 | AbsTime = 702, /// 270 | RelTime = 703, /// 271 | Interval = 704, /// 272 | Unknown = 705, /// 273 | 274 | Circle = 718, /// 275 | Money = 790, /// 276 | MacAddress = 829, /// 277 | HostAddress = 869, /// 278 | NetworkAddress = 650, /// 279 | 280 | FixedString = 1042, /// 281 | VariableString = 1043, /// 282 | 283 | Date = 1082, /// 284 | Time = 1083, /// 285 | TimeStamp = 1114, /// 286 | TimeStampWithZone = 1184, /// 287 | TimeInterval = 1186, /// 288 | TimeWithZone = 1266, /// 289 | 290 | FixedBitString = 1560, /// 291 | VariableBitString = 1562, /// 292 | 293 | Numeric = 1700, /// 294 | RefCursor = 1790, /// 295 | RegProcWithArgs = 2202, /// 296 | RegOperator = 2203, /// 297 | RegOperatorWithArgs = 2204, /// 298 | RegClass = 2205, /// 299 | RegType = 2206, /// 300 | 301 | UUID = 2950, /// 302 | TSVector = 3614, /// 303 | GTSVector = 3642, /// 304 | TSQuery = 3615, /// 305 | RegConfig = 3734, /// 306 | RegDictionary = 3769, /// 307 | TXidSnapshot = 2970, /// 308 | 309 | Int4Range = 3904, /// 310 | NumRange = 3906, /// 311 | TimeStampRange = 3908, /// 312 | TimeStampWithZoneRange = 3910, /// 313 | DateRange = 3912, /// 314 | Int8Range = 3926, /// 315 | 316 | // Arrays 317 | XmlArray = 143, /// 318 | JsonbArray = 3807, /// 319 | JsonArray = 199, /// 320 | LineArray = 629, /// 321 | BoolArray = 1000, /// 322 | ByteArrayArray = 1001, /// 323 | CharArray = 1002, /// 324 | NameArray = 1003, /// 325 | Int2Array = 1005, /// 326 | Int2VectorArray = 1006, /// 327 | Int4Array = 1007, /// 328 | RegProcArray = 1008, /// 329 | TextArray = 1009, /// 330 | OidArray = 1028, /// 331 | TidArray = 1010, /// 332 | XidArray = 1011, /// 333 | CidArray = 1012, /// 334 | OidVectorArray = 1013, /// 335 | FixedStringArray = 1014, /// 336 | VariableStringArray = 1015, /// 337 | Int8Array = 1016, /// 338 | PointArray = 1017, /// 339 | LineSegmentArray = 1018, /// 340 | PathArray = 1019, /// 341 | BoxArray = 1020, /// 342 | Float4Array = 1021, /// 343 | Float8Array = 1022, /// 344 | AbsTimeArray = 1023, /// 345 | RelTimeArray = 1024, /// 346 | IntervalArray = 1025, /// 347 | PolygonArray = 1027, /// 348 | AccessControlListArray = 1034, /// 349 | MacAddressArray = 1040, /// 350 | HostAddressArray = 1041, /// 351 | NetworkAddressArray = 651, /// 352 | CStringArray = 1263, /// 353 | TimeStampArray = 1115, /// 354 | DateArray = 1182, /// 355 | TimeArray = 1183, /// 356 | TimeStampWithZoneArray = 1185, /// 357 | TimeIntervalArray = 1187, /// 358 | NumericArray = 1231, /// 359 | TimeWithZoneArray = 1270, /// 360 | FixedBitStringArray = 1561, /// 361 | VariableBitStringArray = 1563, /// 362 | RefCursorArray = 2201, /// 363 | RegProcWithArgsArray = 2207, /// 364 | RegOperatorArray = 2208, /// 365 | RegOperatorWithArgsArray = 2209, /// 366 | RegClassArray = 2210, /// 367 | RegTypeArray = 2211, /// 368 | UUIDArray = 2951, /// 369 | TSVectorArray = 3643, /// 370 | GTSVectorArray = 3644, /// 371 | TSQueryArray = 3645, /// 372 | RegConfigArray = 3735, /// 373 | RegDictionaryArray = 3770, /// 374 | TXidSnapshotArray = 2949, /// 375 | Int4RangeArray = 3905, /// 376 | NumRangeArray = 3907, /// 377 | TimeStampRangeArray = 3909, /// 378 | TimeStampWithZoneRangeArray = 3911, /// 379 | DateRangeArray = 3913, /// 380 | Int8RangeArray = 3927, /// 381 | 382 | // Pseudo types 383 | Record = 2249, /// 384 | RecordArray = 2287, /// 385 | CString = 2275, /// 386 | AnyVoid = 2276, /// 387 | AnyArray = 2277, /// 388 | Void = 2278, /// 389 | Trigger = 2279, /// 390 | EventTrigger = 3838, /// 391 | LanguageHandler = 2280, /// 392 | Internal = 2281, /// 393 | Opaque = 2282, /// 394 | AnyElement = 2283, /// 395 | AnyNoArray = 2776, /// 396 | AnyEnum = 3500, /// 397 | FDWHandler = 3115, /// 398 | AnyRange = 3831, /// 399 | } 400 | -------------------------------------------------------------------------------- /src/dpq2/package.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Main module 3 | * 4 | * Include it to use common functions. 5 | */ 6 | module dpq2; 7 | 8 | public import dpq2.dynloader; 9 | public import dpq2.connection; 10 | public import dpq2.query; 11 | public import dpq2.result; 12 | public import dpq2.oids; 13 | 14 | 15 | version(Dpq2_Static){} 16 | else version(Dpq2_Dynamic){} 17 | else static assert(false, "dpq2 link type (dynamic or static) isn't defined"); 18 | -------------------------------------------------------------------------------- /src/dpq2/query.d: -------------------------------------------------------------------------------- 1 | /// Query methods 2 | module dpq2.query; 3 | 4 | public import dpq2.args; 5 | 6 | import dpq2.connection: Connection, ConnectionException; 7 | import dpq2.result: Result; 8 | import dpq2.value; 9 | import dpq2.oids: OidType; 10 | import derelict.pq.pq; 11 | import core.time: Duration, dur; 12 | import std.exception: enforce; 13 | 14 | /// Extends Connection by adding query methods 15 | /// 16 | /// Just use it as Connection.* methods. 17 | mixin template Queries() 18 | { 19 | /// Perform SQL query to DB 20 | /// It uses the old wire protocol and all values are returned in textual 21 | /// form. This means that the dpq2.conv.to_d_types.as template will likely 22 | /// not work for anything but strings. 23 | /// Try to use execParams instead, even if now parameters are present. 24 | immutable (Answer) exec( string SQLcmd ) 25 | { 26 | auto pgResult = PQexec(conn, toStringz( SQLcmd )); 27 | 28 | // is guaranteed by libpq that the result will not be changed until it will not be destroyed 29 | auto container = createResultContainer(cast(immutable) pgResult); 30 | 31 | return new immutable Answer(container); 32 | } 33 | 34 | /// Perform SQL query to DB 35 | immutable (Answer) execParams(scope ref const QueryParams qp) 36 | { 37 | auto p = InternalQueryParams(&qp); 38 | auto pgResult = PQexecParams ( 39 | conn, 40 | p.command, 41 | p.nParams, 42 | p.paramTypes, 43 | p.paramValues, 44 | p.paramLengths, 45 | p.paramFormats, 46 | p.resultFormat 47 | ); 48 | 49 | // is guaranteed by libpq that the result will not be changed until it will not be destroyed 50 | auto container = createResultContainer(cast(immutable) pgResult); 51 | 52 | return new immutable Answer(container); 53 | } 54 | 55 | /// Submits a command to the server without waiting for the result(s) 56 | void sendQuery( string SQLcmd ) 57 | { 58 | const size_t r = PQsendQuery( conn, toStringz(SQLcmd) ); 59 | if(r != 1) throw new ConnectionException(this, __FILE__, __LINE__); 60 | } 61 | 62 | /// Submits a command and separate parameters to the server without waiting for the result(s) 63 | void sendQueryParams(scope ref const QueryParams qp) 64 | { 65 | auto p = InternalQueryParams(&qp); 66 | size_t r = PQsendQueryParams ( 67 | conn, 68 | p.command, 69 | p.nParams, 70 | p.paramTypes, 71 | p.paramValues, 72 | p.paramLengths, 73 | p.paramFormats, 74 | p.resultFormat 75 | ); 76 | 77 | if(r != 1) throw new ConnectionException(this, __FILE__, __LINE__); 78 | } 79 | 80 | /// Sends a request to execute a prepared statement with given parameters, without waiting for the result(s) 81 | void sendQueryPrepared(scope ref const QueryParams qp) 82 | { 83 | auto p = InternalQueryParams(&qp); 84 | size_t r = PQsendQueryPrepared( 85 | conn, 86 | p.stmtName, 87 | p.nParams, 88 | p.paramValues, 89 | p.paramLengths, 90 | p.paramFormats, 91 | p.resultFormat 92 | ); 93 | 94 | if(r != 1) throw new ConnectionException(this, __FILE__, __LINE__); 95 | } 96 | 97 | /// Returns null if no notifies was received 98 | Notify getNextNotify() 99 | { 100 | consumeInput(); 101 | auto n = PQnotifies(conn); 102 | return n is null ? null : new Notify( n ); 103 | } 104 | 105 | /// Submits a request to create a prepared statement with the given parameters, and waits for completion. 106 | /// Returns: Result of query preparing 107 | immutable(Result) prepare(string statementName, string sqlStatement, in Oid[] oids = null) 108 | { 109 | PGresult* pgResult = PQprepare( 110 | conn, 111 | toStringz(statementName), 112 | toStringz(sqlStatement), 113 | oids.length.to!int, 114 | oids.ptr 115 | ); 116 | 117 | // is guaranteed by libpq that the result will not be changed until it will not be destroyed 118 | auto container = createResultContainer(cast(immutable) pgResult); 119 | 120 | return new immutable Result(container); 121 | } 122 | 123 | /// Submits a request to create a prepared statement with the given parameters, and waits for completion. 124 | /// 125 | /// Throws an exception if preparing failed. 126 | void prepareEx(string statementName, string sqlStatement, in Oid[] oids = null) 127 | { 128 | auto r = prepare(statementName, sqlStatement, oids); 129 | 130 | if(r.status != PGRES_COMMAND_OK) 131 | throw new ResponseException(r, __FILE__, __LINE__); 132 | } 133 | 134 | /// Submits a request to execute a prepared statement with given parameters, and waits for completion. 135 | immutable(Answer) execPrepared(scope ref const QueryParams qp) 136 | { 137 | auto p = InternalQueryParams(&qp); 138 | auto pgResult = PQexecPrepared( 139 | conn, 140 | p.stmtName, 141 | p.nParams, 142 | cast(const(char*)*)p.paramValues, 143 | p.paramLengths, 144 | p.paramFormats, 145 | p.resultFormat 146 | ); 147 | 148 | // is guaranteed by libpq that the result will not be changed until it will not be destroyed 149 | auto container = createResultContainer(cast(immutable) pgResult); 150 | 151 | return new immutable Answer(container); 152 | } 153 | 154 | /// Sends a request to create a prepared statement with the given parameters, without waiting for completion. 155 | void sendPrepare(string statementName, string sqlStatement, in Oid[] oids = null) 156 | { 157 | size_t r = PQsendPrepare( 158 | conn, 159 | toStringz(statementName), 160 | toStringz(sqlStatement), 161 | oids.length.to!int, 162 | oids.ptr 163 | ); 164 | 165 | if(r != 1) throw new ConnectionException(this, __FILE__, __LINE__); 166 | } 167 | 168 | /// Submits a request to obtain information about the specified prepared statement, and waits for completion. 169 | immutable(Answer) describePrepared(string statementName) 170 | { 171 | PGresult* pgResult = PQdescribePrepared(conn, toStringz(statementName)); 172 | 173 | // is guaranteed by libpq that the result will not be changed until it will not be destroyed 174 | auto container = createResultContainer(cast(immutable) pgResult); 175 | 176 | return new immutable Answer(container); 177 | } 178 | 179 | /// Submits a request to obtain information about the specified prepared statement, without waiting for completion. 180 | void sendDescribePrepared(string statementName) 181 | { 182 | size_t r = PQsendDescribePrepared(conn, statementName.toStringz); 183 | 184 | if(r != 1) throw new ConnectionException(this, __FILE__, __LINE__); 185 | } 186 | 187 | /// Submits a request to obtain information about the specified portal, and waits for completion. 188 | immutable(Answer) describePortal(string portalName) 189 | { 190 | PGresult* pgResult = PQdescribePortal(conn, portalName.toStringz); 191 | 192 | // is guaranteed by libpq that the result will not be changed until it will not be destroyed 193 | auto container = createResultContainer(cast(immutable) pgResult); 194 | 195 | return new immutable Answer(container); 196 | } 197 | 198 | /// Sends a buffer of CSV data to the COPY command 199 | /// 200 | /// Returns: true if the data was queued, false if it was not queued because of full buffers (this will only happen in nonblocking mode) 201 | bool putCopyData( string data ) 202 | { 203 | const int r = PQputCopyData(conn, data.toStringz, data.length.to!int); 204 | 205 | if(r == -1) throw new ConnectionException(this); 206 | 207 | return r != 0; 208 | } 209 | 210 | /// Signals that COPY data send is finished. Finalize and flush the COPY command. 211 | immutable(Answer) putCopyEnd() 212 | { 213 | assert(!isNonBlocking, "Only for blocking connections"); 214 | 215 | const bool r = sendPutCopyEnd; 216 | 217 | assert(r, "Impossible status for blocking connections"); 218 | 219 | // after the copying is finished, and there is no connection error, we must still get the command result 220 | // this will get if there is any errors in the process (invalid data format or constraint violation, etc.) 221 | auto pgResult = PQgetResult(conn); 222 | 223 | // is guaranteed by libpq that the result will not be changed until it will not be destroyed 224 | auto container = createResultContainer(cast(immutable) pgResult); 225 | 226 | return new immutable Answer(container); 227 | } 228 | 229 | /// Signals that COPY data send is finished. 230 | /// 231 | /// Returns: true if the termination data was sent, zero if it was not sent because the attempt would block (this case is only possible if the connection is in nonblocking mode) 232 | bool sendPutCopyEnd() 233 | { 234 | const char* error; 235 | const int r = PQputCopyEnd(conn, error); 236 | 237 | if(error !is null) throw new ConnectionException(error.to!string); 238 | 239 | if(r == -1) throw new ConnectionException(this); 240 | 241 | return r != 0; 242 | } 243 | 244 | // Waiting for completion of reading or writing 245 | // Returns: timeout is not occured 246 | version(integration_tests) 247 | bool waitEndOf(WaitType type, Duration timeout = Duration.zero) 248 | { 249 | import std.socket; 250 | 251 | auto socket = this.socket(); 252 | auto set = new SocketSet; 253 | set.add(socket); 254 | 255 | while(true) 256 | { 257 | if(status() == CONNECTION_BAD) 258 | throw new ConnectionException(this, __FILE__, __LINE__); 259 | 260 | if(poll() == PGRES_POLLING_OK) 261 | { 262 | return true; 263 | } 264 | else 265 | { 266 | size_t sockNum; 267 | 268 | with(WaitType) 269 | final switch(type) 270 | { 271 | case READ: 272 | sockNum = Socket.select(set, null, set, timeout); 273 | break; 274 | 275 | case WRITE: 276 | sockNum = Socket.select(null, set, set, timeout); 277 | break; 278 | 279 | case READ_WRITE: 280 | sockNum = Socket.select(set, set, set, timeout); 281 | break; 282 | } 283 | 284 | enforce(sockNum >= 0); 285 | if(sockNum == 0) return false; // timeout is occurred 286 | 287 | continue; 288 | } 289 | } 290 | } 291 | } 292 | 293 | version(integration_tests) 294 | enum WaitType 295 | { 296 | READ, 297 | WRITE, 298 | READ_WRITE 299 | } 300 | 301 | version (integration_tests) 302 | void _integration_test( string connParam ) @trusted 303 | { 304 | import dpq2.conv.to_d_types; 305 | import dpq2.conv.to_bson; 306 | import dpq2.connection: createTestConn; 307 | 308 | auto conn = createTestConn(connParam); 309 | 310 | // Text type arguments testing 311 | { 312 | string sql_query = 313 | "select now() as time, 'abc'::text as string, 123, 456.78\n"~ 314 | "union all\n"~ 315 | "select now(), 'абвгд'::text, 777, 910.11\n"~ 316 | "union all\n"~ 317 | "select NULL, 'ijk'::text, 789, 12345.115345"; 318 | 319 | auto a = conn.exec( sql_query ); 320 | 321 | assert( a.cmdStatus.length > 2 ); 322 | assert( a.columnCount == 4 ); 323 | assert( a.length == 3 ); 324 | assert( a.columnFormat(1) == ValueFormat.TEXT ); 325 | assert( a.columnFormat(2) == ValueFormat.TEXT ); 326 | } 327 | 328 | // Binary type arguments testing 329 | { 330 | import vibe.data.bson: Bson; 331 | 332 | const string sql_query = 333 | "select $1::text, $2::integer, $3::text, $4, $5::integer[]"; 334 | 335 | Value[5] args; 336 | args[0] = toValue("абвгд"); 337 | args[1] = Value(ValueFormat.BINARY, OidType.Undefined); // undefined type NULL value 338 | args[2] = toValue("123"); 339 | args[3] = Value(ValueFormat.BINARY, OidType.Int8); // NULL value 340 | 341 | Bson binArray = Bson([ 342 | Bson([Bson(null), Bson(123), Bson(456)]), 343 | Bson([Bson(0), Bson(789), Bson(null)]) 344 | ]); 345 | 346 | args[4] = bsonToValue(binArray); 347 | 348 | QueryParams p; 349 | p.sqlCommand = sql_query; 350 | p.args = args[]; 351 | 352 | auto a = conn.execParams( p ); 353 | 354 | foreach(i; 0 .. args.length) 355 | assert(a.columnFormat(i) == ValueFormat.BINARY); 356 | 357 | assert( a.OID(0) == OidType.Text ); 358 | assert( a.OID(1) == OidType.Int4 ); 359 | assert( a.OID(2) == OidType.Text ); 360 | assert( a.OID(3) == OidType.Int8 ); 361 | assert( a.OID(4) == OidType.Int4Array ); 362 | 363 | // binary args array test 364 | assert( a[0][4].as!Bson == binArray ); 365 | } 366 | 367 | { 368 | // Bug #52: empty text argument 369 | QueryParams p; 370 | Value v = toValue(""); 371 | 372 | p.sqlCommand = "SELECT $1"; 373 | p.args = [v]; 374 | 375 | auto a = conn.execParams(p); 376 | 377 | assert( !a[0][0].isNull ); 378 | assert( a[0][0].as!string == "" ); 379 | } 380 | 381 | // checking prepared statements 382 | { 383 | // uses PQprepare: 384 | conn.prepareEx("prepared statement 1", "SELECT $1::integer"); 385 | 386 | QueryParams p; 387 | p.preparedStatementName = "prepared statement 1"; 388 | p.args = [42.toValue]; 389 | auto r = conn.execPrepared(p); 390 | assert (r[0][0].as!int == 42); 391 | } 392 | { 393 | // uses PQsendPrepare: 394 | conn.sendPrepare("prepared statement 2", "SELECT $1::text, $2::integer"); 395 | 396 | conn.waitEndOf(WaitType.READ, dur!"seconds"(5)); 397 | conn.consumeInput(); 398 | 399 | immutable(Result)[] res; 400 | 401 | while(true) 402 | { 403 | auto r = conn.getResult(); 404 | if(r is null) break; 405 | res ~= r; 406 | } 407 | 408 | assert(res.length == 1); 409 | assert(res[0].status == PGRES_COMMAND_OK); 410 | } 411 | { 412 | // check prepared arg types and result types 413 | auto a = conn.describePrepared("prepared statement 2"); 414 | 415 | assert(a.nParams == 2); 416 | assert(a.paramType(0) == OidType.Text); 417 | assert(a.paramType(1) == OidType.Int4); 418 | } 419 | 420 | // checking portal description 421 | { 422 | conn.exec(`BEGIN`); 423 | conn.exec(`DECLARE test_cursor1 CURSOR FOR SELECT 123::integer`); 424 | auto r = conn.describePortal(`test_cursor1`); 425 | conn.exec(`COMMIT`); 426 | } 427 | 428 | { 429 | // async check prepared arg types and result types 430 | conn.sendDescribePrepared("prepared statement 2"); 431 | 432 | conn.waitEndOf(WaitType.READ, dur!"seconds"(5)); 433 | conn.consumeInput(); 434 | 435 | immutable(Result)[] res; 436 | 437 | while(true) 438 | { 439 | auto r = conn.getResult(); 440 | if(r is null) break; 441 | res ~= r; 442 | } 443 | 444 | assert(res.length == 1); 445 | assert(res[0].status == PGRES_COMMAND_OK); 446 | 447 | auto a = res[0].getAnswer; 448 | 449 | assert(a.nParams == 2); 450 | assert(a.paramType(0) == OidType.Text); 451 | assert(a.paramType(1) == OidType.Int4); 452 | } 453 | { 454 | QueryParams p; 455 | p.preparedStatementName = "prepared statement 2"; 456 | p.argsFromArray = ["abc", "123456"]; 457 | 458 | conn.sendQueryPrepared(p); 459 | 460 | conn.waitEndOf(WaitType.READ, dur!"seconds"(5)); 461 | conn.consumeInput(); 462 | 463 | immutable(Result)[] res; 464 | 465 | while(true) 466 | { 467 | auto r = conn.getResult(); 468 | if(r is null) break; 469 | res ~= r; 470 | } 471 | 472 | assert(res.length == 1); 473 | assert(res[0].getAnswer[0][0].as!PGtext == "abc"); 474 | assert(res[0].getAnswer[0][1].as!PGinteger == 123456); 475 | } 476 | { 477 | // test COPY 478 | conn.exec("CREATE TEMP TABLE test_copy (text_field TEXT, int_field INT8)"); 479 | 480 | conn.exec("COPY test_copy FROM STDIN WITH (FORMAT csv)"); 481 | conn.putCopyData("Val1,1\nval2,2\n"); 482 | conn.putCopyData("Val3,3\nval4,4\n"); 483 | conn.putCopyEnd(); 484 | 485 | auto res = conn.exec("SELECT count(text_field), sum(int_field) FROM test_copy"); 486 | assert(res.length == 1); 487 | assert(res[0][0].as!string == "4"); 488 | assert(res[0][1].as!string == "10"); 489 | 490 | // This time with error 491 | import std.exception: assertThrown; 492 | import dpq2.result: ResponseException; 493 | 494 | conn.exec("COPY test_copy FROM STDIN WITH (FORMAT csv)"); 495 | conn.putCopyData("Val1,2\nval2,4,POORLY_FORMATTED_CSV\n"); 496 | 497 | assertThrown!ResponseException(conn.putCopyEnd()); 498 | } 499 | 500 | import std.socket; 501 | conn.socket.shutdown(SocketShutdown.BOTH); // breaks connection 502 | 503 | { 504 | import dpq2.result: ResponseException; 505 | 506 | bool exceptionFlag = false; 507 | string errorMsg; 508 | 509 | try conn.exec("SELECT 'abc'::text").getAnswer; 510 | catch(ConnectionException e) 511 | { 512 | exceptionFlag = true; 513 | errorMsg = e.msg; 514 | assert(e.msg.length > 15); // error message check 515 | } 516 | catch(ResponseException e) 517 | { 518 | exceptionFlag = true; 519 | errorMsg = e.msg; 520 | assert(e.msg.length > 15); // error message check 521 | } 522 | finally { 523 | assert(exceptionFlag, errorMsg); 524 | } 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /src/dpq2/query_gen.d: -------------------------------------------------------------------------------- 1 | /// Generates SQL query with appropriate variable types 2 | module dpq2.query_gen; 3 | 4 | import dpq2.args: QueryParams; 5 | import dpq2.connection: Connection; 6 | import std.conv: to; 7 | import std.traits: isInstanceOf; 8 | import std.array: appender; 9 | import dpq2.conv.from_d_types: toValue; 10 | 11 | private enum ArgLikeIn 12 | { 13 | INSERT, // looks like "FieldName" and passes value as appropriate variable 14 | UPDATE, // looks like "FieldName" = $3 and passes value into appropriate dollar variable 15 | } 16 | 17 | private struct Arg(ArgLikeIn _argLikeIn, T) 18 | { 19 | enum argLikeIn = _argLikeIn; 20 | 21 | string name; 22 | T value; 23 | } 24 | 25 | private struct DollarArg(T) 26 | { 27 | T value; 28 | } 29 | 30 | /// INSERT-like argument 31 | auto i(T)(string statementArgName, T value) 32 | { 33 | return Arg!(ArgLikeIn.INSERT, T)(statementArgName, value); 34 | } 35 | 36 | /// UPDATE-like argument 37 | auto u(T)(string statementArgName, T value) 38 | { 39 | return Arg!(ArgLikeIn.UPDATE, T)(statementArgName, value); 40 | } 41 | 42 | /// Argument representing dollar, usable in SELECT statements 43 | auto d(T)(T value) 44 | { 45 | return DollarArg!T(value); 46 | } 47 | 48 | private struct CTStatement(SQL_CMD...) 49 | { 50 | QueryParams qp; 51 | alias qp this; 52 | 53 | this(Connection conn, SQL_CMD sqlCmd) 54 | { 55 | qp = parseSqlCmd!SQL_CMD(conn, sqlCmd); 56 | } 57 | } 58 | 59 | private string dollarsString(size_t num) 60 | { 61 | string ret; 62 | 63 | foreach(i; 1 .. num+1) 64 | { 65 | ret ~= `$`; 66 | ret ~= i.to!string; 67 | 68 | if(i < num) 69 | ret ~= `,`; 70 | } 71 | 72 | return ret; 73 | } 74 | 75 | private template isStatementArg(T) 76 | { 77 | enum isStatementArg = 78 | isInstanceOf!(Arg, T) || 79 | isInstanceOf!(DollarArg, T); 80 | } 81 | 82 | private bool symbolNeedsDelimit(dchar c) 83 | { 84 | import std.ascii: isAlphaNum; 85 | 86 | return c == '$' || c.isAlphaNum; 87 | } 88 | 89 | private void concatWithDelimiter(A, T)(ref A appender, T val) 90 | { 91 | 92 | if( 93 | val.length && 94 | appender.data.length && 95 | val[0].symbolNeedsDelimit && 96 | appender.data[$-1].symbolNeedsDelimit 97 | ) 98 | appender ~= ' '; 99 | 100 | appender ~= val; 101 | } 102 | 103 | private string escapeName(string s, Connection conn) 104 | { 105 | if(conn !is null) 106 | return conn.escapeIdentifier(s); 107 | else 108 | return '"'~s~'"'; 109 | } 110 | 111 | private QueryParams parseSqlCmd(SQL_CMD...)(Connection conn, SQL_CMD sqlCmd) 112 | { 113 | QueryParams qp; 114 | auto resultSql = appender!string; 115 | 116 | foreach(i, V; sqlCmd) 117 | { 118 | // argument variable is found? 119 | static if(isStatementArg!(typeof(V))) 120 | { 121 | // previous argument already was processed? 122 | static if(i > 0 && isStatementArg!(typeof(sqlCmd[i-1]))) 123 | { 124 | resultSql ~= `,`; 125 | } 126 | 127 | static if(isInstanceOf!(DollarArg, typeof(V))) 128 | { 129 | resultSql.concatWithDelimiter(`$`); 130 | resultSql ~= (qp.args.length + 1).to!string; 131 | } 132 | else static if(V.argLikeIn == ArgLikeIn.UPDATE) 133 | { 134 | resultSql ~= V.name.escapeName(conn); 135 | resultSql ~= `=$`; 136 | resultSql ~= (qp.args.length + 1).to!string; 137 | } 138 | else static if(V.argLikeIn == ArgLikeIn.INSERT) 139 | { 140 | resultSql ~= V.name.escapeName(conn); 141 | } 142 | else 143 | static assert(false); 144 | 145 | qp.args ~= V.value.toValue; 146 | } 147 | else 148 | { 149 | // Usable as INSERT VALUES ($1, $2, ...) argument 150 | static if(is(typeof(V) == Dollars)) 151 | { 152 | resultSql ~= dollarsString(qp.args.length); 153 | } 154 | else 155 | { 156 | // ordinary part of SQL statement 157 | resultSql.concatWithDelimiter(V); 158 | } 159 | } 160 | } 161 | 162 | qp.sqlCommand = resultSql[]; 163 | 164 | return qp; 165 | } 166 | 167 | struct Dollars {} 168 | 169 | /// 170 | auto wrapStatement(C : Connection, T...)(C conn, T statement) 171 | { 172 | return CTStatement!T(conn, statement); 173 | } 174 | 175 | /// 176 | auto wrapStatement(T...)(T statement) 177 | if(!is(T[0] == Connection)) 178 | { 179 | return CTStatement!T(null, statement); 180 | } 181 | 182 | unittest 183 | { 184 | auto stmnt = wrapStatement(`abc=`, d(123)); 185 | 186 | assert(stmnt.qp.sqlCommand == `abc=$1`); 187 | assert(stmnt.qp.args.length == 1); 188 | assert(stmnt.qp.args[0] == 123.toValue); 189 | } 190 | 191 | unittest 192 | { 193 | auto stmnt = wrapStatement( 194 | `SELECT`, d!string("abc"), d!int(123) 195 | ); 196 | 197 | assert(stmnt.qp.args.length == 2); 198 | assert(stmnt.qp.args[0] == "abc".toValue); 199 | assert(stmnt.qp.args[1] == 123.toValue); 200 | } 201 | 202 | unittest 203 | { 204 | auto stmnt = wrapStatement( 205 | `UPDATE table1`, 206 | `SET`, 207 | u(`boolean_field`, true), 208 | u(`integer_field`, 123), 209 | u(`text_field`, `abc`), 210 | ); 211 | 212 | assert(stmnt.qp.sqlCommand.length > 10); 213 | assert(stmnt.qp.args.length == 3); 214 | assert(stmnt.qp.args[0] == true.toValue); 215 | assert(stmnt.qp.args[1] == 123.toValue); 216 | assert(stmnt.qp.args[2] == `abc`.toValue); 217 | } 218 | 219 | unittest 220 | { 221 | int integer = 123; 222 | int another_integer = 456; 223 | string text = "abc"; 224 | 225 | auto stmnt = wrapStatement( 226 | `INSERT INTO table1 (`, 227 | i(`integer_field`, integer), 228 | i(`text_field`, text), 229 | `) WHERE`, 230 | u(`integer_field`, another_integer), 231 | `VALUES(`, Dollars(),`)` 232 | ); 233 | 234 | assert(stmnt.qp.sqlCommand.length > 10); 235 | assert(stmnt.qp.args[0] == 123.toValue); 236 | assert(stmnt.qp.args[1] == `abc`.toValue); 237 | assert(stmnt.qp.args[2] == 456.toValue); 238 | } 239 | 240 | version(integration_tests) 241 | void _integration_test(string connParam) 242 | { 243 | import dpq2.connection: createTestConn; 244 | 245 | auto conn = createTestConn(connParam); 246 | auto stmnt = wrapStatement(conn, i("Some Integer", 123)); 247 | 248 | assert(stmnt.qp.sqlCommand == `"Some Integer"`); 249 | assert(stmnt.qp.args.length == 1); 250 | assert(stmnt.qp.args[0] == 123.toValue); 251 | } 252 | -------------------------------------------------------------------------------- /src/dpq2/value.d: -------------------------------------------------------------------------------- 1 | /// 2 | module dpq2.value; 3 | 4 | import dpq2.oids; 5 | 6 | @safe: 7 | 8 | /** 9 | Represents table cell or argument value 10 | 11 | Internally it is a ubyte[]. 12 | If it returned by Answer methods it contains a reference to the data of 13 | the server answer and it can not be accessed after Answer is destroyed. 14 | */ 15 | struct Value 16 | { 17 | private 18 | { 19 | bool _isNull = true; 20 | OidType _oidType = OidType.Undefined; 21 | 22 | ValueFormat _format; 23 | } 24 | 25 | package immutable(ubyte)[] _data; 26 | 27 | // FIXME: 28 | // The pointer returned by PQgetvalue points to storage that is part of the PGresult structure. 29 | // One should not modify the data it points to, and one must explicitly copy the data into other 30 | // storage if it is to be used past the lifetime of the PGresult structure itself. 31 | // Thus, it is need to store reference to Answer here to ensure that result is still available. 32 | // (Also see DIP1000) 33 | /// ctor 34 | this(immutable(ubyte)[] data, in OidType oidType, bool isNull = false, in ValueFormat format = ValueFormat.BINARY) inout pure 35 | { 36 | import std.exception: enforce; 37 | 38 | //TODO: it is possible to skip this check for fixed-size values? 39 | enforce(data.length <= int.max, `data.length is too big for use as Postgres value`); 40 | 41 | this._data = data; 42 | this._format = format; 43 | this._oidType = oidType; 44 | this._isNull = isNull; 45 | } 46 | 47 | /// Null Value constructor 48 | this(in ValueFormat format, in OidType oidType) pure 49 | { 50 | this._format = format; 51 | this._oidType = oidType; 52 | } 53 | 54 | @safe const pure nothrow scope @nogc 55 | { 56 | /// Indicates if the value is NULL 57 | bool isNull() 58 | { 59 | return _isNull; 60 | } 61 | 62 | /// Indicates if the value is array type 63 | bool isArray() 64 | { 65 | return dpq2.oids.isSupportedArray(oidType); 66 | } 67 | alias isSupportedArray = isArray; //TODO: deprecate 68 | 69 | /// Returns Oid of the value 70 | OidType oidType() 71 | { 72 | return _oidType; 73 | } 74 | 75 | /// Returns ValueFormat representation (text or binary) 76 | ValueFormat format() 77 | { 78 | return _format; 79 | } 80 | } 81 | 82 | package void oidType(OidType type) @safe pure nothrow @nogc 83 | { 84 | _oidType = type; 85 | } 86 | 87 | //TODO: replace template by return modifier 88 | immutable(ubyte)[] data()() pure const scope 89 | { 90 | import std.exception; 91 | import core.exception; 92 | 93 | enforce!AssertError(!isNull, "Attempt to read SQL NULL value", __FILE__, __LINE__); 94 | 95 | return _data; 96 | } 97 | 98 | /// 99 | string toString() const @trusted 100 | { 101 | import dpq2.conv.to_d_types; 102 | import std.conv: to; 103 | import std.variant; 104 | 105 | return this.as!Variant.toString~"::"~oidType.to!string~"("~(format == ValueFormat.TEXT? "t" : "b")~")"; 106 | } 107 | } 108 | 109 | @system unittest 110 | { 111 | import dpq2.conv.to_d_types; 112 | import core.exception: AssertError; 113 | 114 | Value v = Value(ValueFormat.BINARY, OidType.Int4); 115 | 116 | bool exceptionFlag = false; 117 | 118 | try 119 | cast(void) v.as!int; 120 | catch(AssertError e) 121 | exceptionFlag = true; 122 | 123 | assert(exceptionFlag); 124 | } 125 | 126 | /// 127 | enum ValueFormat : int { 128 | TEXT, /// 129 | BINARY /// 130 | } 131 | 132 | import std.conv: to, ConvException; 133 | 134 | /// Conversion exception types 135 | enum ConvExceptionType 136 | { 137 | NOT_ARRAY, /// Format of the value isn't array 138 | NOT_BINARY, /// Format of the column isn't binary 139 | NOT_TEXT, /// Format of the column isn't text string 140 | NOT_IMPLEMENTED, /// Support of this type isn't implemented (or format isn't matches to specified D type) 141 | SIZE_MISMATCH, /// Value size is not matched to the Postgres value or vice versa 142 | CORRUPTED_JSONB, /// Corrupted JSONB value 143 | DATE_VALUE_OVERFLOW, /// Date value isn't fits to Postgres binary Date value 144 | DIMENSION_MISMATCH, /// Array dimension size is not matched to the Postgres array 145 | CORRUPTED_ARRAY, /// Corrupted array value 146 | OUT_OF_RANGE, /// Index is out of range 147 | TOO_PRECISE, /// Too precise value can't be stored in destination variable 148 | } 149 | 150 | /// Value conversion exception 151 | class ValueConvException : ConvException 152 | { 153 | const ConvExceptionType type; /// Exception type 154 | 155 | this(ConvExceptionType t, string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) pure @safe 156 | { 157 | type = t; 158 | super(msg, file, line, next); 159 | } 160 | } 161 | 162 | package void throwTypeComplaint(OidType receivedType, in string expectedType, string file = __FILE__, size_t line = __LINE__) pure 163 | { 164 | throwTypeComplaint(receivedType.to!string, expectedType, file, line); 165 | } 166 | 167 | package void throwTypeComplaint(string receivedTypeName, in string expectedType, string file = __FILE__, size_t line = __LINE__) pure 168 | { 169 | throw new ValueConvException( 170 | ConvExceptionType.NOT_IMPLEMENTED, 171 | "Format of the column ("~receivedTypeName~") doesn't match to D native "~expectedType, 172 | file, line 173 | ); 174 | } 175 | --------------------------------------------------------------------------------