The OpenD Programming Language

1 /++
2 	Uses libpq implement the [arsd.database.Database] interface.
3 
4 	Requires the official pq library from Postgres to be installed to build
5 	and to use. Note that on Windows, it is often distributed as `libpq.lib`.
6 	You will have to copy or rename that to `pq.lib` for dub or dmd to automatically
7 	find it. You will also likely need to add the lib search path yourself on
8 	both Windows and Linux systems (on my Linux box, it is `-L-L/usr/local/pgsql/lib`
9 	to dmd. You can also list things your app's dub.json's lflags too. Note on the
10 	Microsoft linker, the flag is called `/LIBPATH`.)
11 
12 	For example, for the default Postgres install on Windows, try:
13 
14 	```
15 		"lflags-windows": [ "/LIBPATH:C:/Program Files/PostgreSQL/<VERSION>/lib" ],
16 	```
17 
18 	In your dub.json.
19 
20 	When you distribute your application, the user will want to install libpq client on
21 	Linux, and on Windows, you may want to include the libpq.dll in your distribution.
22 	Note it may also depend on OpenSSL ssl and crypto dlls and libintl.dll as well. These
23 	should be found in the PostgreSQL lib and/or bin folders (check them both!).
24 +/
25 module arsd.postgres;
26 
27 version(Windows)
28 	pragma(lib, "libpq");
29 else
30 	pragma(lib, "pq");
31 
32 public import arsd.database;
33 
34 import std.string;
35 import std.exception;
36 
37 // remember to CREATE DATABASE name WITH ENCODING 'utf8'
38 //
39 // http://www.postgresql.org/docs/8.0/static/libpq-exec.html
40 // ExecParams, PQPrepare, PQExecPrepared
41 //
42 // SQL: `DEALLOCATE name` is how to dealloc a prepared statement.
43 
44 /++
45 	The PostgreSql implementation of the [Database] interface.
46 
47 	You should construct this class, but then use it through the
48 	interface functions.
49 
50 	---
51 	auto db = new PostgreSql("dbname=name");
52 	foreach(row; db.query("SELECT id, data FROM table_name"))
53 		writeln(row[0], " = ", row[1]);
54 	---
55 +/
56 class PostgreSql : Database {
57 	/// `dbname=your_database_name` is probably the most common connection string. See section "33.1.1.1. Keyword/Value Connection Strings" on https://www.postgresql.org/docs/10/libpq-connect.html
58 	this(string connectionString) {
59 		this.connectionString = connectionString;
60 		conn = PQconnectdb(toStringz(connectionString));
61 		if(conn is null)
62 			throw new DatabaseException("Unable to allocate PG connection object");
63 		if(PQstatus(conn) != CONNECTION_OK)
64 			throw new DatabaseException(error());
65 		query("SET NAMES 'utf8'"); // D does everything with utf8
66 	}
67 
68 	string connectionString;
69 
70 	~this() {
71 		PQfinish(conn);
72 	}
73 
74 	string sysTimeToValue(SysTime s) {
75 		return "'" ~ escape(s.toISOExtString()) ~ "'::timestamptz";
76 	}
77 
78 	/**
79 		Prepared statement support
80 
81 		This will be added to the Database interface eventually in some form,
82 		but first I need to implement it for all my providers.
83 
84 		The common function of those 4 will be what I put in the interface.
85 	*/
86 
87 	ResultSet executePreparedStatement(T...)(string name, T args) {
88 		const(char)*[args.length] argsStrings;
89 
90 		foreach(idx, arg; args) {
91 			// FIXME: optimize to remove allocations here
92 			import std.conv;
93 			static if(!is(typeof(arg) == typeof(null)))
94 				argsStrings[idx] = toStringz(to!string(arg));
95 			// else make it null
96 		}
97 
98 		auto res = PQexecPrepared(conn, toStringz(name), argsStrings.length, argsStrings.ptr, null, null, 0);
99 
100 		int ress = PQresultStatus(res);
101 		if(ress != PGRES_TUPLES_OK
102 			&& ress != PGRES_COMMAND_OK)
103 			throw new DatabaseException(error());
104 
105 		return new PostgresResult(res);
106 
107 	}
108 
109 	///
110 	override void startTransaction() {
111 		query("START TRANSACTION");
112 	}
113 
114 	ResultSet queryImpl(string sql, Variant[] args...) {
115 		sql = escapedVariants(this, sql, args);
116 
117 		bool first_retry = true;
118 
119 		retry:
120 
121 		auto res = PQexec(conn, toStringz(sql));
122 		int ress = PQresultStatus(res);
123 		// https://www.postgresql.org/docs/current/libpq-exec.html
124 		// FIXME: PQresultErrorField can get a lot more info in a more structured way
125 		if(ress != PGRES_TUPLES_OK
126 			&& ress != PGRES_COMMAND_OK)
127 		{
128 			if(first_retry && error() == "no connection to the server\n") {
129 				first_retry = false;
130 				// try to reconnect...
131 				PQfinish(conn);
132 				conn = PQconnectdb(toStringz(connectionString));
133 				if(conn is null)
134 					throw new DatabaseException("Unable to allocate PG connection object");
135 				if(PQstatus(conn) != CONNECTION_OK)
136 					throw new DatabaseException(error());
137 				goto retry;
138 			}
139 			throw new DatabaseException(error());
140 		}
141 
142 		return new PostgresResult(res);
143 	}
144 
145 	string escape(string sqlData) {
146 		char* buffer = (new char[sqlData.length * 2 + 1]).ptr;
147 		ulong size = PQescapeString (buffer, sqlData.ptr, sqlData.length);
148 
149 		string ret = assumeUnique(buffer[0.. cast(size_t) size]);
150 
151 		return ret;
152 	}
153 
154 	string escapeBinaryString(const(ubyte)[] data) {
155 		// must include '\x ... ' here
156 		size_t len;
157 		char* buf = PQescapeByteaConn(conn, data.ptr, data.length, &len);
158 		if(buf is null)
159 			throw new Exception("pgsql out of memory escaping binary string");
160 
161 		string res;
162 		if(len == 0)
163 			res = "''";
164 		else
165 			res = cast(string) ("'" ~ buf[0 .. len - 1] ~ "'"); // gotta cut the zero terminator off
166 
167 		PQfreemem(buf);
168 
169 		return res;
170 	}
171 
172 
173 	///
174 	string error() {
175 		return copyCString(PQerrorMessage(conn));
176 	}
177 
178 	private:
179 		PGconn* conn;
180 }
181 
182 ///
183 class PostgresResult : ResultSet {
184 	// name for associative array to result index
185 	int getFieldIndex(string field) {
186 		if(mapping is null)
187 			makeFieldMapping();
188 		field = field.toLower;
189 		if(field in mapping)
190 			return mapping[field];
191 		else throw new Exception("no mapping " ~ field);
192 	}
193 
194 
195 	string[] fieldNames() {
196 		if(mapping is null)
197 			makeFieldMapping();
198 		return columnNames;
199 	}
200 
201 	// this is a range that can offer other ranges to access it
202 	bool empty() {
203 		return position == numRows;
204 	}
205 
206 	Row front() {
207 		return row;
208 	}
209 
210 	int affectedRows() @system {
211 		auto g = PQcmdTuples(res);
212 		if(g is null)
213 			return 0;
214 		int num;
215 		while(*g) {
216 			num *= 10;
217 			num += *g - '0';
218 			g++;
219 		}
220 		return num;
221 	}
222 
223 	void popFront() {
224 		position++;
225 		if(position < numRows)
226 			fetchNext();
227 	}
228 
229 	override size_t length() {
230 		return numRows;
231 	}
232 
233 	this(PGresult* res) {
234 		this.res = res;
235 		numFields = PQnfields(res);
236 		numRows = PQntuples(res);
237 
238 		if(numRows)
239 			fetchNext();
240 	}
241 
242 	~this() {
243 		PQclear(res);
244 	}
245 
246 	private:
247 		PGresult* res;
248 		int[string] mapping;
249 		string[] columnNames;
250 		int numFields;
251 
252 		int position;
253 
254 		int numRows;
255 
256 		Row row;
257 
258 		void fetchNext() {
259 			Row r;
260 			r.resultSet = this;
261 			DatabaseDatum[] row;
262 
263 			for(int i = 0; i < numFields; i++) {
264 				string a;
265 
266 				if(PQgetisnull(res, position, i))
267 					a = null;
268 				else {
269 					switch(PQfformat(res, i)) {
270 						case 0: // text representation
271 							switch(PQftype(res, i)) {
272 								case BYTEAOID:
273 									size_t len;
274 									char* c = PQunescapeBytea(PQgetvalue(res, position, i), &len);
275 
276 									a = cast(string) c[0 .. len].idup;
277 
278 									PQfreemem(c);
279 								break;
280 								default:
281 									a = copyCString(PQgetvalue(res, position, i), PQgetlength(res, position, i));
282 							}
283 						break;
284 						case 1: // binary representation
285 							throw new Exception("unexpected format returned by pq");
286 						default:
287 							throw new Exception("unknown pq format");
288 					}
289 
290 				}
291 				row ~= DatabaseDatum(a);
292 			}
293 
294 			r.row = row;
295 			this.row = r;
296 		}
297 
298 		void makeFieldMapping() {
299 			for(int i = 0; i < numFields; i++) {
300 				string a = copyCString(PQfname(res, i));
301 
302 				columnNames ~= a;
303 				mapping[a] = i;
304 			}
305 
306 		}
307 }
308 
309 string copyCString(const char* c, int actualLength = -1) @system {
310 	const(char)* a = c;
311 	if(a is null)
312 		return null;
313 
314 	string ret;
315 	if(actualLength == -1)
316 		while(*a) {
317 			ret ~= *a;
318 			a++;
319 		}
320 	else {
321 		ret = a[0..actualLength].idup;
322 	}
323 
324 	return ret;
325 }
326 
327 extern(C) {
328 	struct PGconn {};
329 	struct PGresult {};
330 
331 	void PQfinish(PGconn*);
332 	PGconn* PQconnectdb(const char*);
333 
334 	int PQstatus(PGconn*); // FIXME check return value
335 
336 	const (char*) PQerrorMessage(PGconn*);
337 
338 	PGresult* PQexec(PGconn*, const char*);
339 	void PQclear(PGresult*);
340 
341 	PGresult* PQprepare(PGconn*, const char* stmtName, const char* query, int nParams, const void* paramTypes);
342 
343 	PGresult* PQexecPrepared(PGconn*, const char* stmtName, int nParams, const char** paramValues, const int* paramLengths, const int* paramFormats, int resultFormat);
344 
345 	int PQresultStatus(PGresult*); // FIXME check return value
346 
347 	int PQnfields(PGresult*); // number of fields in a result
348 	const(char*) PQfname(PGresult*, int); // name of field
349 
350 	int PQntuples(PGresult*); // number of rows in result
351 	const(char*) PQgetvalue(PGresult*, int row, int column);
352 
353 	size_t PQescapeString (char *to, const char *from, size_t length);
354 
355 	enum int CONNECTION_OK = 0;
356 	enum int PGRES_COMMAND_OK = 1;
357 	enum int PGRES_TUPLES_OK = 2;
358 
359 	int PQgetlength(const PGresult *res,
360 			int row_number,
361 			int column_number);
362 	int PQgetisnull(const PGresult *res,
363 			int row_number,
364 			int column_number);
365 
366 	int PQfformat(const PGresult *res, int column_number);
367 
368 	alias Oid = int;
369 	enum BYTEAOID = 17;
370 	Oid PQftype(const PGresult* res, int column_number);
371 
372 	char *PQescapeByteaConn(PGconn *conn,
373                                  const ubyte *from,
374                                  size_t from_length,
375                                  size_t *to_length);
376 	char *PQunescapeBytea(const char *from, size_t *to_length);
377 	void PQfreemem(void *ptr);
378 
379 	char* PQcmdTuples(PGresult *res);
380 
381 }
382 
383 /*
384 import std.stdio;
385 void main() {
386 	auto db = new PostgreSql("dbname = test");
387 
388 	db.query("INSERT INTO users (id, name) values (?, ?)", 30, "hello mang");
389 
390 	foreach(line; db.query("SELECT * FROM users")) {
391 		writeln(line[0], line["name"]);
392 	}
393 }
394 */