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 }