1 module mysql.common;
2 
3 import std.algorithm;
4 import std.conv;
5 import std.datetime;
6 import std.digest.sha;
7 import std.exception;
8 import std.range;
9 import std.socket;
10 import std.stdio;
11 import std.string;
12 import std.traits;
13 import std.variant;
14 
15 version(Have_vibe_d_core)
16 {
17 	static if(__traits(compiles, (){ import vibe.core.net; } ))
18 		import vibe.core.net;
19 	else
20 		static assert(false, "mysql-native can't find Vibe.d's 'vibe.core.net'.");
21 }
22 
23 /++
24 An exception type to distinguish exceptions thrown by this package.
25 +/
26 class MySQLException: Exception
27 {
28 	this(string msg, string file = __FILE__, size_t line = __LINE__) pure
29 	{
30 		super(msg, file, line);
31 	}
32 }
33 alias MYX = MySQLException;
34 
35 /++
36 Thrown when attempting to communicate with the server (ex: executing SQL or
37 creating a new prepared statement) while the server is still sending results
38 data. Any ResultRange must be consumed or purged before anything else
39 can be done on the connection.
40 +/
41 class MySQLDataPendingException: MySQLException
42 {
43 	this(string file = __FILE__, size_t line = __LINE__) pure
44 	{
45 		super("Data is pending on the connection. Any existing ResultRange "~
46 			"must be consumed or purged before performing any other communication "~
47 			"with the server.", file, line);
48 	}
49 }
50 alias MYXDataPending = MySQLDataPendingException;
51 
52 /++
53 Received invalid data from the server which violates the MySQL network protocol.
54 (Quite possibly mysql-native's fault. Please
55 $(LINK2 https://github.com/mysql-d/mysql-native/issues, file an issue)
56 if you receive this.)
57 +/
58 class MySQLProtocolException: MySQLException
59 {
60 	this(string msg, string file, size_t line) pure
61 	{
62 		super(msg, file, line);
63 	}
64 }
65 alias MYXProtocol = MySQLProtocolException;
66 
67 /++
68 Thrown when attempting to use a prepared statement which had already been released.
69 +/
70 class MySQLNotPreparedException: MySQLException
71 {
72 	this(string file = __FILE__, size_t line = __LINE__) pure
73 	{
74 		super("The prepared statement has already been released.", file, line);
75 	}
76 }
77 alias MYXNotPrepared = MySQLNotPreparedException;
78 
79 /++
80 Common base class of MySQLResultRecievedException and MySQLNoResultRecievedException.
81 
82 Thrown when making the wrong choice between exec or query.
83 
84 The query functions (query, querySet, queryRow, etc.) are for SQL statements
85 such as SELECT that return results (even if the result set has zero elements.)
86 
87 The exec functions are for SQL statements, such as INSERT, that never return
88 result sets, but may return rowsAffected.
89 
90 Using one of those functions, when the other should have been used instead,
91 results in an exception derived from this.
92 +/
93 class MySQLWrongFunctionException: MySQLException
94 {
95 	this(string msg, string file = __FILE__, size_t line = __LINE__) pure
96 	{
97 		super(msg, file, line);
98 	}
99 }
100 alias MYXWrongFunction = MySQLWrongFunctionException;
101 
102 /++
103 Thrown when a result set was returned unexpectedly. Use the query functions
104 (query, querySet, queryRow, etc.), not exec for commands that return
105 result sets (such as SELECT), even if the result set has zero elements.
106 +/
107 class MySQLResultRecievedException: MySQLWrongFunctionException
108 {
109 	this(string file = __FILE__, size_t line = __LINE__) pure
110 	{
111 		super(
112 			"A result set was returned. Use the query functions, not exec, "~
113 			"for commands that return result sets.",
114 			file, line
115 		);
116 	}
117 }
118 alias MYXResultRecieved = MySQLResultRecievedException;
119 
120 /++
121 Thrown when the executed query, unexpectedly, did not produce a result set.
122 Use the exec functions, not query (query, querySet, queryRow, etc.),
123 for commands that don't produce result sets (such as INSERT).
124 +/
125 class MySQLNoResultRecievedException: MySQLWrongFunctionException
126 {
127 	this(string msg, string file = __FILE__, size_t line = __LINE__) pure
128 	{
129 		super(
130 			"The executed query did not produce a result set. Use the exec "~
131 			"functions, not query, for commands that don't produce result sets.",
132 			file, line
133 		);
134 	}
135 }
136 alias MYXNoResultRecieved = MySQLNoResultRecievedException;
137 
138 /++
139 Thrown when attempting to use a range that's been invalidated.
140 In particular, when using a ResultRange after a new command
141 has been issued on the same connection.
142 +/
143 class MySQLInvalidatedRangeException: MySQLException
144 {
145 	this(string msg, string file = __FILE__, size_t line = __LINE__) pure
146 	{
147 		super(msg, file, line);
148 	}
149 }
150 alias MYXInvalidatedRange = MySQLInvalidatedRangeException;
151 
152 debug(MYSQL_INTEGRATION_TESTS)
153 unittest
154 {
155 	import mysql.protocol.prepared;
156 	import mysql.protocol.commands;
157 	import mysql.test.common : scopedCn, createCn;
158 	mixin(scopedCn);
159 
160 	cn.exec("DROP TABLE IF EXISTS `wrongFunctionException`");
161 	cn.exec("CREATE TABLE `wrongFunctionException` (
162 		`val` INTEGER
163 	) ENGINE=InnoDB DEFAULT CHARSET=utf8");
164 
165 	immutable insertSQL = "INSERT INTO `wrongFunctionException` VALUES (1), (2)";
166 	immutable selectSQL = "SELECT * FROM `wrongFunctionException`";
167 	Prepared preparedInsert;
168 	Prepared preparedSelect;
169 	int queryTupleResult;
170 	assertNotThrown!MYXWrongFunction(cn.exec(insertSQL));
171 	assertNotThrown!MYXWrongFunction(cn.querySet(selectSQL));
172 	assertNotThrown!MYXWrongFunction(cn.query(selectSQL).each());
173 	assertNotThrown!MYXWrongFunction(cn.queryRowTuple(selectSQL, queryTupleResult));
174 	assertNotThrown!MYXWrongFunction(preparedInsert = cn.prepare(insertSQL));
175 	assertNotThrown!MYXWrongFunction(preparedSelect = cn.prepare(selectSQL));
176 	assertNotThrown!MYXWrongFunction(preparedInsert.exec());
177 	assertNotThrown!MYXWrongFunction(preparedSelect.querySet());
178 	assertNotThrown!MYXWrongFunction(preparedSelect.query().each());
179 	assertNotThrown!MYXWrongFunction(preparedSelect.queryRowTuple(queryTupleResult));
180 
181 	assertThrown!MYXResultRecieved(cn.exec(selectSQL));
182 	assertThrown!MYXNoResultRecieved(cn.querySet(insertSQL));
183 	assertThrown!MYXNoResultRecieved(cn.query(insertSQL).each());
184 	assertThrown!MYXNoResultRecieved(cn.queryRowTuple(insertSQL, queryTupleResult));
185 	assertThrown!MYXResultRecieved(preparedSelect.exec());
186 	assertThrown!MYXNoResultRecieved(preparedInsert.querySet());
187 	assertThrown!MYXNoResultRecieved(preparedInsert.query().each());
188 	assertThrown!MYXNoResultRecieved(preparedInsert.queryRowTuple(queryTupleResult));
189 }
190 
191 
192 // Phobos/Vibe.d type aliases
193 package alias PlainPhobosSocket = std.socket.TcpSocket;
194 version(Have_vibe_d_core)
195 {
196 	package alias PlainVibeDSocket = vibe.core.net.TCPConnection;
197 }
198 else
199 {
200 	// Dummy types
201 	package alias PlainVibeDSocket = Object;
202 }
203 
204 alias OpenSocketCallbackPhobos = PlainPhobosSocket function(string,ushort);
205 alias OpenSocketCallbackVibeD = PlainVibeDSocket function(string,ushort);
206 
207 enum MySQLSocketType { phobos, vibed }
208 
209 // A minimal socket interface similar to Vibe.d's TCPConnection.
210 // Used to wrap both Phobos and Vibe.d sockets with a common interface.
211 package interface MySQLSocket
212 {
213 	void close();
214 	@property bool connected() const;
215 	void read(ubyte[] dst);
216 	void write(in ubyte[] bytes);
217 
218 	void acquire();
219 	void release();
220 	bool isOwner();
221 	bool amOwner();
222 }
223 
224 // Wraps a Phobos socket with the common interface
225 package class MySQLSocketPhobos : MySQLSocket
226 {
227 	private PlainPhobosSocket socket;
228 
229 	// The socket should already be open
230 	this(PlainPhobosSocket socket)
231 	{
232 		enforceEx!MYX(socket, "Tried to use a null Phobos socket - Maybe the 'openSocket' callback returned null?");
233 		enforceEx!MYX(socket.isAlive, "Tried to use a closed Phobos socket - Maybe the 'openSocket' callback created a socket but forgot to open it?");
234 		this.socket = socket;
235 	}
236 
237 	invariant()
238 	{
239 		assert(!!socket);
240 	}
241 
242 	void close()
243 	{
244 		socket.shutdown(SocketShutdown.BOTH);
245 		socket.close();
246 	}
247 
248 	@property bool connected() const
249 	{
250 		return socket.isAlive;
251 	}
252 
253 	void read(ubyte[] dst)
254 	{
255 		// Note: I'm a little uncomfortable with this line as it doesn't
256 		// (and can't) update Connection._open. Not sure what can be done,
257 		// but perhaps Connection._open should be eliminated in favor of
258 		// querying the socket's opened/closed state directly.
259 		scope(failure) socket.close();
260 
261 		for (size_t off, len; off < dst.length; off += len) {
262 			len = socket.receive(dst[off..$]);
263 			enforceEx!MYX(len != 0, "Server closed the connection");
264 			enforceEx!MYX(len != socket.ERROR, "Received std.socket.Socket.ERROR");
265 		}
266 	}
267 
268 	void write(in ubyte[] bytes)
269 	{
270 		socket.send(bytes);
271 	}
272 
273 	void acquire() { /+ Do nothing +/ }
274 	void release() { /+ Do nothing +/ }
275 	bool isOwner() { return true; }
276 	bool amOwner() { return true; }
277 }
278 
279 // Wraps a Vibe.d socket with the common interface
280 version(Have_vibe_d_core) {
281 	package class MySQLSocketVibeD : MySQLSocket
282 	{
283 		private PlainVibeDSocket socket;
284 
285 		// The socket should already be open
286 		this(PlainVibeDSocket socket)
287 		{
288 			enforceEx!MYX(socket, "Tried to use a null Vibe.d socket - Maybe the 'openSocket' callback returned null?");
289 			enforceEx!MYX(socket.connected, "Tried to use a closed Vibe.d socket - Maybe the 'openSocket' callback created a socket but forgot to open it?");
290 			this.socket = socket;
291 		}
292 
293 		invariant()
294 		{
295 			assert(!!socket);
296 		}
297 
298 		void close()
299 		{
300 			socket.close();
301 		}
302 
303 		@property bool connected() const
304 		{
305 			return socket.connected;
306 		}
307 
308 		void read(ubyte[] dst)
309 		{
310 			socket.read(dst);
311 		}
312 
313 		void write(in ubyte[] bytes)
314 		{
315 			socket.write(bytes);
316 		}
317 
318 		static if (is(typeof(&TCPConnection.isOwner))) {
319 			void acquire() { socket.acquire(); }
320 			void release() { socket.release(); }
321 			bool isOwner() { return socket.isOwner(); }
322 			bool amOwner() { return socket.isOwner(); }
323 		} else {
324 			void acquire() { /+ Do nothing +/ }
325 			void release() { /+ Do nothing +/ }
326 			bool isOwner() { return true; }
327 			bool amOwner() { return true; }
328 		}
329 	}
330 }