1 /++ 2 Use a DB via plain SQL statements. 3 4 Commands that are expected to return a result set - queries - have distinctive 5 methods that are enforced. That is it will be an error to call such a method 6 with an SQL command that does not produce a result set. So for commands like 7 SELECT, use the `query` functions. For other commands, like 8 INSERT/UPDATE/CREATE/etc, use `exec`. 9 +/ 10 11 module mysql.commands; 12 13 import std.conv; 14 import std.exception; 15 import std.range; 16 import std.typecons; 17 import std.variant; 18 19 import mysql.connection; 20 import mysql.exceptions; 21 import mysql.prepared; 22 import mysql.protocol.constants; 23 import mysql.protocol.extra_types; 24 import mysql.protocol.packets; 25 import mysql.result; 26 27 /// This feature is not yet implemented. It currently has no effect. 28 /+ 29 A struct to represent specializations of returned statement columns. 30 31 If you are executing a query that will include result columns that are large objects, 32 it may be expedient to deal with the data as it is received rather than first buffering 33 it to some sort of byte array. These two variables allow for this. If both are provided 34 then the corresponding column will be fed to the stipulated delegate in chunks of 35 `chunkSize`, with the possible exception of the last chunk, which may be smaller. 36 The bool argument `finished` will be set to true when the last chunk is set. 37 38 Be aware when specifying types for column specializations that for some reason the 39 field descriptions returned for a resultset have all of the types TINYTEXT, MEDIUMTEXT, 40 TEXT, LONGTEXT, TINYBLOB, MEDIUMBLOB, BLOB, and LONGBLOB lumped as type 0xfc 41 contrary to what it says in the protocol documentation. 42 +/ 43 //TODO: I'm not sure this is tested 44 struct ColumnSpecialization 45 { 46 size_t cIndex; // parameter number 0 - number of params-1 47 ushort type; 48 uint chunkSize; 49 void delegate(const(ubyte)[] chunk, bool finished) chunkDelegate; 50 } 51 ///ditto 52 alias CSN = ColumnSpecialization; 53 54 package struct ExecQueryImplInfo 55 { 56 bool isPrepared; 57 58 // For non-prepared statements: 59 string sql; 60 61 // For prepared statements: 62 uint hStmt; 63 PreparedStmtHeaders psh; 64 Variant[] inParams; 65 ParameterSpecialization[] psa; 66 } 67 68 /++ 69 Internal implementation for the exec and query functions. 70 71 Execute a one-off SQL command. 72 73 Any result set can be accessed via Connection.getNextRow(), but you should really be 74 using the query function for such queries. 75 76 Params: ra = An out parameter to receive the number of rows affected. 77 Returns: true if there was a (possibly empty) result set. 78 +/ 79 //TODO: All low-level commms should be moved into the mysql.protocol package. 80 package bool execQueryImpl(Connection conn, ExecQueryImplInfo info, out ulong ra) 81 { 82 scope(failure) conn.kill(); 83 84 // Send data 85 if(info.isPrepared) 86 ProtocolPrepared.sendCommand(conn, info.hStmt, info.psh, info.inParams, info.psa); 87 else 88 { 89 conn.sendCmd(CommandType.QUERY, info.sql); 90 conn._fieldCount = 0; 91 } 92 93 // Handle response 94 ubyte[] packet = conn.getPacket(); 95 bool rv; 96 if (packet.front == ResultPacketMarker.ok || packet.front == ResultPacketMarker.error) 97 { 98 conn.resetPacket(); 99 auto okp = OKErrorPacket(packet); 100 enforcePacketOK(okp); 101 ra = okp.affected; 102 conn._serverStatus = okp.serverStatus; 103 conn._insertID = okp.insertID; 104 rv = false; 105 } 106 else 107 { 108 // There was presumably a result set 109 assert(packet.front >= 1 && packet.front <= 250); // Result set packet header should have this value 110 conn._headersPending = conn._rowsPending = true; 111 conn._binaryPending = info.isPrepared; 112 auto lcb = packet.consumeIfComplete!LCB(); 113 assert(!lcb.isNull); 114 assert(!lcb.isIncomplete); 115 conn._fieldCount = cast(ushort)lcb.value; 116 assert(conn._fieldCount == lcb.value); 117 rv = true; 118 ra = 0; 119 } 120 return rv; 121 } 122 123 ///ditto 124 package bool execQueryImpl(Connection conn, ExecQueryImplInfo info) 125 { 126 ulong rowsAffected; 127 return execQueryImpl(conn, info, rowsAffected); 128 } 129 130 /++ 131 Execute an SQL command or prepared statement, such as INSERT/UPDATE/CREATE/etc. 132 133 This method is intended for commands such as which do not produce a result set 134 (otherwise, use one of the `query` functions instead.) If the SQL command does 135 produces a result set (such as SELECT), `mysql.exceptions.MYXResultRecieved` 136 will be thrown. 137 138 Only use the `string sql` overload when you are not going to be using the same 139 command repeatedly and you are CERTAIN all the data you're sending is properly 140 escaped. Otherwise, consider using overload that takes a `Prepared`. 141 142 Type_Mappings: $(TYPE_MAPPINGS) 143 144 Params: 145 conn = An open `mysql.connection.Connection` to the database. 146 sql = The SQL command to be run. 147 prepared = The prepared statement to be run. 148 149 Returns: The number of rows affected. 150 +/ 151 ulong exec(Connection conn, string sql) 152 { 153 return execImpl(conn, ExecQueryImplInfo(false, sql)); 154 } 155 156 ///ditto 157 ulong exec(Connection conn, ref Prepared prepared) 158 { 159 auto preparedInfo = conn.registerIfNeeded(prepared.sql); 160 auto ra = execImpl(conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); 161 prepared._lastInsertID = conn.lastInsertID; 162 return ra; 163 } 164 165 ///ditto 166 ulong exec(Connection conn, ref BackwardCompatPrepared prepared) 167 { 168 auto p = prepared.prepared; 169 auto result = exec(conn, p); 170 prepared._prepared = p; 171 return result; 172 } 173 174 /// Common implementation for `exec` overloads 175 package ulong execImpl(Connection conn, ExecQueryImplInfo info) 176 { 177 ulong rowsAffected; 178 bool receivedResultSet = execQueryImpl(conn, info, rowsAffected); 179 if(receivedResultSet) 180 { 181 conn.purgeResult(); 182 throw new MYXResultRecieved(); 183 } 184 185 return rowsAffected; 186 } 187 188 /++ 189 Execute an SQL SELECT command or prepared statement. 190 191 This returns an input range of `mysql.result.Row`, so if you need random access 192 to the `mysql.result.Row` elements, simply call 193 $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`) 194 on the result. 195 196 If the SQL command does not produce a result set (such as INSERT/CREATE/etc), 197 then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use 198 `exec` instead for such commands. 199 200 Only use the `string sql` overload when you are not going to be using the same 201 command repeatedly and you are CERTAIN all the data you're sending is properly 202 escaped. Otherwise, consider using overload that takes a `Prepared`. 203 204 If there are long data items among the expected result columns you can use 205 the `csa` param to specify that they are to be subject to chunked transfer via a 206 delegate. 207 208 Type_Mappings: $(TYPE_MAPPINGS) 209 210 Params: 211 conn = An open `mysql.connection.Connection` to the database. 212 sql = The SQL command to be run. 213 prepared = The prepared statement to be run. 214 csa = Not yet implemented. 215 216 Returns: A (possibly empty) `mysql.result.ResultRange`. 217 218 Example: 219 --- 220 ResultRange oneAtATime = myConnection.query("SELECT * from myTable"); 221 Row[] allAtOnce = myConnection.query("SELECT * from myTable").array; 222 --- 223 +/ 224 /+ 225 Future text: 226 If there are long data items among the expected result columns you can use 227 the `csa` param to specify that they are to be subject to chunked transfer via a 228 delegate. 229 230 csa = An optional array of `ColumnSpecialization` structs. If you need to 231 use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. 232 +/ 233 ResultRange query(Connection conn, string sql, ColumnSpecialization[] csa = null) 234 { 235 return queryImpl(csa, conn, ExecQueryImplInfo(false, sql)); 236 } 237 238 ///ditto 239 ResultRange query(Connection conn, ref Prepared prepared) 240 { 241 auto preparedInfo = conn.registerIfNeeded(prepared.sql); 242 auto result = queryImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); 243 prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. 244 return result; 245 } 246 247 ///ditto 248 ResultRange query(Connection conn, ref BackwardCompatPrepared prepared) 249 { 250 auto p = prepared.prepared; 251 auto result = query(conn, p); 252 prepared._prepared = p; 253 return result; 254 } 255 256 /// Common implementation for `query` overloads 257 package ResultRange queryImpl(ColumnSpecialization[] csa, 258 Connection conn, ExecQueryImplInfo info) 259 { 260 ulong ra; 261 enforceEx!MYXNoResultRecieved(execQueryImpl(conn, info, ra)); 262 263 conn._rsh = ResultSetHeaders(conn, conn._fieldCount); 264 if (csa !is null) 265 conn._rsh.addSpecializations(csa); 266 267 conn._headersPending = false; 268 return ResultRange(conn, conn._rsh, conn._rsh.fieldNames); 269 } 270 271 /++ 272 Execute an SQL SELECT command or prepared statement where you only want the 273 first `mysql.result.Row`, if any. 274 275 If the SQL command does not produce a result set (such as INSERT/CREATE/etc), 276 then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use 277 `exec` instead for such commands. 278 279 Only use the `string sql` overload when you are not going to be using the same 280 command repeatedly and you are CERTAIN all the data you're sending is properly 281 escaped. Otherwise, consider using overload that takes a `Prepared`. 282 283 If there are long data items among the expected result columns you can use 284 the `csa` param to specify that they are to be subject to chunked transfer via a 285 delegate. 286 287 Type_Mappings: $(TYPE_MAPPINGS) 288 289 Params: 290 conn = An open `mysql.connection.Connection` to the database. 291 sql = The SQL command to be run. 292 prepared = The prepared statement to be run. 293 csa = Not yet implemented. 294 295 Returns: `Nullable!(mysql.result.Row)`: This will be null (check via `Nullable.isNull`) if the 296 query resulted in an empty result set. 297 +/ 298 /+ 299 Future text: 300 If there are long data items among the expected result columns you can use 301 the `csa` param to specify that they are to be subject to chunked transfer via a 302 delegate. 303 304 csa = An optional array of `ColumnSpecialization` structs. If you need to 305 use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. 306 +/ 307 Nullable!Row queryRow(Connection conn, string sql, ColumnSpecialization[] csa = null) 308 { 309 return queryRowImpl(csa, conn, ExecQueryImplInfo(false, sql)); 310 } 311 312 ///ditto 313 Nullable!Row queryRow(Connection conn, ref Prepared prepared) 314 { 315 auto preparedInfo = conn.registerIfNeeded(prepared.sql); 316 auto result = queryRowImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); 317 prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. 318 return result; 319 } 320 321 ///ditto 322 Nullable!Row queryRow(Connection conn, ref BackwardCompatPrepared prepared) 323 { 324 auto p = prepared.prepared; 325 auto result = queryRow(conn, p); 326 prepared._prepared = p; 327 return result; 328 } 329 330 /// Common implementation for `querySet` overloads. 331 package Nullable!Row queryRowImpl(ColumnSpecialization[] csa, Connection conn, 332 ExecQueryImplInfo info) 333 { 334 auto results = queryImpl(csa, conn, info); 335 if(results.empty) 336 return Nullable!Row(); 337 else 338 { 339 auto row = results.front; 340 results.close(); 341 return Nullable!Row(row); 342 } 343 } 344 345 /++ 346 Execute an SQL SELECT command or prepared statement where you only want the 347 first `mysql.result.Row`, and place result values into a set of D variables. 348 349 This method will throw if any column type is incompatible with the corresponding D variable. 350 351 Unlike the other query functions, queryRowTuple will throw 352 `mysql.exceptions.MYX` if the result set is empty 353 (and thus the reference variables passed in cannot be filled). 354 355 If the SQL command does not produce a result set (such as INSERT/CREATE/etc), 356 then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use 357 `exec` instead for such commands. 358 359 Only use the `string sql` overload when you are not going to be using the same 360 command repeatedly and you are CERTAIN all the data you're sending is properly 361 escaped. Otherwise, consider using overload that takes a `Prepared`. 362 363 Type_Mappings: $(TYPE_MAPPINGS) 364 365 Params: 366 conn = An open `mysql.connection.Connection` to the database. 367 sql = The SQL command to be run. 368 prepared = The prepared statement to be run. 369 args = The variables, taken by reference, to receive the values. 370 +/ 371 void queryRowTuple(T...)(Connection conn, string sql, ref T args) 372 { 373 return queryRowTupleImpl(conn, ExecQueryImplInfo(false, sql), args); 374 } 375 376 ///ditto 377 void queryRowTuple(T...)(Connection conn, ref Prepared prepared, ref T args) 378 { 379 auto preparedInfo = conn.registerIfNeeded(prepared.sql); 380 queryRowTupleImpl(conn, prepared.getExecQueryImplInfo(preparedInfo.statementId), args); 381 prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. 382 } 383 384 ///ditto 385 void queryRowTuple(T...)(Connection conn, ref BackwardCompatPrepared prepared, ref T args) 386 { 387 auto p = prepared.prepared; 388 queryRowTuple(conn, p, args); 389 prepared._prepared = p; 390 } 391 392 /// Common implementation for `queryRowTuple` overloads. 393 package void queryRowTupleImpl(T...)(Connection conn, ExecQueryImplInfo info, ref T args) 394 { 395 ulong ra; 396 enforceEx!MYXNoResultRecieved(execQueryImpl(conn, info, ra)); 397 398 Row rr = conn.getNextRow(); 399 /+if (!rr._valid) // The result set was empty - not a crime. 400 return;+/ 401 enforceEx!MYX(rr._values.length == args.length, "Result column count does not match the target tuple."); 402 foreach (size_t i, dummy; args) 403 { 404 enforceEx!MYX(typeid(args[i]).toString() == rr._values[i].type.toString(), 405 "Tuple "~to!string(i)~" type and column type are not compatible."); 406 args[i] = rr._values[i].get!(typeof(args[i])); 407 } 408 // If there were more rows, flush them away 409 // Question: Should I check in purgeResult and throw if there were - it's very inefficient to 410 // allow sloppy SQL that does not ensure just one row! 411 conn.purgeResult(); 412 } 413 414 // Test what happends when queryRowTuple receives no rows 415 debug(MYSQLN_TESTS) 416 unittest 417 { 418 import mysql.prepared; 419 import mysql.test.common : scopedCn, createCn; 420 mixin(scopedCn); 421 422 cn.exec("DROP TABLE IF EXISTS `queryRowTuple`"); 423 cn.exec("CREATE TABLE `queryRowTuple` ( 424 `val` INTEGER 425 ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 426 427 immutable selectSQL = "SELECT * FROM `queryRowTuple`"; 428 int queryTupleResult; 429 assertThrown!MYX(cn.queryRowTuple(selectSQL, queryTupleResult)); 430 } 431 432 /++ 433 Execute an SQL SELECT command or prepared statement and return a single value: 434 the first column of the first row received. 435 436 If the query did not produce any rows, or the rows it produced have zero columns, 437 this will return `Nullable!Variant()`, ie, null. Test for this with `result.isNull`. 438 439 If the query DID produce a result, but the value actually received is NULL, 440 then `result.isNull` will be FALSE, and `result.get` will produce a Variant 441 which CONTAINS null. Check for this with `result.get.type == typeid(typeof(null))`. 442 443 If the SQL command does not produce a result set (such as INSERT/CREATE/etc), 444 then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use 445 `exec` instead for such commands. 446 447 Only use the `string sql` overload when you are not going to be using the same 448 command repeatedly and you are CERTAIN all the data you're sending is properly 449 escaped. Otherwise, consider using overload that takes a `Prepared`. 450 451 If there are long data items among the expected result columns you can use 452 the `csa` param to specify that they are to be subject to chunked transfer via a 453 delegate. 454 455 Type_Mappings: $(TYPE_MAPPINGS) 456 457 Params: 458 conn = An open `mysql.connection.Connection` to the database. 459 sql = The SQL command to be run. 460 prepared = The prepared statement to be run. 461 csa = Not yet implemented. 462 463 Returns: `Nullable!Variant`: This will be null (check via `Nullable.isNull`) if the 464 query resulted in an empty result set. 465 +/ 466 /+ 467 Future text: 468 If there are long data items among the expected result columns you can use 469 the `csa` param to specify that they are to be subject to chunked transfer via a 470 delegate. 471 472 csa = An optional array of `ColumnSpecialization` structs. If you need to 473 use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. 474 +/ 475 Nullable!Variant queryValue(Connection conn, string sql, ColumnSpecialization[] csa = null) 476 { 477 return queryValueImpl(csa, conn, ExecQueryImplInfo(false, sql)); 478 } 479 480 ///ditto 481 Nullable!Variant queryValue(Connection conn, ref Prepared prepared) 482 { 483 auto preparedInfo = conn.registerIfNeeded(prepared.sql); 484 auto result = queryValueImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); 485 prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. 486 return result; 487 } 488 489 ///ditto 490 Nullable!Variant queryValue(Connection conn, ref BackwardCompatPrepared prepared) 491 { 492 auto p = prepared.prepared; 493 auto result = queryValue(conn, p); 494 prepared._prepared = p; 495 return result; 496 } 497 498 /// Common implementation for `queryValue` overloads. 499 package Nullable!Variant queryValueImpl(ColumnSpecialization[] csa, Connection conn, 500 ExecQueryImplInfo info) 501 { 502 auto results = queryImpl(csa, conn, info); 503 if(results.empty) 504 return Nullable!Variant(); 505 else 506 { 507 auto row = results.front; 508 results.close(); 509 510 if(row.length == 0) 511 return Nullable!Variant(); 512 else 513 return Nullable!Variant(row[0]); 514 } 515 }