├── .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 |
--------------------------------------------------------------------------------