├── docs ├── .git_preserve_dir └── _config.yml ├── .gitignore ├── .github └── workflows │ ├── compilers.json │ ├── docs.yml │ └── ci.yml ├── src └── dpq2 │ ├── exception.d │ ├── package.d │ ├── conv │ ├── jsonb.d │ ├── numeric.d │ ├── to_variant.d │ ├── from_bson.d │ ├── inet.d │ ├── to_bson.d │ ├── arrays.d │ ├── to_d_types.d │ ├── geometric.d │ ├── time.d │ └── native_tests.d │ ├── socket_stuff.d │ ├── cancellation.d │ ├── args.d │ ├── value.d │ ├── dynloader.d │ ├── query_gen.d │ ├── oids.d │ ├── connection.d │ └── query.d ├── integration_tests └── integration_tests.d ├── LICENSE ├── dub.json ├── example └── example.d └── README.md /docs/.git_preserve_dir: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | .dub 3 | dub.selections.json 4 | *_integration_* 5 | *.lst 6 | docs 7 | docs.json 8 | -------------------------------------------------------------------------------- /.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 | ] -------------------------------------------------------------------------------- /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/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.cancellation; 10 | public import dpq2.connection; 11 | public import dpq2.query; 12 | public import dpq2.result; 13 | public import dpq2.oids; 14 | 15 | 16 | version(Dpq2_Static){} 17 | else version(Dpq2_Dynamic){} 18 | else static assert(false, "dpq2 link type (dynamic or static) isn't defined"); 19 | -------------------------------------------------------------------------------- /src/dpq2/conv/jsonb.d: -------------------------------------------------------------------------------- 1 | /// 2 | module dpq2.conv.jsonb; 3 | 4 | @safe: 5 | 6 | import vibe.data.json; 7 | import dpq2.value; 8 | import dpq2.oids: OidType; 9 | 10 | package: 11 | 12 | import std.string; 13 | import std.conv: to; 14 | 15 | /// 16 | Json jsonbValueToJson(in Value v) 17 | { 18 | assert(v.oidType == OidType.Jsonb); 19 | 20 | if(v.data[0] != 1) 21 | throw new ValueConvException( 22 | ConvExceptionType.CORRUPTED_JSONB, 23 | "Unknown jsonb format byte: "~v._data[0].to!string, 24 | __FILE__, __LINE__ 25 | ); 26 | 27 | string s = (cast(const(char[])) v._data[1 .. $]).to!string; 28 | 29 | return parseJsonString(s); 30 | } 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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": { 12 | "version": "b5bb17f582dbfb78dda8ebb99e0205f219b65fd1", 13 | "repository": "git+https://github.com/denizzzka/DerelictPQ.git" 14 | }, 15 | "vibe-serialization": "~>1.0.4", 16 | "money": "~>3.0.2" 17 | }, 18 | "targetType": "sourceLibrary", 19 | "libs-windows": ["ws2_32"], 20 | "-ddoxTool": "scod", 21 | "configurations": [ 22 | { 23 | "name": "static", 24 | "versions": ["Dpq2_Static"], 25 | "subConfigurations": { 26 | "derelict-pq": "derelict-pq-static" 27 | }, 28 | "libs": ["pq"] 29 | }, 30 | { 31 | "name": "dynamic-unmanaged", 32 | "versions": ["Dpq2_Static"], 33 | "subConfigurations": { 34 | "derelict-pq": "derelict-pq-dynamic" 35 | } 36 | }, 37 | { 38 | "name": "dynamic", 39 | "versions": ["Dpq2_Dynamic"], 40 | "subConfigurations": { 41 | "derelict-pq": "derelict-pq-dynamic" 42 | } 43 | } 44 | ], 45 | "subPackages": [ 46 | { 47 | "name": "integration_tests", 48 | "targetType": "executable", 49 | "dflags-dmd": ["-preview=in"], 50 | "dependencies": 51 | { 52 | "dpq2": { "version": "*", "dflags-dmd": ["-preview=in"] }, 53 | "vibe-serialization": { "version": "*", "dflags-dmd": ["-preview=in"] }, 54 | "vibe-core": { "version": "*", "dflags-dmd": ["-preview=in"] }, 55 | "gfm:math": "~>8.0.6" 56 | }, 57 | "configurations": [ 58 | { 59 | "name": "dynamic", 60 | "subConfigurations": { 61 | "dpq2": "dynamic" 62 | } 63 | }, 64 | { 65 | "name": "dynamic-unmanaged", 66 | "versions": ["Test_Dynamic_Unmanaged"], 67 | "subConfigurations": { 68 | "derelict-pq": "derelict-pq-dynamic", 69 | "dpq2": "dynamic-unmanaged" 70 | } 71 | }, 72 | { 73 | "name": "static", 74 | "subConfigurations": { 75 | "dpq2": "static" 76 | } 77 | } 78 | ], 79 | "sourcePaths": [ "integration_tests" ], 80 | "versions": ["integration_tests"] 81 | }, 82 | { 83 | "name": "example", 84 | "targetType": "executable", 85 | "dflags": ["-preview=in"], 86 | "dependencies": 87 | { 88 | "dpq2": { "version": "*", "dflags": ["-preview=in"] }, 89 | "vibe-serialization": { "version": "*", "dflags": ["-preview=in"] }, 90 | }, 91 | "sourcePaths": [ "example" ] 92 | } 93 | ] 94 | } 95 | -------------------------------------------------------------------------------- /src/dpq2/socket_stuff.d: -------------------------------------------------------------------------------- 1 | /// 2 | module dpq2.socket_stuff; 3 | 4 | import dpq2.connection: ConnectionException; 5 | import std.socket; 6 | 7 | /// Obtains duplicate file descriptor number of the socket 8 | version(Posix) 9 | socket_t duplicateSocket(int socket) 10 | { 11 | import core.sys.posix.unistd: dup; 12 | 13 | static assert(socket_t.sizeof == int.sizeof); 14 | 15 | int ret = dup(socket); 16 | 17 | if(ret == -1) 18 | throw new ConnectionException("Socket duplication error"); 19 | 20 | return cast(socket_t) ret; 21 | } 22 | 23 | /// Obtains duplicate file descriptor number of the socket 24 | version(Windows) 25 | SOCKET duplicateSocket(int socket) 26 | { 27 | import core.stdc.stdlib: malloc, free; 28 | import core.sys.windows.winbase: GetCurrentProcessId; 29 | 30 | static assert(SOCKET.sizeof == socket_t.sizeof); 31 | 32 | auto protocolInfo = cast(WSAPROTOCOL_INFOW*) malloc(WSAPROTOCOL_INFOW.sizeof); 33 | scope(failure) free(protocolInfo); 34 | 35 | int dupStatus = WSADuplicateSocketW(socket, GetCurrentProcessId, protocolInfo); 36 | 37 | if(dupStatus) 38 | throw new ConnectionException("WSADuplicateSocketW error, code "~WSAGetLastError().to!string); 39 | 40 | SOCKET s = WSASocketW( 41 | FROM_PROTOCOL_INFO, 42 | FROM_PROTOCOL_INFO, 43 | FROM_PROTOCOL_INFO, 44 | protocolInfo, 45 | 0, 46 | 0 47 | ); 48 | 49 | if(s == INVALID_SOCKET) 50 | throw new ConnectionException("WSASocket error, code "~WSAGetLastError().to!string); 51 | 52 | return s; 53 | } 54 | 55 | // Socket duplication structs for Win32 56 | version(Windows) 57 | private 58 | { 59 | import core.sys.windows.windef; 60 | import core.sys.windows.basetyps: GUID; 61 | 62 | alias GROUP = uint; 63 | 64 | enum INVALID_SOCKET = 0; 65 | enum FROM_PROTOCOL_INFO =-1; 66 | enum MAX_PROTOCOL_CHAIN = 7; 67 | enum WSAPROTOCOL_LEN = 255; 68 | 69 | struct WSAPROTOCOLCHAIN 70 | { 71 | int ChainLen; 72 | DWORD[MAX_PROTOCOL_CHAIN] ChainEntries; 73 | } 74 | 75 | struct WSAPROTOCOL_INFOW 76 | { 77 | DWORD dwServiceFlags1; 78 | DWORD dwServiceFlags2; 79 | DWORD dwServiceFlags3; 80 | DWORD dwServiceFlags4; 81 | DWORD dwProviderFlags; 82 | GUID ProviderId; 83 | DWORD dwCatalogEntryId; 84 | WSAPROTOCOLCHAIN ProtocolChain; 85 | int iVersion; 86 | int iAddressFamily; 87 | int iMaxSockAddr; 88 | int iMinSockAddr; 89 | int iSocketType; 90 | int iProtocol; 91 | int iProtocolMaxOffset; 92 | int iNetworkByteOrder; 93 | int iSecurityScheme; 94 | DWORD dwMessageSize; 95 | DWORD dwProviderReserved; 96 | WCHAR[WSAPROTOCOL_LEN+1] szProtocol; 97 | } 98 | 99 | extern(Windows) nothrow @nogc 100 | { 101 | import core.sys.windows.winsock2: WSAGetLastError; 102 | int WSADuplicateSocketW(SOCKET s, DWORD dwProcessId, WSAPROTOCOL_INFOW* lpProtocolInfo); 103 | SOCKET WSASocketW(int af, int type, int protocol, WSAPROTOCOL_INFOW*, GROUP, DWORD dwFlags); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/dpq2/cancellation.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents connection over which a cancel request can be sent 3 | * 4 | * Most functions is correspond to those in the documentation of Postgres: 5 | * $(HTTPS https://www.postgresql.org/docs/current/static/libpq.html) 6 | */ 7 | module dpq2.cancellation; 8 | 9 | import dpq2.connection: Connection; 10 | import dpq2.exception; 11 | 12 | import derelict.pq.pq; 13 | import std.conv: to; 14 | 15 | /// Represents query cancellation process (cancellation connection) 16 | class Cancellation 17 | { 18 | version(Dpq2_Dynamic) 19 | { 20 | import dpq2.dynloader: ReferenceCounter; 21 | private immutable ReferenceCounter dynLoaderRefCnt; 22 | } 23 | 24 | protected PGcancelConn* cancelConn; 25 | 26 | /// 27 | this(Connection c) 28 | { 29 | version(Dpq2_Dynamic) dynLoaderRefCnt = ReferenceCounter(true); 30 | 31 | cancelConn = PQcancelCreate(c.conn); 32 | 33 | if(status != CONNECTION_ALLOCATED) 34 | throw new CancellationException(errorMessage); 35 | } 36 | 37 | /// 38 | ~this() 39 | { 40 | if(cancelConn !is null) 41 | PQcancelFinish(cancelConn); 42 | 43 | version(Dpq2_Dynamic) dynLoaderRefCnt.__custom_dtor(); 44 | } 45 | 46 | /// Returns the status of the cancel connection 47 | ConnStatusType status() const nothrow 48 | { 49 | return PQcancelStatus(cancelConn); 50 | } 51 | 52 | /// Returns the error message most recently generated by an operation on the cancel connection 53 | string errorMessage() const 54 | { 55 | return PQcancelErrorMessage(cancelConn).to!string; 56 | } 57 | 58 | /** 59 | Requests that the server abandons processing of the current command in a blocking manner 60 | 61 | Throws exception if cancel request was not successfully dispatched. 62 | 63 | Successful dispatch is no guarantee that the request will have any 64 | effect, however. If the cancellation is effective, the current 65 | command will terminate early and return an error result 66 | (exception). If the cancellation fails (say, because the server 67 | was already done processing the command), then there will be no 68 | visible result at all. 69 | */ 70 | void doCancelBlocking() 71 | { 72 | auto res = PQcancelBlocking(cancelConn); 73 | 74 | if(res != 1) 75 | throw new CancellationException(errorMessage); 76 | } 77 | 78 | /// Requests that the server abandons processing of the current command in a non-blocking manner 79 | void start() 80 | { 81 | if(PQcancelStart(cancelConn) != 1) 82 | throw new CancellationException(errorMessage); 83 | } 84 | 85 | /// 86 | PostgresPollingStatusType poll() nothrow 87 | { 88 | return PQcancelPoll(cancelConn); 89 | } 90 | 91 | /// 92 | auto socket() 93 | { 94 | import dpq2.socket_stuff: duplicateSocket; 95 | 96 | auto s = PQcancelSocket(cancelConn); 97 | 98 | if(s == -1) 99 | throw new CancellationException(errorMessage); 100 | 101 | return s; 102 | } 103 | } 104 | 105 | /// 106 | class CancellationException : Dpq2Exception 107 | { 108 | this(string msg, string file = __FILE__, size_t line = __LINE__) 109 | { 110 | super(msg, file, line); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /example/example.d: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rdmd 2 | 3 | import dpq2; 4 | import std.getopt; 5 | import std.stdio: writefln, writeln; 6 | import std.typecons: Nullable; 7 | import std.variant: Variant; 8 | import vibe.data.bson; 9 | 10 | void main(string[] args) 11 | { 12 | string connInfo; 13 | getopt(args, "conninfo", &connInfo); 14 | 15 | Connection conn = new Connection(connInfo); 16 | 17 | // Only text query result can be obtained by this call: 18 | auto answer = conn.exec( 19 | "SELECT now()::timestamp as current_time, 'abc'::text as field_name, "~ 20 | "123 as field_3, 456.78 as field_4, '{\"JSON field name\": 123.456}'::json" 21 | ); 22 | 23 | writeln( "Text query result by name: ", answer[0]["current_time"].as!string ); 24 | writeln( "Text query result by index: ", answer[0][3].as!string ); 25 | 26 | // Binary arguments query with binary result: 27 | QueryParams p; 28 | p.sqlCommand = "SELECT "~ 29 | "$1::double precision as double_field, "~ 30 | "$2::text, "~ 31 | "$3::text as null_field, "~ 32 | "array['first', 'second', NULL]::text[] as array_field, "~ 33 | "$4::integer[] as multi_array, "~ 34 | "'{\"float_value\": 123.456,\"text_str\": \"text string\"}'::json as json_value"; 35 | 36 | p.argsVariadic( 37 | -1234.56789012345, 38 | "first line\nsecond line", 39 | Nullable!string.init, 40 | [[1, 2, 3], [4, 5, 6]] 41 | ); 42 | 43 | auto r = conn.execParams(p); 44 | scope(exit) destroy(r); 45 | 46 | writeln( "0: ", r[0]["double_field"].as!double ); 47 | writeln( "1: ", r.oneRow[1].as!string ); // .oneRow additionally checks that here is only one row was returned 48 | writeln( "2.1 isNull: ", r[0][2].isNull ); 49 | writeln( "2.2 isNULL: ", r[0].isNULL(2) ); 50 | writeln( "3.1: ", r[0][3].asArray[0].as!string ); 51 | writeln( "3.2: ", r[0][3].asArray[1].as!string ); 52 | writeln( "3.3: ", r[0]["array_field"].asArray[2].isNull ); 53 | writeln( "3.4: ", r[0]["array_field"].asArray.isNULL(2) ); 54 | writeln( "4.1: ", r[0]["multi_array"].asArray.getValue(1, 2).as!int ); 55 | writeln( "4.2: ", r[0]["multi_array"].as!(int[][]) ); 56 | writeln( "5.1 Json: ", r[0]["json_value"].as!Json); 57 | writeln( "5.2 Bson: ", r[0]["json_value"].as!Bson); 58 | 59 | // It is possible to read values of unknown type 60 | // using std.variant.Variant or vibe.data.bson.Bson: 61 | for(auto column = 0; column < r.columnCount; column++) 62 | { 63 | writeln( 64 | "column: '", r.columnName(column), "', ", 65 | "Variant: ", r[0][column].as!Variant, ", ", 66 | "Bson: ", r[0][column].as!Bson 67 | ); 68 | } 69 | 70 | // It is possible to upload CSV data ultra-fast: 71 | conn.exec("CREATE TEMP TABLE test_dpq2_copy (v1 TEXT, v2 INT)"); 72 | 73 | // Init the COPY command. This sets the connection in a COPY receive 74 | // mode until putCopyEnd() is called. Copy CSV data, because it's standard, 75 | // ultra fast, and readable: 76 | conn.exec("COPY test_dpq2_copy FROM STDIN WITH (FORMAT csv)"); 77 | 78 | // Write 2 lines of CSV, including text that contains the delimiter. 79 | // Postgresql handles it well: 80 | string data = "\"This, right here, is a test\",8\nWow! it works,13\n"; 81 | conn.putCopyData(data); 82 | 83 | // Write 2 more lines 84 | data = "Horray!,3456\nSuper fast!,325\n"; 85 | conn.putCopyData(data); 86 | 87 | // Signal that the COPY is finished. Let Postgresql finalize the command 88 | // and return any errors with the data. 89 | conn.putCopyEnd(); 90 | 91 | import std.range: enumerate; 92 | 93 | // rangify() template helps to iterate over Answer and Row: 94 | auto few_rows = conn.exec("SELECT v1, v2 FROM test_dpq2_copy"); 95 | foreach(row_num, row; few_rows.rangify.enumerate) 96 | { 97 | foreach(cell; row.rangify) 98 | writefln("row_num: %d value: %s", row_num, cell.as!Bson); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 test --d-version=NO_VARIANT 122 | dub run dpq2:integration_tests --build=unittest-cov -- --conninfo="$CONN_STRING" 123 | dub run dpq2:integration_tests --build=cov -- --conninfo="$CONN_STRING" 124 | dub run dpq2:example --build=release -- --conninfo="$CONN_STRING" 125 | dub run doveralls 126 | shell: bash 127 | - name: Upload coverage data 128 | if: matrix.build == 'tests_and_cov' 129 | uses: codecov/codecov-action@v2 130 | -------------------------------------------------------------------------------- /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 | version(NO_VARIANT) { 99 | } else { 100 | /// 101 | string toString() const @trusted 102 | { 103 | import dpq2.conv.to_d_types; 104 | import std.conv: to; 105 | import std.variant; 106 | 107 | return this.as!Variant.toString~"::"~oidType.to!string~"("~(format == ValueFormat.TEXT? "t" : "b")~")"; 108 | } 109 | } 110 | } 111 | 112 | @system unittest 113 | { 114 | import dpq2.conv.to_d_types; 115 | import core.exception: AssertError; 116 | 117 | Value v = Value(ValueFormat.BINARY, OidType.Int4); 118 | 119 | bool exceptionFlag = false; 120 | 121 | try 122 | cast(void) v.as!int; 123 | catch(AssertError e) 124 | exceptionFlag = true; 125 | 126 | assert(exceptionFlag); 127 | } 128 | 129 | /// 130 | enum ValueFormat : int { 131 | TEXT, /// 132 | BINARY /// 133 | } 134 | 135 | import std.conv: to, ConvException; 136 | 137 | /// Conversion exception types 138 | enum ConvExceptionType 139 | { 140 | NOT_ARRAY, /// Format of the value isn't array 141 | NOT_BINARY, /// Format of the column isn't binary 142 | NOT_TEXT, /// Format of the column isn't text string 143 | NOT_IMPLEMENTED, /// Support of this type isn't implemented (or format isn't matches to specified D type) 144 | SIZE_MISMATCH, /// Value size is not matched to the Postgres value or vice versa 145 | CORRUPTED_JSONB, /// Corrupted JSONB value 146 | DATE_VALUE_OVERFLOW, /// Date value isn't fits to Postgres binary Date value 147 | DIMENSION_MISMATCH, /// Array dimension size is not matched to the Postgres array 148 | CORRUPTED_ARRAY, /// Corrupted array value 149 | OUT_OF_RANGE, /// Index is out of range 150 | TOO_PRECISE, /// Too precise value can't be stored in destination variable 151 | } 152 | 153 | /// Value conversion exception 154 | class ValueConvException : ConvException 155 | { 156 | const ConvExceptionType type; /// Exception type 157 | 158 | this(ConvExceptionType t, string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) pure @safe 159 | { 160 | type = t; 161 | super(msg, file, line, next); 162 | } 163 | } 164 | 165 | package void throwTypeComplaint(OidType receivedType, in string expectedType, string file = __FILE__, size_t line = __LINE__) pure 166 | { 167 | throwTypeComplaint(receivedType.to!string, expectedType, file, line); 168 | } 169 | 170 | package void throwTypeComplaint(string receivedTypeName, in string expectedType, string file = __FILE__, size_t line = __LINE__) pure 171 | { 172 | throw new ValueConvException( 173 | ConvExceptionType.NOT_IMPLEMENTED, 174 | "Format of the column ("~receivedTypeName~") doesn't match to D native "~expectedType, 175 | file, line 176 | ); 177 | } 178 | -------------------------------------------------------------------------------- /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/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/conv/to_variant.d: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | module dpq2.conv.to_variant; 4 | 5 | version(NO_VARIANT) { 6 | /* Without std.variant dpq2 compiles significantly faster, and often the 7 | * ability explore unknown database schemas is not needed, removing the need 8 | * for a Variant type. 9 | */ 10 | } else { 11 | 12 | import dpq2.value; 13 | import dpq2.oids: OidType; 14 | import dpq2.result: ArrayProperties; 15 | import dpq2.conv.inet: InetAddress, CidrAddress; 16 | import dpq2.conv.to_d_types; 17 | import dpq2.conv.numeric: rawValueToNumeric; 18 | import dpq2.conv.time: TimeStampUTC; 19 | static import geom = dpq2.conv.geometric; 20 | import std.bitmanip: bigEndianToNative, BitArray; 21 | import std.datetime: SysTime, dur, TimeZone, UTC; 22 | import std.conv: to; 23 | import std.typecons: Nullable; 24 | import std.uuid; 25 | import std.variant: Variant; 26 | import vibe.data.json: VibeJson = Json; 27 | 28 | /// 29 | Variant toVariant(bool isNullablePayload = true)(in Value v) @safe 30 | { 31 | auto getNative(T)() 32 | if(!is(T == Variant)) 33 | { 34 | static if(isNullablePayload) 35 | { 36 | Nullable!T ret; 37 | 38 | if (v.isNull) 39 | return ret; 40 | 41 | ret = v.as!T; 42 | 43 | return ret; 44 | } 45 | else 46 | { 47 | return v.as!T; 48 | } 49 | } 50 | 51 | Variant retVariant(T)() @trusted 52 | { 53 | return Variant(getNative!T); 54 | } 55 | 56 | if(v.format == ValueFormat.TEXT) 57 | return retVariant!string; 58 | 59 | template retArray__(NativeT) 60 | { 61 | /* 62 | Variant storage haven't heuristics to understand 63 | what array elements can contain NULLs. So, to 64 | simplify things, if declared that cell itself is 65 | not nullable then we decease that array elements 66 | also can't contain NULL values 67 | */ 68 | static if(isNullablePayload) 69 | alias ArrType = Nullable!NativeT; 70 | else 71 | alias ArrType = NativeT; 72 | 73 | auto retArray__() @trusted 74 | { 75 | if(isNullablePayload && v.isNull) 76 | { 77 | /* 78 | One-dimensional array return is used here only to 79 | highlight that the value contains an array. For 80 | NULL cell we can determine only its type, but not 81 | the number of dimensions 82 | */ 83 | return Variant( 84 | Nullable!(ArrType[]).init 85 | ); 86 | } 87 | 88 | import dpq2.conv.arrays: ab = binaryValueAs; 89 | 90 | const ap = ArrayProperties(v); 91 | 92 | switch(ap.dimsSize.length) 93 | { 94 | case 0: return Variant(v.ab!(ArrType[])); // PG can return zero-dimensional arrays 95 | case 1: return Variant(v.ab!(ArrType[])); 96 | case 2: return Variant(v.ab!(ArrType[][])); 97 | case 3: return Variant(v.ab!(ArrType[][][])); 98 | case 4: return Variant(v.ab!(ArrType[][][][])); 99 | default: throw new ValueConvException( 100 | ConvExceptionType.DIMENSION_MISMATCH, 101 | "Attempt to convert an array of dimension "~ap.dimsSize.length.to!string~" to type Variant: dimensions greater than 4 are not supported" 102 | ); 103 | } 104 | } 105 | } 106 | 107 | with(OidType) 108 | switch(v.oidType) 109 | { 110 | case Bool: return retVariant!PGboolean; 111 | case BoolArray: return retArray__!PGboolean; 112 | 113 | case Int2: return retVariant!short; 114 | case Int2Array: return retArray__!short; 115 | 116 | case Int4: return retVariant!int; 117 | case Int4Array: return retArray__!int; 118 | 119 | case Int8: return retVariant!long; 120 | case Int8Array: return retArray__!long; 121 | 122 | case Float4: return retVariant!float; 123 | case Float4Array: return retArray__!float; 124 | 125 | case Float8: return retVariant!double; 126 | case Float8Array: return retArray__!double; 127 | 128 | case Numeric: 129 | case Text: 130 | case FixedString: 131 | case VariableString: 132 | return retVariant!string; 133 | 134 | case NumericArray: 135 | case TextArray: 136 | case FixedStringArray: 137 | case VariableStringArray: 138 | return retArray__!string; 139 | 140 | case ByteArray: return retVariant!PGbytea; 141 | 142 | case UUID: return retVariant!PGuuid; 143 | case UUIDArray: return retArray__!PGuuid; 144 | 145 | case Date: return retVariant!PGdate; 146 | case DateArray: return retArray__!PGdate; 147 | 148 | case HostAddress: return retVariant!InetAddress; 149 | case HostAddressArray: return retArray__!InetAddress; 150 | 151 | case NetworkAddress: return retVariant!CidrAddress; 152 | case NetworkAddressArray: return retArray__!CidrAddress; 153 | 154 | case Time: return retVariant!PGtime_without_time_zone; 155 | case TimeArray: return retArray__!PGtime_without_time_zone; 156 | 157 | case TimeWithZone: return retVariant!PGtime_with_time_zone; 158 | case TimeWithZoneArray: return retArray__!PGtime_with_time_zone; 159 | 160 | case TimeStamp: return retVariant!PGtimestamp; 161 | case TimeStampArray: return retArray__!PGtimestamp; 162 | 163 | case TimeStampWithZone: return retVariant!PGtimestamptz; 164 | case TimeStampWithZoneArray: return retArray__!PGtimestamptz; 165 | 166 | case TimeInterval: return retVariant!PGinterval; 167 | 168 | case Json: 169 | case Jsonb: 170 | return retVariant!VibeJson; 171 | 172 | case JsonArray: 173 | case JsonbArray: 174 | return retArray__!VibeJson; 175 | 176 | case Line: return retVariant!(geom.Line); 177 | case LineArray: return retArray__!(geom.Line); 178 | 179 | default: 180 | throw new ValueConvException( 181 | ConvExceptionType.NOT_IMPLEMENTED, 182 | "Format of the column ("~to!(immutable(char)[])(v.oidType)~") doesn't supported by Value to Variant converter", 183 | __FILE__, __LINE__ 184 | ); 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /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/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/inet.d: -------------------------------------------------------------------------------- 1 | module dpq2.conv.inet; 2 | 3 | import dpq2.conv.to_d_types; 4 | import dpq2.oids: OidType; 5 | import dpq2.value; 6 | 7 | import std.bitmanip: bigEndianToNative, nativeToBigEndian; 8 | import std.conv: to; 9 | import std.socket; 10 | 11 | @safe: 12 | 13 | enum PgFamily : ubyte { 14 | PGSQL_AF_INET = AddressFamily.INET, 15 | PGSQL_AF_INET6, 16 | } 17 | 18 | alias InetAddress = TInetAddress!false; /// Represents inet PG value 19 | alias CidrAddress = TInetAddress!true; /// Represents cidr PG value 20 | 21 | /// 22 | package struct TInetAddress (bool isCIDR) 23 | { 24 | PgFamily family; 25 | ubyte netmask; 26 | AddrValue addr; 27 | alias addr this; 28 | 29 | /// 30 | this(in InternetAddress ia, ubyte mask = 32) 31 | { 32 | addr4 = ia.addr; 33 | family = PgFamily.PGSQL_AF_INET; 34 | netmask = mask; 35 | } 36 | 37 | /// 38 | this(in Internet6Address ia, ubyte mask = 128) 39 | { 40 | addr6 = ia.addr; 41 | family = PgFamily.PGSQL_AF_INET6; 42 | netmask = mask; 43 | } 44 | 45 | /// 46 | Address createStdAddr(ushort port = InternetAddress.PORT_ANY) const 47 | { 48 | switch(family) 49 | { 50 | case PgFamily.PGSQL_AF_INET: 51 | return new InternetAddress(addr4, port); 52 | 53 | case PgFamily.PGSQL_AF_INET6: 54 | return new Internet6Address(addr6, port); 55 | 56 | default: 57 | assert(0, family.unsup); 58 | } 59 | } 60 | 61 | /// 62 | auto toString() const 63 | { 64 | import std.format: format; 65 | 66 | switch (family) 67 | { 68 | case PgFamily.PGSQL_AF_INET: 69 | case PgFamily.PGSQL_AF_INET6: 70 | return format("%s/%d", createStdAddr.toAddrString, this.netmask); 71 | 72 | default: 73 | return family.unsup; 74 | } 75 | } 76 | } 77 | 78 | unittest 79 | { 80 | auto std_addr = new InternetAddress("127.0.0.1", 123); 81 | auto pg_addr = InetAddress(std_addr); 82 | 83 | assert(pg_addr.createStdAddr.toAddrString == "127.0.0.1"); 84 | 85 | auto v = pg_addr.toValue; 86 | assert(v.binaryValueAs!InetAddress.toString == "127.0.0.1/32"); 87 | assert(v.binaryValueAs!InetAddress.createStdAddr.toAddrString == "127.0.0.1"); 88 | assert(v.binaryValueAs!InetAddress == pg_addr); 89 | } 90 | 91 | unittest 92 | { 93 | auto std_addr = new Internet6Address("::1", 123); 94 | auto pg_addr = InetAddress(std_addr); 95 | 96 | assert(pg_addr.createStdAddr.toAddrString == "::1"); 97 | 98 | auto v = pg_addr.toValue; 99 | assert(v.binaryValueAs!InetAddress.toString == "::1/128"); 100 | assert(v.binaryValueAs!InetAddress.createStdAddr.toAddrString == "::1"); 101 | assert(v.binaryValueAs!InetAddress == pg_addr); 102 | } 103 | 104 | /// 105 | InetAddress vibe2pg(VibeNetworkAddress)(VibeNetworkAddress a) 106 | { 107 | InetAddress r; 108 | 109 | switch(a.family) 110 | { 111 | case AddressFamily.INET: 112 | r.family = PgFamily.PGSQL_AF_INET; 113 | r.netmask = 32; 114 | r.addr4 = a.sockAddrInet4.sin_addr.s_addr.representAsBytes.bigEndianToNative!uint; 115 | break; 116 | 117 | case AddressFamily.INET6: 118 | r.family = PgFamily.PGSQL_AF_INET6; 119 | r.netmask = 128; 120 | r.addr6 = AddrValue(a.sockAddrInet6.sin6_addr.s6_addr).swapEndiannesForBigEndianSystems; 121 | break; 122 | 123 | default: 124 | throwUnsup(a.family); 125 | } 126 | 127 | return r; 128 | } 129 | 130 | private ref ubyte[T.sizeof] representAsBytes(T)(const ref return T s) @trusted 131 | { 132 | return *cast(ubyte[T.sizeof]*) &s; 133 | } 134 | 135 | private union Hdr 136 | { 137 | ubyte[4] bytes; 138 | 139 | struct 140 | { 141 | PgFamily family; 142 | ubyte netmask; 143 | ubyte always_zero; 144 | ubyte addr_len; 145 | } 146 | } 147 | 148 | /// Constructs Value from InetAddress or from CidrAddress 149 | Value toValue(T)(T v) 150 | if(is(T == InetAddress) || is(T == CidrAddress)) 151 | { 152 | Hdr hdr; 153 | hdr.family = v.family; 154 | 155 | ubyte[] addr_net_byte_order; 156 | 157 | switch(v.family) 158 | { 159 | case PgFamily.PGSQL_AF_INET: 160 | addr_net_byte_order ~= v.addr4.nativeToBigEndian; 161 | break; 162 | 163 | case PgFamily.PGSQL_AF_INET6: 164 | addr_net_byte_order ~= v.addr.swapEndiannesForBigEndianSystems; 165 | break; 166 | 167 | default: 168 | throwUnsup(v.family); 169 | } 170 | 171 | hdr.addr_len = addr_net_byte_order.length.to!ubyte; 172 | hdr.netmask = v.netmask; 173 | 174 | immutable r = (hdr.bytes ~ addr_net_byte_order).idup; 175 | return Value(r, OidType.HostAddress); 176 | } 177 | 178 | package: 179 | 180 | /// Convert Value to network address type 181 | T binaryValueAs(T)(in Value v) 182 | if(is(T == InetAddress) || is(T == CidrAddress)) 183 | { 184 | enum oidType = is(T == InetAddress) ? OidType.HostAddress : OidType.NetworkAddress; 185 | enum typeName = is(T == InetAddress) ? "inet" : "cidr"; 186 | 187 | if(v.oidType != oidType) 188 | throwTypeComplaint(v.oidType, typeName); 189 | 190 | Hdr hdr; 191 | enum headerLen = hdr.sizeof; 192 | enum ipv4_addr_len = 4; 193 | 194 | if(v.data.length < hdr.sizeof + ipv4_addr_len) 195 | throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH, "unexpected data ending"); 196 | 197 | hdr.bytes = v.data[0 .. hdr.bytes.length]; 198 | 199 | ubyte lenMustBe; 200 | switch(hdr.family) 201 | { 202 | case PgFamily.PGSQL_AF_INET: lenMustBe = ipv4_addr_len; break; 203 | case PgFamily.PGSQL_AF_INET6: lenMustBe = 16; break; 204 | default: throwUnsup(hdr.family); 205 | } 206 | 207 | if(hdr.addr_len != lenMustBe && hdr.always_zero == 0) 208 | throw new ValueConvException( 209 | ConvExceptionType.SIZE_MISMATCH, 210 | "Wrong address length, must be "~lenMustBe.to!string 211 | ); 212 | 213 | if(headerLen + hdr.addr_len != v.data.length) 214 | throw new ValueConvException( 215 | ConvExceptionType.SIZE_MISMATCH, 216 | "Address length not matches to Value data length" 217 | ); 218 | 219 | import std.bitmanip: bigEndianToNative; 220 | 221 | T r; 222 | r.family = hdr.family; 223 | r.netmask = hdr.netmask; 224 | 225 | switch(hdr.family) 226 | { 227 | case PgFamily.PGSQL_AF_INET: 228 | const ubyte[4] b = v.data[headerLen..$]; 229 | r.addr4 = b.bigEndianToNative!uint; 230 | break; 231 | 232 | case PgFamily.PGSQL_AF_INET6: 233 | AddrValue av; 234 | av.addr6 = v.data[headerLen..$]; 235 | r.addr6 = av.swapEndiannesForBigEndianSystems; 236 | break; 237 | 238 | default: assert(0); 239 | } 240 | 241 | return r; 242 | } 243 | 244 | private: 245 | 246 | //TODO: noreturn? 247 | void throwUnsup(T)(T family) 248 | { 249 | throw new ValueConvException(ConvExceptionType.NOT_IMPLEMENTED, family.unsup); 250 | } 251 | 252 | string unsup(T)(in T family) 253 | { 254 | return "Unsupported address family: "~family.to!string; 255 | } 256 | 257 | private union AddrValue 258 | { 259 | ubyte[16] addr6; // IPv6 address in native byte order 260 | short[8] addr6_parts; // for endiannes swap purpose 261 | 262 | struct 263 | { 264 | ubyte[12] __unused; 265 | uint addr4; // IPv4 address in native byte order 266 | } 267 | } 268 | 269 | import std.system: Endian, endian; 270 | 271 | static if(endian == Endian.littleEndian) 272 | auto swapEndiannesForBigEndianSystems(in AddrValue s) 273 | { 274 | // do nothing for little endian 275 | return s.addr6; 276 | } 277 | else 278 | { 279 | 280 | ubyte[16] swapEndiannesForBigEndianSystems(in AddrValue s) 281 | { 282 | import std.bitmanip: swapEndian; 283 | 284 | AddrValue r; 285 | enum len = AddrValue.addr6_parts.length; 286 | 287 | foreach(ubyte i; 0 .. len) 288 | r.addr6_parts[i] = s.addr6_parts[i].swapEndian; 289 | 290 | return r.addr6; 291 | } 292 | 293 | } 294 | -------------------------------------------------------------------------------- /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 | If version **NO_VARIANT** is supplied the function 41 | ``` 42 | T as(T : Variant, bool isNullablePayload = true)(in Value v); 43 | ``` 44 | is no longer available and std.variant.Variant is no longer used. 45 | This can speed up compilation significant. 46 | 47 | ## Example 48 | ```d 49 | #!/usr/bin/env rdmd 50 | 51 | import dpq2; 52 | import std.getopt; 53 | import std.stdio: writefln, writeln; 54 | import std.typecons: Nullable; 55 | import std.variant: Variant; 56 | import vibe.data.bson; 57 | 58 | void main(string[] args) 59 | { 60 | string connInfo; 61 | getopt(args, "conninfo", &connInfo); 62 | 63 | Connection conn = new Connection(connInfo); 64 | 65 | // Only text query result can be obtained by this call: 66 | auto answer = conn.exec( 67 | "SELECT now()::timestamp as current_time, 'abc'::text as field_name, "~ 68 | "123 as field_3, 456.78 as field_4, '{\"JSON field name\": 123.456}'::json" 69 | ); 70 | 71 | writeln( "Text query result by name: ", answer[0]["current_time"].as!string ); 72 | writeln( "Text query result by index: ", answer[0][3].as!string ); 73 | 74 | // Binary arguments query with binary result: 75 | QueryParams p; 76 | p.sqlCommand = "SELECT "~ 77 | "$1::double precision as double_field, "~ 78 | "$2::text, "~ 79 | "$3::text as null_field, "~ 80 | "array['first', 'second', NULL]::text[] as array_field, "~ 81 | "$4::integer[] as multi_array, "~ 82 | "'{\"float_value\": 123.456,\"text_str\": \"text string\"}'::json as json_value"; 83 | 84 | p.argsVariadic( 85 | -1234.56789012345, 86 | "first line\nsecond line", 87 | Nullable!string.init, 88 | [[1, 2, 3], [4, 5, 6]] 89 | ); 90 | 91 | auto r = conn.execParams(p); 92 | scope(exit) destroy(r); 93 | 94 | writeln( "0: ", r[0]["double_field"].as!double ); 95 | writeln( "1: ", r.oneRow[1].as!string ); // .oneRow additionally checks that here is only one row was returned 96 | writeln( "2.1 isNull: ", r[0][2].isNull ); 97 | writeln( "2.2 isNULL: ", r[0].isNULL(2) ); 98 | writeln( "3.1: ", r[0][3].asArray[0].as!string ); 99 | writeln( "3.2: ", r[0][3].asArray[1].as!string ); 100 | writeln( "3.3: ", r[0]["array_field"].asArray[2].isNull ); 101 | writeln( "3.4: ", r[0]["array_field"].asArray.isNULL(2) ); 102 | writeln( "4.1: ", r[0]["multi_array"].asArray.getValue(1, 2).as!int ); 103 | writeln( "4.2: ", r[0]["multi_array"].as!(int[][]) ); 104 | writeln( "5.1 Json: ", r[0]["json_value"].as!Json); 105 | writeln( "5.2 Bson: ", r[0]["json_value"].as!Bson); 106 | 107 | // It is possible to read values of unknown type 108 | // using std.variant.Variant or vibe.data.bson.Bson: 109 | for(auto column = 0; column < r.columnCount; column++) 110 | { 111 | writeln( 112 | "column: '", r.columnName(column), "', ", 113 | "Variant: ", r[0][column].as!Variant, ", ", 114 | "Bson: ", r[0][column].as!Bson 115 | ); 116 | } 117 | 118 | // It is possible to upload CSV data ultra-fast: 119 | conn.exec("CREATE TEMP TABLE test_dpq2_copy (v1 TEXT, v2 INT)"); 120 | 121 | // Init the COPY command. This sets the connection in a COPY receive 122 | // mode until putCopyEnd() is called. Copy CSV data, because it's standard, 123 | // ultra fast, and readable: 124 | conn.exec("COPY test_dpq2_copy FROM STDIN WITH (FORMAT csv)"); 125 | 126 | // Write 2 lines of CSV, including text that contains the delimiter. 127 | // Postgresql handles it well: 128 | string data = "\"This, right here, is a test\",8\nWow! it works,13\n"; 129 | conn.putCopyData(data); 130 | 131 | // Write 2 more lines 132 | data = "Horray!,3456\nSuper fast!,325\n"; 133 | conn.putCopyData(data); 134 | 135 | // Signal that the COPY is finished. Let Postgresql finalize the command 136 | // and return any errors with the data. 137 | conn.putCopyEnd(); 138 | 139 | import std.range: enumerate; 140 | 141 | // rangify() template helps to iterate over Answer and Row: 142 | auto few_rows = conn.exec("SELECT v1, v2 FROM test_dpq2_copy"); 143 | foreach(row_num, row; few_rows.rangify.enumerate) 144 | { 145 | foreach(cell; row.rangify) 146 | writefln("row_num: %d value: %s", row_num, cell.as!Bson); 147 | } 148 | } 149 | ``` 150 | 151 | Compile and run: 152 | ``` 153 | Running ./dpq2_example --conninfo=user=postgres 154 | Text query result by name: 2025-08-22 15:33:57.417629 155 | Text query result by index: 456.78 156 | bson: "2025-08-22 15:33:57.417629" 157 | bson: "abc" 158 | bson: "123" 159 | bson: "456.78" 160 | bson: {"JSON field name":123.456} 161 | 0: -1234.57 162 | 1: first line 163 | second line 164 | 2.1 isNull: true 165 | 2.2 isNULL: true 166 | 3.1: first 167 | 3.2: second 168 | 3.3: true 169 | 3.4: true 170 | 4.1: 6 171 | 4.2: [[1, 2, 3], [4, 5, 6]] 172 | 5.1 Json: {"text_str":"text string","float_value":123.456} 173 | 5.2 Bson: {"text_str":"text string","float_value":123.456} 174 | column: 'double_field', Variant: -1234.57, Bson: -1234.56789012345 175 | column: 'text', Variant: first line 176 | second line, Bson: "first line\nsecond line" 177 | column: 'null_field', Variant: Nullable.null, Bson: null 178 | column: 'array_field', Variant: [first, second, Nullable.null], Bson: ["first","second",null] 179 | column: 'multi_array', Variant: [[1, 2, 3], [4, 5, 6]], Bson: [[1,2,3],[4,5,6]] 180 | column: 'json_value', Variant: {"text_str":"text string","float_value":123.456}, Bson: {"text_str":"text string","float_value":123.456} 181 | row_num: 0 value: "This, right here, is a test" 182 | row_num: 0 value: "8" 183 | row_num: 1 value: "Wow! it works" 184 | row_num: 1 value: "13" 185 | row_num: 2 value: "Horray!" 186 | row_num: 2 value: "3456" 187 | row_num: 3 value: "Super fast!" 188 | row_num: 3 value: "325" 189 | ``` 190 | 191 | ## Using dynamic version of libpq 192 | Is provided two ways to load `libpq` dynamically: 193 | 194 | * Automatic load and unload (`dynamic` build config option) 195 | * Manual load (and unload, if need) (`dynamic-unmanaged`) 196 | 197 | To load automatically it is necessary to allocate `ConnectionFactory`. 198 | This class is only available then `dynamic` config is used. 199 | Only one instance of `ConnectionFactory` is allowed. 200 | It is possible to specify filepath to a library/libraries what you want to use, otherwise default will be used: 201 | ```D 202 | // Argument is a string containing one or more comma-separated 203 | // shared library names 204 | auto connFactory = new immutable ConnectionFactory("path/to/libpq.dll"); 205 | ``` 206 | 207 | Then you can create connection by calling `createConnection` method: 208 | ```D 209 | Connection conn = connFactory.createConnection(params); 210 | ``` 211 | And then this connection can be used as usual. 212 | 213 | When all objects related to `libpq` (including `ConnectionFactory`) is destroyed library will be unloaded automatically. 214 | 215 | To load `libpq` manually it is necessary to use build config `dynamic-unmanaged`. 216 | Manual dynamic `libpq` loading example: 217 | ```D 218 | import derelict.pq.pq: DerelictPQ; 219 | import core.memory: GC; 220 | 221 | DerelictPQ.load(); 222 | 223 | auto conn = new Connection(connInfo); 224 | /* Skipped rest of useful SQL processing */ 225 | conn.destroy(); // Ensure that all related to libpq objects are destroyed 226 | 227 | GC.collect(); // Forced removal of references to libpq before library unload 228 | DerelictPQ.unload(); 229 | ``` 230 | In this case is not need to use `ConnectionFactory` - just create `Connection` by the same way as for `static` config. 231 | -------------------------------------------------------------------------------- /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/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/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/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 | version(NO_VARIANT) { 25 | private struct Variant {} 26 | } else { 27 | import std.variant: Variant; 28 | } 29 | import std.typecons : Nullable; 30 | import std.bitmanip: bigEndianToNative, BitArray; 31 | import std.conv: to; 32 | version (unittest) import std.exception : assertThrown; 33 | 34 | // Supported PostgreSQL binary types 35 | alias PGboolean = bool; /// boolean 36 | alias PGsmallint = short; /// smallint 37 | alias PGinteger = int; /// integer 38 | alias PGbigint = long; /// bigint 39 | alias PGreal = float; /// real 40 | alias PGdouble_precision = double; /// double precision 41 | alias PGtext = string; /// text 42 | alias PGnumeric = string; /// numeric represented as string 43 | alias PGbytea = immutable(ubyte)[]; /// bytea 44 | alias PGuuid = UUID; /// UUID 45 | alias PGdate = Date; /// Date (no time of day) 46 | alias PGtime_without_time_zone = TimeOfDay; /// Time of day (no date) 47 | alias PGtime_with_time_zone = TimeOfDayWithTZ; /// Time of day with TZ(no date) 48 | alias PGtimestamp = TimeStamp; /// Both date and time without time zone 49 | alias PGtimestamptz = TimeStampUTC; /// Both date and time stored in UTC time zone 50 | alias PGinterval = Interval; /// Interval 51 | alias PGjson = Json; /// json or jsonb 52 | alias PGline = Line; /// Line (geometric type) 53 | alias PGvarbit = BitArray; /// BitArray 54 | 55 | private alias VF = ValueFormat; 56 | private alias AE = ValueConvException; 57 | private alias ET = ConvExceptionType; 58 | 59 | version(NO_VARIANT) { 60 | } else { 61 | /** 62 | Returns cell value as a Variant type. 63 | */ 64 | T as(T : Variant, bool isNullablePayload = true)(in Value v) 65 | { 66 | import dpq2.conv.to_variant; 67 | 68 | return v.toVariant!isNullablePayload; 69 | } 70 | 71 | @system unittest 72 | { 73 | import core.exception: AssertError; 74 | 75 | auto v = Value(ValueFormat.BINARY, OidType.Text); 76 | 77 | assert(v.isNull); 78 | assertThrown!AssertError(v.as!string == ""); 79 | assert(v.as!(Nullable!string).isNull == true); 80 | 81 | assert(v.as!Variant.get!(Nullable!string).isNull == true); 82 | } 83 | } 84 | 85 | /** 86 | Returns cell value as a Nullable type using the underlying type conversion after null check. 87 | */ 88 | T as(T : Nullable!R, R)(in Value v) 89 | { 90 | if (v.isNull) 91 | return T.init; 92 | else 93 | return T(v.as!R); 94 | } 95 | 96 | /** 97 | Returns cell value as a native string based type from text or binary formatted field. 98 | Throws: AssertError if the db value is NULL. 99 | */ 100 | T as(T)(in Value v) pure @trusted 101 | if(is(T : const(char)[]) && !is(T == Nullable!R, R)) 102 | { 103 | if(v.format == VF.BINARY) 104 | { 105 | if(!( 106 | v.oidType == OidType.Text || 107 | v.oidType == OidType.FixedString || 108 | v.oidType == OidType.VariableString || 109 | v.oidType == OidType.Numeric || 110 | v.oidType == OidType.Json || 111 | v.oidType == OidType.Jsonb || 112 | v.oidType == OidType.Name 113 | )) 114 | throwTypeComplaint(v.oidType, "Text, FixedString, VariableString, Name, Numeric, Json or Jsonb", __FILE__, __LINE__); 115 | } 116 | 117 | if(v.format == VF.BINARY && v.oidType == OidType.Numeric) 118 | return rawValueToNumeric(v.data); // special case for 'numeric' which represented in dpq2 as string 119 | else 120 | return v.valueAsString; 121 | } 122 | 123 | /** 124 | Returns value as D type value from binary formatted field. 125 | Throws: AssertError if the db value is NULL. 126 | */ 127 | T as(T)(in Value v) 128 | if(!is(T : const(char)[]) && !is(T == Bson) && !is(T == Variant) && !is(T == Nullable!R,R)) 129 | { 130 | if(!(v.format == VF.BINARY)) 131 | throw new AE(ET.NOT_BINARY, 132 | msg_NOT_BINARY, __FILE__, __LINE__); 133 | 134 | return binaryValueAs!T(v); 135 | } 136 | 137 | @system unittest 138 | { 139 | auto v = Value([1], OidType.Int4, false, ValueFormat.TEXT); 140 | assertThrown!AE(v.as!int); 141 | } 142 | 143 | Value[] deserializeRecord(in Value v) 144 | { 145 | if(!(v.oidType == OidType.Record)) 146 | throwTypeComplaint(v.oidType, "record", __FILE__, __LINE__); 147 | 148 | if(!(v.data.length >= uint.sizeof)) 149 | throw new AE(ET.SIZE_MISMATCH, 150 | "Value length isn't enough to hold a size", __FILE__, __LINE__); 151 | 152 | immutable(ubyte)[] data = v.data; 153 | uint entries = bigEndianToNative!uint(v.data[0 .. uint.sizeof]); 154 | data = data[uint.sizeof .. $]; 155 | 156 | Value[] ret = new Value[entries]; 157 | 158 | foreach (ref res; ret) { 159 | if (!(data.length >= 2*int.sizeof)) 160 | throw new AE(ET.SIZE_MISMATCH, 161 | "Value length isn't enough to hold an oid and a size", __FILE__, __LINE__); 162 | OidType oidType = cast(OidType)bigEndianToNative!int(data[0 .. int.sizeof]); 163 | data = data[int.sizeof .. $]; 164 | int size = bigEndianToNative!int(data[0 .. int.sizeof]); 165 | data = data[int.sizeof .. $]; 166 | 167 | if (size == -1) 168 | { 169 | res = Value(null, oidType, true); 170 | continue; 171 | } 172 | assert(size >= 0); 173 | if (!(data.length >= size)) 174 | throw new AE(ET.SIZE_MISMATCH, 175 | "Value length isn't enough to hold object body", __FILE__, __LINE__); 176 | immutable(ubyte)[] resData = data[0 .. size]; 177 | data = data[size .. $]; 178 | res = Value(resData.idup, oidType); 179 | } 180 | 181 | return ret; 182 | } 183 | 184 | package: 185 | 186 | /* 187 | * Something was broken in DMD64 D Compiler v2.079.0-rc.1 so I made this "tunnel" 188 | * TODO: remove it and replace by direct binaryValueAs calls 189 | */ 190 | auto tunnelForBinaryValueAsCalls(T)(in Value v) 191 | { 192 | return binaryValueAs!T(v); 193 | } 194 | 195 | char[] valueAsString(in Value v) pure 196 | { 197 | return (cast(const(char[])) v.data).to!(char[]); 198 | } 199 | 200 | /// Returns value as bytes from binary formatted field 201 | T binaryValueAs(T)(in Value v) 202 | if(is(T : const ubyte[])) 203 | { 204 | if(!(v.oidType == OidType.ByteArray)) 205 | throwTypeComplaint(v.oidType, "immutable ubyte[]", __FILE__, __LINE__); 206 | 207 | return v.data; 208 | } 209 | 210 | @system unittest 211 | { 212 | auto v = Value([1], OidType.Bool); 213 | assertThrown!ValueConvException(v.binaryValueAs!(const ubyte[])); 214 | } 215 | 216 | /// Returns cell value as native integer or decimal values 217 | /// 218 | /// Postgres type "numeric" is oversized and not supported by now 219 | T binaryValueAs(T)(in Value v) 220 | if( isNumeric!(T) ) 221 | { 222 | static if(isIntegral!(T)) 223 | if(!isNativeInteger(v.oidType)) 224 | throwTypeComplaint(v.oidType, "integral types", __FILE__, __LINE__); 225 | 226 | static if(isFloatingPoint!(T)) 227 | if(!isNativeFloat(v.oidType)) 228 | throwTypeComplaint(v.oidType, "floating point types", __FILE__, __LINE__); 229 | 230 | if(!(v.data.length == T.sizeof)) 231 | throw new AE(ET.SIZE_MISMATCH, 232 | to!string(v.oidType)~" length ("~to!string(v.data.length)~") isn't equal to native D type "~ 233 | to!string(typeid(T))~" size ("~to!string(T.sizeof)~")", 234 | __FILE__, __LINE__); 235 | 236 | ubyte[T.sizeof] s = v.data[0..T.sizeof]; 237 | return bigEndianToNative!(T)(s); 238 | } 239 | 240 | @system unittest 241 | { 242 | auto v = Value([1], OidType.Bool); 243 | assertThrown!ValueConvException(v.binaryValueAs!int); 244 | assertThrown!ValueConvException(v.binaryValueAs!float); 245 | 246 | v = Value([1], OidType.Int4); 247 | assertThrown!ValueConvException(v.binaryValueAs!int); 248 | } 249 | 250 | package void checkValue( 251 | in Value v, 252 | in OidType enforceOid, 253 | in size_t enforceSize, 254 | in string typeName 255 | ) pure @safe 256 | { 257 | if(!(v.oidType == enforceOid)) 258 | throwTypeComplaint(v.oidType, typeName); 259 | 260 | if(!(v.data.length == enforceSize)) 261 | throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH, 262 | `Value length isn't equal to Postgres `~typeName~` size`); 263 | } 264 | 265 | /// Returns UUID as native UUID value 266 | UUID binaryValueAs(T)(in Value v) 267 | if( is( T == UUID ) ) 268 | { 269 | v.checkValue(OidType.UUID, 16, "UUID"); 270 | 271 | UUID r; 272 | r.data = v.data; 273 | return r; 274 | } 275 | 276 | @system unittest 277 | { 278 | auto v = Value([1], OidType.Int4); 279 | assertThrown!ValueConvException(v.binaryValueAs!UUID); 280 | 281 | v = Value([1], OidType.UUID); 282 | assertThrown!ValueConvException(v.binaryValueAs!UUID); 283 | } 284 | 285 | /// Returns boolean as native bool value 286 | bool binaryValueAs(T : bool)(in Value v) 287 | if (!is(T == Nullable!R, R)) 288 | { 289 | v.checkValue(OidType.Bool, 1, "bool"); 290 | 291 | return v.data[0] != 0; 292 | } 293 | 294 | @system unittest 295 | { 296 | auto v = Value([1], OidType.Int4); 297 | assertThrown!ValueConvException(v.binaryValueAs!bool); 298 | 299 | v = Value([1,2], OidType.Bool); 300 | assertThrown!ValueConvException(v.binaryValueAs!bool); 301 | } 302 | 303 | /// Returns Vibe.d's Json 304 | Json binaryValueAs(T)(in Value v) @trusted 305 | if( is( T == Json ) ) 306 | { 307 | import dpq2.conv.jsonb: jsonbValueToJson; 308 | 309 | Json res; 310 | 311 | switch(v.oidType) 312 | { 313 | case OidType.Json: 314 | // represent value as text and parse it into Json 315 | string t = v.valueAsString; 316 | res = parseJsonString(t); 317 | break; 318 | 319 | case OidType.Jsonb: 320 | res = v.jsonbValueToJson; 321 | break; 322 | 323 | default: 324 | throwTypeComplaint(v.oidType, "json or jsonb", __FILE__, __LINE__); 325 | } 326 | 327 | return res; 328 | } 329 | 330 | @system unittest 331 | { 332 | auto v = Value([1], OidType.Int4); 333 | assertThrown!ValueConvException(v.binaryValueAs!Json); 334 | } 335 | 336 | import money: currency, roundingMode; 337 | 338 | /// Returns money type 339 | /// 340 | /// Caution: here is no check of fractional precision while conversion! 341 | /// See also: PostgreSQL's "lc_monetary" description and "money" package description 342 | T binaryValueAs(T)(in Value v) @trusted 343 | if( isInstanceOf!(currency, T) && T.amount.sizeof == 8 ) 344 | { 345 | import std.format: format; 346 | 347 | if(v.data.length != T.amount.sizeof) 348 | throw new AE( 349 | ET.SIZE_MISMATCH, 350 | format( 351 | "%s length (%d) isn't equal to D money type %s size (%d)", 352 | v.oidType.to!string, 353 | v.data.length, 354 | typeid(T).to!string, 355 | T.amount.sizeof 356 | ) 357 | ); 358 | 359 | T r; 360 | 361 | r.amount = v.data[0 .. T.amount.sizeof].bigEndianToNative!long; 362 | 363 | return r; 364 | } 365 | 366 | package alias PGTestMoney = currency!("TEST_CURR", 2); //TODO: roundingMode.UNNECESSARY 367 | 368 | unittest 369 | { 370 | auto v = Value([1], OidType.Money); 371 | assertThrown!ValueConvException(v.binaryValueAs!PGTestMoney); 372 | } 373 | 374 | T binaryValueAs(T)(in Value v) @trusted 375 | if( is(T == BitArray) ) 376 | { 377 | import core.bitop : bitswap; 378 | import std.bitmanip; 379 | import std.format: format; 380 | import std.range : chunks; 381 | 382 | if(v.data.length < int.sizeof) 383 | throw new AE( 384 | ET.SIZE_MISMATCH, 385 | format( 386 | "%s length (%d) is less than minimum int type size (%d)", 387 | v.oidType.to!string, 388 | v.data.length, 389 | int.sizeof 390 | ) 391 | ); 392 | 393 | auto data = v.data[]; 394 | size_t len = data.read!int; 395 | size_t[] newData; 396 | foreach (ch; data.chunks(size_t.sizeof)) 397 | { 398 | ubyte[size_t.sizeof] tmpData; 399 | tmpData[0 .. ch.length] = ch[]; 400 | 401 | //FIXME: DMD Issue 19693 402 | version(DigitalMars) 403 | auto re = softBitswap(bigEndianToNative!size_t(tmpData)); 404 | else 405 | auto re = bitswap(bigEndianToNative!size_t(tmpData)); 406 | newData ~= re; 407 | } 408 | return T(newData, len); 409 | } 410 | 411 | unittest 412 | { 413 | auto v = Value([1], OidType.VariableBitString); 414 | assertThrown!ValueConvException(v.binaryValueAs!BitArray); 415 | } 416 | -------------------------------------------------------------------------------- /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 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 immutable POSTGRES_EPOCH_DATE = Date(2000, 1, 1); 346 | package immutable POSTGRES_EPOCH_JDATE = Date(2000, 1, 1).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/native_tests.d: -------------------------------------------------------------------------------- 1 | module dpq2.conv.native_tests; 2 | 3 | import dpq2; 4 | import dpq2.conv.arrays : isArrayType; 5 | import dpq2.conv.geometric: Line; 6 | import dpq2.conv.inet: InetAddress, CidrAddress; 7 | import std.bitmanip : BitArray; 8 | import std.datetime; 9 | import std.socket: InternetAddress, Internet6Address; 10 | import std.typecons: Nullable; 11 | import std.uuid: UUID; 12 | import std.variant: Variant; 13 | import vibe.data.json: Json, parseJsonString; 14 | 15 | version (integration_tests) 16 | private bool compareArraysWithCareAboutNullables(A, B)(A _a, B _b) 17 | { 18 | static assert(is(A == B)); 19 | 20 | import std.algorithm.comparison : equal; 21 | import std.traits: isInstanceOf; 22 | 23 | return equal!( 24 | (a, b) 25 | { 26 | static if(isInstanceOf!(Nullable, A)) 27 | { 28 | if(a.isNull != b.isNull) 29 | return false; 30 | 31 | if(a.isNull) 32 | return true; 33 | } 34 | 35 | return a == b; 36 | } 37 | )(_a, _b); 38 | } 39 | 40 | version (integration_tests) 41 | public void _integration_test( string connParam ) @system 42 | { 43 | import std.format: format; 44 | import dpq2.connection: createTestConn; 45 | import vibe.core.net: VibeNetworkAddress = NetworkAddress; 46 | 47 | auto conn = createTestConn(connParam); 48 | 49 | // to return times in other than UTC time zone but fixed time zone so make the test reproducible in databases with other TZ 50 | conn.exec("SET TIMEZONE TO +02"); 51 | 52 | // It is found what Linux and Windows have different approach for monetary 53 | // types formatting at same locales. This line sets equal approach. 54 | conn.exec("SET lc_monetary = 'C'"); 55 | 56 | QueryParams params; 57 | params.resultFormat = ValueFormat.BINARY; 58 | 59 | { 60 | import dpq2.conv.geometric: GeometricInstancesForIntegrationTest; 61 | mixin GeometricInstancesForIntegrationTest; 62 | 63 | void testIt(T)(T nativeValue, in string pgType, string pgValue) 64 | { 65 | import std.algorithm : strip; 66 | import std.string : representation; 67 | import std.meta: AliasSeq, anySatisfy; 68 | 69 | static string formatValue(T val) 70 | { 71 | import std.algorithm : joiner, map; 72 | import std.conv : text, to; 73 | import std.range : chain, ElementType; 74 | 75 | // Nullable format deprecation workaround 76 | static if (is(T == Nullable!R, R)) 77 | return val.isNull ? "null" : val.get.to!string; 78 | else static if (isArrayType!T && is(ElementType!T == Nullable!E, E)) 79 | return chain("[", val.map!(a => 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[]) || // Not-nullable Value cell but can contain nullable elements which prohibited if Variant used 93 | is(T == Nullable!(int[])) || // Nullable Value implies array elements should be Nullable too if Variant used 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 | import dpq2.conv.inet: vibe2pg; 265 | 266 | // inet 267 | const testInetAddr1 = InetAddress(new InternetAddress("127.0.0.1", InternetAddress.PORT_ANY), 9); 268 | C!InetAddress(testInetAddr1, "inet", `'127.0.0.1/9'`); 269 | const testInetAddr2 = InetAddress(new InternetAddress("127.0.0.1", InternetAddress.PORT_ANY)); 270 | C!InetAddress(testInetAddr2, "inet", `'127.0.0.1/32'`); 271 | const testInetAddr3 = VibeNetworkAddress(new InternetAddress("127.0.0.1", InternetAddress.PORT_ANY)).vibe2pg; 272 | C!InetAddress(testInetAddr3, "inet", `'127.0.0.1/32'`); 273 | 274 | // inet6 275 | const testInet6Addr1 = InetAddress(new Internet6Address("2::1", InternetAddress.PORT_ANY)); 276 | C!InetAddress(testInet6Addr1, "inet", `'2::1/128'`); 277 | const testInet6Addr2 = InetAddress(new Internet6Address("2001:0:130F::9C0:876A:130B", InternetAddress.PORT_ANY),24); 278 | C!InetAddress(testInet6Addr2, "inet", `'2001:0:130f::9c0:876a:130b/24'`); 279 | const testInet6Addr3 = VibeNetworkAddress(new Internet6Address("2001:0:130F::9C0:876A:130B", InternetAddress.PORT_ANY)).vibe2pg; 280 | C!InetAddress(testInet6Addr3, "inet", `'2001:0:130f::9c0:876a:130b/128'`); 281 | 282 | // nullable inet 283 | C!(Nullable!InetAddress)(Nullable!InetAddress.init, "inet", "NULL"); 284 | C!(Nullable!CidrAddress)(Nullable!CidrAddress.init, "cidr", "NULL"); 285 | 286 | // cidr 287 | const testCidrAddr1 = CidrAddress(new InternetAddress("192.168.0.0", InternetAddress.PORT_ANY), 25); 288 | C!CidrAddress(testCidrAddr1, "cidr", `'192.168.0.0/25'`); 289 | const testCidrAddr2 = CidrAddress(new Internet6Address("::", InternetAddress.PORT_ANY), 64); 290 | C!CidrAddress(testCidrAddr2, "cidr", `'::/64'`); 291 | 292 | // json 293 | C!PGjson(Json(["float_value": Json(123.456), "text_str": Json("text string")]), "json", `'{"float_value": 123.456,"text_str": "text string"}'`); 294 | C!(Nullable!PGjson)(Nullable!Json(Json(["foo": Json("bar")])), "json", `'{"foo":"bar"}'`); 295 | 296 | // json as string 297 | C!string(`{"float_value": 123.456}`, "json", `'{"float_value": 123.456}'`); 298 | 299 | // jsonb 300 | C!PGjson(Json(["float_value": Json(123.456), "text_str": Json("text string"), "abc": Json(["key": Json("value")])]), "jsonb", 301 | `'{"float_value": 123.456, "text_str": "text string", "abc": {"key": "value"}}'`); 302 | 303 | // Geometric 304 | C!Point(Point(1,2), "point", "'(1,2)'"); 305 | C!PGline(Line(1,2,3), "line", "'{1,2,3}'"); 306 | C!LineSegment(LineSegment(Point(1,2), Point(3,4)), "lseg", "'[(1,2),(3,4)]'"); 307 | 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 308 | C!TestPath(TestPath(true, [Point(1,1), Point(2,2), Point(3,3)]), "path", "'((1,1),(2,2),(3,3))'"); 309 | C!TestPath(TestPath(false, [Point(1,1), Point(2,2), Point(3,3)]), "path", "'[(1,1),(2,2),(3,3)]'"); 310 | C!Polygon(([Point(1,1), Point(2,2), Point(3,3)]), "polygon", "'((1,1),(2,2),(3,3))'"); 311 | C!TestCircle(TestCircle(Point(1,2), 10), "circle", "'<(1,2),10>'"); 312 | C!(Nullable!Point)(Nullable!Point(Point(1,2)), "point", "'(1,2)'"); 313 | 314 | //Arrays 315 | C!(int[][])([[1,2],[3,4]], "int[]", "'{{1,2},{3,4}}'"); 316 | C!(int[])([], "int[]", "'{}'"); // empty array test 317 | C!((Nullable!string)[])([Nullable!string("foo"), Nullable!string.init], "text[]", "'{foo,NULL}'"); 318 | C!(string[])(["foo","bar", "baz"], "text[]", "'{foo,bar,baz}'"); 319 | C!(PGjson[])([Json(["foo": Json(42)])], "json[]", `'{"{\"foo\":42}"}'`); 320 | C!(PGuuid[])([UUID("8b9ab33a-96e9-499b-9c36-aad1fe86d640")], "uuid[]", "'{8b9ab33a-96e9-499b-9c36-aad1fe86d640}'"); 321 | C!(PGline[])([Line(1,2,3), Line(4,5,6)], "line[]", `'{"{1,2,3}","{4,5,6}"}'`); 322 | C!(PGtimestamp[])([PGtimestamp(DateTime(1997, 12, 17, 7, 37, 16), dur!"usecs"(12))], "timestamp[]", `'{"1997-12-17 07:37:16.000012"}'`); 323 | C!(InetAddress[])([testInetAddr1, testInet6Addr2], "inet[]", `'{127.0.0.1/9,2001:0:130f::9c0:876a:130b/24}'`); 324 | C!(Nullable!(int[]))(Nullable!(int[]).init, "int[]", "NULL"); 325 | C!(Nullable!(int[]))(Nullable!(int[])([1,2,3]), "int[]", "'{1,2,3}'"); 326 | C!(Nullable!(Nullable!int[]))(Nullable!(Nullable!int[]).init, "int[]", "NULL"); 327 | } 328 | 329 | // test round-trip compound types 330 | { 331 | conn.exec("CREATE TYPE test_type AS (x int, y int)"); 332 | scope(exit) conn.exec("DROP TYPE test_type"); 333 | 334 | params.sqlCommand = "SELECT 'test_type'::regtype::oid"; 335 | OidType oid = cast(OidType)conn.execParams(params)[0][0].as!Oid; 336 | 337 | Value input = Value(toRecordValue([17.toValue, Nullable!int.init.toValue]).data, oid); 338 | 339 | params.sqlCommand = "SELECT $1::text"; 340 | params.args = [input]; 341 | Value v = conn.execParams(params)[0][0]; 342 | assert(v.as!string == `(17,)`, v.as!string); 343 | params.sqlCommand = "SELECT $1"; 344 | v = conn.execParams(params)[0][0]; 345 | assert(v.oidType == oid && v.data == input.data); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/dpq2/connection.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents connection to the PostgreSQL server 3 | * 4 | * Most functions is correspond to those in the documentation of Postgres: 5 | * $(HTTPS https://www.postgresql.org/docs/current/static/libpq.html) 6 | */ 7 | module dpq2.connection; 8 | 9 | import dpq2.query; 10 | import dpq2.args: QueryParams; 11 | import dpq2.cancellation; 12 | import dpq2.result; 13 | import dpq2.exception; 14 | 15 | import derelict.pq.pq; 16 | import std.conv: to; 17 | import std.string: toStringz, fromStringz; 18 | import std.exception: enforce; 19 | import std.range; 20 | import std.stdio: File; 21 | import std.socket; 22 | import core.exception; 23 | import core.time: Duration; 24 | 25 | /* 26 | * Bugs: On Unix connection is not thread safe. 27 | * 28 | * On Unix, forking a process with open libpq connections can lead 29 | * to unpredictable results because the parent and child processes share 30 | * the same sockets and operating system resources. For this reason, 31 | * such usage is not recommended, though doing an exec from the child 32 | * process to load a new executable is safe. 33 | 34 | 35 | 36 | int PQisthreadsafe(); 37 | Returns 1 if the libpq is thread-safe and 0 if it is not. 38 | */ 39 | 40 | private mixin template ConnectionCtors() 41 | { 42 | 43 | /// Makes a new connection to the database server 44 | this(string connString) 45 | { 46 | conn = PQconnectdb(toStringz(connString)); 47 | version(Dpq2_Dynamic) dynLoaderRefCnt = ReferenceCounter(true); 48 | checkCreatedConnection(); 49 | } 50 | 51 | /// ditto 52 | this(in string[string] keyValueParams) 53 | { 54 | auto a = keyValueParams.keyValToPQparamsArrays; 55 | 56 | conn = PQconnectdbParams(&a.keys[0], &a.vals[0], 0); 57 | version(Dpq2_Dynamic) dynLoaderRefCnt = ReferenceCounter(true); 58 | checkCreatedConnection(); 59 | } 60 | 61 | /// Starts creation of a connection to the database server in a nonblocking manner 62 | this(ConnectionStart, string connString) 63 | { 64 | conn = PQconnectStart(toStringz(connString)); 65 | version(Dpq2_Dynamic) dynLoaderRefCnt = ReferenceCounter(true); 66 | checkCreatedConnection(); 67 | } 68 | 69 | /// ditto 70 | this(ConnectionStart, in string[string] keyValueParams) 71 | { 72 | auto a = keyValueParams.keyValToPQparamsArrays; 73 | 74 | conn = PQconnectStartParams(&a.keys[0], &a.vals[0], 0); 75 | version(Dpq2_Dynamic) dynLoaderRefCnt = ReferenceCounter(true); 76 | checkCreatedConnection(); 77 | } 78 | } 79 | 80 | /// dumb flag for Connection ctor parametrization 81 | struct ConnectionStart {}; 82 | 83 | /// Connection 84 | class Connection 85 | { 86 | package PGconn* conn; 87 | 88 | invariant 89 | { 90 | assert(conn !is null); 91 | } 92 | 93 | version(Dpq2_Static) 94 | mixin ConnectionCtors; 95 | else 96 | { 97 | import dpq2.dynloader: ReferenceCounter; 98 | 99 | private immutable ReferenceCounter dynLoaderRefCnt; 100 | 101 | package mixin ConnectionCtors; 102 | } 103 | 104 | private void checkCreatedConnection() 105 | { 106 | enforce!OutOfMemoryError(conn, "Unable to allocate libpq connection data"); 107 | 108 | if( status == CONNECTION_BAD ) 109 | throw new ConnectionException(this, __FILE__, __LINE__); 110 | } 111 | 112 | ~this() 113 | { 114 | PQfinish( conn ); 115 | 116 | version(Dpq2_Dynamic) dynLoaderRefCnt.__custom_dtor(); 117 | } 118 | 119 | mixin Queries; 120 | 121 | /// Returns the blocking status of the database connection 122 | bool isNonBlocking() 123 | { 124 | return PQisnonblocking(conn) == 1; 125 | } 126 | 127 | /// Sets the nonblocking status of the connection 128 | private void setNonBlocking(bool state) 129 | { 130 | if( PQsetnonblocking(conn, state ? 1 : 0 ) == -1 ) 131 | throw new ConnectionException(this, __FILE__, __LINE__); 132 | } 133 | 134 | /// Begin reset the communication channel to the server, in a nonblocking manner 135 | /// 136 | /// Useful only for non-blocking operations. 137 | void resetStart() 138 | { 139 | if(PQresetStart(conn) == 0) 140 | throw new ConnectionException(this, __FILE__, __LINE__); 141 | } 142 | 143 | /// Useful only for non-blocking operations. 144 | PostgresPollingStatusType poll() nothrow 145 | { 146 | assert(conn); 147 | 148 | return PQconnectPoll(conn); 149 | } 150 | 151 | /// Useful only for non-blocking operations. 152 | PostgresPollingStatusType resetPoll() nothrow 153 | { 154 | assert(conn); 155 | 156 | return PQresetPoll(conn); 157 | } 158 | 159 | /// Returns the status of the connection 160 | ConnStatusType status() nothrow 161 | { 162 | return PQstatus(conn); 163 | } 164 | 165 | /** 166 | Returns the current in-transaction status of the server. 167 | The status can be: 168 | * PQTRANS_IDLE - currently idle 169 | * PQTRANS_ACTIVE - a command is in progress (reported only when a query has been sent to the server and not yet completed) 170 | * PQTRANS_INTRANS - idle, in a valid transaction block 171 | * PQTRANS_INERROR - idle, in a failed transaction block 172 | * PQTRANS_UNKNOWN - reported if the connection is bad 173 | */ 174 | PGTransactionStatusType transactionStatus() nothrow 175 | { 176 | return PQtransactionStatus(conn); 177 | } 178 | 179 | /// If input is available from the server, consume it 180 | /// 181 | /// Useful only for non-blocking operations. 182 | void consumeInput() 183 | { 184 | assert(conn); 185 | 186 | const size_t r = PQconsumeInput( conn ); 187 | if( r != 1 ) throw new ConnectionException(this, __FILE__, __LINE__); 188 | } 189 | 190 | /// Attempts to flush any queued output data to the server. 191 | /// 192 | /// Returns: true if successful (or if the send queue is empty), or 1 193 | /// if it was unable to send all the data in the send queue yet (this 194 | /// case can only occur if the connection is nonblocking). 195 | bool flush() 196 | { 197 | assert(conn); 198 | 199 | auto r = PQflush(conn); 200 | if( r == -1 ) throw new ConnectionException(this, __FILE__, __LINE__); 201 | return r == 0; 202 | } 203 | 204 | /// Obtains the file descriptor number of the connection socket to the server 205 | int posixSocket() 206 | { 207 | int r = PQsocket(conn); 208 | 209 | if(r == -1) 210 | throw new ConnectionException(this, __FILE__, __LINE__); 211 | 212 | return r; 213 | } 214 | 215 | /// Obtains duplicate file descriptor number of the connection socket to the server 216 | auto posixSocketDuplicate() 217 | { 218 | import dpq2.socket_stuff; 219 | 220 | return posixSocket.duplicateSocket; 221 | } 222 | 223 | /// Obtains std.socket.Socket of the connection to the server 224 | /// 225 | /// Due to a limitation of Dlang Socket actually for the Socket creation 226 | /// duplicate of internal posix socket will be used. 227 | Socket socket() 228 | { 229 | return new Socket(cast(socket_t) posixSocketDuplicate, AddressFamily.UNSPEC); 230 | } 231 | 232 | /// Returns the error message most recently generated by an operation on the connection 233 | string errorMessage() const nothrow 234 | { 235 | return PQerrorMessage(conn).to!string; 236 | } 237 | 238 | /** 239 | * Sets or examines the current notice processor 240 | * 241 | * Returns the previous notice receiver or processor function pointer, and sets the new value. 242 | * If you supply a null function pointer, no action is taken, but the current pointer is returned. 243 | */ 244 | PQnoticeProcessor setNoticeProcessor(PQnoticeProcessor proc, void* arg) nothrow 245 | { 246 | assert(conn); 247 | 248 | return PQsetNoticeProcessor(conn, proc, arg); 249 | } 250 | 251 | /// Get next result after sending a non-blocking commands. Can return null. 252 | /// 253 | /// Useful only for non-blocking operations. 254 | immutable(Result) getResult() 255 | { 256 | // is guaranteed by libpq that the result will not be changed until it will not be destroyed 257 | auto r = cast(immutable) PQgetResult(conn); 258 | 259 | if(r) 260 | { 261 | auto container = new immutable ResultContainer(r); 262 | return new immutable Result(container); 263 | } 264 | 265 | return null; 266 | } 267 | 268 | /// Get result after PQexec* functions or throw exception if pull is empty 269 | package immutable(ResultContainer) createResultContainer(immutable PGresult* r) const 270 | { 271 | if(r is null) throw new ConnectionException(this, __FILE__, __LINE__); 272 | 273 | return new immutable ResultContainer(r); 274 | } 275 | 276 | /// Select single-row mode for the currently-executing query 277 | bool setSingleRowMode() 278 | { 279 | return PQsetSingleRowMode(conn) == 1; 280 | } 281 | 282 | /// Causes a connection to enter pipeline mode if it is currently idle or already in pipeline mode. 283 | void enterPipelineMode() 284 | { 285 | if(PQenterPipelineMode(conn) == 0) 286 | throw new ConnectionException(this); 287 | } 288 | 289 | /// Causes a connection to exit pipeline mode if it is currently in pipeline mode with an empty queue and no pending results. 290 | void exitPipelineMode() 291 | { 292 | if(PQexitPipelineMode(conn) == 0) 293 | throw new ConnectionException(this); 294 | } 295 | 296 | /// Sends a request for the server to flush its output buffer. 297 | void sendFlushRequest() 298 | { 299 | if(PQsendFlushRequest(conn) == 0) 300 | throw new ConnectionException(this); 301 | } 302 | 303 | /// Marks a synchronization point in a pipeline by sending a sync message and flushing the send buffer. 304 | void pipelineSync() 305 | { 306 | if(PQpipelineSync(conn) != 1) 307 | throw new ConnectionException(this); 308 | } 309 | 310 | /// 311 | PGpipelineStatus pipelineStatus() 312 | { 313 | return PQpipelineStatus(conn); 314 | } 315 | 316 | /** 317 | Try to cancel query in a blocking manner 318 | 319 | If the cancellation is effective, the current command will 320 | terminate early and return an error result or exception. If the 321 | cancellation will fails (say, because the server was already done 322 | processing the command) there will be no visible result at all. 323 | */ 324 | void cancel() 325 | { 326 | auto c = new Cancellation(this); 327 | c.doCancelBlocking; 328 | } 329 | 330 | /// 331 | bool isBusy() nothrow 332 | { 333 | assert(conn); 334 | 335 | return PQisBusy(conn) == 1; 336 | } 337 | 338 | /// 339 | string parameterStatus(string paramName) 340 | { 341 | assert(conn); 342 | 343 | auto res = PQparameterStatus(conn, toStringz(paramName)); 344 | 345 | if(res is null) 346 | throw new ConnectionException(this, __FILE__, __LINE__); 347 | 348 | return to!string(fromStringz(res)); 349 | } 350 | 351 | /// 352 | string escapeLiteral(string msg) 353 | { 354 | assert(conn); 355 | 356 | auto buf = PQescapeLiteral(conn, msg.toStringz, msg.length); 357 | 358 | if(buf is null) 359 | throw new ConnectionException(this, __FILE__, __LINE__); 360 | 361 | string res = buf.fromStringz.to!string; 362 | 363 | PQfreemem(buf); 364 | 365 | return res; 366 | } 367 | 368 | /// 369 | string escapeIdentifier(string msg) 370 | { 371 | assert(conn); 372 | 373 | auto buf = PQescapeIdentifier(conn, msg.toStringz, msg.length); 374 | 375 | if(buf is null) 376 | throw new ConnectionException(this, __FILE__, __LINE__); 377 | 378 | string res = buf.fromStringz.to!string; 379 | 380 | PQfreemem(buf); 381 | 382 | return res; 383 | } 384 | 385 | /// 386 | string dbName() const nothrow 387 | { 388 | assert(conn); 389 | 390 | return PQdb(conn).fromStringz.to!string; 391 | } 392 | 393 | /// 394 | string host() const nothrow 395 | { 396 | assert(conn); 397 | 398 | return PQhost(conn).fromStringz.to!string; 399 | } 400 | 401 | /// 402 | int protocolVersion() const nothrow 403 | { 404 | assert(conn); 405 | 406 | return PQprotocolVersion(conn); 407 | } 408 | 409 | /// 410 | int serverVersion() const nothrow 411 | { 412 | assert(conn); 413 | 414 | return PQserverVersion(conn); 415 | } 416 | 417 | /// 418 | void trace(ref File stream) 419 | { 420 | PQtrace(conn, stream.getFP); 421 | } 422 | 423 | /// 424 | void untrace() 425 | { 426 | PQuntrace(conn); 427 | } 428 | 429 | /// 430 | void setClientEncoding(string encoding) 431 | { 432 | if(PQsetClientEncoding(conn, encoding.toStringz) != 0) 433 | throw new ConnectionException(this, __FILE__, __LINE__); 434 | } 435 | } 436 | 437 | private auto keyValToPQparamsArrays(in string[string] keyValueParams) 438 | { 439 | static struct PQparamsArrays 440 | { 441 | immutable(char)*[] keys; 442 | immutable(char)*[] vals; 443 | } 444 | 445 | PQparamsArrays a; 446 | a.keys.length = keyValueParams.length + 1; 447 | a.vals.length = keyValueParams.length + 1; 448 | 449 | size_t i; 450 | foreach(e; keyValueParams.byKeyValue) 451 | { 452 | a.keys[i] = e.key.toStringz; 453 | a.vals[i] = e.value.toStringz; 454 | 455 | i++; 456 | } 457 | 458 | assert(i == keyValueParams.length); 459 | 460 | return a; 461 | } 462 | 463 | /// Check connection options in the provided connection string 464 | /// 465 | /// Throws exception if connection string isn't passes check. 466 | version(Dpq2_Static) 467 | void connStringCheck(string connString) 468 | { 469 | _connStringCheck(connString); 470 | } 471 | 472 | /// ditto 473 | package void _connStringCheck(string connString) 474 | { 475 | char* errmsg = null; 476 | PQconninfoOption* r = PQconninfoParse(connString.toStringz, &errmsg); 477 | 478 | if(r is null) 479 | { 480 | enforce!OutOfMemoryError(errmsg, "Unable to allocate libpq conninfo data"); 481 | } 482 | else 483 | { 484 | PQconninfoFree(r); 485 | } 486 | 487 | if(errmsg !is null) 488 | { 489 | string s = errmsg.fromStringz.to!string; 490 | PQfreemem(cast(void*) errmsg); 491 | 492 | throw new ConnectionException(s, __FILE__, __LINE__); 493 | } 494 | } 495 | 496 | /// Connection exception 497 | class ConnectionException : Dpq2Exception 498 | { 499 | this(in Connection c, string file = __FILE__, size_t line = __LINE__) 500 | { 501 | super(c.errorMessage(), file, line); 502 | } 503 | 504 | this(string msg, string file = __FILE__, size_t line = __LINE__) 505 | { 506 | super(msg, file, line); 507 | } 508 | } 509 | 510 | version (integration_tests) 511 | Connection createTestConn(T...)(T params) 512 | { 513 | version(Dpq2_Static) 514 | auto c = new Connection(params); 515 | else 516 | { 517 | import dpq2.dynloader: connFactory; 518 | 519 | Connection c = connFactory.createConnection(params); 520 | } 521 | 522 | return c; 523 | } 524 | 525 | version (integration_tests) 526 | void _integration_test( string connParam ) 527 | { 528 | { 529 | debug import std.experimental.logger; 530 | 531 | auto c = createTestConn(connParam); 532 | 533 | assert( PQlibVersion() >= 9_0100 ); 534 | 535 | auto dbname = c.dbName(); 536 | auto pver = c.protocolVersion(); 537 | auto sver = c.serverVersion(); 538 | 539 | debug 540 | { 541 | trace("DB name: ", dbname); 542 | trace("Protocol version: ", pver); 543 | trace("Server version: ", sver); 544 | } 545 | 546 | destroy(c); 547 | } 548 | 549 | { 550 | version(Dpq2_Dynamic) 551 | { 552 | void csc(string s) 553 | { 554 | import dpq2.dynloader: connFactory; 555 | 556 | connFactory.connStringCheck(s); 557 | } 558 | } 559 | else 560 | void csc(string s){ connStringCheck(s); } 561 | 562 | csc("dbname=postgres user=postgres"); 563 | 564 | { 565 | bool raised = false; 566 | 567 | try 568 | csc("wrong conninfo string"); 569 | catch(ConnectionException e) 570 | raised = true; 571 | 572 | assert(raised); 573 | } 574 | } 575 | 576 | { 577 | bool exceptionFlag = false; 578 | 579 | try 580 | auto c = createTestConn(ConnectionStart(), "!!!some incorrect connection string!!!"); 581 | catch(ConnectionException e) 582 | { 583 | exceptionFlag = true; 584 | assert(e.msg.length > 40); // error message check 585 | } 586 | finally 587 | assert(exceptionFlag); 588 | } 589 | 590 | { 591 | auto c = createTestConn(connParam); 592 | 593 | assert(c.escapeLiteral("abc'def") == "'abc''def'"); 594 | assert(c.escapeIdentifier("abc'def") == "\"abc'def\""); 595 | 596 | c.setClientEncoding("WIN866"); 597 | assert(c.exec("show client_encoding")[0][0].as!string == "WIN866"); 598 | } 599 | 600 | { 601 | auto c = createTestConn(connParam); 602 | 603 | assert(c.transactionStatus == PQTRANS_IDLE); 604 | 605 | c.exec("BEGIN"); 606 | assert(c.transactionStatus == PQTRANS_INTRANS); 607 | 608 | try c.exec("DISCARD ALL"); 609 | catch (Exception) {} 610 | assert(c.transactionStatus == PQTRANS_INERROR); 611 | 612 | c.exec("ROLLBACK"); 613 | assert(c.transactionStatus == PQTRANS_IDLE); 614 | } 615 | 616 | { 617 | import std.exception: assertThrown; 618 | 619 | string[string] kv; 620 | kv["host"] = "wrong-host"; 621 | kv["dbname"] = "wrong-db-name"; 622 | 623 | assertThrown!ConnectionException(createTestConn(kv)); 624 | assertThrown!ConnectionException(createTestConn(ConnectionStart(), kv)); 625 | } 626 | } 627 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------