1 /// Use a DB via SQL prepared statements. 2 module mysql.prepared; 3 4 import std.algorithm; 5 import std.conv; 6 import std.datetime; 7 import std.exception; 8 import std.range; 9 import std.traits; 10 import std.typecons; 11 import std.variant; 12 13 import mysql.commands; 14 import mysql.connection; 15 import mysql.exceptions; 16 import mysql.protocol.constants; 17 import mysql.protocol.extra_types; 18 import mysql.protocol.packets; 19 import mysql.protocol.packet_helpers; 20 import mysql.protocol.sockets; 21 import mysql.result; 22 import mysql.types; 23 24 /++ 25 A struct to represent specializations of prepared statement parameters. 26 27 Strongly considering the removal of the isNull field, now that Prepared 28 can handle `null` as a value just fine. 29 30 There are two specializations. First you can set an isNull flag to indicate that the 31 parameter is to have the SQL NULL value. 32 33 Second, if you need to send large objects to the database it might be convenient to 34 send them in pieces. These two variables allow for this. If both are provided 35 then the corresponding column will be populated by calling the delegate repeatedly. 36 the source should fill the indicated slice with data and arrange for the delegate to 37 return the length of the data supplied. Af that is less than the chunkSize 38 then the chunk will be assumed to be the last one. 39 +/ 40 struct ParameterSpecialization 41 { 42 import mysql.protocol.constants; 43 44 size_t pIndex; //parameter number 0 - number of params-1 45 SQLType type = SQLType.INFER_FROM_D_TYPE; 46 uint chunkSize; 47 uint delegate(ubyte[]) chunkDelegate; 48 } 49 ///ditto 50 alias PSN = ParameterSpecialization; 51 52 /++ 53 Encapsulation of a prepared statement. 54 55 Commands that are expected to return a result set - queries - have distinctive 56 methods that are enforced. That is it will be an error to call such a method 57 with an SQL command that does not produce a result set. So for commands like 58 SELECT, use the `query` functions. For other commands, like 59 INSERT/UPDATE/CREATE/etc, use `exec`. 60 61 Internally, `Prepared` simply wraps a `PreparedImpl` with 62 $(LINK2 https://dlang.org/phobos/std_typecons.html#.RefCounted, `RefCounted`), 63 and offers access to the `PreparedImpl` members via "alias this". 64 65 See the `PreparedImpl` documentation for the bulk of the `Prepared` interface. 66 +/ 67 struct Prepared 68 { 69 RefCounted!(PreparedImpl, RefCountedAutoInitialize.no) preparedImpl; 70 alias preparedImpl this; 71 72 @property bool isPrepared() pure const 73 { 74 return preparedImpl.refCountedStore.isInitialized && !preparedImpl.isReleased; 75 } 76 } 77 78 /++ 79 Submit an SQL command to the server to be compiled into a prepared statement. 80 81 The result of a successful outcome will be a statement handle - an ID - 82 for the prepared statement, a count of the parameters required for 83 excution of the statement, and a count of the columns that will be present 84 in any result set that the command generates. These values will be stored 85 in the Command struct. 86 87 The server will then proceed to send prepared statement headers, 88 including parameter descriptions, and result set field descriptions, 89 followed by an EOF packet. 90 91 Throws: MySQLException if there are pending result set items, or if the 92 server has a problem. 93 +/ 94 Prepared prepare(Connection conn, string sql) 95 { 96 return Prepared( refCounted(PreparedImpl(conn, sql)) ); 97 } 98 99 /++ 100 Convenience function to create a prepared statement which calls a stored function. 101 102 Throws: MySQLException if there are pending result set items, or if the 103 server has a problem. 104 105 Params: 106 name = The name of the stored function. 107 numArgs = The number of arguments the stored procedure takes. 108 +/ 109 Prepared prepareFunction(Connection conn, string name, int numArgs) 110 { 111 auto sql = "select " ~ name ~ preparedPlaceholderArgs(numArgs); 112 return prepare(conn, sql); 113 } 114 115 /// 116 unittest 117 { 118 debug(MYSQL_INTEGRATION_TESTS) 119 { 120 import mysql.test.common; 121 mixin(scopedCn); 122 123 exec(cn, `DROP FUNCTION IF EXISTS hello`); 124 exec(cn, ` 125 CREATE FUNCTION hello (s CHAR(20)) 126 RETURNS CHAR(50) DETERMINISTIC 127 RETURN CONCAT('Hello ',s,'!') 128 `); 129 130 auto preparedHello = prepareFunction(cn, "hello", 1); 131 preparedHello.setArgs("World"); 132 ResultSet rs = preparedHello.querySet(); 133 assert(rs.length == 1); 134 assert(rs[0][0] == "Hello World!"); 135 } 136 } 137 138 /++ 139 Convenience function to create a prepared statement which calls a stored procedure. 140 141 OUT parameters are currently not supported. It should generally be 142 possible with MySQL to present them as a result set. 143 144 Throws: MySQLException if there are pending result set items, or if the 145 server has a problem. 146 147 Params: 148 name = The name of the stored procedure. 149 numArgs = The number of arguments the stored procedure takes. 150 151 +/ 152 Prepared prepareProcedure(Connection conn, string name, int numArgs) 153 { 154 auto sql = "call " ~ name ~ preparedPlaceholderArgs(numArgs); 155 return prepare(conn, sql); 156 } 157 158 /// 159 unittest 160 { 161 debug(MYSQL_INTEGRATION_TESTS) 162 { 163 import mysql.test.common; 164 import mysql.test.integration; 165 mixin(scopedCn); 166 initBaseTestTables(cn); 167 168 exec(cn, `DROP PROCEDURE IF EXISTS insert2`); 169 exec(cn, ` 170 CREATE PROCEDURE insert2 (IN p1 INT, IN p2 CHAR(50)) 171 BEGIN 172 INSERT INTO basetest (intcol, stringcol) VALUES(p1, p2); 173 END 174 `); 175 176 auto preparedInsert2 = prepareProcedure(cn, "insert2", 2); 177 preparedInsert2.setArgs(2001, "inserted string 1"); 178 preparedInsert2.exec(); 179 180 ResultSet rs = querySet(cn, "SELECT stringcol FROM basetest WHERE intcol=2001"); 181 assert(rs.length == 1); 182 assert(rs[0][0] == "inserted string 1"); 183 } 184 } 185 186 private string preparedPlaceholderArgs(int numArgs) 187 { 188 auto sql = "("; 189 bool comma = false; 190 foreach(i; 0..numArgs) 191 { 192 if (comma) 193 sql ~= ",?"; 194 else 195 { 196 sql ~= "?"; 197 comma = true; 198 } 199 } 200 sql ~= ")"; 201 202 return sql; 203 } 204 205 debug(MYSQL_INTEGRATION_TESTS) 206 unittest 207 { 208 assert(preparedPlaceholderArgs(3) == "(?,?,?)"); 209 assert(preparedPlaceholderArgs(2) == "(?,?)"); 210 assert(preparedPlaceholderArgs(1) == "(?)"); 211 assert(preparedPlaceholderArgs(0) == "()"); 212 } 213 214 /++ 215 This is the internal implementation of `Prepared`. It is not intended to be 216 used directly, as `Prepared` wraps a `PreparedImpl` with 217 $(LINK2 https://dlang.org/phobos/std_typecons.html#.RefCounted, `RefCounted`), 218 and offers access to the public `PreparedImpl` members via "alias this". 219 +/ 220 struct PreparedImpl 221 { 222 private: 223 Connection _conn; 224 string _sql; 225 226 void enforceNotReleased() 227 { 228 enforceNotReleased(_hStmt); 229 } 230 231 static void enforceNotReleased(uint hStmt) 232 { 233 enforceEx!MYXNotPrepared(hStmt); 234 } 235 236 debug(MYSQL_INTEGRATION_TESTS) 237 unittest 238 { 239 import mysql.prepared; 240 import mysql.test.common; 241 mixin(scopedCn); 242 243 cn.exec("DROP TABLE IF EXISTS `enforceNotReleased`"); 244 cn.exec("CREATE TABLE `enforceNotReleased` ( 245 `val` INTEGER 246 ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 247 248 immutable insertSQL = "INSERT INTO `enforceNotReleased` VALUES (1), (2)"; 249 immutable selectSQL = "SELECT * FROM `enforceNotReleased`"; 250 Prepared preparedInsert; 251 Prepared preparedSelect; 252 int queryTupleResult; 253 assertNotThrown!MYXNotPrepared(preparedInsert = cn.prepare(insertSQL)); 254 assertNotThrown!MYXNotPrepared(preparedSelect = cn.prepare(selectSQL)); 255 assertNotThrown!MYXNotPrepared(preparedInsert.exec()); 256 assertNotThrown!MYXNotPrepared(preparedSelect.querySet()); 257 assertNotThrown!MYXNotPrepared(preparedSelect.query().each()); 258 assertNotThrown!MYXNotPrepared(preparedSelect.queryRowTuple(queryTupleResult)); 259 260 preparedInsert.release(); 261 assertThrown!MYXNotPrepared(preparedInsert.exec()); 262 assertNotThrown!MYXNotPrepared(preparedSelect.querySet()); 263 assertNotThrown!MYXNotPrepared(preparedSelect.query().each()); 264 assertNotThrown!MYXNotPrepared(preparedSelect.queryRowTuple(queryTupleResult)); 265 266 preparedSelect.release(); 267 assertThrown!MYXNotPrepared(preparedInsert.exec()); 268 assertThrown!MYXNotPrepared(preparedSelect.querySet()); 269 assertThrown!MYXNotPrepared(preparedSelect.query().each()); 270 assertThrown!MYXNotPrepared(preparedSelect.queryRowTuple(queryTupleResult)); 271 } 272 273 @disable this(this); // Not copyable 274 275 /++ 276 Submit an SQL command to the server to be compiled into a prepared statement. 277 278 The result of a successful outcome will be a statement handle - an ID - 279 for the prepared statement, a count of the parameters required for 280 excution of the statement, and a count of the columns that will be present 281 in any result set that the command generates. These values will be stored 282 in the Command struct. 283 284 The server will then proceed to send prepared statement headers, 285 including parameter descriptions, and result set field descriptions, 286 followed by an EOF packet. 287 288 If there is an existing statement handle in the Command struct, that 289 prepared statement is released. 290 +/ 291 public this(Connection conn, string sql) 292 { 293 this._conn = conn; 294 this._sql = sql; 295 296 scope(failure) conn.kill(); 297 298 conn.sendCmd(CommandType.STMT_PREPARE, sql); 299 conn._fieldCount = 0; 300 301 //TODO: All packet handling should be moved into the mysql.protocol package. 302 ubyte[] packet = conn.getPacket(); 303 if (packet.front == ResultPacketMarker.ok) 304 { 305 packet.popFront(); 306 _hStmt = packet.consume!int(); 307 conn._fieldCount = packet.consume!short(); 308 _psParams = packet.consume!short(); 309 310 _inParams.length = _psParams; 311 _psa.length = _psParams; 312 313 packet.popFront(); // one byte filler 314 _psWarnings = packet.consume!short(); 315 316 // At this point the server also sends field specs for parameters 317 // and columns if there were any of each 318 _psh = PreparedStmtHeaders(conn, conn._fieldCount, _psParams); 319 } 320 else if(packet.front == ResultPacketMarker.error) 321 { 322 auto error = OKErrorPacket(packet); 323 enforcePacketOK(error); 324 assert(0); // FIXME: what now? 325 } 326 else 327 assert(0); // FIXME: what now? 328 } 329 330 package: 331 uint _hStmt; // Server's identifier for this prepared statement. This is 0 when released. 332 ushort _psParams, _psWarnings; 333 PreparedStmtHeaders _psh; 334 Variant[] _inParams; 335 ParameterSpecialization[] _psa; 336 337 static ubyte[] makeBitmap(in Variant[] inParams) 338 { 339 size_t bml = (inParams.length+7)/8; 340 ubyte[] bma; 341 bma.length = bml; 342 foreach (i; 0..inParams.length) 343 { 344 if(inParams[i].type != typeid(typeof(null))) 345 continue; 346 size_t bn = i/8; 347 size_t bb = i%8; 348 ubyte sr = 1; 349 sr <<= bb; 350 bma[bn] |= sr; 351 } 352 return bma; 353 } 354 355 static ubyte[] makePSPrefix(uint hStmt, ubyte flags = 0) pure nothrow 356 { 357 ubyte[] prefix; 358 prefix.length = 14; 359 360 prefix[4] = CommandType.STMT_EXECUTE; 361 hStmt.packInto(prefix[5..9]); 362 prefix[9] = flags; // flags, no cursor 363 prefix[10] = 1; // iteration count - currently always 1 364 prefix[11] = 0; 365 prefix[12] = 0; 366 prefix[13] = 0; 367 368 return prefix; 369 } 370 371 //TODO: All low-level commms should be moved into the mysql.protocol package. 372 static ubyte[] analyseParams(Variant[] inParams, ParameterSpecialization[] psa, 373 out ubyte[] vals, out bool longData) 374 { 375 size_t pc = inParams.length; 376 ubyte[] types; 377 types.length = pc*2; 378 size_t alloc = pc*20; 379 vals.length = alloc; 380 uint vcl = 0, len; 381 int ct = 0; 382 383 void reAlloc(size_t n) 384 { 385 if (vcl+n < alloc) 386 return; 387 size_t inc = (alloc*3)/2; 388 if (inc < n) 389 inc = n; 390 alloc += inc; 391 vals.length = alloc; 392 } 393 394 foreach (size_t i; 0..pc) 395 { 396 enum UNSIGNED = 0x80; 397 enum SIGNED = 0; 398 if (psa[i].chunkSize) 399 longData= true; 400 if (inParams[i].type == typeid(typeof(null))) 401 { 402 types[ct++] = SQLType.NULL; 403 types[ct++] = SIGNED; 404 continue; 405 } 406 Variant v = inParams[i]; 407 SQLType ext = psa[i].type; 408 string ts = v.type.toString(); 409 bool isRef; 410 if (ts[$-1] == '*') 411 { 412 ts.length = ts.length-1; 413 isRef= true; 414 } 415 416 switch (ts) 417 { 418 case "bool": 419 if (ext == SQLType.INFER_FROM_D_TYPE) 420 types[ct++] = SQLType.BIT; 421 else 422 types[ct++] = cast(ubyte) ext; 423 types[ct++] = SIGNED; 424 reAlloc(2); 425 bool bv = isRef? *(v.get!(bool*)): v.get!(bool); 426 vals[vcl++] = 1; 427 vals[vcl++] = bv? 0x31: 0x30; 428 break; 429 case "byte": 430 types[ct++] = SQLType.TINY; 431 types[ct++] = SIGNED; 432 reAlloc(1); 433 vals[vcl++] = isRef? *(v.get!(byte*)): v.get!(byte); 434 break; 435 case "ubyte": 436 types[ct++] = SQLType.TINY; 437 types[ct++] = UNSIGNED; 438 reAlloc(1); 439 vals[vcl++] = isRef? *(v.get!(ubyte*)): v.get!(ubyte); 440 break; 441 case "short": 442 types[ct++] = SQLType.SHORT; 443 types[ct++] = SIGNED; 444 reAlloc(2); 445 short si = isRef? *(v.get!(short*)): v.get!(short); 446 vals[vcl++] = cast(ubyte) (si & 0xff); 447 vals[vcl++] = cast(ubyte) ((si >> 8) & 0xff); 448 break; 449 case "ushort": 450 types[ct++] = SQLType.SHORT; 451 types[ct++] = UNSIGNED; 452 reAlloc(2); 453 ushort us = isRef? *(v.get!(ushort*)): v.get!(ushort); 454 vals[vcl++] = cast(ubyte) (us & 0xff); 455 vals[vcl++] = cast(ubyte) ((us >> 8) & 0xff); 456 break; 457 case "int": 458 types[ct++] = SQLType.INT; 459 types[ct++] = SIGNED; 460 reAlloc(4); 461 int ii = isRef? *(v.get!(int*)): v.get!(int); 462 vals[vcl++] = cast(ubyte) (ii & 0xff); 463 vals[vcl++] = cast(ubyte) ((ii >> 8) & 0xff); 464 vals[vcl++] = cast(ubyte) ((ii >> 16) & 0xff); 465 vals[vcl++] = cast(ubyte) ((ii >> 24) & 0xff); 466 break; 467 case "uint": 468 types[ct++] = SQLType.INT; 469 types[ct++] = UNSIGNED; 470 reAlloc(4); 471 uint ui = isRef? *(v.get!(uint*)): v.get!(uint); 472 vals[vcl++] = cast(ubyte) (ui & 0xff); 473 vals[vcl++] = cast(ubyte) ((ui >> 8) & 0xff); 474 vals[vcl++] = cast(ubyte) ((ui >> 16) & 0xff); 475 vals[vcl++] = cast(ubyte) ((ui >> 24) & 0xff); 476 break; 477 case "long": 478 types[ct++] = SQLType.LONGLONG; 479 types[ct++] = SIGNED; 480 reAlloc(8); 481 long li = isRef? *(v.get!(long*)): v.get!(long); 482 vals[vcl++] = cast(ubyte) (li & 0xff); 483 vals[vcl++] = cast(ubyte) ((li >> 8) & 0xff); 484 vals[vcl++] = cast(ubyte) ((li >> 16) & 0xff); 485 vals[vcl++] = cast(ubyte) ((li >> 24) & 0xff); 486 vals[vcl++] = cast(ubyte) ((li >> 32) & 0xff); 487 vals[vcl++] = cast(ubyte) ((li >> 40) & 0xff); 488 vals[vcl++] = cast(ubyte) ((li >> 48) & 0xff); 489 vals[vcl++] = cast(ubyte) ((li >> 56) & 0xff); 490 break; 491 case "ulong": 492 types[ct++] = SQLType.LONGLONG; 493 types[ct++] = UNSIGNED; 494 reAlloc(8); 495 ulong ul = isRef? *(v.get!(ulong*)): v.get!(ulong); 496 vals[vcl++] = cast(ubyte) (ul & 0xff); 497 vals[vcl++] = cast(ubyte) ((ul >> 8) & 0xff); 498 vals[vcl++] = cast(ubyte) ((ul >> 16) & 0xff); 499 vals[vcl++] = cast(ubyte) ((ul >> 24) & 0xff); 500 vals[vcl++] = cast(ubyte) ((ul >> 32) & 0xff); 501 vals[vcl++] = cast(ubyte) ((ul >> 40) & 0xff); 502 vals[vcl++] = cast(ubyte) ((ul >> 48) & 0xff); 503 vals[vcl++] = cast(ubyte) ((ul >> 56) & 0xff); 504 break; 505 case "float": 506 types[ct++] = SQLType.FLOAT; 507 types[ct++] = SIGNED; 508 reAlloc(4); 509 float f = isRef? *(v.get!(float*)): v.get!(float); 510 ubyte* ubp = cast(ubyte*) &f; 511 vals[vcl++] = *ubp++; 512 vals[vcl++] = *ubp++; 513 vals[vcl++] = *ubp++; 514 vals[vcl++] = *ubp; 515 break; 516 case "double": 517 types[ct++] = SQLType.DOUBLE; 518 types[ct++] = SIGNED; 519 reAlloc(8); 520 double d = isRef? *(v.get!(double*)): v.get!(double); 521 ubyte* ubp = cast(ubyte*) &d; 522 vals[vcl++] = *ubp++; 523 vals[vcl++] = *ubp++; 524 vals[vcl++] = *ubp++; 525 vals[vcl++] = *ubp++; 526 vals[vcl++] = *ubp++; 527 vals[vcl++] = *ubp++; 528 vals[vcl++] = *ubp++; 529 vals[vcl++] = *ubp; 530 break; 531 case "std.datetime.date.Date": 532 case "std.datetime.Date": 533 types[ct++] = SQLType.DATE; 534 types[ct++] = SIGNED; 535 Date date = isRef? *(v.get!(Date*)): v.get!(Date); 536 ubyte[] da = pack(date); 537 size_t l = da.length; 538 reAlloc(l); 539 vals[vcl..vcl+l] = da[]; 540 vcl += l; 541 break; 542 case "std.datetime.TimeOfDay": 543 case "std.datetime.Time": 544 types[ct++] = SQLType.TIME; 545 types[ct++] = SIGNED; 546 TimeOfDay time = isRef? *(v.get!(TimeOfDay*)): v.get!(TimeOfDay); 547 ubyte[] ta = pack(time); 548 size_t l = ta.length; 549 reAlloc(l); 550 vals[vcl..vcl+l] = ta[]; 551 vcl += l; 552 break; 553 case "std.datetime.date.DateTime": 554 case "std.datetime.DateTime": 555 types[ct++] = SQLType.DATETIME; 556 types[ct++] = SIGNED; 557 DateTime dt = isRef? *(v.get!(DateTime*)): v.get!(DateTime); 558 ubyte[] da = pack(dt); 559 size_t l = da.length; 560 reAlloc(l); 561 vals[vcl..vcl+l] = da[]; 562 vcl += l; 563 break; 564 case "connect.Timestamp": 565 types[ct++] = SQLType.TIMESTAMP; 566 types[ct++] = SIGNED; 567 Timestamp tms = isRef? *(v.get!(Timestamp*)): v.get!(Timestamp); 568 DateTime dt = mysql.protocol.packet_helpers.toDateTime(tms.rep); 569 ubyte[] da = pack(dt); 570 size_t l = da.length; 571 reAlloc(l); 572 vals[vcl..vcl+l] = da[]; 573 vcl += l; 574 break; 575 case "immutable(char)[]": 576 if (ext == SQLType.INFER_FROM_D_TYPE) 577 types[ct++] = SQLType.VARCHAR; 578 else 579 types[ct++] = cast(ubyte) ext; 580 types[ct++] = SIGNED; 581 string s = isRef? *(v.get!(string*)): v.get!(string); 582 ubyte[] packed = packLCS(cast(void[]) s); 583 reAlloc(packed.length); 584 vals[vcl..vcl+packed.length] = packed[]; 585 vcl += packed.length; 586 break; 587 case "char[]": 588 if (ext == SQLType.INFER_FROM_D_TYPE) 589 types[ct++] = SQLType.VARCHAR; 590 else 591 types[ct++] = cast(ubyte) ext; 592 types[ct++] = SIGNED; 593 char[] ca = isRef? *(v.get!(char[]*)): v.get!(char[]); 594 ubyte[] packed = packLCS(cast(void[]) ca); 595 reAlloc(packed.length); 596 vals[vcl..vcl+packed.length] = packed[]; 597 vcl += packed.length; 598 break; 599 case "byte[]": 600 if (ext == SQLType.INFER_FROM_D_TYPE) 601 types[ct++] = SQLType.TINYBLOB; 602 else 603 types[ct++] = cast(ubyte) ext; 604 types[ct++] = SIGNED; 605 byte[] ba = isRef? *(v.get!(byte[]*)): v.get!(byte[]); 606 ubyte[] packed = packLCS(cast(void[]) ba); 607 reAlloc(packed.length); 608 vals[vcl..vcl+packed.length] = packed[]; 609 vcl += packed.length; 610 break; 611 case "ubyte[]": 612 if (ext == SQLType.INFER_FROM_D_TYPE) 613 types[ct++] = SQLType.TINYBLOB; 614 else 615 types[ct++] = cast(ubyte) ext; 616 types[ct++] = SIGNED; 617 ubyte[] uba = isRef? *(v.get!(ubyte[]*)): v.get!(ubyte[]); 618 ubyte[] packed = packLCS(cast(void[]) uba); 619 reAlloc(packed.length); 620 vals[vcl..vcl+packed.length] = packed[]; 621 vcl += packed.length; 622 break; 623 case "void": 624 throw new MYX("Unbound parameter " ~ to!string(i), __FILE__, __LINE__); 625 default: 626 throw new MYX("Unsupported parameter type " ~ ts, __FILE__, __LINE__); 627 } 628 } 629 vals.length = vcl; 630 return types; 631 } 632 633 static void sendLongData(Connection conn, uint hStmt, ParameterSpecialization[] psa) 634 { 635 assert(psa.length <= ushort.max); // parameter number is sent as short 636 foreach (ushort i, PSN psn; psa) 637 { 638 if (!psn.chunkSize) continue; 639 uint cs = psn.chunkSize; 640 uint delegate(ubyte[]) dg = psn.chunkDelegate; 641 642 //TODO: All low-level commms should be moved into the mysql.protocol package. 643 ubyte[] chunk; 644 chunk.length = cs+11; 645 chunk.setPacketHeader(0 /*each chunk is separate cmd*/); 646 chunk[4] = CommandType.STMT_SEND_LONG_DATA; 647 hStmt.packInto(chunk[5..9]); // statement handle 648 packInto(i, chunk[9..11]); // parameter number 649 650 // byte 11 on is payload 651 for (;;) 652 { 653 uint sent = dg(chunk[11..cs+11]); 654 if (sent < cs) 655 { 656 if (sent == 0) // data was exact multiple of chunk size - all sent 657 break; 658 sent += 7; // adjust for non-payload bytes 659 chunk.length = chunk.length - (cs-sent); // trim the chunk 660 packInto!(uint, true)(cast(uint)sent, chunk[0..3]); 661 conn.send(chunk); 662 break; 663 } 664 conn.send(chunk); 665 } 666 } 667 } 668 669 static void sendCommand(Connection conn, uint hStmt, PreparedStmtHeaders psh, 670 Variant[] inParams, ParameterSpecialization[] psa) 671 { 672 conn.autoPurge(); 673 674 //TODO: All low-level commms should be moved into the mysql.protocol package. 675 ubyte[] packet; 676 conn.resetPacket(); 677 678 ubyte[] prefix = makePSPrefix(hStmt, 0); 679 size_t len = prefix.length; 680 bool longData; 681 682 if (psh.paramCount) 683 { 684 ubyte[] one = [ 1 ]; 685 ubyte[] vals; 686 ubyte[] types = analyseParams(inParams, psa, vals, longData); 687 ubyte[] nbm = makeBitmap(inParams); 688 packet = prefix ~ nbm ~ one ~ types ~ vals; 689 } 690 else 691 packet = prefix; 692 693 if (longData) 694 sendLongData(conn, hStmt, psa); 695 696 assert(packet.length <= uint.max); 697 packet.setPacketHeader(conn.pktNumber); 698 conn.bumpPacket(); 699 conn.send(packet); 700 } 701 702 //TODO: This awkward func is only needed by the deprecated Command struct. 703 // Remove this once Command struct is finally deleted. 704 bool execQueryImpl2(out ulong ra) 705 { 706 return execQueryImpl(_conn, 707 ExecQueryImplInfo(true, null, _hStmt, _psh, _inParams, _psa), ra); 708 } 709 710 /// Has this statement been released? 711 @property bool isReleased() pure const nothrow 712 { 713 return _hStmt == 0; 714 } 715 716 public: 717 /++ 718 Execute a prepared command, such as INSERT/UPDATE/CREATE/etc. 719 720 This method is intended for commands which do not produce a result set 721 (otherwise, use one of the query functions instead.) If the SQL command does 722 produces a result set (such as SELECT), `mysql.exceptions.MySQLResultRecievedException` 723 will be thrown. 724 725 Returns: The number of rows affected. 726 +/ 727 ulong exec() 728 { 729 enforceNotReleased(); 730 return execImpl( 731 _conn, 732 ExecQueryImplInfo(true, null, _hStmt, _psh, _inParams, _psa) 733 ); 734 } 735 736 /++ 737 Execute a prepared SQL SELECT command where you expect the entire 738 result set all at once. 739 740 This is being considered for deprecation in a future release of mysql-native, 741 because the same thing can be achieved via `query`(). 742 $(LINK2 https://dlang.org/phobos/std_array.html#array, `array()`). 743 744 If the SQL command does not produce a result set (such as INSERT/CREATE/etc), 745 then `mysql.exceptions.MySQLNoResultRecievedException` will be thrown. Use 746 `exec` instead for such commands. 747 748 If there are long data items among the expected result columns you can use 749 the csa param to specify that they are to be subject to chunked transfer via a 750 delegate. 751 752 Params: csa = An optional array of ColumnSpecialization structs. 753 Returns: A (possibly empty) ResultSet. 754 +/ 755 ResultSet querySet(ColumnSpecialization[] csa = null) 756 { 757 enforceNotReleased(); 758 return querySetImpl( 759 csa, true, _conn, 760 ExecQueryImplInfo(true, null, _hStmt, _psh, _inParams, _psa) 761 ); 762 } 763 764 ///ditto 765 deprecated("Use querySet instead.") 766 alias queryResult = querySet; 767 768 /++ 769 Execute a prepared SQL SELECT command where you want to deal with the 770 result set one row at a time. 771 772 If you need random access to the resulting Row elements, 773 simply call $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`) 774 on the result. 775 776 If the SQL command does not produce a result set (such as INSERT/CREATE/etc), 777 then `mysql.exceptions.MySQLNoResultRecievedException` will be thrown. Use 778 `exec` instead for such commands. 779 780 If there are long data items among the expected result columns you can use 781 the csa param to specify that they are to be subject to chunked transfer via a 782 delegate. 783 784 Params: csa = An optional array of ColumnSpecialization structs. 785 Returns: A (possibly empty) ResultRange. 786 +/ 787 ResultRange query(ColumnSpecialization[] csa = null) 788 { 789 enforceNotReleased(); 790 return queryImpl( 791 csa, _conn, 792 ExecQueryImplInfo(true, null, _hStmt, _psh, _inParams, _psa) 793 ); 794 } 795 796 ///ditto 797 deprecated("Use query instead.") 798 alias querySequence = query; 799 800 /++ 801 Execute a prepared SQL SELECT command where you only want the first Row (if any). 802 803 If the SQL command does not produce a result set (such as INSERT/CREATE/etc), 804 then `mysql.exceptions.MySQLNoResultRecievedException` will be thrown. Use 805 `exec` instead for such commands. 806 807 If there are long data items among the expected result columns you can use 808 the csa param to specify that they are to be subject to chunked transfer via a 809 delegate. 810 811 Params: csa = An optional array of ColumnSpecialization structs. 812 Returns: Nullable!Row: This will be null (check via Nullable.isNull) if the 813 query resulted in an empty result set. 814 +/ 815 Nullable!Row queryRow(ColumnSpecialization[] csa = null) 816 { 817 enforceNotReleased(); 818 return queryRowImpl(csa, _conn, 819 ExecQueryImplInfo(true, null, _hStmt, _psh, _inParams, _psa)); 820 } 821 822 /++ 823 Execute a prepared SQL SELECT command where you only want the first Row, and 824 place result values into a set of D variables. 825 826 This method will throw if any column type is incompatible with the corresponding D variable. 827 828 Unlike the other query functions, queryRowTuple will throw 829 `mysql.exceptions.MySQLException` if the result set is empty 830 (and thus the reference variables passed in cannot be filled). 831 832 If the SQL command does not produce a result set (such as INSERT/CREATE/etc), 833 then `mysql.exceptions.MySQLNoResultRecievedException` will be thrown. Use 834 `exec` instead for such commands. 835 836 Params: args = A tuple of D variables to receive the results. 837 +/ 838 void queryRowTuple(T...)(ref T args) 839 { 840 enforceNotReleased(); 841 return queryRowTupleImpl( 842 _conn, 843 ExecQueryImplInfo(true, null, _hStmt, _psh, _inParams, _psa), 844 args 845 ); 846 } 847 848 ///ditto 849 deprecated("Use queryRowTuple instead.") 850 alias queryTuple = queryRowTuple; 851 852 /++ 853 Execute a prepared SQL SELECT command and returns a single value, 854 the first column of the first row received. 855 856 If the query did not produce any rows, OR the rows it produced have zero columns, 857 this will return `Nullable!Variant()`, ie, null. Test for this with `result.isNull`. 858 859 If the query DID produce a result, but the value actually received is NULL, 860 then `result.isNull` will be FALSE, and `result.get` will produce a Variant 861 which CONTAINS null. Check for this with `result.get.type == typeid(typeof(null))`. 862 863 If the SQL command does not produce a result set (such as INSERT/CREATE/etc), 864 then `mysql.exceptions.MySQLNoResultRecievedException` will be thrown. Use 865 `exec` instead for such commands. 866 867 If there are long data items among the expected result columns you can use 868 the csa param to specify that they are to be subject to chunked transfer via a 869 delegate. 870 871 Params: csa = An optional array of ColumnSpecialization structs. 872 Returns: Nullable!Variant: This will be null (check via Nullable.isNull) if the 873 query resulted in an empty result set. 874 +/ 875 Nullable!Variant queryValue(ColumnSpecialization[] csa = null) 876 { 877 return queryValueImpl(csa, _conn, 878 ExecQueryImplInfo(true, null, _hStmt, _psh, _inParams, _psa)); 879 } 880 881 /++ 882 Prepared statement parameter setter. 883 884 The value may, but doesn't have to be, wrapped in a Variant. If so, 885 null is handled correctly. 886 887 The value may, but doesn't have to be, a pointer to the desired value. 888 889 The value may, but doesn't have to be, wrapped in a Nullable!T. If so, 890 null is handled correctly. 891 892 The value can be null. 893 894 Params: index = The zero based index 895 +/ 896 void setArg(T)(size_t index, T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) 897 if(!isInstanceOf!(Nullable, T)) 898 { 899 // Now in theory we should be able to check the parameter type here, since the 900 // protocol is supposed to send us type information for the parameters, but this 901 // capability seems to be broken. This assertion is supported by the fact that 902 // the same information is not available via the MySQL C API either. It is up 903 // to the programmer to ensure that appropriate type information is embodied 904 // in the variant array, or provided explicitly. This sucks, but short of 905 // having a client side SQL parser I don't see what can be done. 906 907 enforceNotReleased(); 908 enforceEx!MYX(index < _psParams, "Parameter index out of range."); 909 910 _inParams[index] = val; 911 psn.pIndex = index; 912 _psa[index] = psn; 913 } 914 915 void setArg(T)(size_t index, Nullable!T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) 916 { 917 enforceNotReleased(); 918 if(val.isNull) 919 setArg(index, null, psn); 920 else 921 setArg(index, val.get(), psn); 922 } 923 924 /++ 925 Bind a tuple of D variables to the parameters of a prepared statement. 926 927 You can use this method to bind a set of variables if you don't need any specialization, 928 that is chunked transfer is not neccessary. 929 930 The tuple must match the required number of parameters, and it is the programmer's 931 responsibility to ensure that they are of appropriate types. 932 +/ 933 void setArgs(T...)(T args) 934 if(T.length == 0 || !is(T[0] == Variant[])) 935 { 936 enforceNotReleased(); 937 enforceEx!MYX(args.length == _psParams, "Argument list supplied does not match the number of parameters."); 938 939 foreach (size_t i, arg; args) 940 setArg(i, arg); 941 } 942 943 /++ 944 Bind a Variant[] as the parameters of a prepared statement. 945 946 You can use this method to bind a set of variables in Variant form to 947 the parameters of a prepared statement. 948 949 Parameter specializations can be added if required. This method could be 950 used to add records from a data entry form along the lines of 951 ------------ 952 auto c = Command(con, "insert into table42 values(?, ?, ?)"); 953 c.prepare(); 954 Variant[] va; 955 va.length = 3; 956 DataRecord dr; // Some data input facility 957 ulong ra; 958 do 959 { 960 dr.get(); 961 va[0] = dr("Name"); 962 va[1] = dr("City"); 963 va[2] = dr("Whatever"); 964 c.bindParameters(va); 965 c.execPrepared(ra); 966 } while(tod < "17:30"); 967 ------------ 968 Params: va = External list of Variants to be used as parameters 969 psnList = any required specializations 970 +/ 971 void setArgs(Variant[] va, ParameterSpecialization[] psnList= null) 972 { 973 enforceNotReleased(); 974 enforceEx!MYX(va.length == _psParams, "Param count supplied does not match prepared statement"); 975 _inParams[] = va[]; 976 if (psnList !is null) 977 { 978 foreach (PSN psn; psnList) 979 _psa[psn.pIndex] = psn; 980 } 981 } 982 983 /++ 984 Prepared statement parameter getter. 985 986 Params: index = The zero based index 987 +/ 988 Variant getArg(size_t index) 989 { 990 enforceNotReleased(); 991 enforceEx!MYX(index < _psParams, "Parameter index out of range."); 992 return _inParams[index]; 993 } 994 995 /++ 996 Sets a prepared statement parameter to NULL. 997 998 This is here mainly for legacy reasons. You can set a field to null 999 simply by saying `prepared.setArg(index, null);` 1000 1001 Params: index = The zero based index 1002 +/ 1003 void setNullArg(size_t index) 1004 { 1005 enforceNotReleased(); 1006 setArg(index, null); 1007 } 1008 1009 /// Gets the SQL command for this prepared statement 1010 string sql() 1011 { 1012 return _sql; 1013 } 1014 1015 debug(MYSQL_INTEGRATION_TESTS) 1016 unittest 1017 { 1018 import mysql.prepared; 1019 import mysql.test.common; 1020 mixin(scopedCn); 1021 1022 cn.exec("DROP TABLE IF EXISTS `setNullArg`"); 1023 cn.exec("CREATE TABLE `setNullArg` ( 1024 `val` INTEGER 1025 ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 1026 1027 immutable insertSQL = "INSERT INTO `setNullArg` VALUES (?)"; 1028 immutable selectSQL = "SELECT * FROM `setNullArg`"; 1029 auto preparedInsert = cn.prepare(insertSQL); 1030 assert(preparedInsert.sql == insertSQL); 1031 ResultSet rs; 1032 1033 { 1034 Nullable!int nullableInt; 1035 nullableInt.nullify(); 1036 preparedInsert.setArg(0, nullableInt); 1037 assert(preparedInsert.getArg(0).type == typeid(typeof(null))); 1038 nullableInt = 7; 1039 preparedInsert.setArg(0, nullableInt); 1040 assert(preparedInsert.getArg(0) == 7); 1041 1042 nullableInt.nullify(); 1043 preparedInsert.setArgs(nullableInt); 1044 assert(preparedInsert.getArg(0).type == typeid(typeof(null))); 1045 nullableInt = 7; 1046 preparedInsert.setArgs(nullableInt); 1047 assert(preparedInsert.getArg(0) == 7); 1048 } 1049 1050 preparedInsert.setArg(0, 5); 1051 preparedInsert.exec(); 1052 rs = cn.querySet(selectSQL); 1053 assert(rs.length == 1); 1054 assert(rs[0][0] == 5); 1055 1056 preparedInsert.setArg(0, null); 1057 preparedInsert.exec(); 1058 rs = cn.querySet(selectSQL); 1059 assert(rs.length == 2); 1060 assert(rs[0][0] == 5); 1061 assert(rs[1].isNull(0)); 1062 assert(rs[1][0].type == typeid(typeof(null))); 1063 1064 preparedInsert.setArg(0, Variant(null)); 1065 preparedInsert.exec(); 1066 rs = cn.querySet(selectSQL); 1067 assert(rs.length == 3); 1068 assert(rs[0][0] == 5); 1069 assert(rs[1].isNull(0)); 1070 assert(rs[2].isNull(0)); 1071 assert(rs[1][0].type == typeid(typeof(null))); 1072 assert(rs[2][0].type == typeid(typeof(null))); 1073 } 1074 1075 /++ 1076 Release a prepared statement. 1077 1078 This method tells the server that it can dispose of the information it 1079 holds about the current prepared statement. 1080 1081 This method can be called during a GC collection. Allocations should be 1082 avoided if possible as it could crash the GC. 1083 1084 Notes: 1085 1086 In actuality, the server might not immediately be told to release the 1087 statement (although this instance of Prepared will still behave as though 1088 it's been released, regardless). 1089 1090 This is because there could be a ResultRange with results still pending 1091 for retreival, and the protocol doesn't allow sending commands (such as 1092 "release a prepared statement") to the server while data is pending. 1093 Therefore, this function may instead queue the statement to be released 1094 when it is safe to do so: Either the next time a result set is purged or 1095 the next time a command (such as query or exec) is performed (because 1096 such commands automatically purge any pending results). 1097 +/ 1098 void release() 1099 { 1100 if(_conn is null || !_hStmt || _conn.closed()) 1101 return; 1102 1103 _conn.statementsToRelease.add(_hStmt); 1104 _hStmt = 0; 1105 } 1106 package static void immediateRelease(Connection conn, uint statementId) 1107 { 1108 if(!statementId) 1109 return; 1110 1111 scope(failure) conn.kill(); 1112 1113 if(conn.closed()) 1114 return; 1115 1116 //TODO: All low-level commms should be moved into the mysql.protocol package. 1117 ubyte[9] packet_buf; 1118 ubyte[] packet = packet_buf; 1119 packet.setPacketHeader(0/*packet number*/); 1120 conn.bumpPacket(); 1121 packet[4] = CommandType.STMT_CLOSE; 1122 statementId.packInto(packet[5..9]); 1123 conn.purgeResult(); 1124 conn.send(packet); 1125 // It seems that the server does not find it necessary to send a response 1126 // for this command. 1127 } 1128 1129 /// Gets the number of arguments this prepared statement expects to be passed in. 1130 @property ushort numArgs() pure const nothrow 1131 { 1132 return _psParams; 1133 } 1134 1135 /// Gets the prepared header's field descriptions. 1136 @property FieldDescription[] preparedFieldDescriptions() pure { return _psh.fieldDescriptions; } 1137 1138 /// Gets the prepared header's param descriptions. 1139 @property ParamDescription[] preparedParamDescriptions() pure { return _psh.paramDescriptions; } 1140 }