1 /// Use a DB via SQL prepared statements. 2 module mysql.prepared; 3 4 import std.exception; 5 import std.range; 6 import std.traits; 7 import std.typecons; 8 import std.variant; 9 10 import mysql.commands; 11 import mysql.exceptions; 12 import mysql.protocol.constants; 13 import mysql.protocol.packets; 14 import mysql.result; 15 debug(MYSQLN_TESTS) 16 import mysql.test.common; 17 18 /++ 19 A struct to represent specializations of prepared statement parameters. 20 21 If you need to send large objects to the database it might be convenient to 22 send them in pieces. The `chunkSize` and `chunkDelegate` variables allow for this. 23 If both are provided then the corresponding column will be populated by calling the delegate repeatedly. 24 The source should fill the indicated slice with data and arrange for the delegate to 25 return the length of the data supplied (in bytes). If that is less than the `chunkSize` 26 then the chunk will be assumed to be the last one. 27 +/ 28 struct ParameterSpecialization 29 { 30 import mysql.protocol.constants; 31 32 size_t pIndex; //parameter number 0 - number of params-1 33 SQLType type = SQLType.INFER_FROM_D_TYPE; 34 uint chunkSize; /// In bytes 35 uint delegate(ubyte[]) chunkDelegate; 36 } 37 ///ditto 38 alias PSN = ParameterSpecialization; 39 40 @("paramSpecial") 41 debug(MYSQLN_TESTS) 42 unittest 43 { 44 import std.array; 45 import std.range; 46 import mysql.connection; 47 import mysql.test.common; 48 mixin(scopedCn); 49 50 // Setup 51 cn.exec("DROP TABLE IF EXISTS `paramSpecial`"); 52 cn.exec("CREATE TABLE `paramSpecial` ( 53 `data` LONGBLOB 54 ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 55 56 immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below 57 auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 58 auto data = alph.cycle.take(totalSize).array; 59 60 int chunkSize; 61 const(ubyte)[] dataToSend; 62 bool finished; 63 uint sender(ubyte[] chunk) 64 { 65 assert(!finished); 66 assert(chunk.length == chunkSize); 67 68 if(dataToSend.length < chunkSize) 69 { 70 auto actualSize = cast(uint) dataToSend.length; 71 chunk[0..actualSize] = dataToSend[]; 72 finished = true; 73 dataToSend.length = 0; 74 return actualSize; 75 } 76 else 77 { 78 chunk[] = dataToSend[0..chunkSize]; 79 dataToSend = dataToSend[chunkSize..$]; 80 return chunkSize; 81 } 82 } 83 84 immutable selectSQL = "SELECT `data` FROM `paramSpecial`"; 85 86 // Sanity check 87 cn.exec("INSERT INTO `paramSpecial` VALUES (\""~(cast(string)data)~"\")"); 88 auto value = cn.queryValue(selectSQL); 89 assert(!value.isNull); 90 assert(value.get == data); 91 92 { 93 // Clear table 94 cn.exec("DELETE FROM `paramSpecial`"); 95 value = cn.queryValue(selectSQL); // Ensure deleted 96 assert(value.isNull); 97 98 // Test: totalSize as a multiple of chunkSize 99 chunkSize = 100; 100 assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); 101 auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); 102 103 finished = false; 104 dataToSend = data; 105 auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); 106 prepared.setArg(0, cast(ubyte[])[], paramSpecial); 107 assert(cn.exec(prepared) == 1); 108 value = cn.queryValue(selectSQL); 109 assert(!value.isNull); 110 assert(value.get == data); 111 } 112 113 { 114 // Clear table 115 cn.exec("DELETE FROM `paramSpecial`"); 116 value = cn.queryValue(selectSQL); // Ensure deleted 117 assert(value.isNull); 118 119 // Test: totalSize as a non-multiple of chunkSize 120 chunkSize = 64; 121 assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize); 122 auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); 123 124 finished = false; 125 dataToSend = data; 126 auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); 127 prepared.setArg(0, cast(ubyte[])[], paramSpecial); 128 assert(cn.exec(prepared) == 1); 129 value = cn.queryValue(selectSQL); 130 assert(!value.isNull); 131 assert(value.get == data); 132 } 133 } 134 135 /++ 136 Encapsulation of a prepared statement. 137 138 Create this via the function `mysql.connection.prepare`. Set your arguments (if any) via 139 the functions provided, and then run the statement by passing it to 140 `mysql.commands.exec`/`mysql.commands.query`/etc in place of the sql string parameter. 141 142 Commands that are expected to return a result set - queries - have distinctive 143 methods that are enforced. That is it will be an error to call such a method 144 with an SQL command that does not produce a result set. So for commands like 145 SELECT, use the `mysql.commands.query` functions. For other commands, like 146 INSERT/UPDATE/CREATE/etc, use `mysql.commands.exec`. 147 +/ 148 struct Prepared 149 { 150 private: 151 string _sql; 152 153 package: 154 ushort _numParams; /// Number of parameters this prepared statement takes 155 PreparedStmtHeaders _headers; 156 Variant[] _inParams; 157 ParameterSpecialization[] _psa; 158 ColumnSpecialization[] _columnSpecials; 159 ulong _lastInsertID; 160 161 ExecQueryImplInfo getExecQueryImplInfo(uint statementId) 162 { 163 return ExecQueryImplInfo(true, null, statementId, _headers, _inParams, _psa); 164 } 165 166 public: 167 /++ 168 Constructor. You probably want `mysql.connection.prepare` instead of this. 169 170 Call `mysqln.connection.prepare` instead of this, unless you are creating 171 your own transport bypassing `mysql.connection.Connection` entirely. 172 The prepared statement must be registered on the server BEFORE this is 173 called (which `mysqln.connection.prepare` does). 174 175 Internally, the result of a successful outcome will be a statement handle - an ID - 176 for the prepared statement, a count of the parameters required for 177 execution of the statement, and a count of the columns that will be present 178 in any result set that the command generates. 179 180 The server will then proceed to send prepared statement headers, 181 including parameter descriptions, and result set field descriptions, 182 followed by an EOF packet. 183 +/ 184 this(string sql, PreparedStmtHeaders headers, ushort numParams) 185 { 186 this._sql = sql; 187 this._headers = headers; 188 this._numParams = numParams; 189 _inParams.length = numParams; 190 _psa.length = numParams; 191 } 192 193 /++ 194 Prepared statement parameter setter. 195 196 The value may, but doesn't have to be, wrapped in a Variant. If so, 197 null is handled correctly. 198 199 The value may, but doesn't have to be, a pointer to the desired value. 200 201 The value may, but doesn't have to be, wrapped in a Nullable!T. If so, 202 null is handled correctly. 203 204 The value can be null. 205 206 Parameter specializations (ie, for chunked transfer) can be added if required. 207 If you wish to use chunked transfer (via `psn`), note that you must supply 208 a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. 209 210 Type_Mappings: $(TYPE_MAPPINGS) 211 212 Params: index = The zero based index 213 +/ 214 void setArg(T)(size_t index, T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) 215 if(!isInstanceOf!(Nullable, T)) 216 { 217 // Now in theory we should be able to check the parameter type here, since the 218 // protocol is supposed to send us type information for the parameters, but this 219 // capability seems to be broken. This assertion is supported by the fact that 220 // the same information is not available via the MySQL C API either. It is up 221 // to the programmer to ensure that appropriate type information is embodied 222 // in the variant array, or provided explicitly. This sucks, but short of 223 // having a client side SQL parser I don't see what can be done. 224 225 enforceEx!MYX(index < _numParams, "Parameter index out of range."); 226 227 _inParams[index] = val; 228 psn.pIndex = index; 229 _psa[index] = psn; 230 } 231 232 ///ditto 233 void setArg(T)(size_t index, Nullable!T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) 234 { 235 if(val.isNull) 236 setArg(index, null, psn); 237 else 238 setArg(index, val.get(), psn); 239 } 240 241 @("setArg-typeMods") 242 debug(MYSQLN_TESTS) 243 unittest 244 { 245 import mysql.test.common; 246 mixin(scopedCn); 247 248 // Setup 249 cn.exec("DROP TABLE IF EXISTS `setArg-typeMods`"); 250 cn.exec("CREATE TABLE `setArg-typeMods` ( 251 `i` INTEGER 252 ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 253 254 auto insertSQL = "INSERT INTO `setArg-typeMods` VALUES (?)"; 255 256 // Sanity check 257 { 258 int i = 111; 259 assert(cn.exec(insertSQL, i) == 1); 260 auto value = cn.queryValue("SELECT `i` FROM `setArg-typeMods`"); 261 assert(!value.isNull); 262 assert(value.get == i); 263 } 264 265 // Test const(int) 266 { 267 const(int) i = 112; 268 //TODO Fix #13 269 //assert(cn.exec(insertSQL, i) == 1); 270 } 271 272 // Test immutable(int) 273 { 274 immutable(int) i = 113; 275 //TODO Fix #13 276 //assert(cn.exec(insertSQL, i) == 1); 277 } 278 } 279 280 /++ 281 Bind a tuple of D variables to the parameters of a prepared statement. 282 283 You can use this method to bind a set of variables if you don't need any specialization, 284 that is chunked transfer is not neccessary. 285 286 The tuple must match the required number of parameters, and it is the programmer's 287 responsibility to ensure that they are of appropriate types. 288 289 Type_Mappings: $(TYPE_MAPPINGS) 290 +/ 291 void setArgs(T...)(T args) 292 if(T.length == 0 || !is(T[0] == Variant[])) 293 { 294 enforceEx!MYX(args.length == _numParams, "Argument list supplied does not match the number of parameters."); 295 296 foreach (size_t i, arg; args) 297 setArg(i, arg); 298 } 299 300 /++ 301 Bind a Variant[] as the parameters of a prepared statement. 302 303 You can use this method to bind a set of variables in Variant form to 304 the parameters of a prepared statement. 305 306 Parameter specializations (ie, for chunked transfer) can be added if required. 307 If you wish to use chunked transfer (via `psn`), note that you must supply 308 a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. 309 310 This method could be 311 used to add records from a data entry form along the lines of 312 ------------ 313 auto stmt = conn.prepare("INSERT INTO `table42` VALUES(?, ?, ?)"); 314 DataRecord dr; // Some data input facility 315 ulong ra; 316 do 317 { 318 dr.get(); 319 stmt.setArgs(dr("Name"), dr("City"), dr("Whatever")); 320 ulong rowsAffected = conn.exec(stmt); 321 } while(!dr.done); 322 ------------ 323 324 Type_Mappings: $(TYPE_MAPPINGS) 325 326 Params: 327 args = External list of Variants to be used as parameters 328 psnList = Any required specializations 329 +/ 330 void setArgs(Variant[] args, ParameterSpecialization[] psnList=null) 331 { 332 enforceEx!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); 333 _inParams[] = args[]; 334 if (psnList !is null) 335 { 336 foreach (PSN psn; psnList) 337 _psa[psn.pIndex] = psn; 338 } 339 } 340 341 /++ 342 Prepared statement parameter getter. 343 344 Type_Mappings: $(TYPE_MAPPINGS) 345 346 Params: index = The zero based index 347 +/ 348 Variant getArg(size_t index) 349 { 350 enforceEx!MYX(index < _numParams, "Parameter index out of range."); 351 return _inParams[index]; 352 } 353 354 /++ 355 Sets a prepared statement parameter to NULL. 356 357 This is here mainly for legacy reasons. You can set a field to null 358 simply by saying `prepared.setArg(index, null);` 359 360 Type_Mappings: $(TYPE_MAPPINGS) 361 362 Params: index = The zero based index 363 +/ 364 void setNullArg(size_t index) 365 { 366 setArg(index, null); 367 } 368 369 /// Gets the SQL command for this prepared statement. 370 string sql() 371 { 372 return _sql; 373 } 374 375 @("setNullArg") 376 debug(MYSQLN_TESTS) 377 unittest 378 { 379 import mysql.connection; 380 import mysql.test.common; 381 mixin(scopedCn); 382 383 cn.exec("DROP TABLE IF EXISTS `setNullArg`"); 384 cn.exec("CREATE TABLE `setNullArg` ( 385 `val` INTEGER 386 ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 387 388 immutable insertSQL = "INSERT INTO `setNullArg` VALUES (?)"; 389 immutable selectSQL = "SELECT * FROM `setNullArg`"; 390 auto preparedInsert = cn.prepare(insertSQL); 391 assert(preparedInsert.sql == insertSQL); 392 Row[] rs; 393 394 { 395 Nullable!int nullableInt; 396 nullableInt.nullify(); 397 preparedInsert.setArg(0, nullableInt); 398 assert(preparedInsert.getArg(0).type == typeid(typeof(null))); 399 nullableInt = 7; 400 preparedInsert.setArg(0, nullableInt); 401 assert(preparedInsert.getArg(0) == 7); 402 403 nullableInt.nullify(); 404 preparedInsert.setArgs(nullableInt); 405 assert(preparedInsert.getArg(0).type == typeid(typeof(null))); 406 nullableInt = 7; 407 preparedInsert.setArgs(nullableInt); 408 assert(preparedInsert.getArg(0) == 7); 409 } 410 411 preparedInsert.setArg(0, 5); 412 cn.exec(preparedInsert); 413 rs = cn.query(selectSQL).array; 414 assert(rs.length == 1); 415 assert(rs[0][0] == 5); 416 417 preparedInsert.setArg(0, null); 418 cn.exec(preparedInsert); 419 rs = cn.query(selectSQL).array; 420 assert(rs.length == 2); 421 assert(rs[0][0] == 5); 422 assert(rs[1].isNull(0)); 423 assert(rs[1][0].type == typeid(typeof(null))); 424 425 preparedInsert.setArg(0, Variant(null)); 426 cn.exec(preparedInsert); 427 rs = cn.query(selectSQL).array; 428 assert(rs.length == 3); 429 assert(rs[0][0] == 5); 430 assert(rs[1].isNull(0)); 431 assert(rs[2].isNull(0)); 432 assert(rs[1][0].type == typeid(typeof(null))); 433 assert(rs[2][0].type == typeid(typeof(null))); 434 } 435 436 /// Gets the number of arguments this prepared statement expects to be passed in. 437 @property ushort numArgs() pure const nothrow 438 { 439 return _numParams; 440 } 441 442 /// After a command that inserted a row into a table with an auto-increment 443 /// ID column, this method allows you to retrieve the last insert ID generated 444 /// from this prepared statement. 445 @property ulong lastInsertID() pure const nothrow { return _lastInsertID; } 446 447 @("lastInsertID") 448 debug(MYSQLN_TESTS) 449 unittest 450 { 451 import mysql.connection; 452 mixin(scopedCn); 453 cn.exec("DROP TABLE IF EXISTS `testPreparedLastInsertID`"); 454 cn.exec("CREATE TABLE `testPreparedLastInsertID` ( 455 `a` INTEGER NOT NULL AUTO_INCREMENT, 456 PRIMARY KEY (a) 457 ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 458 459 auto stmt = cn.prepare("INSERT INTO `testPreparedLastInsertID` VALUES()"); 460 cn.exec(stmt); 461 assert(stmt.lastInsertID == 1); 462 cn.exec(stmt); 463 assert(stmt.lastInsertID == 2); 464 cn.exec(stmt); 465 assert(stmt.lastInsertID == 3); 466 } 467 468 /// Gets the prepared header's field descriptions. 469 @property FieldDescription[] preparedFieldDescriptions() pure { return _headers.fieldDescriptions; } 470 471 /// Gets the prepared header's param descriptions. 472 @property ParamDescription[] preparedParamDescriptions() pure { return _headers.paramDescriptions; } 473 474 /// Get/set the column specializations. 475 @property ColumnSpecialization[] columnSpecials() pure { return _columnSpecials; } 476 477 ///ditto 478 @property void columnSpecials(ColumnSpecialization[] csa) pure { _columnSpecials = csa; } 479 } 480 481 /// Template constraint for `PreparedRegistrations` 482 private enum isPreparedRegistrationsPayload(Payload) = 483 __traits(compiles, (){ 484 static assert(Payload.init.queuedForRelease == false); 485 Payload p; 486 p.queuedForRelease = true; 487 }); 488 489 /++ 490 Common functionality for recordkeeping of prepared statement registration 491 and queueing for unregister. 492 493 Used by `Connection` and `MySQLPool`. 494 495 Templated on payload type. The payload should be an aggregate that includes 496 the field: `bool queuedForRelease = false;` 497 498 Allowing access to `directLookup` from other parts of mysql-native IS intentional. 499 `PreparedRegistrations` isn't intended as 100% encapsulation, it's mainly just 500 to factor out common functionality needed by both `Connection` and `MySQLPool`. 501 +/ 502 package struct PreparedRegistrations(Payload) 503 if( isPreparedRegistrationsPayload!Payload) 504 { 505 /++ 506 Lookup payload by sql string. 507 508 Allowing access to `directLookup` from other parts of mysql-native IS intentional. 509 `PreparedRegistrations` isn't intended as 100% encapsulation, it's mainly just 510 to factor out common functionality needed by both `Connection` and `MySQLPool`. 511 +/ 512 Payload[string] directLookup; 513 514 /// Returns null if not found 515 Nullable!Payload opIndex(const string sql) pure nothrow 516 { 517 Nullable!Payload result; 518 519 auto pInfo = sql in directLookup; 520 if(pInfo) 521 result = *pInfo; 522 523 return result; 524 } 525 526 /// Set `queuedForRelease` flag for a statement in `directLookup`. 527 /// Does nothing if statement not in `directLookup`. 528 private void setQueuedForRelease(string sql, bool value) 529 { 530 if(auto pInfo = sql in directLookup) 531 { 532 pInfo.queuedForRelease = value; 533 directLookup[sql] = *pInfo; 534 } 535 } 536 537 /// Queue a prepared statement for release. 538 void queueForRelease(string sql) 539 { 540 setQueuedForRelease(sql, true); 541 } 542 543 /// Remove a statement from the queue to be released. 544 void unqueueForRelease(string sql) 545 { 546 setQueuedForRelease(sql, false); 547 } 548 549 /// Queues all prepared statements for release. 550 void queueAllForRelease() 551 { 552 foreach(sql, info; directLookup) 553 queueForRelease(sql); 554 } 555 556 /// Eliminate all records of both registered AND queued-for-release statements. 557 void clear() 558 { 559 static if(__traits(compiles, (){ int[int] aa; aa.clear(); })) 560 directLookup.clear(); 561 else 562 directLookup = null; 563 } 564 565 /// If already registered, simply returns the cached Payload. 566 Payload registerIfNeeded(string sql, Payload delegate(string) doRegister) 567 out(info) 568 { 569 // I'm confident this can't currently happen, but 570 // let's make sure that doesn't change. 571 assert(!info.queuedForRelease); 572 } 573 body 574 { 575 if(auto pInfo = sql in directLookup) 576 { 577 // The statement is registered. It may, or may not, be queued 578 // for release. Either way, all we need to do is make sure it's 579 // un-queued and then return. 580 pInfo.queuedForRelease = false; 581 return *pInfo; 582 } 583 584 auto info = doRegister(sql); 585 directLookup[sql] = info; 586 587 return info; 588 } 589 } 590 591 // Test PreparedRegistrations 592 debug(MYSQLN_TESTS) 593 { 594 // Test template constraint 595 struct TestPreparedRegistrationsBad1 { } 596 struct TestPreparedRegistrationsBad2 { bool foo = false; } 597 struct TestPreparedRegistrationsBad3 { int queuedForRelease = 1; } 598 struct TestPreparedRegistrationsBad4 { bool queuedForRelease = true; } 599 struct TestPreparedRegistrationsGood1 { bool queuedForRelease = false; } 600 struct TestPreparedRegistrationsGood2 { bool queuedForRelease = false; string id; } 601 602 static assert(!isPreparedRegistrationsPayload!int); 603 static assert(!isPreparedRegistrationsPayload!bool); 604 static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad1); 605 static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad2); 606 static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad3); 607 static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad4); 608 //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood1); 609 //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood2); 610 PreparedRegistrations!TestPreparedRegistrationsGood1 testPreparedRegistrationsGood1; 611 PreparedRegistrations!TestPreparedRegistrationsGood2 testPreparedRegistrationsGood2; 612 613 @("PreparedRegistrations") 614 unittest 615 { 616 // Test init 617 PreparedRegistrations!TestPreparedRegistrationsGood2 pr; 618 assert(pr.directLookup.keys.length == 0); 619 620 void resetData(bool isQueued1, bool isQueued2, bool isQueued3) 621 { 622 pr.directLookup["1"] = TestPreparedRegistrationsGood2(isQueued1, "1"); 623 pr.directLookup["2"] = TestPreparedRegistrationsGood2(isQueued2, "2"); 624 pr.directLookup["3"] = TestPreparedRegistrationsGood2(isQueued3, "3"); 625 assert(pr.directLookup.keys.length == 3); 626 } 627 628 // Test resetData (sanity check) 629 resetData(false, true, false); 630 assert(pr.directLookup["1"] == TestPreparedRegistrationsGood2(false, "1")); 631 assert(pr.directLookup["2"] == TestPreparedRegistrationsGood2(true, "2")); 632 assert(pr.directLookup["3"] == TestPreparedRegistrationsGood2(false, "3")); 633 634 // Test opIndex 635 resetData(false, true, false); 636 pr.directLookup["1"] = TestPreparedRegistrationsGood2(false, "1"); 637 pr.directLookup["2"] = TestPreparedRegistrationsGood2(true, "2"); 638 pr.directLookup["3"] = TestPreparedRegistrationsGood2(false, "3"); 639 assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); 640 assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); 641 assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); 642 assert(pr["4"].isNull); 643 644 // Test queueForRelease 645 resetData(false, true, false); 646 pr.queueForRelease("2"); 647 assert(pr.directLookup.keys.length == 3); 648 assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); 649 assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); 650 assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); 651 652 pr.queueForRelease("3"); 653 assert(pr.directLookup.keys.length == 3); 654 assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); 655 assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); 656 assert(pr["3"] == TestPreparedRegistrationsGood2(true, "3")); 657 658 pr.queueForRelease("4"); 659 assert(pr.directLookup.keys.length == 3); 660 assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); 661 assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); 662 assert(pr["3"] == TestPreparedRegistrationsGood2(true, "3")); 663 664 // Test unqueueForRelease 665 resetData(false, true, false); 666 pr.unqueueForRelease("1"); 667 assert(pr.directLookup.keys.length == 3); 668 assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); 669 assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); 670 assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); 671 672 pr.unqueueForRelease("2"); 673 assert(pr.directLookup.keys.length == 3); 674 assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); 675 assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2")); 676 assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); 677 678 pr.unqueueForRelease("4"); 679 assert(pr.directLookup.keys.length == 3); 680 assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); 681 assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2")); 682 assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3")); 683 684 // Test queueAllForRelease 685 resetData(false, true, false); 686 pr.queueAllForRelease(); 687 assert(pr["1"] == TestPreparedRegistrationsGood2(true, "1")); 688 assert(pr["2"] == TestPreparedRegistrationsGood2(true, "2")); 689 assert(pr["3"] == TestPreparedRegistrationsGood2(true, "3")); 690 assert(pr["4"].isNull); 691 692 // Test clear 693 resetData(false, true, false); 694 pr.clear(); 695 assert(pr.directLookup.keys.length == 0); 696 697 // Test registerIfNeeded 698 auto doRegister(string sql) { return TestPreparedRegistrationsGood2(false, sql); } 699 pr.registerIfNeeded("1", &doRegister); 700 assert(pr.directLookup.keys.length == 1); 701 assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); 702 703 pr.registerIfNeeded("1", &doRegister); 704 assert(pr.directLookup.keys.length == 1); 705 assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); 706 707 pr.registerIfNeeded("2", &doRegister); 708 assert(pr.directLookup.keys.length == 2); 709 assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1")); 710 assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2")); 711 } 712 }