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