1 /++
2 Connect to a MySQL/MariaDB database using vibe.d's
3 $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool).
4 
5 You have to include vibe.d in your project to be able to use this class.
6 If you don't want to, refer to `mysql.connection.Connection`.
7 
8 This provides various benefits over creating a new connection manually,
9 such as automatically reusing old connections, and automatic cleanup (no need to close
10 the connection when done).
11 +/
12 module mysql.pool;
13 
14 import std.conv;
15 import std.typecons;
16 import mysql.connection;
17 import mysql.prepared;
18 import mysql.protocol.constants;
19 debug(MYSQLN_TESTS)
20 {
21 	import mysql.test.common;
22 }
23 
24 version(Have_vibe_d_core) version = IncludeMySQLPool;
25 version(MySQLDocs)        version = IncludeMySQLPool;
26 
27 version(IncludeMySQLPool)
28 {
29 	version(Have_vibe_d_core)
30 		import vibe.core.connectionpool;
31 	else version(MySQLDocs)
32 	{
33 		/++
34 		Vibe.d's
35 		$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool)
36 		class.
37 
38 		Not actually included in module `mysql.pool`. Only listed here for
39 		documentation purposes. For ConnectionPool and it's documentation, see:
40 		$(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool)
41 		+/
42 		class ConnectionPool(T)
43 		{
44 			/// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.this)
45 			this(Connection delegate() connection_factory, uint max_concurrent = (uint).max)
46 			{}
47 
48 			/// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.lockConnection)
49 			LockedConnection!T lockConnection() { return LockedConnection!T(); }
50 
51 			/// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.maxConcurrency)
52 			uint maxConcurrency;
53 		}
54 
55 		/++
56 		Vibe.d's
57 		$(LINK2 http://vibed.org/api/vibe.core.connectionpool/LockedConnection, LockedConnection)
58 		struct.
59 
60 		Not actually included in module `mysql.pool`. Only listed here for
61 		documentation purposes. For LockedConnection and it's documentation, see:
62 		$(LINK http://vibed.org/api/vibe.core.connectionpool/LockedConnection)
63 		+/
64 		struct LockedConnection(Connection) { Connection c; alias c this; }
65 	}
66 
67 	/++
68 	A lightweight convenience interface to a MySQL/MariaDB database using vibe.d's
69 	$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool).
70 
71 	You have to include vibe.d in your project to be able to use this class.
72 	If you don't want to, refer to `mysql.connection.Connection`.
73 
74 	If, for any reason, this class doesn't suit your needs, it's easy to just
75 	use vibe.d's $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool)
76 	directly. Simply provide it with a delegate that creates a new `mysql.connection.Connection`
77 	and does any other custom processing if needed.
78 	+/
79 	class MySQLPool
80 	{
81 		private
82 		{
83 			string m_host;
84 			string m_user;
85 			string m_password;
86 			string m_database;
87 			ushort m_port;
88 			SvrCapFlags m_capFlags;
89 			void delegate(Connection) m_onNewConnection;
90 			ConnectionPool!Connection m_pool;
91 			PreparedRegistrations!PreparedInfo preparedRegistrations;
92 
93 			struct PreparedInfo
94 			{
95 				bool queuedForRelease = false;
96 			}
97 		}
98 
99 		/// Sets up a connection pool with the provided connection settings.
100 		///
101 		/// The optional `onNewConnection` param allows you to set a callback
102 		/// which will be run every time a new connection is created.
103 		this(string host, string user, string password, string database,
104 			ushort port = 3306, uint maxConcurrent = (uint).max,
105 			SvrCapFlags capFlags = defaultClientFlags,
106 			void delegate(Connection) onNewConnection = null)
107 		{
108 			m_host = host;
109 			m_user = user;
110 			m_password = password;
111 			m_database = database;
112 			m_port = port;
113 			m_capFlags = capFlags;
114 			m_onNewConnection = onNewConnection;
115 			m_pool = new ConnectionPool!Connection(&createConnection);
116 		}
117 
118 		///ditto
119 		this(string host, string user, string password, string database,
120 			ushort port, SvrCapFlags capFlags, void delegate(Connection) onNewConnection = null)
121 		{
122 			this(host, user, password, database, port, (uint).max, capFlags, onNewConnection);
123 		}
124 
125 		///ditto
126 		this(string host, string user, string password, string database,
127 			ushort port, void delegate(Connection) onNewConnection)
128 		{
129 			this(host, user, password, database, port, (uint).max, defaultClientFlags, onNewConnection);
130 		}
131 
132 		///ditto
133 		this(string connStr, uint maxConcurrent = (uint).max, SvrCapFlags capFlags = defaultClientFlags,
134 			void delegate(Connection) onNewConnection = null)
135 		{
136 			auto parts = Connection.parseConnectionString(connStr);
137 			this(parts[0], parts[1], parts[2], parts[3], to!ushort(parts[4]), capFlags, onNewConnection);
138 		}
139 
140 		///ditto
141 		this(string connStr, SvrCapFlags capFlags, void delegate(Connection) onNewConnection = null)
142 		{
143 			this(connStr, (uint).max, capFlags, onNewConnection);
144 		}
145 
146 		///ditto
147 		this(string connStr, void delegate(Connection) onNewConnection)
148 		{
149 			this(connStr, (uint).max, defaultClientFlags, onNewConnection);
150 		}
151 
152 		/++
153 		Obtain a connection. If one isn't available, a new one will be created.
154 
155 		The connection returned is actually a `LockedConnection!Connection`,
156 		but it uses `alias this`, and so can be used just like a Connection.
157 		(See vibe.d's
158 		$(LINK2 http://vibed.org/api/vibe.core.connectionpool/LockedConnection, LockedConnection documentation).)
159 
160 		No other fiber will be given this `mysql.connection.Connection` as long as your fiber still holds it.
161 
162 		There is no need to close, release or unlock this connection. It is
163 		reference-counted and will automatically be returned to the pool once
164 		your fiber is done with it.
165 		
166 		If you have passed any prepared statements to  `autoRegister`
167 		or `autoRelease`, then those statements will automatically be
168 		registered/released on the connection. (Currently, this automatic
169 		register/release may actually occur upon the first command sent via
170 		the connection.)
171 		+/
172 		auto lockConnection()
173 		{
174 			Connection conn = m_pool.lockConnection();
175 			applyAuto(conn);
176 			return conn;
177 		}
178 
179 		/// Applies any `autoRegister`/`autoRelease` settings to a connection,
180 		/// if necessary.
181 		private void applyAuto(T)(T conn)
182 		{
183 			foreach(sql, info; preparedRegistrations.directLookup)
184 			{
185 				auto registeredOnPool = !info.queuedForRelease;
186 				auto registeredOnConnection = conn.isRegistered(sql);
187 				
188 				if(registeredOnPool && !registeredOnConnection) // Need to register?
189 					conn.register(sql);
190 				else if(!registeredOnPool && registeredOnConnection) // Need to release?
191 					conn.release(sql);
192 			}
193 		}
194 
195 		private Connection createConnection()
196 		{
197 			auto conn = new Connection(m_host, m_user, m_password, m_database, m_port, m_capFlags);
198 
199 			if(m_onNewConnection)
200 				m_onNewConnection(conn);
201 
202 			return conn;
203 		}
204 
205 		/// Get/set a callback delegate to be run every time a new connection
206 		/// is created.
207 		@property void onNewConnection(void delegate(Connection) onNewConnection)
208 		{
209 			m_onNewConnection = onNewConnection;
210 		}
211 
212 		///ditto
213 		@property void delegate(Connection) onNewConnection()
214 		{
215 			return m_onNewConnection;
216 		}
217 
218 		@("onNewConnection")
219 		debug(MYSQLN_TESTS)
220 		unittest
221 		{
222  			auto count = 0;
223 			void callback(Connection conn)
224 			{
225 				count++;
226 			}
227 
228 			// Test getting/setting
229 			auto poolA = new MySQLPool(testConnectionStr, &callback);
230 			auto poolB = new MySQLPool(testConnectionStr);
231 			auto poolNoCallback = new MySQLPool(testConnectionStr);
232 
233 			assert(poolA.onNewConnection == &callback);
234 			assert(poolB.onNewConnection is null);
235 			assert(poolNoCallback.onNewConnection is null);
236 			
237 			poolB.onNewConnection = &callback;
238 			assert(poolB.onNewConnection == &callback);
239 			assert(count == 0);
240 
241 			// Ensure callback is called
242 			{
243 				auto connA = poolA.lockConnection();
244 				assert(!connA.closed);
245 				assert(count == 1);
246 				
247 				auto connB = poolB.lockConnection();
248 				assert(!connB.closed);
249 				assert(count == 2);
250 			}
251 
252 			// Ensure works with no callback
253 			{
254 				auto oldCount = count;
255 				auto poolC = new MySQLPool(testConnectionStr);
256 				auto connC = poolC.lockConnection();
257 				assert(!connC.closed);
258 				assert(count == oldCount);
259 			}
260 		}
261 
262 		/++
263 		Forwards to vibe.d's
264 		$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.maxConcurrency, ConnectionPool.maxConcurrency)
265 		+/
266 		@property uint maxConcurrency()
267 		{
268 			return m_pool.maxConcurrency;
269 		}
270 
271 		///ditto
272 		@property void maxConcurrency(uint maxConcurrent)
273 		{
274 			m_pool.maxConcurrency = maxConcurrent;
275 		}
276 
277 		/++
278 		Set a prepared statement to be automatically registered on all
279 		connections received from this pool.
280 
281 		This also clears any `autoRelease` which may have been set for this statement.
282 
283 		Calling this is not strictly necessary, as a prepared statement will
284 		automatically be registered upon its first use on any `Connection`.
285 		This is provided for those who prefer eager registration over lazy
286 		for performance reasons.
287 
288 		Once this has been called, obtaining a connection via `lockConnection`
289 		will automatically register the prepared statement on the connection
290 		if it isn't already registered on the connection. This single
291 		registration safely persists after the connection is reclaimed by the
292 		pool and locked again by another Vibe.d task.
293 		
294 		Note, due to the way Vibe.d works, it is not possible to eagerly
295 		register or release a statement on all connections already sitting
296 		in the pool. This can only be done when locking a connection.
297 		
298 		You can stop the pool from continuing to auto-register the statement
299 		by calling either `autoRelease` or `clearAuto`.
300 		+/
301 		void autoRegister(Prepared prepared)
302 		{
303 			autoRegister(prepared.sql);
304 		}
305 
306 		///ditto
307 		void autoRegister(string sql)
308 		{
309 			preparedRegistrations.registerIfNeeded(sql, (sql) => PreparedInfo());
310 		}
311 
312 		/++
313 		Set a prepared statement to be automatically released from all
314 		connections received from this pool.
315 
316 		This also clears any `autoRegister` which may have been set for this statement.
317 
318 		Calling this is not strictly necessary. The server considers prepared
319 		statements to be per-connection, so they'll go away when the connection
320 		closes anyway. This is provided in case direct control is actually needed.
321 
322 		Once this has been called, obtaining a connection via `lockConnection`
323 		will automatically release the prepared statement from the connection
324 		if it isn't already releases from the connection.
325 		
326 		Note, due to the way Vibe.d works, it is not possible to eagerly
327 		register or release a statement on all connections already sitting
328 		in the pool. This can only be done when locking a connection.
329 
330 		You can stop the pool from continuing to auto-release the statement
331 		by calling either `autoRegister` or `clearAuto`.
332 		+/
333 		void autoRelease(Prepared prepared)
334 		{
335 			autoRelease(prepared.sql);
336 		}
337 
338 		///ditto
339 		void autoRelease(string sql)
340 		{
341 			preparedRegistrations.queueForRelease(sql);
342 		}
343 
344 		/// Is the given statement set to be automatically registered on all
345 		/// connections obtained from this connection pool?
346 		bool isAutoRegistered(Prepared prepared)
347 		{
348 			return isAutoRegistered(prepared.sql);
349 		}
350 		///ditto
351 		bool isAutoRegistered(string sql)
352 		{
353 			return isAutoRegistered(preparedRegistrations[sql]);
354 		}
355 		///ditto
356 		package bool isAutoRegistered(Nullable!PreparedInfo info)
357 		{
358 			return info.isNull || !info.queuedForRelease;
359 		}
360 
361 		/// Is the given statement set to be automatically released on all
362 		/// connections obtained from this connection pool?
363 		bool isAutoReleased(Prepared prepared)
364 		{
365 			return isAutoReleased(prepared.sql);
366 		}
367 		///ditto
368 		bool isAutoReleased(string sql)
369 		{
370 			return isAutoReleased(preparedRegistrations[sql]);
371 		}
372 		///ditto
373 		package bool isAutoReleased(Nullable!PreparedInfo info)
374 		{
375 			return info.isNull || info.queuedForRelease;
376 		}
377 
378 		/++
379 		Is the given statement set for NEITHER auto-register
380 		NOR auto-release on connections obtained from
381 		this connection pool?
382 
383 		Equivalent to `!isAutoRegistered && !isAutoReleased`.
384 		+/
385 		bool isAutoCleared(Prepared prepared)
386 		{
387 			return isAutoCleared(prepared.sql);
388 		}
389 		///ditto
390 		bool isAutoCleared(string sql)
391 		{
392 			return isAutoCleared(preparedRegistrations[sql]);
393 		}
394 		///ditto
395 		package bool isAutoCleared(Nullable!PreparedInfo info)
396 		{
397 			return info.isNull;
398 		}
399 
400 		/++
401 		Removes any `autoRegister` or `autoRelease` which may have been set
402 		for this prepared statement.
403 
404 		Does nothing if the statement has not been set for auto-register or auto-release.
405 
406 		This releases any relevent memory for potential garbage collection.
407 		+/
408 		void clearAuto(Prepared prepared)
409 		{
410 			return clearAuto(prepared.sql);
411 		}
412 		///ditto
413 		void clearAuto(string sql)
414 		{
415 			preparedRegistrations.directLookup.remove(sql);
416 		}
417 		
418 		/++
419 		Removes ALL prepared statement `autoRegister` and `autoRelease` which have been set.
420 		
421 		This releases all relevent memory for potential garbage collection.
422 		+/
423 		void clearAllRegistrations()
424 		{
425 			preparedRegistrations.clear();
426 		}
427 	}
428 	 
429 	@("registration")
430 	debug(MYSQLN_TESTS)
431 	unittest
432 	{
433 		import mysql.commands;
434 		auto pool = new MySQLPool(testConnectionStr);
435 
436 		// Setup
437 		Connection cn = pool.lockConnection();
438 		cn.exec("DROP TABLE IF EXISTS `poolRegistration`");
439 		cn.exec("CREATE TABLE `poolRegistration` (
440 			`data` LONGBLOB
441 		) ENGINE=InnoDB DEFAULT CHARSET=utf8");
442 		immutable sql = "SELECT * from `poolRegistration`";
443 		//auto cn2 = pool.lockConnection(); // Seems to return the same connection as `cn`
444 		auto cn2 = pool.createConnection();
445 		pool.applyAuto(cn2);
446 		assert(cn !is cn2);
447 
448 		// Tests:
449 		// Initial
450 		assert(pool.isAutoCleared(sql));
451 		assert(pool.isAutoRegistered(sql));
452 		assert(pool.isAutoReleased(sql));
453 		assert(!cn.isRegistered(sql));
454 		assert(!cn2.isRegistered(sql));
455 
456 		// Register on connection #1
457 		auto prepared = cn.prepare(sql);
458 		{
459 			assert(pool.isAutoCleared(sql));
460 			assert(pool.isAutoRegistered(sql));
461 			assert(pool.isAutoReleased(sql));
462 			assert(cn.isRegistered(sql));
463 			assert(!cn2.isRegistered(sql));
464 
465 			//auto cn3 = pool.lockConnection(); // Seems to return the same connection as `cn`
466 			auto cn3 = pool.createConnection();
467 			pool.applyAuto(cn3);
468 			assert(!cn3.isRegistered(sql));
469 		}
470 
471 		// autoRegister
472 		pool.autoRegister(prepared);
473 		{
474 			assert(!pool.isAutoCleared(sql));
475 			assert(pool.isAutoRegistered(sql));
476 			assert(!pool.isAutoReleased(sql));
477 			assert(cn.isRegistered(sql));
478 			assert(!cn2.isRegistered(sql));
479 
480 			//auto cn3 = pool.lockConnection(); // Seems to return the *same* connection as `cn`
481 			auto cn3 = pool.createConnection();
482 			pool.applyAuto(cn3);
483 			assert(cn3.isRegistered(sql));
484 		}
485 
486 		// autoRelease
487 		pool.autoRelease(prepared);
488 		{
489 			assert(!pool.isAutoCleared(sql));
490 			assert(!pool.isAutoRegistered(sql));
491 			assert(pool.isAutoReleased(sql));
492 			assert(cn.isRegistered(sql));
493 			assert(!cn2.isRegistered(sql));
494 
495 			//auto cn3 = pool.lockConnection(); // Seems to return the same connection as `cn`
496 			auto cn3 = pool.createConnection();
497 			pool.applyAuto(cn3);
498 			assert(!cn3.isRegistered(sql));
499 		}
500 
501 		// clearAuto
502 		pool.clearAuto(prepared);
503 		{
504 			assert(pool.isAutoCleared(sql));
505 			assert(pool.isAutoRegistered(sql));
506 			assert(pool.isAutoReleased(sql));
507 			assert(cn.isRegistered(sql));
508 			assert(!cn2.isRegistered(sql));
509 
510 			//auto cn3 = pool.lockConnection(); // Seems to return the same connection as `cn`
511 			auto cn3 = pool.createConnection();
512 			pool.applyAuto(cn3);
513 			assert(!cn3.isRegistered(sql));
514 		}
515 	}
516 }