├── .gitignore ├── examples ├── sql │ └── test-create.sql ├── Makefile └── test.d ├── source ├── postgres │ └── db.d └── ddb │ ├── db.d │ └── postgres.d └── dub.json /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | libddb.a -------------------------------------------------------------------------------- /examples/sql/test-create.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE MyTest ( 2 | name varchar(255) not null, 3 | value integer not null, 4 | primary key (name) 5 | ); -------------------------------------------------------------------------------- /examples/Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: test.d libddb.a 3 | dmd -I../source -J./sql test.d libddb.a 4 | 5 | libddb.a: ../source/ddb/db.d ../source/ddb/postgres.d 6 | dmd -lib -oflibddb.a -debug -g -w -I../source ../source/ddb/db.d ../source/ddb/postgres.d ../source/postgres/db.d 7 | 8 | clean: 9 | -rm test test.o libddb.a 10 | -------------------------------------------------------------------------------- /source/postgres/db.d: -------------------------------------------------------------------------------- 1 | module postgres.db; 2 | 3 | // This file supports backwards compatibility only. 4 | // Use `import ddb.postgres;` for new code 5 | 6 | public import ddb.postgres; 7 | 8 | import std.stdio; 9 | 10 | shared static this() { 11 | stderr.writeln("WARNING: Module postgres.db deprecated. Replace with module ddb.postgres"); 12 | } 13 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ddb", 3 | "description": "Asynchronous Postgresql Binding", 4 | "license": "BSL-1.0", 5 | "copyright": "Copyright © 2013 Piotr Szturmaj", 6 | "homepage": "https://github.com/pszturmaj/", 7 | "authors": ["Piotr Szturmaj"], 8 | "dependencies": { 9 | "vibe-d": {"version": ">=0.7.19", "optional": true} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/test.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | import ddb.postgres; 3 | 4 | int main(string[] argv) 5 | { 6 | // Point this to a valid test database before compiling 7 | auto conn = new PGConnection([ 8 | "host" : "localhost", 9 | "database" : "test", 10 | "user" : "test", 11 | "password" : "test" 12 | ]); 13 | 14 | scope(exit) conn.close; 15 | 16 | auto cmd = new PGCommand(conn, "DROP TABLE MyTest;"); 17 | try { 18 | cmd.executeNonQuery; 19 | } 20 | catch (ServerErrorException e) { 21 | // Probably table does not exist - ignore 22 | } 23 | 24 | // Re-use PGCommand object by reassigning to cmd.query 25 | // using strings, multi-line strings, or string imports 26 | cmd.query = import("test-create.sql"); 27 | cmd.executeNonQuery; 28 | 29 | cmd.query = "INSERT INTO MyTest (name, value) VALUES ('foo', 1);"; 30 | assert(cmd.executeNonQuery == 1); // 1 row inserted 31 | 32 | cmd.query = "SELECT name, value FROM MyTest;"; 33 | auto result = cmd.executeQuery; 34 | try 35 | { 36 | foreach (row; result) 37 | { 38 | // Access results using column name or column index 39 | assert(row["name"] == "foo"); 40 | assert(row[0] == "foo"); 41 | assert(row["value"] == 1); 42 | assert(row[1] == 1); 43 | writeln(row["name"], " = ", row[1]); 44 | } 45 | } 46 | finally 47 | { 48 | result.close; 49 | } 50 | 51 | cmd.query = "INSERT INTO MyTest (name, value) VALUES ('bar', 1);"; 52 | assert(cmd.executeNonQuery == 1); // 1 row inserted 53 | 54 | cmd.query = "UPDATE MyTest SET value = 2 where value = 1"; 55 | assert(cmd.executeNonQuery == 2); // 2 rows updated 56 | 57 | // reversing fields in SELECT means column indices change 58 | cmd.query = q"{SELECT value, name FROM MyTest 59 | WHERE name = 'foo';}";; 60 | result = cmd.executeQuery; 61 | try 62 | { 63 | foreach (row; result) 64 | { 65 | assert(row["name"] == "foo"); 66 | assert(row[0] == 2); 67 | assert(row["value"] == 2); 68 | assert(row[1] == "foo"); 69 | writeln(row["name"], " = ", row["value"]); 70 | } 71 | } 72 | finally 73 | { 74 | result.close; 75 | } 76 | 77 | return 0; 78 | } 79 | -------------------------------------------------------------------------------- /source/ddb/db.d: -------------------------------------------------------------------------------- 1 | module ddb.db; 2 | 3 | /** 4 | Common relational database interfaces. 5 | 6 | Copyright: Copyright Piotr Szturmaj 2011-. 7 | License: $(LINK2 http://boost.org/LICENSE_1_0.txt, Boost License 1.0). 8 | Authors: Piotr Szturmaj 9 | */ 10 | 11 | //module db; 12 | 13 | import std.conv, std.traits, std.typecons, std.typetuple, std.variant; 14 | 15 | /** 16 | Data row returned from database servers. 17 | 18 | DBRow may be instantiated with any number of arguments. It subtypes base type which 19 | depends on that number: 20 | 21 | $(TABLE 22 | $(TR $(TH Number of arguments) $(TH Base type)) 23 | $(TR $(TD 0) $(TD Variant[] $(BR)$(BR) 24 | It is default dynamic row, which can handle arbitrary number of columns and any of their types. 25 | )) 26 | $(TR $(TD 1) $(TD Specs itself, more precisely Specs[0] $(BR) 27 | --- 28 | struct S { int i, float f } 29 | 30 | DBRow!int rowInt; 31 | DBRow!S rowS; 32 | DBRow!(Tuple!(string, bool)) rowTuple; 33 | DBRow!(int[10]) rowSA; 34 | DBRow!(bool[]) rowDA; 35 | --- 36 | )) 37 | $(TR $(TD >= 2) $(TD Tuple!Specs $(BR) 38 | --- 39 | DBRow!(int, string) row1; // two arguments 40 | DBRow!(int, "i") row2; // two arguments 41 | --- 42 | )) 43 | ) 44 | 45 | If there is only one argument, the semantics depend on its type: 46 | 47 | $(TABLE 48 | $(TR $(TH Type) $(TH Semantics)) 49 | $(TR $(TD base type, such as int) $(TD Row contains only one column of that type)) 50 | $(TR $(TD struct) $(TD Row columns are mapped to fields of the struct in the same order)) 51 | $(TR $(TD Tuple) $(TD Row columns are mapped to tuple fields in the same order)) 52 | $(TR $(TD static array) $(TD Row columns are mapped to array items, they share the same type)) 53 | $(TR $(TD dynamic array) $(TD Same as static array, except that column count may change during runtime)) 54 | ) 55 | Note: String types are treated as base types. 56 | 57 | There is an exception for RDBMSes which are capable of returning arrays and/or composite types. If such a 58 | database server returns array or composite in one column it may be mapped to DBRow as if it was many columns. 59 | For example: 60 | --- 61 | struct S { string field1; int field2; } 62 | DBRow!S row; 63 | --- 64 | In this case row may handle result that either: 65 | $(UL 66 | $(LI has two columns convertible to respectively, string and int) 67 | $(LI has one column with composite type compatible with S) 68 | ) 69 | 70 | _DBRow's instantiated with dynamic array (and thus default Variant[]) provide additional bracket syntax 71 | for accessing fields: 72 | --- 73 | auto value = row["columnName"]; 74 | --- 75 | There are cases when result contains duplicate column names. Normally column name inside brackets refers 76 | to the first column of that name. To access other columns with that name, use additional index parameter: 77 | --- 78 | auto value = row["columnName", 1]; // second column named "columnName" 79 | 80 | auto value = row["columnName", 0]; // first column named "columnName" 81 | auto value = row["columnName"]; // same as above 82 | --- 83 | 84 | Examples: 85 | 86 | Default untyped (dynamic) _DBRow: 87 | --- 88 | DBRow!() row1; 89 | DBRow!(Variant[]) row2; 90 | 91 | assert(is(typeof(row1.base == row2.base))); 92 | 93 | auto cmd = new PGCommand(conn, "SElECT typname, typlen FROM pg_type"); 94 | auto result = cmd.executeQuery; 95 | 96 | foreach (i, row; result) 97 | { 98 | writeln(i, " - ", row["typname"], ", ", row["typlen"]); 99 | } 100 | 101 | result.close; 102 | --- 103 | _DBRow with only one field: 104 | --- 105 | DBRow!int row; 106 | row = 10; 107 | row += 1; 108 | assert(row == 11); 109 | 110 | DBRow!Variant untypedRow; 111 | untypedRow = 10; 112 | --- 113 | _DBRow with more than one field: 114 | --- 115 | struct S { int i; string s; } 116 | alias Tuple!(int, "i", string, "s") TS; 117 | 118 | // all three rows are compatible 119 | DBRow!S row1; 120 | DBRow!TS row2; 121 | DBRow!(int, "i", string, "s") row3; 122 | 123 | row1.i = row2.i = row3.i = 10; 124 | row1.s = row2.s = row3.s = "abc"; 125 | 126 | // these two rows are also compatible 127 | DBRow!(int, int) row4; 128 | DBRow!(int[2]) row5; 129 | 130 | row4[0] = row5[0] = 10; 131 | row4[1] = row5[1] = 20; 132 | --- 133 | Advanced example: 134 | --- 135 | enum Axis { x, y, z } 136 | struct SubRow1 { string s; int[] nums; int num; } 137 | alias Tuple!(int, "num", string, "s") SubRow2; 138 | struct Row { SubRow1 left; SubRow2[] right; Axis axis; string text; } 139 | 140 | auto cmd = new PGCommand(conn, "SELECT ROW('text', ARRAY[1, 2, 3], 100), 141 | ARRAY[ROW(1, 'str'), ROW(2, 'aab')], 'x', 'anotherText'"); 142 | 143 | auto row = cmd.executeRow!Row; 144 | 145 | assert(row.left.s == "text"); 146 | assert(row.left.nums == [1, 2, 3]); 147 | assert(row.left.num == 100); 148 | assert(row.right[0].num == 1 && row.right[0].s == "str"); 149 | assert(row.right[1].num == 2 && row.right[1].s == "aab"); 150 | assert(row.axis == Axis.x); 151 | assert(row.s == "anotherText"); 152 | --- 153 | */ 154 | struct DBRow(Specs...) 155 | { 156 | static if (Specs.length == 0) 157 | alias Variant[] T; 158 | else static if (Specs.length == 1) 159 | alias Specs[0] T; 160 | else 161 | alias Tuple!Specs T; 162 | 163 | T base; 164 | alias base this; 165 | 166 | static if (isDynamicArray!T && !isSomeString!T) 167 | { 168 | mixin template elmnt(U : U[]){ 169 | alias U ElemType; 170 | } 171 | mixin elmnt!T; 172 | enum hasStaticLength = false; 173 | 174 | void setLength(size_t length) 175 | { 176 | base.length = length; 177 | } 178 | 179 | void setNull(size_t index) 180 | { 181 | static if (isNullable!ElemType) 182 | base[index] = null; 183 | else 184 | throw new Exception("Cannot set NULL to field " ~ to!string(index) ~ " of " ~ T.stringof ~ ", it is not nullable"); 185 | } 186 | 187 | ColumnToIndexDelegate columnToIndex; 188 | 189 | ElemType opIndex(string column, size_t index) 190 | { 191 | return base[columnToIndex(column, index)]; 192 | } 193 | 194 | ElemType opIndexAssign(ElemType value, string column, size_t index) 195 | { 196 | return base[columnToIndex(column, index)] = value; 197 | } 198 | 199 | ElemType opIndex(string column) 200 | { 201 | return base[columnToIndex(column, 0)]; 202 | } 203 | 204 | ElemType opIndexAssign(ElemType value, string column) 205 | { 206 | return base[columnToIndex(column, 0)] = value; 207 | } 208 | 209 | ElemType opIndex(size_t index) 210 | { 211 | return base[index]; 212 | } 213 | 214 | ElemType opIndexAssign(ElemType value, size_t index) 215 | { 216 | return base[index] = value; 217 | } 218 | } 219 | else static if (isCompositeType!T) 220 | { 221 | static if (isStaticArray!T) 222 | { 223 | template ArrayTypeTuple(AT : U[N], U, size_t N) 224 | { 225 | static if (N > 1) 226 | alias TypeTuple!(U, ArrayTypeTuple!(U[N - 1])) ArrayTypeTuple; 227 | else 228 | alias TypeTuple!U ArrayTypeTuple; 229 | } 230 | 231 | alias ArrayTypeTuple!T fieldTypes; 232 | } 233 | else 234 | alias FieldTypeTuple!T fieldTypes; 235 | 236 | enum hasStaticLength = true; 237 | 238 | void set(U, size_t index)(U value) 239 | { 240 | static if (isStaticArray!T) 241 | base[index] = value; 242 | else 243 | base.tupleof[index] = value; 244 | } 245 | 246 | void setNull(size_t index)() 247 | { 248 | static if (isNullable!(fieldTypes[index])) 249 | { 250 | static if (isStaticArray!T) 251 | base[index] = null; 252 | else 253 | base.tupleof[index] = null; 254 | } 255 | else 256 | throw new Exception("Cannot set NULL to field " ~ to!string(index) ~ " of " ~ T.stringof ~ ", it is not nullable"); 257 | } 258 | } 259 | else static if (Specs.length == 1) 260 | { 261 | alias TypeTuple!T fieldTypes; 262 | enum hasStaticLength = true; 263 | 264 | void set(T, size_t index)(T value) 265 | { 266 | base = value; 267 | } 268 | 269 | void setNull(size_t index)() 270 | { 271 | static if (isNullable!T) 272 | base = null; 273 | else 274 | throw new Exception("Cannot set NULL to " ~ T.stringof ~ ", it is not nullable"); 275 | } 276 | } 277 | 278 | static if (hasStaticLength) 279 | { 280 | /** 281 | Checks if received field count matches field count of this row type. 282 | 283 | This is used internally by clients and it applies only to DBRow types, which have static number of fields. 284 | */ 285 | static pure void checkReceivedFieldCount(int fieldCount) 286 | { 287 | if (fieldTypes.length != fieldCount) 288 | throw new Exception("Received field count is not equal to " ~ T.stringof ~ "'s field count"); 289 | } 290 | } 291 | 292 | string toString() 293 | { 294 | return to!string(base); 295 | } 296 | } 297 | 298 | alias size_t delegate(string column, size_t index) ColumnToIndexDelegate; 299 | 300 | /** 301 | Check if type is a composite. 302 | 303 | Composite is a type with static number of fields. These types are: 304 | $(UL 305 | $(LI Tuples) 306 | $(LI structs) 307 | $(LI static arrays) 308 | ) 309 | */ 310 | template isCompositeType(T) 311 | { 312 | static if (isTuple!T || is(T == struct) || isStaticArray!T) 313 | enum isCompositeType = true; 314 | else 315 | enum isCompositeType = false; 316 | } 317 | 318 | template Nullable(T) 319 | if (!__traits(compiles, { T t = null; })) 320 | { 321 | /* 322 | Currently with void*, because otherwise it wont accept nulls. 323 | VariantN need to be changed to support nulls without using void*, which may 324 | be a legitimate type to store, as pointed out by Andrei. 325 | Preferable alias would be then Algebraic!(T, void) or even Algebraic!T, since 326 | VariantN already may hold "uninitialized state". 327 | */ 328 | alias Algebraic!(T, void*) Nullable; 329 | } 330 | 331 | template isVariantN(T) 332 | { 333 | //static if (is(T X == VariantN!(N, Types), uint N, Types...)) // doesn't work due to BUG 5784 334 | static if (T.stringof.length >= 8 && T.stringof[0..8] == "VariantN") // ugly temporary workaround 335 | enum isVariantN = true; 336 | else 337 | enum isVariantN = false; 338 | } 339 | 340 | static assert(isVariantN!Variant); 341 | static assert(isVariantN!(Algebraic!(int, string))); 342 | static assert(isVariantN!(Nullable!int)); 343 | 344 | template isNullable(T) 345 | { 346 | static if ((isVariantN!T && T.allowed!(void*)) || is(T X == Nullable!U, U)) 347 | enum isNullable = true; 348 | else 349 | enum isNullable = false; 350 | } 351 | 352 | static assert(isNullable!Variant); 353 | static assert(isNullable!(Nullable!int)); 354 | 355 | template nullableTarget(T) 356 | if (isVariantN!T && T.allowed!(void*)) 357 | { 358 | alias T nullableTarget; 359 | } 360 | 361 | template nullableTarget(T : Nullable!U, U) 362 | { 363 | alias U nullableTarget; 364 | } 365 | -------------------------------------------------------------------------------- /source/ddb/postgres.d: -------------------------------------------------------------------------------- 1 | module ddb.postgres; 2 | 3 | /** 4 | PostgreSQL client implementation. 5 | 6 | Features: 7 | $(UL 8 | $(LI Standalone (does not depend on libpq)) 9 | $(LI Binary formatting (avoids parsing overhead)) 10 | $(LI Prepared statements) 11 | $(LI Parametrized queries (partially working)) 12 | $(LI $(LINK2 http://www.postgresql.org/docs/9.0/static/datatype-enum.html, Enums)) 13 | $(LI $(LINK2 http://www.postgresql.org/docs/9.0/static/arrays.html, Arrays)) 14 | $(LI $(LINK2 http://www.postgresql.org/docs/9.0/static/rowtypes.html, Composite types)) 15 | ) 16 | 17 | TODOs: 18 | $(UL 19 | $(LI Redesign parametrized queries) 20 | $(LI BigInt/Numeric types support) 21 | $(LI Geometric types support) 22 | $(LI Network types support) 23 | $(LI Bit string types support) 24 | $(LI UUID type support) 25 | $(LI XML types support) 26 | $(LI Transaction support) 27 | $(LI Asynchronous notifications) 28 | $(LI Better memory management) 29 | $(LI More friendly PGFields) 30 | ) 31 | 32 | Bugs: 33 | $(UL 34 | $(LI Support only cleartext and MD5 $(LINK2 http://www.postgresql.org/docs/9.0/static/auth-methods.html, authentication)) 35 | $(LI Unfinished parameter handling) 36 | $(LI interval is converted to Duration, which does not support months) 37 | ) 38 | 39 | $(B Data type mapping:) 40 | 41 | $(TABLE 42 | $(TR $(TH PostgreSQL type) $(TH Aliases) $(TH Default D type) $(TH D type mapping possibilities)) 43 | $(TR $(TD smallint) $(TD int2) $(TD short) Any type convertible from default D type) 44 | $(TR $(TD integer) $(TD int4) $(TD int)) 45 | $(TR $(TD bigint) $(TD int8) $(TD long)) 46 | $(TR $(TD oid) $(TD reg***) $(TD uint)) 47 | $(TR $(TD decimal) $(TD numeric) $(TD not yet supported)) 48 | $(TR $(TD real) $(TD float4) $(TD float)) 49 | $(TR $(TD double precision) $(TD float8) $(TD double)) 50 | $(TR $(TD character varying(n)) $(TD varchar(n)) $(TD string)) 51 | $(TR $(TD character(n)) $(TD char(n)) $(TD string)) 52 | $(TR $(TD text) $(TD) $(TD string)) 53 | $(TR $(TD "char") $(TD) $(TD char)) 54 | $(TR $(TD bytea) $(TD) $(TD ubyte[])) 55 | $(TR $(TD timestamp without time zone) $(TD timestamp) $(TD DateTime)) 56 | $(TR $(TD timestamp with time zone) $(TD timestamptz) $(TD SysTime)) 57 | $(TR $(TD date) $(TD) $(TD Date)) 58 | $(TR $(TD time without time zone) $(TD time) $(TD TimeOfDay)) 59 | $(TR $(TD time with time zone) $(TD timetz) $(TD SysTime)) 60 | $(TR $(TD interval) $(TD) $(TD Duration (without months and years))) 61 | $(TR $(TD boolean) $(TD bool) $(TD bool)) 62 | $(TR $(TD enums) $(TD) $(TD string) $(TD enum)) 63 | $(TR $(TD arrays) $(TD) $(TD Variant[]) $(TD dynamic/static array with compatible element type)) 64 | $(TR $(TD composites) $(TD record, row) $(TD Variant[]) $(TD dynamic/static array, struct or Tuple)) 65 | ) 66 | 67 | Examples: 68 | with vibe.d use -version=Have_vibe_d and use a ConnectionPool (PostgresDB Object & lockConnection) 69 | --- 70 | 71 | auto pdb = new PostgresDB([ 72 | "host" : "192.168.2.50", 73 | "database" : "postgres", 74 | "user" : "postgres", 75 | "password" : "" 76 | ]); 77 | auto conn = pdb.lockConnection(); 78 | 79 | auto cmd = new PGCommand(conn, "SELECT typname, typlen FROM pg_type"); 80 | auto result = cmd.executeQuery; 81 | 82 | try 83 | { 84 | foreach (row; result) 85 | { 86 | writeln(row["typname"], ", ", row[1]); 87 | } 88 | } 89 | finally 90 | { 91 | result.close; 92 | } 93 | 94 | --- 95 | without vibe.d you can use std sockets with PGConnection object 96 | 97 | import std.stdio; 98 | import ddb.postgres; 99 | 100 | int main(string[] argv) 101 | { 102 | auto conn = new PGConnection([ 103 | "host" : "localhost", 104 | "database" : "test", 105 | "user" : "postgres", 106 | "password" : "postgres" 107 | ]); 108 | 109 | scope(exit) conn.close; 110 | 111 | auto cmd = new PGCommand(conn, "SELECT typname, typlen FROM pg_type"); 112 | auto result = cmd.executeQuery; 113 | 114 | try 115 | { 116 | foreach (row; result) 117 | { 118 | writeln(row[0], ", ", row[1]); 119 | } 120 | } 121 | finally 122 | { 123 | result.close; 124 | } 125 | 126 | return 0; 127 | } 128 | --- 129 | 130 | Copyright: Copyright Piotr Szturmaj 2011-. 131 | License: $(LINK2 http://boost.org/LICENSE_1_0.txt, Boost License 1.0). 132 | Authors: Piotr Szturmaj 133 | *//* 134 | Documentation contains portions copied from PostgreSQL manual (mainly field information and 135 | connection parameters description). License: 136 | 137 | Portions Copyright (c) 1996-2010, The PostgreSQL Global Development Group 138 | Portions Copyright (c) 1994, The Regents of the University of California 139 | 140 | Permission to use, copy, modify, and distribute this software and its documentation for any purpose, 141 | without fee, and without a written agreement is hereby granted, provided that the above copyright 142 | notice and this paragraph and the following two paragraphs appear in all copies. 143 | 144 | IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, 145 | INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, 146 | ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY 147 | OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 148 | 149 | THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 150 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 151 | PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 152 | CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, 153 | OR MODIFICATIONS. 154 | */ 155 | 156 | 157 | 158 | version (Have_vibe_d) { 159 | import vibe.core.net; 160 | import vibe.core.stream; 161 | } else { 162 | import std.socket; 163 | import std.socketstream; 164 | } 165 | import std.bitmanip; 166 | import std.exception; 167 | import std.conv; 168 | import std.traits; 169 | import std.typecons; 170 | import std.string; 171 | import std.digest.md; 172 | import core.bitop; 173 | import std.variant; 174 | import std.algorithm; 175 | import std.stdio; 176 | import std.datetime; 177 | public import ddb.db; 178 | 179 | private: 180 | 181 | const PGEpochDate = Date(2000, 1, 1); 182 | const PGEpochDay = PGEpochDate.dayOfGregorianCal; 183 | const PGEpochTime = TimeOfDay(0, 0, 0); 184 | const PGEpochDateTime = DateTime(2000, 1, 1, 0, 0, 0); 185 | 186 | class PGStream 187 | { 188 | private { 189 | version (Have_vibe_d) TCPConnection m_socket; 190 | else SocketStream m_socket; 191 | } 192 | version (Have_vibe_d){ 193 | @property TCPConnection socket() { return m_socket; } 194 | this(TCPConnection socket) 195 | { 196 | m_socket = socket; 197 | } 198 | }else{ 199 | @property SocketStream socket() { return m_socket; } 200 | this(SocketStream socket){ 201 | m_socket = socket; 202 | } 203 | } 204 | 205 | 206 | /* 207 | * I'm not too sure about this function 208 | * Should I keep the length? 209 | */ 210 | void write(ubyte[] x) 211 | { 212 | m_socket.write(x); 213 | } 214 | 215 | void write(ubyte x) 216 | { 217 | write(nativeToBigEndian(x)); // ubyte[] 218 | } 219 | 220 | void write(short x) 221 | { 222 | write(nativeToBigEndian(x)); // ubyte[] 223 | } 224 | 225 | void write(int x) 226 | { 227 | write(nativeToBigEndian(x)); // ubyte[] 228 | } 229 | 230 | void write(long x) 231 | { 232 | write(nativeToBigEndian(x)); 233 | } 234 | 235 | void write(float x) 236 | { 237 | write(nativeToBigEndian(x)); // ubyte[] 238 | } 239 | 240 | void write(double x) 241 | { 242 | write(nativeToBigEndian(x)); 243 | } 244 | 245 | void writeCString(string x) 246 | { 247 | ubyte[] ub = cast(ubyte[])(x ~ "\0"); 248 | write(ub); 249 | } 250 | 251 | void write(const ref Date x) 252 | { 253 | write(cast(int)(x.dayOfGregorianCal - PGEpochDay)); 254 | } 255 | 256 | void write(const ref TimeOfDay x) 257 | { 258 | write(cast(int)((x - PGEpochTime).total!"usecs")); 259 | } 260 | 261 | void write(const ref DateTime x) // timestamp 262 | { 263 | write(cast(int)((x - PGEpochDateTime).total!"usecs")); 264 | } 265 | 266 | void write(const ref SysTime x) // timestamptz 267 | { 268 | write(cast(int)((x - SysTime(PGEpochDateTime, UTC())).total!"usecs")); 269 | } 270 | 271 | // BUG: Does not support months 272 | void write(const ref core.time.Duration x) // interval 273 | { 274 | int months = cast(int)(x.weeks/4); 275 | int days = cast(int)x.days; 276 | long usecs = x.total!"usecs" - convert!("days", "usecs")(days); 277 | 278 | write(usecs); 279 | write(days); 280 | write(months); 281 | } 282 | 283 | void writeTimeTz(const ref SysTime x) // timetz 284 | { 285 | TimeOfDay t = cast(TimeOfDay)x; 286 | write(t); 287 | write(cast(int)0); 288 | } 289 | } 290 | 291 | string MD5toHex(in void[][] data...) 292 | { 293 | return md5Of(data).toHexString; 294 | } 295 | 296 | struct Message 297 | { 298 | PGConnection conn; 299 | char type; 300 | ubyte[] data; 301 | 302 | private size_t position = 0; 303 | 304 | T read(T, Params...)(Params p) 305 | { 306 | T value; 307 | read(value, p); 308 | return value; 309 | } 310 | 311 | void read()(out char x) 312 | { 313 | x = data[position++]; 314 | } 315 | 316 | void read()(out short x) 317 | { 318 | x = bigEndianToNative!short(cast(ubyte[short.sizeof])data[position..position+short.sizeof]); 319 | position += 2; 320 | } 321 | 322 | void read()(out int x) 323 | { 324 | x = bigEndianToNative!int(cast(ubyte[int.sizeof])data[position..position+int.sizeof]); 325 | position += 4; 326 | } 327 | 328 | void read()(out long x) 329 | { 330 | x = bigEndianToNative!long(cast(ubyte[8])data[position..position+long.sizeof]); 331 | position += 8; 332 | } 333 | 334 | void read()(out float x) 335 | { 336 | x = bigEndianToNative!float(cast(ubyte[float.sizeof])data[position..position+float.sizeof]); 337 | position += float.sizeof; 338 | } 339 | 340 | void read()(out double x) 341 | { 342 | x = bigEndianToNative!double(cast(ubyte[double.sizeof])data[position..position+double.sizeof]); 343 | position += double.sizeof; 344 | } 345 | 346 | string readCString() 347 | { 348 | string x; 349 | readCString(x); 350 | return x; 351 | } 352 | 353 | void readCString(out string x) 354 | { 355 | ubyte* p = data.ptr + position; 356 | 357 | while (*p > 0) 358 | p++; 359 | x = cast(string)data[position .. cast(size_t)(p - data.ptr)]; 360 | position = cast(size_t)(p - data.ptr + 1); 361 | } 362 | 363 | string readString(int len) 364 | { 365 | string x; 366 | readString(x, len); 367 | return x; 368 | } 369 | 370 | void readString(out string x, int len) 371 | { 372 | x = cast(string)(data[position .. position + len]); 373 | position += len; 374 | } 375 | 376 | void read()(out uint x) 377 | { 378 | x = bigEndianToNative!uint( cast(ubyte[4]) data[position .. position + uint.sizeof] ); 379 | position += 4; 380 | } 381 | 382 | void read()(out bool x) 383 | { 384 | x = cast(bool)data[position++]; 385 | } 386 | 387 | void read()(out ubyte[] x, int len) 388 | { 389 | enforce(position + len <= data.length); 390 | x = data[position .. position + len]; 391 | position += len; 392 | } 393 | 394 | void read()(out Date x) // date 395 | { 396 | int days = read!int; // number of days since 1 Jan 2000 397 | x = PGEpochDate + dur!"days"(days); 398 | } 399 | 400 | void read()(out TimeOfDay x) // time 401 | { 402 | long usecs = read!long; 403 | x = PGEpochTime + dur!"usecs"(usecs); 404 | } 405 | 406 | void read()(out DateTime x) // timestamp 407 | { 408 | long usecs = read!long; 409 | x = PGEpochDateTime + dur!"usecs"(usecs); 410 | } 411 | 412 | void read()(out SysTime x) // timestamptz 413 | { 414 | long usecs = read!long; 415 | x = SysTime(PGEpochDateTime + dur!"usecs"(usecs), UTC()); 416 | x.timezone = LocalTime(); 417 | } 418 | 419 | // BUG: Does not support months 420 | void read()(out core.time.Duration x) // interval 421 | { 422 | long usecs = read!long; 423 | int days = read!int; 424 | int months = read!int; 425 | 426 | x = dur!"days"(days) + dur!"usecs"(usecs); 427 | } 428 | 429 | SysTime readTimeTz() // timetz 430 | { 431 | TimeOfDay time = read!TimeOfDay; 432 | int zone = read!int / 60; // originally in seconds, convert it to minutes 433 | auto stz = new immutable SimpleTimeZone(zone); 434 | return SysTime(DateTime(Date(0, 1, 1), time), stz); 435 | } 436 | 437 | T readComposite(T)() 438 | { 439 | alias DBRow!T Record; 440 | 441 | static if (Record.hasStaticLength) 442 | { 443 | alias Record.fieldTypes fieldTypes; 444 | 445 | static string genFieldAssigns() // CTFE 446 | { 447 | string s = ""; 448 | 449 | foreach (i; 0 .. fieldTypes.length) 450 | { 451 | s ~= "read(fieldOid);\n"; 452 | s ~= "read(fieldLen);\n"; 453 | s ~= "if (fieldLen == -1)\n"; 454 | s ~= text("record.setNull!(", i, ");\n"); 455 | s ~= "else\n"; 456 | s ~= text("record.set!(fieldTypes[", i, "], ", i, ")(", 457 | "readBaseType!(fieldTypes[", i, "])(fieldOid, fieldLen)", 458 | ");\n"); 459 | // text() doesn't work with -inline option, CTFE bug 460 | } 461 | 462 | return s; 463 | } 464 | } 465 | 466 | Record record; 467 | 468 | int fieldCount, fieldLen; 469 | uint fieldOid; 470 | 471 | read(fieldCount); 472 | 473 | static if (Record.hasStaticLength) 474 | mixin(genFieldAssigns); 475 | else 476 | { 477 | record.setLength(fieldCount); 478 | 479 | foreach (i; 0 .. fieldCount) 480 | { 481 | read(fieldOid); 482 | read(fieldLen); 483 | 484 | if (fieldLen == -1) 485 | record.setNull(i); 486 | else 487 | record[i] = readBaseType!(Record.ElemType)(fieldOid, fieldLen); 488 | } 489 | } 490 | 491 | return record.base; 492 | } 493 | mixin template elmnt(U : U[]) 494 | { 495 | alias U ElemType; 496 | } 497 | private AT readDimension(AT)(int[] lengths, uint elementOid, int dim) 498 | { 499 | 500 | mixin elmnt!AT; 501 | 502 | int length = lengths[dim]; 503 | 504 | AT array; 505 | static if (isDynamicArray!AT) 506 | array.length = length; 507 | 508 | int fieldLen; 509 | 510 | foreach(i; 0 .. length) 511 | { 512 | static if (isArray!ElemType && !isSomeString!ElemType) 513 | array[i] = readDimension!ElemType(lengths, elementOid, dim + 1); 514 | else 515 | { 516 | static if (isNullable!ElemType) 517 | alias nullableTarget!ElemType E; 518 | else 519 | alias ElemType E; 520 | 521 | read(fieldLen); 522 | if (fieldLen == -1) 523 | { 524 | static if (isNullable!ElemType || isSomeString!ElemType) 525 | array[i] = null; 526 | else 527 | throw new Exception("Can't set NULL value to non nullable type"); 528 | } 529 | else 530 | array[i] = readBaseType!E(elementOid, fieldLen); 531 | } 532 | } 533 | 534 | return array; 535 | } 536 | 537 | T readArray(T)() 538 | if (isArray!T) 539 | { 540 | alias multiArrayElemType!T U; 541 | 542 | // todo: more validation, better lowerBounds support 543 | int dims, hasNulls; 544 | uint elementOid; 545 | int[] lengths, lowerBounds; 546 | 547 | read(dims); 548 | read(hasNulls); // 0 or 1 549 | read(elementOid); 550 | 551 | if (dims == 0) 552 | return T.init; 553 | 554 | enforce(arrayDimensions!T == dims, "Dimensions of arrays do not match"); 555 | static if (!isNullable!U && !isSomeString!U) 556 | enforce(!hasNulls, "PostgreSQL returned NULLs but array elements are not Nullable"); 557 | 558 | lengths.length = lowerBounds.length = dims; 559 | 560 | int elementCount = 1; 561 | 562 | foreach(i; 0 .. dims) 563 | { 564 | int len; 565 | 566 | read(len); 567 | read(lowerBounds[i]); 568 | lengths[i] = len; 569 | 570 | elementCount *= len; 571 | } 572 | 573 | T array = readDimension!T(lengths, elementOid, 0); 574 | 575 | return array; 576 | } 577 | 578 | T readEnum(T)(int len) 579 | { 580 | string genCases() // CTFE 581 | { 582 | string s; 583 | 584 | foreach (name; __traits(allMembers, T)) 585 | { 586 | s ~= text(`case "`, name, `": return T.`, name, `;`); 587 | } 588 | 589 | return s; 590 | } 591 | 592 | string enumMember = readString(len); 593 | 594 | switch (enumMember) 595 | { 596 | mixin(genCases); 597 | default: throw new ConvException("Can't set enum value '" ~ enumMember ~ "' to enum type " ~ T.stringof); 598 | } 599 | } 600 | 601 | T readBaseType(T)(uint oid, int len = 0) 602 | { 603 | void convError(T)() 604 | { 605 | string* type = oid in baseTypes; 606 | throw new ConvException("Can't convert PostgreSQL's type " ~ (type ? *type : to!string(oid)) ~ " to " ~ T.stringof); 607 | } 608 | 609 | switch (oid) 610 | { 611 | case 16: // bool 612 | static if (isConvertible!(T, bool)) 613 | return _to!T(read!bool); 614 | else 615 | convError!T; 616 | case 26, 24, 2202, 2203, 2204, 2205, 2206, 3734, 3769: // oid and reg*** aliases 617 | static if (isConvertible!(T, uint)) 618 | return _to!T(read!uint); 619 | else 620 | convError!T; 621 | case 21: // int2 622 | static if (isConvertible!(T, short)) 623 | return _to!T(read!short); 624 | else 625 | convError!T; 626 | case 23: // int4 627 | static if (isConvertible!(T, int)) 628 | return _to!T(read!int); 629 | else 630 | convError!T; 631 | case 20: // int8 632 | static if (isConvertible!(T, long)) 633 | return _to!T(read!long); 634 | else 635 | convError!T; 636 | case 700: // float4 637 | static if (isConvertible!(T, float)) 638 | return _to!T(read!float); 639 | else 640 | convError!T; 641 | case 701: // float8 642 | static if (isConvertible!(T, double)) 643 | return _to!T(read!double); 644 | else 645 | convError!T; 646 | case 1042, 1043, 25, 19, 705: // bpchar, varchar, text, name, unknown 647 | static if (isConvertible!(T, string)) 648 | return _to!T(readString(len)); 649 | else 650 | convError!T; 651 | case 17: // bytea 652 | static if (isConvertible!(T, ubyte[])) 653 | return _to!T(read!(ubyte[])(len)); 654 | else { 655 | convError!T; 656 | break; 657 | } 658 | case 18: // "char" 659 | static if (isConvertible!(T, char)) 660 | return _to!T(read!char); 661 | else { 662 | convError!T; 663 | break; 664 | } 665 | case 1082: // date 666 | static if (isConvertible!(T, Date)) 667 | return _to!T(read!Date); 668 | else { 669 | convError!T; 670 | break; 671 | } 672 | case 1083: // time 673 | static if (isConvertible!(T, TimeOfDay)) 674 | return _to!T(read!TimeOfDay); 675 | else { 676 | convError!T; 677 | break; 678 | } 679 | case 1114: // timestamp 680 | static if (isConvertible!(T, DateTime)) 681 | return _to!T(read!DateTime); 682 | else { 683 | convError!T; 684 | break; 685 | } 686 | case 1184: // timestamptz 687 | static if (isConvertible!(T, SysTime)) 688 | return _to!T(read!SysTime); 689 | else { 690 | convError!T; 691 | break; 692 | } 693 | case 1186: // interval 694 | static if (isConvertible!(T, core.time.Duration)) 695 | return _to!T(read!(core.time.Duration)); 696 | else { 697 | convError!T; 698 | break; 699 | } 700 | 701 | case 1266: // timetz 702 | static if (isConvertible!(T, SysTime)) 703 | return _to!T(readTimeTz); 704 | else { 705 | convError!T; 706 | break; 707 | } 708 | case 2249: // record and other composite types 709 | static if (isVariantN!T && T.allowed!(Variant[])) 710 | return T(readComposite!(Variant[])); 711 | else 712 | return readComposite!T; 713 | case 2287: // _record and other arrays 714 | static if (isArray!T && !isSomeString!T) 715 | return readArray!T; 716 | else static if (isVariantN!T && T.allowed!(Variant[])) 717 | return T(readArray!(Variant[])); 718 | else { 719 | convError!T; 720 | break; 721 | } 722 | default: 723 | if (oid in conn.arrayTypes) 724 | goto case 2287; 725 | else if (oid in conn.compositeTypes) 726 | goto case 2249; 727 | else if (oid in conn.enumTypes) 728 | { 729 | static if (is(T == enum)) 730 | return readEnum!T(len); 731 | else static if (isConvertible!(T, string)) 732 | return _to!T(readString(len)); 733 | else 734 | convError!T; 735 | } 736 | } 737 | 738 | convError!T; 739 | assert(0); 740 | } 741 | } 742 | 743 | // workaround, because std.conv currently doesn't support VariantN 744 | template _to(T) 745 | { 746 | static if (isVariantN!T) 747 | T _to(S)(S value) { T t = value; return t; } 748 | else 749 | T _to(A...)(A args) { return toImpl!T(args); } 750 | } 751 | 752 | template isConvertible(T, S) 753 | { 754 | static if (__traits(compiles, { S s; _to!T(s); }) || (isVariantN!T && T.allowed!S)) 755 | enum isConvertible = true; 756 | else 757 | enum isConvertible = false; 758 | } 759 | 760 | template arrayDimensions(T : T[]) 761 | { 762 | static if (isArray!T && !isSomeString!T) 763 | enum arrayDimensions = arrayDimensions!T + 1; 764 | else 765 | enum arrayDimensions = 1; 766 | } 767 | 768 | template arrayDimensions(T) 769 | { 770 | enum arrayDimensions = 0; 771 | } 772 | 773 | template multiArrayElemType(T : T[]) 774 | { 775 | static if (isArray!T && !isSomeString!T) 776 | alias multiArrayElemType!T multiArrayElemType; 777 | else 778 | alias T multiArrayElemType; 779 | } 780 | 781 | template multiArrayElemType(T) 782 | { 783 | alias T multiArrayElemType; 784 | } 785 | 786 | static assert(arrayDimensions!(int) == 0); 787 | static assert(arrayDimensions!(int[]) == 1); 788 | static assert(arrayDimensions!(int[][]) == 2); 789 | static assert(arrayDimensions!(int[][][]) == 3); 790 | 791 | enum TransactionStatus : char { OutsideTransaction = 'I', InsideTransaction = 'T', InsideFailedTransaction = 'E' }; 792 | 793 | enum string[int] baseTypes = [ 794 | // boolean types 795 | 16 : "bool", 796 | // bytea types 797 | 17 : "bytea", 798 | // character types 799 | 18 : `"char"`, // "char" - 1 byte internal type 800 | 1042 : "bpchar", // char(n) - blank padded 801 | 1043 : "varchar", 802 | 25 : "text", 803 | 19 : "name", 804 | // numeric types 805 | 21 : "int2", 806 | 23 : "int4", 807 | 20 : "int8", 808 | 700 : "float4", 809 | 701 : "float8", 810 | 1700 : "numeric" 811 | ]; 812 | 813 | public: 814 | 815 | enum PGType : int 816 | { 817 | OID = 26, 818 | NAME = 19, 819 | REGPROC = 24, 820 | BOOLEAN = 16, 821 | BYTEA = 17, 822 | CHAR = 18, // 1 byte "char", used internally in PostgreSQL 823 | BPCHAR = 1042, // Blank Padded char(n), fixed size 824 | VARCHAR = 1043, 825 | TEXT = 25, 826 | INT2 = 21, 827 | INT4 = 23, 828 | INT8 = 20, 829 | FLOAT4 = 700, 830 | FLOAT8 = 701 831 | }; 832 | 833 | class ParamException : Exception 834 | { 835 | this(string msg) 836 | { 837 | super(msg); 838 | } 839 | } 840 | 841 | /// Exception thrown on server error 842 | class ServerErrorException: Exception 843 | { 844 | /// Contains information about this _error. Aliased to this. 845 | ResponseMessage error; 846 | alias error this; 847 | 848 | this(string msg) 849 | { 850 | super(msg); 851 | } 852 | 853 | this(ResponseMessage error) 854 | { 855 | super(error.toString()); 856 | this.error = error; 857 | } 858 | } 859 | 860 | /** 861 | Class encapsulating errors and notices. 862 | 863 | This class provides access to fields of ErrorResponse and NoticeResponse 864 | sent by the server. More information about these fields can be found 865 | $(LINK2 http://www.postgresql.org/docs/9.0/static/protocol-error-fields.html,here). 866 | */ 867 | class ResponseMessage 868 | { 869 | private string[char] fields; 870 | 871 | private string getOptional(char type) 872 | { 873 | string* p = type in fields; 874 | return p ? *p : ""; 875 | } 876 | 877 | /// Message fields 878 | @property string severity() 879 | { 880 | return fields['S']; 881 | } 882 | 883 | /// ditto 884 | @property string code() 885 | { 886 | return fields['C']; 887 | } 888 | 889 | /// ditto 890 | @property string message() 891 | { 892 | return fields['M']; 893 | } 894 | 895 | /// ditto 896 | @property string detail() 897 | { 898 | return getOptional('D'); 899 | } 900 | 901 | /// ditto 902 | @property string hint() 903 | { 904 | return getOptional('H'); 905 | } 906 | 907 | /// ditto 908 | @property string position() 909 | { 910 | return getOptional('P'); 911 | } 912 | 913 | /// ditto 914 | @property string internalPosition() 915 | { 916 | return getOptional('p'); 917 | } 918 | 919 | /// ditto 920 | @property string internalQuery() 921 | { 922 | return getOptional('q'); 923 | } 924 | 925 | /// ditto 926 | @property string where() 927 | { 928 | return getOptional('W'); 929 | } 930 | 931 | /// ditto 932 | @property string file() 933 | { 934 | return getOptional('F'); 935 | } 936 | 937 | /// ditto 938 | @property string line() 939 | { 940 | return getOptional('L'); 941 | } 942 | 943 | /// ditto 944 | @property string routine() 945 | { 946 | return getOptional('R'); 947 | } 948 | 949 | /** 950 | Returns summary of this message using the most common fields (severity, 951 | code, message, detail, hint) 952 | */ 953 | override string toString() 954 | { 955 | string s = severity ~ ' ' ~ code ~ ": " ~ message; 956 | 957 | string* detail = 'D' in fields; 958 | if (detail) 959 | s ~= "\nDETAIL: " ~ *detail; 960 | 961 | string* hint = 'H' in fields; 962 | if (hint) 963 | s ~= "\nHINT: " ~ *hint; 964 | 965 | return s; 966 | } 967 | } 968 | 969 | /** 970 | Class representing connection to PostgreSQL server. 971 | */ 972 | class PGConnection 973 | { 974 | private: 975 | PGStream stream; 976 | string[string] serverParams; 977 | int serverProcessID; 978 | int serverSecretKey; 979 | TransactionStatus trStatus; 980 | ulong lastPrepared = 0; 981 | uint[uint] arrayTypes; 982 | uint[][uint] compositeTypes; 983 | string[uint][uint] enumTypes; 984 | bool activeResultSet; 985 | 986 | string reservePrepared() 987 | { 988 | synchronized (this) 989 | { 990 | 991 | return to!string(lastPrepared++); 992 | } 993 | } 994 | 995 | Message getMessage() 996 | { 997 | 998 | char type; 999 | int len; 1000 | ubyte[1] ub; 1001 | stream.socket.read(ub); // message type 1002 | 1003 | type = bigEndianToNative!char(ub); 1004 | ubyte[4] ubi; 1005 | stream.socket.read(ubi); // message length, doesn't include type byte 1006 | 1007 | len = bigEndianToNative!int(ubi) - 4; 1008 | 1009 | ubyte[] msg = new ubyte[len]; 1010 | 1011 | stream.socket.read(msg); 1012 | 1013 | return Message(this, type, msg); 1014 | } 1015 | 1016 | void sendStartupMessage(const string[string] params) 1017 | { 1018 | bool localParam(string key) 1019 | { 1020 | switch (key) 1021 | { 1022 | case "host", "port", "password": return true; 1023 | default: return false; 1024 | } 1025 | } 1026 | 1027 | int len = 9; // length (int), version number (int) and parameter-list's delimiter (byte) 1028 | 1029 | foreach (key, value; params) 1030 | { 1031 | if (localParam(key)) 1032 | continue; 1033 | 1034 | len += key.length + value.length + 2; 1035 | } 1036 | 1037 | stream.write(len); 1038 | stream.write(0x0003_0000); // version number 3 1039 | foreach (key, value; params) 1040 | { 1041 | if (localParam(key)) 1042 | continue; 1043 | stream.writeCString(key); 1044 | stream.writeCString(value); 1045 | } 1046 | stream.write(cast(ubyte)0); 1047 | } 1048 | 1049 | void sendPasswordMessage(string password) 1050 | { 1051 | int len = cast(int)(4 + password.length + 1); 1052 | 1053 | stream.write('p'); 1054 | stream.write(len); 1055 | stream.writeCString(password); 1056 | } 1057 | 1058 | void sendParseMessage(string statementName, string query, int[] oids) 1059 | { 1060 | int len = cast(int)(4 + statementName.length + 1 + query.length + 1 + 2 + oids.length * 4); 1061 | 1062 | stream.write('P'); 1063 | stream.write(len); 1064 | stream.writeCString(statementName); 1065 | stream.writeCString(query); 1066 | stream.write(cast(short)oids.length); 1067 | 1068 | foreach (oid; oids) 1069 | stream.write(oid); 1070 | } 1071 | 1072 | void sendCloseMessage(DescribeType type, string name) 1073 | { 1074 | stream.write('C'); 1075 | stream.write(cast(int)(4 + 1 + name.length + 1)); 1076 | stream.write(cast(char)type); 1077 | stream.writeCString(name); 1078 | } 1079 | 1080 | void sendTerminateMessage() 1081 | { 1082 | stream.write('X'); 1083 | stream.write(cast(int)4); 1084 | } 1085 | 1086 | void sendBindMessage(string portalName, string statementName, PGParameters params) 1087 | { 1088 | int paramsLen = 0; 1089 | bool hasText = false; 1090 | 1091 | foreach (param; params) 1092 | { 1093 | enforce(param.value.hasValue, new ParamException("Parameter $" ~ to!string(param.index) ~ " value is not initialized")); 1094 | 1095 | void checkParam(T)(int len) 1096 | { 1097 | if (param.value != null) 1098 | { 1099 | enforce(param.value.convertsTo!T, new ParamException("Parameter's value is not convertible to " ~ T.stringof)); 1100 | paramsLen += len; 1101 | } 1102 | } 1103 | 1104 | /*final*/ switch (param.type) 1105 | { 1106 | case PGType.INT2: checkParam!short(2); break; 1107 | case PGType.INT4: checkParam!int(4); break; 1108 | case PGType.INT8: checkParam!long(8); break; 1109 | case PGType.TEXT: 1110 | paramsLen += param.value.coerce!string.length; 1111 | hasText = true; 1112 | break; 1113 | default: assert(0, "Not implemented"); 1114 | } 1115 | } 1116 | 1117 | int len = cast(int)( 4 + portalName.length + 1 + statementName.length + 1 + (hasText ? (params.length*2) : 2) + 2 + 2 + 1118 | params.length * 4 + paramsLen + 2 + 2 ); 1119 | 1120 | stream.write('B'); 1121 | stream.write(len); 1122 | stream.writeCString(portalName); 1123 | stream.writeCString(statementName); 1124 | if(hasText) 1125 | { 1126 | stream.write(cast(short) params.length); 1127 | foreach(param; params) 1128 | if(param.type == PGType.TEXT) 1129 | stream.write(cast(short) 0); // text format 1130 | else 1131 | stream.write(cast(short) 1); // binary format 1132 | } else { 1133 | stream.write(cast(short)1); // one parameter format code 1134 | stream.write(cast(short)1); // binary format 1135 | } 1136 | stream.write(cast(short)params.length); 1137 | 1138 | foreach (param; params) 1139 | { 1140 | if (param.value == null) 1141 | { 1142 | stream.write(-1); 1143 | continue; 1144 | } 1145 | 1146 | switch (param.type) 1147 | { 1148 | case PGType.INT2: 1149 | stream.write(cast(int)2); 1150 | stream.write(param.value.coerce!short); 1151 | break; 1152 | case PGType.INT4: 1153 | stream.write(cast(int)4); 1154 | stream.write(param.value.coerce!int); 1155 | break; 1156 | case PGType.INT8: 1157 | stream.write(cast(int)8); 1158 | stream.write(param.value.coerce!long); 1159 | break; 1160 | case PGType.TEXT: 1161 | auto s = param.value.coerce!string; 1162 | stream.write(cast(int) s.length); 1163 | stream.write(cast(ubyte[]) s); 1164 | break; 1165 | default: 1166 | assert(0, "Not implemented"); 1167 | } 1168 | } 1169 | 1170 | stream.write(cast(short)1); // one result format code 1171 | stream.write(cast(short)1); // binary format 1172 | } 1173 | 1174 | enum DescribeType : char { Statement = 'S', Portal = 'P' } 1175 | 1176 | void sendDescribeMessage(DescribeType type, string name) 1177 | { 1178 | stream.write('D'); 1179 | stream.write(cast(int)(4 + 1 + name.length + 1)); 1180 | stream.write(cast(char)type); 1181 | stream.writeCString(name); 1182 | } 1183 | 1184 | void sendExecuteMessage(string portalName, int maxRows) 1185 | { 1186 | stream.write('E'); 1187 | stream.write(cast(int)(4 + portalName.length + 1 + 4)); 1188 | stream.writeCString(portalName); 1189 | stream.write(cast(int)maxRows); 1190 | } 1191 | 1192 | void sendFlushMessage() 1193 | { 1194 | stream.write('H'); 1195 | stream.write(cast(int)4); 1196 | } 1197 | 1198 | void sendSyncMessage() 1199 | { 1200 | stream.write('S'); 1201 | stream.write(cast(int)4); 1202 | } 1203 | 1204 | ResponseMessage handleResponseMessage(Message msg) 1205 | { 1206 | enforce(msg.data.length >= 2); 1207 | 1208 | char ftype; 1209 | string fvalue; 1210 | ResponseMessage response = new ResponseMessage; 1211 | 1212 | while (msg.read(ftype), ftype > 0) 1213 | { 1214 | msg.readCString(fvalue); 1215 | response.fields[ftype] = fvalue; 1216 | } 1217 | 1218 | return response; 1219 | } 1220 | 1221 | void checkActiveResultSet() 1222 | { 1223 | enforce(!activeResultSet, "There's active result set, which must be closed first."); 1224 | } 1225 | 1226 | void prepare(string statementName, string query, PGParameters params) 1227 | { 1228 | checkActiveResultSet(); 1229 | sendParseMessage(statementName, query, params.getOids()); 1230 | 1231 | sendFlushMessage(); 1232 | 1233 | receive: 1234 | 1235 | Message msg = getMessage(); 1236 | 1237 | switch (msg.type) 1238 | { 1239 | case 'E': 1240 | // ErrorResponse 1241 | ResponseMessage response = handleResponseMessage(msg); 1242 | throw new ServerErrorException(response); 1243 | case '1': 1244 | // ParseComplete 1245 | return; 1246 | default: 1247 | // async notice, notification 1248 | goto receive; 1249 | } 1250 | } 1251 | 1252 | void unprepare(string statementName) 1253 | { 1254 | checkActiveResultSet(); 1255 | sendCloseMessage(DescribeType.Statement, statementName); 1256 | sendFlushMessage(); 1257 | 1258 | receive: 1259 | 1260 | Message msg = getMessage(); 1261 | 1262 | switch (msg.type) 1263 | { 1264 | case 'E': 1265 | // ErrorResponse 1266 | ResponseMessage response = handleResponseMessage(msg); 1267 | throw new ServerErrorException(response); 1268 | case '3': 1269 | // CloseComplete 1270 | return; 1271 | default: 1272 | // async notice, notification 1273 | goto receive; 1274 | } 1275 | } 1276 | 1277 | PGFields bind(string portalName, string statementName, PGParameters params) 1278 | { 1279 | checkActiveResultSet(); 1280 | sendCloseMessage(DescribeType.Portal, portalName); 1281 | sendBindMessage(portalName, statementName, params); 1282 | sendDescribeMessage(DescribeType.Portal, portalName); 1283 | sendFlushMessage(); 1284 | 1285 | receive: 1286 | 1287 | Message msg = getMessage(); 1288 | 1289 | switch (msg.type) 1290 | { 1291 | case 'E': 1292 | // ErrorResponse 1293 | ResponseMessage response = handleResponseMessage(msg); 1294 | throw new ServerErrorException(response); 1295 | case '3': 1296 | // CloseComplete 1297 | goto receive; 1298 | case '2': 1299 | // BindComplete 1300 | goto receive; 1301 | case 'T': 1302 | // RowDescription (response to Describe) 1303 | PGField[] fields; 1304 | short fieldCount; 1305 | short formatCode; 1306 | PGField fi; 1307 | 1308 | msg.read(fieldCount); 1309 | 1310 | fields.length = fieldCount; 1311 | 1312 | foreach (i; 0..fieldCount) 1313 | { 1314 | msg.readCString(fi.name); 1315 | msg.read(fi.tableOid); 1316 | msg.read(fi.index); 1317 | msg.read(fi.oid); 1318 | msg.read(fi.typlen); 1319 | msg.read(fi.modifier); 1320 | msg.read(formatCode); 1321 | 1322 | enforce(formatCode == 1, new Exception("Field's format code returned in RowDescription is not 1 (binary)")); 1323 | 1324 | fields[i] = fi; 1325 | } 1326 | 1327 | return cast(PGFields)fields; 1328 | case 'n': 1329 | // NoData (response to Describe) 1330 | return new immutable(PGField)[0]; 1331 | default: 1332 | // async notice, notification 1333 | goto receive; 1334 | } 1335 | } 1336 | 1337 | ulong executeNonQuery(string portalName, out uint oid) 1338 | { 1339 | checkActiveResultSet(); 1340 | ulong rowsAffected = 0; 1341 | 1342 | sendExecuteMessage(portalName, 0); 1343 | sendSyncMessage(); 1344 | sendFlushMessage(); 1345 | 1346 | receive: 1347 | 1348 | Message msg = getMessage(); 1349 | 1350 | switch (msg.type) 1351 | { 1352 | case 'E': 1353 | // ErrorResponse 1354 | ResponseMessage response = handleResponseMessage(msg); 1355 | throw new ServerErrorException(response); 1356 | case 'D': 1357 | // DataRow 1358 | finalizeQuery(); 1359 | throw new Exception("This query returned rows."); 1360 | case 'C': 1361 | // CommandComplete 1362 | string tag; 1363 | 1364 | msg.readCString(tag); 1365 | 1366 | // GDC indexOf name conflict in std.string and std.algorithm 1367 | auto s1 = std.string.indexOf(tag, ' '); 1368 | if (s1 >= 0) { 1369 | switch (tag[0 .. s1]) { 1370 | case "INSERT": 1371 | // INSERT oid rows 1372 | auto s2 = lastIndexOf(tag, ' '); 1373 | assert(s2 > s1); 1374 | oid = to!uint(tag[s1 + 1 .. s2]); 1375 | rowsAffected = to!ulong(tag[s2 + 1 .. $]); 1376 | break; 1377 | case "DELETE", "UPDATE", "MOVE", "FETCH": 1378 | // DELETE rows 1379 | rowsAffected = to!ulong(tag[s1 + 1 .. $]); 1380 | break; 1381 | default: 1382 | // CREATE TABLE 1383 | break; 1384 | } 1385 | } 1386 | 1387 | goto receive; 1388 | 1389 | case 'I': 1390 | // EmptyQueryResponse 1391 | goto receive; 1392 | case 'Z': 1393 | // ReadyForQuery 1394 | return rowsAffected; 1395 | default: 1396 | // async notice, notification 1397 | goto receive; 1398 | } 1399 | } 1400 | 1401 | DBRow!Specs fetchRow(Specs...)(ref Message msg, ref PGFields fields) 1402 | { 1403 | alias DBRow!Specs Row; 1404 | 1405 | static if (Row.hasStaticLength) 1406 | { 1407 | alias Row.fieldTypes fieldTypes; 1408 | 1409 | static string genFieldAssigns() // CTFE 1410 | { 1411 | string s = ""; 1412 | 1413 | foreach (i; 0 .. fieldTypes.length) 1414 | { 1415 | s ~= "msg.read(fieldLen);\n"; 1416 | s ~= "if (fieldLen == -1)\n"; 1417 | s ~= text("row.setNull!(", i, ")();\n"); 1418 | s ~= "else\n"; 1419 | s ~= text("row.set!(fieldTypes[", i, "], ", i, ")(", 1420 | "msg.readBaseType!(fieldTypes[", i, "])(fields[", i, "].oid, fieldLen)", 1421 | ");\n"); 1422 | // text() doesn't work with -inline option, CTFE bug 1423 | } 1424 | 1425 | return s; 1426 | } 1427 | } 1428 | 1429 | Row row; 1430 | short fieldCount; 1431 | int fieldLen; 1432 | 1433 | msg.read(fieldCount); 1434 | 1435 | static if (Row.hasStaticLength) 1436 | { 1437 | Row.checkReceivedFieldCount(fieldCount); 1438 | mixin(genFieldAssigns); 1439 | } 1440 | else 1441 | { 1442 | row.setLength(fieldCount); 1443 | 1444 | foreach (i; 0 .. fieldCount) 1445 | { 1446 | msg.read(fieldLen); 1447 | if (fieldLen == -1) 1448 | row.setNull(i); 1449 | else 1450 | row[i] = msg.readBaseType!(Row.ElemType)(fields[i].oid, fieldLen); 1451 | } 1452 | } 1453 | 1454 | return row; 1455 | } 1456 | 1457 | void finalizeQuery() 1458 | { 1459 | Message msg; 1460 | 1461 | do 1462 | { 1463 | msg = getMessage(); 1464 | 1465 | // TODO: process async notifications 1466 | } 1467 | while (msg.type != 'Z'); // ReadyForQuery 1468 | } 1469 | 1470 | PGResultSet!Specs executeQuery(Specs...)(string portalName, ref PGFields fields) 1471 | { 1472 | checkActiveResultSet(); 1473 | 1474 | PGResultSet!Specs result = new PGResultSet!Specs(this, fields, &fetchRow!Specs); 1475 | 1476 | ulong rowsAffected = 0; 1477 | 1478 | sendExecuteMessage(portalName, 0); 1479 | sendSyncMessage(); 1480 | sendFlushMessage(); 1481 | 1482 | receive: 1483 | 1484 | Message msg = getMessage(); 1485 | 1486 | switch (msg.type) 1487 | { 1488 | case 'D': 1489 | // DataRow 1490 | alias DBRow!Specs Row; 1491 | 1492 | result.row = fetchRow!Specs(msg, fields); 1493 | static if (!Row.hasStaticLength) 1494 | result.row.columnToIndex = &result.columnToIndex; 1495 | result.validRow = true; 1496 | result.nextMsg = getMessage(); 1497 | 1498 | activeResultSet = true; 1499 | 1500 | return result; 1501 | case 'C': 1502 | // CommandComplete 1503 | string tag; 1504 | 1505 | msg.readCString(tag); 1506 | 1507 | auto s2 = lastIndexOf(tag, ' '); 1508 | if (s2 >= 0) 1509 | { 1510 | rowsAffected = to!ulong(tag[s2 + 1 .. $]); 1511 | } 1512 | 1513 | goto receive; 1514 | case 'I': 1515 | // EmptyQueryResponse 1516 | throw new Exception("Query string is empty."); 1517 | case 's': 1518 | // PortalSuspended 1519 | throw new Exception("Command suspending is not supported."); 1520 | case 'Z': 1521 | // ReadyForQuery 1522 | result.nextMsg = msg; 1523 | return result; 1524 | case 'E': 1525 | // ErrorResponse 1526 | ResponseMessage response = handleResponseMessage(msg); 1527 | throw new ServerErrorException(response); 1528 | default: 1529 | // async notice, notification 1530 | goto receive; 1531 | } 1532 | 1533 | assert(0); 1534 | } 1535 | 1536 | public: 1537 | 1538 | 1539 | /** 1540 | Opens connection to server. 1541 | 1542 | Params: 1543 | params = Associative array of string keys and values. 1544 | 1545 | Currently recognized parameters are: 1546 | $(UL 1547 | $(LI host - Host name or IP address of the server. Required.) 1548 | $(LI port - Port number of the server. Defaults to 5432.) 1549 | $(LI user - The database user. Required.) 1550 | $(LI database - The database to connect to. Defaults to the user name.) 1551 | $(LI options - Command-line arguments for the backend. (This is deprecated in favor of setting individual run-time parameters.)) 1552 | ) 1553 | 1554 | In addition to the above, any run-time parameter that can be set at backend start time might be listed. 1555 | Such settings will be applied during backend start (after parsing the command-line options if any). 1556 | The values will act as session defaults. 1557 | 1558 | Examples: 1559 | --- 1560 | auto conn = new PGConnection([ 1561 | "host" : "localhost", 1562 | "database" : "test", 1563 | "user" : "postgres", 1564 | "password" : "postgres" 1565 | ]); 1566 | --- 1567 | */ 1568 | this(const string[string] params) 1569 | { 1570 | enforce("host" in params, new ParamException("Required parameter 'host' not found")); 1571 | enforce("user" in params, new ParamException("Required parameter 'user' not found")); 1572 | 1573 | string[string] p = cast(string[string])params; 1574 | 1575 | ushort port = "port" in params? parse!ushort(p["port"]) : 5432; 1576 | 1577 | version(Have_vibe_d){ 1578 | stream = new PGStream(connectTCP(params["host"], port)); 1579 | } else { 1580 | stream = new PGStream(new SocketStream(new TcpSocket)); 1581 | stream.socket.socket.connect(new InternetAddress(params["host"], port)); 1582 | } 1583 | 1584 | sendStartupMessage(params); 1585 | 1586 | receive: 1587 | 1588 | Message msg = getMessage();import std.stdio; 1589 | 1590 | 1591 | switch (msg.type) 1592 | { 1593 | case 'E', 'N': 1594 | // ErrorResponse, NoticeResponse 1595 | 1596 | ResponseMessage response = handleResponseMessage(msg); 1597 | 1598 | if (msg.type == 'N') 1599 | goto receive; 1600 | 1601 | throw new ServerErrorException(response); 1602 | case 'R': 1603 | // AuthenticationXXXX 1604 | enforce(msg.data.length >= 4); 1605 | 1606 | int atype; 1607 | 1608 | msg.read(atype); 1609 | 1610 | switch (atype) 1611 | { 1612 | case 0: 1613 | // authentication successful, now wait for another messages 1614 | goto receive; 1615 | case 3: 1616 | // clear-text password is required 1617 | enforce("password" in params, new ParamException("Required parameter 'password' not found")); 1618 | enforce(msg.data.length == 4); 1619 | 1620 | sendPasswordMessage(params["password"]); 1621 | 1622 | goto receive; 1623 | case 5: 1624 | // MD5-hashed password is required, formatted as: 1625 | // "md5" + md5(md5(password + username) + salt) 1626 | // where md5() returns lowercase hex-string 1627 | enforce("password" in params, new ParamException("Required parameter 'password' not found")); 1628 | enforce(msg.data.length == 8); 1629 | 1630 | ubyte[16] digest; 1631 | string password = "md5" ~ MD5toHex(MD5toHex( 1632 | params["password"], params["user"]), msg.data[4 .. 8]); 1633 | 1634 | sendPasswordMessage(password); 1635 | 1636 | goto receive; 1637 | default: 1638 | // non supported authentication type, close connection 1639 | this.close(); 1640 | throw new Exception("Unsupported authentication type"); 1641 | } 1642 | 1643 | case 'S': 1644 | // ParameterStatus 1645 | enforce(msg.data.length >= 2); 1646 | 1647 | string pname, pvalue; 1648 | 1649 | msg.readCString(pname); 1650 | msg.readCString(pvalue); 1651 | 1652 | serverParams[pname] = pvalue; 1653 | 1654 | goto receive; 1655 | 1656 | case 'K': 1657 | // BackendKeyData 1658 | enforce(msg.data.length == 8); 1659 | 1660 | msg.read(serverProcessID); 1661 | msg.read(serverSecretKey); 1662 | 1663 | goto receive; 1664 | 1665 | case 'Z': 1666 | // ReadyForQuery 1667 | enforce(msg.data.length == 1); 1668 | 1669 | msg.read(cast(char)trStatus); 1670 | 1671 | // check for validity 1672 | switch (trStatus) 1673 | { 1674 | case 'I', 'T', 'E': break; 1675 | default: throw new Exception("Invalid transaction status"); 1676 | } 1677 | 1678 | // connection is opened and now it's possible to send queries 1679 | reloadAllTypes(); 1680 | return; 1681 | default: 1682 | // unknown message type, ignore it 1683 | goto receive; 1684 | } 1685 | } 1686 | 1687 | /// Closes current connection to the server. 1688 | void close() 1689 | { 1690 | sendTerminateMessage(); 1691 | stream.socket.close(); 1692 | } 1693 | 1694 | /// Shorthand methods using temporary PGCommand. Semantics is the same as PGCommand's. 1695 | ulong executeNonQuery(string query) 1696 | { 1697 | scope cmd = new PGCommand(this, query); 1698 | return cmd.executeNonQuery(); 1699 | } 1700 | 1701 | /// ditto 1702 | PGResultSet!Specs executeQuery(Specs...)(string query) 1703 | { 1704 | scope cmd = new PGCommand(this, query); 1705 | return cmd.executeQuery!Specs(); 1706 | } 1707 | 1708 | /// ditto 1709 | DBRow!Specs executeRow(Specs...)(string query, throwIfMoreRows = true) 1710 | { 1711 | scope cmd = new PGCommand(this, query); 1712 | return cmd.executeRow!Specs(throwIfMoreRows); 1713 | } 1714 | 1715 | /// ditto 1716 | T executeScalar(T)(string query, throwIfMoreRows = true) 1717 | { 1718 | scope cmd = new PGCommand(this, query); 1719 | return cmd.executeScalar!T(throwIfMoreRows); 1720 | } 1721 | 1722 | void reloadArrayTypes() 1723 | { 1724 | auto cmd = new PGCommand(this, "SELECT oid, typelem FROM pg_type WHERE typcategory = 'A'"); 1725 | auto result = cmd.executeQuery!(uint, "arrayOid", uint, "elemOid"); 1726 | scope(exit) result.close; 1727 | 1728 | arrayTypes = null; 1729 | 1730 | foreach (row; result) 1731 | { 1732 | arrayTypes[row.arrayOid] = row.elemOid; 1733 | } 1734 | 1735 | arrayTypes.rehash; 1736 | } 1737 | 1738 | void reloadCompositeTypes() 1739 | { 1740 | auto cmd = new PGCommand(this, "SELECT a.attrelid, a.atttypid FROM pg_attribute a JOIN pg_type t ON 1741 | a.attrelid = t.typrelid WHERE a.attnum > 0 ORDER BY a.attrelid, a.attnum"); 1742 | auto result = cmd.executeQuery!(uint, "typeOid", uint, "memberOid"); 1743 | scope(exit) result.close; 1744 | 1745 | compositeTypes = null; 1746 | 1747 | uint lastOid = 0; 1748 | uint[]* memberOids; 1749 | 1750 | foreach (row; result) 1751 | { 1752 | if (row.typeOid != lastOid) 1753 | { 1754 | compositeTypes[lastOid = row.typeOid] = new uint[0]; 1755 | memberOids = &compositeTypes[lastOid]; 1756 | } 1757 | 1758 | *memberOids ~= row.memberOid; 1759 | } 1760 | 1761 | compositeTypes.rehash; 1762 | } 1763 | 1764 | void reloadEnumTypes() 1765 | { 1766 | auto cmd = new PGCommand(this, "SELECT enumtypid, oid, enumlabel FROM pg_enum ORDER BY enumtypid, oid"); 1767 | auto result = cmd.executeQuery!(uint, "typeOid", uint, "valueOid", string, "valueLabel"); 1768 | scope(exit) result.close; 1769 | 1770 | enumTypes = null; 1771 | 1772 | uint lastOid = 0; 1773 | string[uint]* enumValues; 1774 | 1775 | foreach (row; result) 1776 | { 1777 | if (row.typeOid != lastOid) 1778 | { 1779 | if (lastOid > 0) 1780 | (*enumValues).rehash; 1781 | 1782 | enumTypes[lastOid = row.typeOid] = null; 1783 | enumValues = &enumTypes[lastOid]; 1784 | } 1785 | 1786 | (*enumValues)[row.valueOid] = row.valueLabel; 1787 | } 1788 | 1789 | if (lastOid > 0) 1790 | (*enumValues).rehash; 1791 | 1792 | enumTypes.rehash; 1793 | } 1794 | 1795 | void reloadAllTypes() 1796 | { 1797 | // todo: make simpler type lists, since we need only oids of types (without their members) 1798 | reloadArrayTypes(); 1799 | reloadCompositeTypes(); 1800 | reloadEnumTypes(); 1801 | } 1802 | } 1803 | 1804 | /// Class representing single query parameter 1805 | class PGParameter 1806 | { 1807 | private PGParameters params; 1808 | immutable short index; 1809 | immutable PGType type; 1810 | private Variant _value; 1811 | 1812 | /// Value bound to this parameter 1813 | @property Variant value() 1814 | { 1815 | return _value; 1816 | } 1817 | /// ditto 1818 | @property Variant value(Variant v) 1819 | { 1820 | params.changed = true; 1821 | return _value = v; 1822 | } 1823 | 1824 | private this(PGParameters params, short index, PGType type) 1825 | { 1826 | enforce(index > 0, new ParamException("Parameter's index must be > 0")); 1827 | this.params = params; 1828 | this.index = index; 1829 | this.type = type; 1830 | } 1831 | } 1832 | 1833 | /// Collection of query parameters 1834 | class PGParameters 1835 | { 1836 | private PGParameter[short] params; 1837 | private PGCommand cmd; 1838 | private bool changed; 1839 | 1840 | private int[] getOids() 1841 | { 1842 | short[] keys = params.keys; 1843 | sort(keys); 1844 | 1845 | int[] oids = new int[params.length]; 1846 | 1847 | foreach (int i, key; keys) 1848 | { 1849 | oids[i] = params[key].type; 1850 | } 1851 | 1852 | return oids; 1853 | } 1854 | 1855 | /// 1856 | @property short length() 1857 | { 1858 | return cast(short)params.length; 1859 | } 1860 | 1861 | private this(PGCommand cmd) 1862 | { 1863 | this.cmd = cmd; 1864 | } 1865 | 1866 | /** 1867 | Creates and returns new parameter. 1868 | Examples: 1869 | --- 1870 | // without spaces between $ and number 1871 | auto cmd = new PGCommand(conn, "INSERT INTO users (name, surname) VALUES ($ 1, $ 2)"); 1872 | cmd.parameters.add(1, PGType.TEXT).value = "John"; 1873 | cmd.parameters.add(2, PGType.TEXT).value = "Doe"; 1874 | 1875 | assert(cmd.executeNonQuery == 1); 1876 | --- 1877 | */ 1878 | PGParameter add(short index, PGType type) 1879 | { 1880 | enforce(!cmd.prepared, "Can't add parameter to prepared statement."); 1881 | changed = true; 1882 | return params[index] = new PGParameter(this, index, type); 1883 | } 1884 | 1885 | // todo: remove() 1886 | 1887 | PGParameter opIndex(short index) 1888 | { 1889 | return params[index]; 1890 | } 1891 | 1892 | int opApply(int delegate(ref PGParameter param) dg) 1893 | { 1894 | int result = 0; 1895 | 1896 | foreach (number; sort(params.keys)) 1897 | { 1898 | result = dg(params[number]); 1899 | 1900 | if (result) 1901 | break; 1902 | } 1903 | 1904 | return result; 1905 | } 1906 | } 1907 | 1908 | /// Array of fields returned by the server 1909 | alias immutable(PGField)[] PGFields; 1910 | 1911 | /// Contains information about fields returned by the server 1912 | struct PGField 1913 | { 1914 | /// The field name. 1915 | string name; 1916 | /// If the field can be identified as a column of a specific table, the object ID of the table; otherwise zero. 1917 | uint tableOid; 1918 | /// If the field can be identified as a column of a specific table, the attribute number of the column; otherwise zero. 1919 | short index; 1920 | /// The object ID of the field's data type. 1921 | uint oid; 1922 | /// The data type size (see pg_type.typlen). Note that negative values denote variable-width types. 1923 | short typlen; 1924 | /// The type modifier (see pg_attribute.atttypmod). The meaning of the modifier is type-specific. 1925 | int modifier; 1926 | } 1927 | 1928 | /// Class encapsulating prepared or non-prepared statements (commands). 1929 | class PGCommand 1930 | { 1931 | private PGConnection conn; 1932 | private string _query; 1933 | private PGParameters params; 1934 | private PGFields _fields = null; 1935 | private string preparedName; 1936 | private uint _lastInsertOid; 1937 | private bool prepared; 1938 | 1939 | /// List of parameters bound to this command 1940 | @property PGParameters parameters() 1941 | { 1942 | return params; 1943 | } 1944 | 1945 | /// List of fields that will be returned from the server. Available after successful call to bind(). 1946 | @property PGFields fields() 1947 | { 1948 | return _fields; 1949 | } 1950 | 1951 | /** 1952 | Checks if this is query or non query command. Available after successful call to bind(). 1953 | Returns: true if server returns at least one field (column). Otherwise false. 1954 | */ 1955 | @property bool isQuery() 1956 | { 1957 | enforce(_fields !is null, new Exception("bind() must be called first.")); 1958 | return _fields.length > 0; 1959 | } 1960 | 1961 | /// Returns: true if command is currently prepared, otherwise false. 1962 | @property bool isPrepared() 1963 | { 1964 | return prepared; 1965 | } 1966 | 1967 | /// Query assigned to this command. 1968 | @property string query() 1969 | { 1970 | return _query; 1971 | } 1972 | /// ditto 1973 | @property string query(string query) 1974 | { 1975 | enforce(!prepared, "Can't change query for prepared statement."); 1976 | return _query = query; 1977 | } 1978 | 1979 | /// If table is with OIDs, it contains last inserted OID. 1980 | @property uint lastInsertOid() 1981 | { 1982 | return _lastInsertOid; 1983 | } 1984 | 1985 | this(PGConnection conn, string query = "") 1986 | { 1987 | this.conn = conn; 1988 | _query = query; 1989 | params = new PGParameters(this); 1990 | _fields = new immutable(PGField)[0]; 1991 | preparedName = ""; 1992 | prepared = false; 1993 | } 1994 | 1995 | /// Prepare this statement, i.e. cache query plan. 1996 | void prepare() 1997 | { 1998 | enforce(!prepared, "This command is already prepared."); 1999 | preparedName = conn.reservePrepared(); 2000 | conn.prepare(preparedName, _query, params); 2001 | prepared = true; 2002 | params.changed = true; 2003 | } 2004 | 2005 | /// Unprepare this statement. Goes back to normal query planning. 2006 | void unprepare() 2007 | { 2008 | enforce(prepared, "This command is not prepared."); 2009 | conn.unprepare(preparedName); 2010 | preparedName = ""; 2011 | prepared = false; 2012 | params.changed = true; 2013 | } 2014 | 2015 | /** 2016 | Binds values to parameters and updates list of returned fields. 2017 | 2018 | This is normally done automatically, but it may be useful to check what fields 2019 | would be returned from a query, before executing it. 2020 | */ 2021 | void bind() 2022 | { 2023 | checkPrepared(false); 2024 | _fields = conn.bind(preparedName, preparedName, params); 2025 | params.changed = false; 2026 | } 2027 | 2028 | private void checkPrepared(bool bind) 2029 | { 2030 | if (!prepared) 2031 | { 2032 | // use unnamed statement & portal 2033 | conn.prepare("", _query, params); 2034 | if (bind) 2035 | { 2036 | _fields = conn.bind("", "", params); 2037 | params.changed = false; 2038 | } 2039 | } 2040 | } 2041 | 2042 | private void checkBound() 2043 | { 2044 | if (params.changed) 2045 | bind(); 2046 | } 2047 | 2048 | /** 2049 | Executes a non query command, i.e. query which doesn't return any rows. Commonly used with 2050 | data manipulation commands, such as INSERT, UPDATE and DELETE. 2051 | Examples: 2052 | --- 2053 | auto cmd = new PGCommand(conn, "DELETE * FROM table"); 2054 | auto deletedRows = cmd.executeNonQuery; 2055 | cmd.query = "UPDATE table SET quantity = 1 WHERE price > 100"; 2056 | auto updatedRows = cmd.executeNonQuery; 2057 | cmd.query = "INSERT INTO table VALUES(1, 50)"; 2058 | assert(cmd.executeNonQuery == 1); 2059 | --- 2060 | Returns: Number of affected rows. 2061 | */ 2062 | ulong executeNonQuery() 2063 | { 2064 | checkPrepared(true); 2065 | checkBound(); 2066 | return conn.executeNonQuery(preparedName, _lastInsertOid); 2067 | } 2068 | 2069 | /** 2070 | Executes query which returns row sets, such as SELECT command. 2071 | Params: 2072 | bufferedRows = Number of rows that may be allocated at the same time. 2073 | Returns: InputRange of DBRow!Specs. 2074 | */ 2075 | PGResultSet!Specs executeQuery(Specs...)() 2076 | { 2077 | checkPrepared(true); 2078 | checkBound(); 2079 | return conn.executeQuery!Specs(preparedName, _fields); 2080 | } 2081 | 2082 | /** 2083 | Executes query and returns only first row of the result. 2084 | Params: 2085 | throwIfMoreRows = If true, throws Exception when result contains more than one row. 2086 | Examples: 2087 | --- 2088 | auto cmd = new PGCommand(conn, "SELECT 1, 'abc'"); 2089 | auto row1 = cmd.executeRow!(int, string); // returns DBRow!(int, string) 2090 | assert(is(typeof(i[0]) == int) && is(typeof(i[1]) == string)); 2091 | auto row2 = cmd.executeRow; // returns DBRow!(Variant[]) 2092 | --- 2093 | Throws: Exception if result doesn't contain any rows or field count do not match. 2094 | Throws: Exception if result contains more than one row when throwIfMoreRows is true. 2095 | */ 2096 | DBRow!Specs executeRow(Specs...)(throwIfMoreRows = true) 2097 | { 2098 | auto result = executeQuery!Specs(); 2099 | scope(exit) result.close(); 2100 | enforce(!result.empty(), "Result doesn't contain any rows."); 2101 | auto row = result.front(); 2102 | if (throwIfMoreRows) 2103 | { 2104 | result.popFront(); 2105 | enforce(result.empty(), "Result contains more than one row."); 2106 | } 2107 | return row; 2108 | } 2109 | 2110 | /** 2111 | Executes query returning exactly one row and field. By default, returns Variant type. 2112 | Params: 2113 | throwIfMoreRows = If true, throws Exception when result contains more than one row. 2114 | Examples: 2115 | --- 2116 | auto cmd = new PGCommand(conn, "SELECT 1"); 2117 | auto i = cmd.executeScalar!int; // returns int 2118 | assert(is(typeof(i) == int)); 2119 | auto v = cmd.executeScalar; // returns Variant 2120 | --- 2121 | Throws: Exception if result doesn't contain any rows or if it contains more than one field. 2122 | Throws: Exception if result contains more than one row when throwIfMoreRows is true. 2123 | */ 2124 | T executeScalar(T = Variant)(bool throwIfMoreRows = true) 2125 | { 2126 | auto result = executeQuery!T(); 2127 | scope(exit) result.close(); 2128 | enforce(!result.empty(), "Result doesn't contain any rows."); 2129 | T row = result.front(); 2130 | if (throwIfMoreRows) 2131 | { 2132 | result.popFront(); 2133 | enforce(result.empty(), "Result contains more than one row."); 2134 | } 2135 | return row; 2136 | } 2137 | } 2138 | 2139 | /// Input range of DBRow!Specs 2140 | class PGResultSet(Specs...) 2141 | { 2142 | alias DBRow!Specs Row; 2143 | alias Row delegate(ref Message msg, ref PGFields fields) FetchRowDelegate; 2144 | 2145 | private FetchRowDelegate fetchRow; 2146 | private PGConnection conn; 2147 | private PGFields fields; 2148 | private Row row; 2149 | private bool validRow; 2150 | private Message nextMsg; 2151 | private size_t[][string] columnMap; 2152 | 2153 | private this(PGConnection conn, ref PGFields fields, FetchRowDelegate dg) 2154 | { 2155 | this.conn = conn; 2156 | this.fields = fields; 2157 | this.fetchRow = dg; 2158 | validRow = false; 2159 | 2160 | foreach (i, field; fields) 2161 | { 2162 | size_t[]* indices = field.name in columnMap; 2163 | 2164 | if (indices) 2165 | *indices ~= i; 2166 | else 2167 | columnMap[field.name] = [i]; 2168 | } 2169 | } 2170 | 2171 | private size_t columnToIndex(string column, size_t index) 2172 | { 2173 | size_t[]* indices = column in columnMap; 2174 | enforce(indices, "Unknown column name"); 2175 | return (*indices)[index]; 2176 | } 2177 | 2178 | pure nothrow bool empty() 2179 | { 2180 | return !validRow; 2181 | } 2182 | 2183 | void popFront() 2184 | { 2185 | if (nextMsg.type == 'D') 2186 | { 2187 | row = fetchRow(nextMsg, fields); 2188 | static if (!Row.hasStaticLength) 2189 | row.columnToIndex = &columnToIndex; 2190 | validRow = true; 2191 | nextMsg = conn.getMessage(); 2192 | } 2193 | else 2194 | validRow = false; 2195 | } 2196 | 2197 | pure nothrow Row front() 2198 | { 2199 | return row; 2200 | } 2201 | 2202 | /// Closes current result set. It must be closed before issuing another query on the same connection. 2203 | void close() 2204 | { 2205 | if (nextMsg.type != 'Z') 2206 | conn.finalizeQuery(); 2207 | conn.activeResultSet = false; 2208 | } 2209 | 2210 | int opApply(int delegate(ref Row row) dg) 2211 | { 2212 | int result = 0; 2213 | 2214 | while (!empty) 2215 | { 2216 | result = dg(row); 2217 | popFront; 2218 | 2219 | if (result) 2220 | break; 2221 | } 2222 | 2223 | return result; 2224 | } 2225 | 2226 | int opApply(int delegate(ref size_t i, ref Row row) dg) 2227 | { 2228 | int result = 0; 2229 | size_t i; 2230 | 2231 | while (!empty) 2232 | { 2233 | result = dg(i, row); 2234 | popFront; 2235 | i++; 2236 | 2237 | if (result) 2238 | break; 2239 | } 2240 | 2241 | return result; 2242 | } 2243 | } 2244 | 2245 | 2246 | version(Have_vibe_d) 2247 | { 2248 | import vibe.core.connectionpool; 2249 | 2250 | class PostgresDB { 2251 | private { 2252 | string[string] m_params; 2253 | ConnectionPool!PGConnection m_pool; 2254 | } 2255 | 2256 | this(string[string] conn_params) 2257 | { 2258 | m_params = conn_params.dup; 2259 | m_pool = new ConnectionPool!PGConnection(&createConnection); 2260 | } 2261 | 2262 | auto lockConnection() { return m_pool.lockConnection(); } 2263 | 2264 | private PGConnection createConnection() 2265 | { 2266 | return new PGConnection(m_params); 2267 | } 2268 | } 2269 | } 2270 | else 2271 | { 2272 | class PostgresDB() { 2273 | static assert(false, 2274 | "The 'PostgresDB' connection pool requires Vibe.d and therefore "~ 2275 | "must be used with -version=Have_vibe_d" 2276 | ); 2277 | } 2278 | } 2279 | 2280 | --------------------------------------------------------------------------------