diff --git a/README.md b/README.md index cf6d233b..6e4121a0 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,10 @@ Features * Immutable query result for simplify multithreading * Async queries support * Reading of the text query results to native D text types -* Representation of the binary query results to native D types +* Representation of binary arguments and binary query results as native D types * Text types * Integer and decimal types + * Money type (into money.currency, https://github.com/qznc/d-money) * Some data and time types * JSON type (stored into vibe.data.json.Json) * JSONB type (ditto) diff --git a/dub.json b/dub.json index c8e1b8b5..e71313d7 100644 --- a/dub.json +++ b/dub.json @@ -9,7 +9,8 @@ "targetPath": "bin", "dependencies": { "derelict-pq": "~>4.0.0-alpha.2", - "vibe-d:data": "~>0.8.3-beta.1" + "vibe-d:data": "~>0.8.3-beta.1", + "money": "~>2.3.0" }, "targetType": "sourceLibrary", "-ddoxTool": "scod", diff --git a/src/dpq2/conv/from_d_types.d b/src/dpq2/conv/from_d_types.d index 001f41c4..58d77079 100644 --- a/src/dpq2/conv/from_d_types.d +++ b/src/dpq2/conv/from_d_types.d @@ -13,10 +13,11 @@ import std.bitmanip: nativeToBigEndian; import std.datetime.date: Date, DateTime, TimeOfDay; import std.datetime.systime: SysTime; import std.datetime.timezone: LocalTime, TimeZone, UTC; -import std.traits: isImplicitlyConvertible, isNumeric, OriginalType, Unqual; +import std.traits: isImplicitlyConvertible, isNumeric, isInstanceOf, OriginalType, Unqual; import std.typecons : Nullable; import std.uuid: UUID; import vibe.data.json: Json; +import money: currency; /// Converts Nullable!T to Value Value toValue(T)(T v) @@ -48,6 +49,28 @@ if(isNumeric!(T)) return Value(v.nativeToBigEndian.dup, detectOidTypeFromNative!T, false, ValueFormat.BINARY); } +/// Convert money.currency to PG value +/// +/// Caution: here is no check of fractional precision while conversion! +/// See also: PostgreSQL's "lc_monetary" description and "money" package description +Value toValue(T)(T v) +if(isInstanceOf!(currency, T) && T.amount.sizeof == 8) +{ + return Value(v.amount.nativeToBigEndian.dup, OidType.Money, false, ValueFormat.BINARY); +} + +unittest +{ + import dpq2.conv.to_d_types: PGTestMoney; + + const pgtm = PGTestMoney(-123.45); + + Value v = pgtm.toValue; + + assert(v.oidType == OidType.Money); + assert(v.as!PGTestMoney == pgtm); +} + /** Converts types implicitly convertible to string to PG Value. Note that if string is null it is written as an empty string. @@ -73,7 +96,7 @@ if(is(T : immutable(ubyte)[])) return Value(v, detectOidTypeFromNative!(ubyte[]), false, ValueFormat.BINARY); } -/// +/// Constructs Value from boolean Value toValue(T : bool)(T v) @trusted if (!is(T == Nullable!R, R)) { diff --git a/src/dpq2/conv/native_tests.d b/src/dpq2/conv/native_tests.d index f03ab10e..310758f9 100644 --- a/src/dpq2/conv/native_tests.d +++ b/src/dpq2/conv/native_tests.d @@ -44,6 +44,8 @@ public void _integration_test( string connParam ) @system // to return times in other than UTC time zone but fixed time zone so make the test reproducible in databases with other TZ conn.exec("SET TIMEZONE TO +02"); + conn.exec("SET lc_monetary = 'en_US.UTF-8'"); + QueryParams params; params.resultFormat = ValueFormat.BINARY; @@ -106,6 +108,8 @@ public void _integration_test( string connParam ) @system alias C = testIt; // "C" means "case" + import dpq2.conv.to_d_types: PGTestMoney; + C!PGboolean(true, "boolean", "true"); C!PGboolean(false, "boolean", "false"); C!(Nullable!PGboolean)(Nullable!PGboolean.init, "boolean", "NULL"); @@ -113,6 +117,7 @@ public void _integration_test( string connParam ) @system C!PGsmallint(-32_761, "smallint", "-32761"); C!PGinteger(-2_147_483_646, "integer", "-2147483646"); C!PGbigint(-9_223_372_036_854_775_806, "bigint", "-9223372036854775806"); + C!PGTestMoney(PGTestMoney(-123.45), "money", "'-$123.45'"); C!PGreal(-12.3456f, "real", "-12.3456"); C!PGdouble_precision(-1234.56789012345, "double precision", "-1234.56789012345"); C!PGtext("first line\nsecond line", "text", "'first line\nsecond line'"); diff --git a/src/dpq2/conv/to_d_types.d b/src/dpq2/conv/to_d_types.d index 0643b5c4..a2acb50d 100644 --- a/src/dpq2/conv/to_d_types.d +++ b/src/dpq2/conv/to_d_types.d @@ -276,3 +276,41 @@ if( is( T == Json ) ) auto v = Value([1], OidType.Int4); assertThrown!ValueConvException(v.binaryValueAs!Json); } + +import money: currency, roundingMode; + +/// Returns money type +/// +/// Caution: here is no check of fractional precision while conversion! +/// See also: PostgreSQL's "lc_monetary" description and "money" package description +T binaryValueAs(T)(in Value v) @trusted +if( isInstanceOf!(currency, T) && T.amount.sizeof == 8 ) +{ + import std.format: format; + + if(v.data.length != T.amount.sizeof) + throw new AE( + ET.SIZE_MISMATCH, + format( + "%s length (%d) isn't equal to D money type %s size (%d)", + v.oidType.to!string, + v.data.length, + typeid(T).to!string, + T.amount.sizeof + ) + ); + + T r; + + r.amount = v.data[0 .. T.amount.sizeof].bigEndianToNative!long; + + return r; +} + +package alias PGTestMoney = currency!("TEST_CURR", 2); //TODO: roundingMode.UNNECESSARY + +unittest +{ + auto v = Value([1], OidType.Money); + assertThrown!ValueConvException(v.binaryValueAs!PGTestMoney); +}