1 /** 2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 10 This module contains the a basic database interface that have minimal dependencies on internal modules. 11 It is intended to be reusable from the test suite. 12 13 Note that a `commit` may result in an exception. 14 The correct way to handle a commit that fail is to rollback. Or rather it is the easy way. 15 The other way would be to try the commmit at a later time. 16 For now though this mean that `scope(success) db.commit` should never be used. 17 Always use `scope(failure) db.rollback`. This ensures that a failed commit results in a rollback. 18 By combining this with spinSqlQuery it means that it will be retried at a later time. 19 20 The only acceptable dependency are: 21 * ../type.d 22 * ..backend/type.d 23 * ../database/type.d 24 * ../database/schema.d 25 */ 26 module dextool.plugin.mutate.backend.database.standalone; 27 28 import core.time : Duration; 29 import logger = std.experimental.logger; 30 import std.algorithm : map; 31 import std.array : Appender, appender, array; 32 import std.datetime : SysTime; 33 import std.format : format; 34 35 import dextool.type : AbsolutePath, Path; 36 37 import dextool.plugin.mutate.backend.database.schema; 38 import dextool.plugin.mutate.backend.database.type; 39 import dextool.plugin.mutate.backend.type : Language; 40 41 /** Database wrapper with minimal dependencies. 42 */ 43 struct Database { 44 import std.conv : to; 45 import std.exception : collectException; 46 import std.typecons : Nullable, Flag, No; 47 import miniorm : Miniorm, select, insert; 48 import d2sqlite3 : SqlDatabase = Database; 49 import dextool.plugin.mutate.backend.type : MutationPoint, Mutation, Checksum; 50 51 Miniorm db; 52 alias db this; 53 54 /** Create a database by either opening an existing or initializing a new. 55 * 56 * Params: 57 * db = path to the database 58 */ 59 static auto make(string db) @safe { 60 return Database(initializeDB(db)); 61 } 62 63 // Not movable. The database should only be passed around as a reference, 64 // if at all. 65 @disable this(this); 66 67 /// If the file has already been analyzed. 68 bool isAnalyzed(const Path p) @trusted { 69 auto stmt = db.prepare("SELECT count(*) FROM files WHERE path=:path LIMIT 1"); 70 stmt.bind(":path", cast(string) p); 71 auto res = stmt.execute; 72 return res.oneValue!long != 0; 73 } 74 75 /// If the file has already been analyzed. 76 bool isAnalyzed(const Path p, const Checksum cs) @trusted { 77 auto stmt = db.prepare( 78 "SELECT count(*) FROM files WHERE path=:path AND checksum0=:cs0 AND checksum1=:cs1 LIMIT 1"); 79 stmt.bind(":path", cast(string) p); 80 stmt.bind(":cs0", cs.c0); 81 stmt.bind(":cs1", cs.c1); 82 auto res = stmt.execute; 83 return res.oneValue!long != 0; 84 } 85 86 Nullable!FileId getFileId(const Path p) @trusted { 87 enum sql = format("SELECT id FROM %s WHERE path=:path", filesTable); 88 auto stmt = db.prepare(sql); 89 stmt.bind(":path", cast(string) p); 90 auto res = stmt.execute; 91 92 typeof(return) rval; 93 if (!res.empty) 94 rval = FileId(res.oneValue!long); 95 return rval; 96 } 97 98 /// Returns: the path ID for the mutant. 99 Nullable!FileId getFileId(const MutationId id) @trusted { 100 enum sql = format("SELECT t1.file_id 101 FROM %s t0, %s t1 102 WHERE t0.id = :id AND t0.mp_id = t1.id", 103 mutationTable, mutationPointTable); 104 auto stmt = db.prepare(sql); 105 stmt.bind(":id", cast(long) id); 106 107 typeof(return) rval; 108 foreach (r; stmt.execute) 109 rval = FileId(r.peek!long(0)); 110 return rval; 111 } 112 113 /// Returns: the file path that the id correspond to. 114 Nullable!Path getFile(const FileId id) @trusted { 115 enum sql = format("SELECT path FROM %s WHERE id = :id", filesTable); 116 auto stmt = db.prepare(sql); 117 stmt.bind(":id", cast(long) id); 118 119 typeof(return) rval; 120 foreach (r; stmt.execute) 121 rval = Path(r.peek!string(0)); 122 return rval; 123 } 124 125 /// Remove the file with all mutations that are coupled to it. 126 void removeFile(const Path p) @trusted { 127 auto stmt = db.prepare(format!"DELETE FROM %s WHERE path=:path"(filesTable)); 128 stmt.bind(":path", cast(string) p); 129 stmt.execute; 130 } 131 132 /// Returns: All files in the database as relative paths. 133 Path[] getFiles() @trusted { 134 auto stmt = db.prepare(format!"SELECT path from %s"(filesTable)); 135 auto res = stmt.execute; 136 137 auto app = appender!(Path[]); 138 foreach (ref r; res) { 139 app.put(Path(r.peek!string(0))); 140 } 141 142 return app.data; 143 } 144 145 enum CntAction { 146 /// Increment the counter 147 incr, 148 /// Reset the counter to zero 149 reset, 150 } 151 152 /** Update the status of a mutant. 153 * 154 * Params: 155 * id = ID of the mutant 156 * st = status to broadcast 157 * d = time spent on veryfing the mutant 158 * tcs = test cases that killed the mutant 159 * counter = how to act with the counter 160 */ 161 void updateMutation(const MutationId id, const Mutation.Status st, 162 const Duration d, const(TestCase)[] tcs, CntAction counter = CntAction.incr) @trusted { 163 import std.datetime : SysTime, Clock; 164 165 enum sql = "UPDATE %s SET 166 status=:st,time=:time,update_ts=:update_ts,%s 167 WHERE 168 id IN (SELECT st_id FROM %s WHERE id = :id)"; 169 170 auto stmt = () { 171 final switch (counter) { 172 case CntAction.incr: 173 return db.prepare(format(sql, 174 mutationStatusTable, "test_cnt=test_cnt+1", mutationTable)); 175 case CntAction.reset: 176 return db.prepare(format(sql, 177 mutationStatusTable, "test_cnt=0", mutationTable)); 178 } 179 }(); 180 181 stmt.bind(":st", st.to!long); 182 stmt.bind(":id", id.to!long); 183 stmt.bind(":time", d.total!"msecs"); 184 stmt.bind(":update_ts", Clock.currTime.toUTC.toSqliteDateTime); 185 stmt.execute; 186 187 updateMutationTestCases(id, tcs); 188 } 189 190 /** Update the status of a mutant. 191 * 192 * Params: 193 * id = mutation status ID 194 * st = new status 195 * update_ts = if the update timestamp should be updated. 196 */ 197 void updateMutationStatus(const MutationStatusId id, const Mutation.Status st, 198 Flag!"updateTs" update_ts = No.updateTs) @trusted { 199 import std.datetime : SysTime, Clock; 200 201 auto stmt = () { 202 if (update_ts) { 203 const ts = Clock.currTime.toUTC.toSqliteDateTime; 204 auto s = db.prepare(format("UPDATE %s SET status=:st,update_ts=:update_ts WHERE id=:id", 205 mutationStatusTable)); 206 s.bind(":update_ts", ts); 207 return s; 208 } else 209 return db.prepare(format("UPDATE %s SET status=:st WHERE id=:id", 210 mutationStatusTable)); 211 }(); 212 stmt.bind(":st", st.to!long); 213 stmt.bind(":id", id.to!long); 214 stmt.execute; 215 } 216 217 /// Returns: all mutation status IDs. 218 MutationStatusId[] getAllMutationStatus() @trusted { 219 enum sql = format("SELECT id FROM %s", mutationStatusTable); 220 221 auto app = appender!(MutationStatusId[])(); 222 foreach (r; db.prepare(sql).execute) 223 app.put(MutationStatusId(r.peek!long(0))); 224 return app.data; 225 } 226 227 Nullable!MutationEntry getMutation(const MutationId id) @trusted { 228 import dextool.plugin.mutate.backend.type; 229 import dextool.type : FileName; 230 231 typeof(return) rval; 232 233 enum get_mut_sql = format("SELECT 234 t0.id, 235 t0.kind, 236 t3.time, 237 t1.offset_begin, 238 t1.offset_end, 239 t1.line, 240 t1.column, 241 t2.path, 242 t2.lang 243 FROM %s t0,%s t1,%s t2,%s t3 244 WHERE 245 t0.id == :id AND 246 t0.mp_id == t1.id AND 247 t1.file_id == t2.id AND 248 t3.id = t0.st_id 249 ", mutationTable, mutationPointTable, 250 filesTable, mutationStatusTable); 251 252 auto stmt = db.prepare(get_mut_sql); 253 stmt.bind(":id", cast(long) id); 254 auto res = stmt.execute; 255 256 if (res.empty) 257 return rval; 258 259 auto v = res.front; 260 261 auto mp = MutationPoint(Offset(v.peek!uint(3), v.peek!uint(4))); 262 mp.mutations = [Mutation(v.peek!long(1).to!(Mutation.Kind))]; 263 auto pkey = MutationId(v.peek!long(0)); 264 auto file = Path(FileName(v.peek!string(7))); 265 auto sloc = SourceLoc(v.peek!uint(5), v.peek!uint(6)); 266 auto lang = v.peek!long(8).to!Language; 267 268 import core.time : dur; 269 270 rval = MutationEntry(pkey, file, sloc, mp, v.peek!long(2).dur!"msecs", lang); 271 272 return rval; 273 } 274 275 MutantMetaData getMutantationMetaData(const MutationId id) @trusted { 276 auto rval = MutantMetaData(id); 277 foreach (res; db.run(select!NomutDataTable.where("mut_id =", cast(long) id))) { 278 rval.set(NoMut(res.tag, res.comment)); 279 } 280 return rval; 281 } 282 283 //TODO: this is a bit inefficient. it should use a callback iterator 284 MutantMetaData[] getMutantationMetaData(const Mutation.Kind[] kinds, const Mutation 285 .Status status) @trusted { 286 const sql = format!"SELECT DISTINCT t.mut_id, t.tag, t.comment 287 FROM %s t, %s t1, %s t2 288 WHERE 289 t.mut_id = t1.id AND 290 t1.st_id = t2.id AND 291 t2.status = :status AND 292 t1.kind IN (%(%s,%)) 293 ORDER BY 294 t.mut_id"(nomutDataTable, mutationTable, 295 mutationStatusTable, kinds.map!(a => cast(long) a)); 296 auto stmt = db.prepare(sql); 297 stmt.bind(":status", cast(long) status); 298 299 auto app = appender!(MutantMetaData[])(); 300 foreach (res; stmt.execute) { 301 app.put(MutantMetaData(MutationId(res.peek!long(0)), 302 MutantAttr(NoMut(res.peek!string(1), res.peek!string(2))))); 303 } 304 return app.data; 305 } 306 307 Nullable!Path getPath(const MutationId id) @trusted { 308 enum get_path_sql = format("SELECT t2.path 309 FROM 310 %s t0, %s t1, %s t2 311 WHERE 312 t0.id = :id AND 313 t0.mp_id = t1.id AND 314 t1.file_id = t2.id 315 ", mutationTable, mutationPointTable, filesTable); 316 317 auto stmt = db.prepare(get_path_sql); 318 stmt.bind(":id", cast(long) id); 319 auto res = stmt.execute; 320 321 typeof(return) rval; 322 if (!res.empty) 323 rval = Path(res.front.peek!string(0)); 324 return rval; 325 } 326 327 /// Returns: the mutants that are connected to the mutation statuses. 328 MutantInfo[] getMutantsInfo(const Mutation.Kind[] kinds, const(MutationStatusId)[] id) @trusted { 329 const get_mutid_sql = format("SELECT t0.id,t2.status,t0.kind,t1.line,t1.column 330 FROM %s t0,%s t1, %s t2 331 WHERE 332 t0.st_id IN (%(%s,%)) AND 333 t0.st_id = t2.id AND 334 t0.kind IN (%(%s,%)) AND 335 t0.mp_id = t1.id", 336 mutationTable, mutationPointTable, 337 mutationStatusTable, id.map!(a => cast(long) a), kinds.map!(a => cast(int) a)); 338 auto stmt = db.prepare(get_mutid_sql); 339 340 auto app = appender!(MutantInfo[])(); 341 foreach (res; stmt.execute) 342 app.put(MutantInfo(MutationId(res.peek!long(0)), res.peek!long(1) 343 .to!(Mutation.Status), res.peek!long(2).to!(Mutation.Kind), 344 SourceLoc(res.peek!uint(3), res.peek!uint(4)))); 345 346 return app.data; 347 } 348 349 /// Returns: the mutants that are connected to the mutation statuses. 350 MutationId[] getMutationIds(const(Mutation.Kind)[] kinds, const(MutationStatusId)[] id) @trusted { 351 if (id.length == 0) 352 return null; 353 354 const get_mutid_sql = format("SELECT id FROM %s t0 355 WHERE 356 t0.st_id IN (%(%s,%)) AND 357 t0.kind IN (%(%s,%))", mutationTable, 358 id.map!(a => cast(long) a), kinds.map!(a => cast(int) a)); 359 auto stmt = db.prepare(get_mutid_sql); 360 361 auto app = appender!(MutationId[])(); 362 foreach (res; stmt.execute) 363 app.put(MutationId(res.peek!long(0))); 364 return app.data; 365 } 366 367 Nullable!MutationStatusId getMutationStatusId(const MutationId id) @trusted { 368 auto stmt = db.prepare(format("SELECT st_id FROM %s WHERE id=:id", mutationTable)); 369 stmt.bind(":id", cast(long) id); 370 typeof(return) rval; 371 foreach (res; stmt.execute) 372 rval = MutationStatusId(res.peek!long(0)); 373 return rval; 374 } 375 376 /// Returns: the mutants in the file at the line. 377 MutationStatusId[] getMutationsOnLine(const(Mutation.Kind)[] kinds, FileId fid, SourceLoc sloc) @trusted { 378 // TODO: should it also be line_end? 379 const sql = format("SELECT DISTINCT t0.id FROM %s t0, %s t1, %s t2 380 WHERE 381 t1.st_id = t0.id AND 382 t1.kind IN (%(%s,%)) AND 383 t1.mp_id = t2.id AND 384 t2.file_id = :fid AND 385 (:line BETWEEN t2.line AND t2.line_end) 386 ", 387 mutationStatusTable, mutationTable, mutationPointTable, 388 kinds.map!(a => cast(int) a)); 389 auto stmt = db.prepare(sql); 390 stmt.bind(":fid", cast(long) fid); 391 stmt.bind(":line", sloc.line); 392 393 auto app = appender!(typeof(return))(); 394 foreach (res; stmt.execute) 395 app.put(MutationStatusId(res.peek!long(0))); 396 return app.data; 397 } 398 399 LineMetadata getLineMetadata(const FileId fid, const SourceLoc sloc) @trusted { 400 // TODO: change this select to using microrm 401 enum sql = format("SELECT nomut,tag,comment FROM %s 402 WHERE 403 file_id = :fid AND 404 line = :line", rawSrcMetadataTable); 405 auto stmt = db.prepare(sql); 406 stmt.bind(":fid", cast(long) fid); 407 stmt.bind(":line", sloc.line); 408 409 auto rval = typeof(return)(fid, sloc.line); 410 foreach (res; stmt.execute) { 411 if (res.peek!long(0) != 0) 412 rval.set(NoMut(res.peek!string(1), res.peek!string(2))); 413 } 414 415 return rval; 416 } 417 418 /// Returns: the `nr` mutants that where the longst since they where tested. 419 MutationStatusTime[] getOldestMutants(const(Mutation.Kind)[] kinds, const long nr) @trusted { 420 const sql = format("SELECT t0.id,t0.update_ts FROM %s t0, %s t1 421 WHERE 422 t0.update_ts IS NOT NULL AND 423 t1.st_id = t0.id AND 424 t1.kind IN (%(%s,%)) 425 ORDER BY t0.update_ts ASC LIMIT :limit", 426 mutationStatusTable, mutationTable, kinds.map!(a => cast(int) a)); 427 auto stmt = db.prepare(sql); 428 stmt.bind(":limit", nr); 429 430 auto app = appender!(MutationStatusTime[])(); 431 foreach (res; stmt.execute) 432 app.put(MutationStatusTime(MutationStatusId(res.peek!long(0)), 433 res.peek!string(1).fromSqLiteDateTime)); 434 return app.data; 435 } 436 437 /// Returns: the `nr` mutant with the highest count that has not been killed and existed in the system the longest. 438 MutationStatus[] getHardestToKillMutant(const(Mutation.Kind)[] kinds, 439 const Mutation.Status status, const long nr) @trusted { 440 const sql = format("SELECT t0.id,t0.status,t0.test_cnt,t0.update_ts,t0.added_ts 441 FROM %s t0, %s t1 442 WHERE 443 t0.update_ts IS NOT NULL AND 444 t0.status = :status AND 445 t1.st_id = t0.id AND 446 t1.kind IN (%(%s,%)) AND 447 t1.st_id NOT IN (SELECT st_id FROM %s WHERE nomut != 0) 448 ORDER BY 449 t0.test_cnt DESC, 450 t0.added_ts ASC, 451 t0.update_ts ASC 452 LIMIT :limit", 453 mutationStatusTable, mutationTable, kinds.map!(a => cast(int) a), srcMetadataTable); 454 auto stmt = db.prepare(sql); 455 stmt.bind(":status", cast(long) status); 456 stmt.bind(":limit", nr); 457 458 auto app = appender!(MutationStatus[])(); 459 foreach (res; stmt.execute) { 460 auto added = () { 461 auto raw = res.peek!string(4); 462 if (raw.length == 0) 463 return Nullable!SysTime(); 464 return Nullable!SysTime(raw.fromSqLiteDateTime); 465 }(); 466 467 // dfmt off 468 app.put(MutationStatus( 469 MutationStatusId(res.peek!long(0)), 470 res.peek!long(1).to!(Mutation.Status), 471 res.peek!long(2).MutantTestCount, 472 res.peek!string(3).fromSqLiteDateTime, 473 added, 474 )); 475 // dfmt on 476 } 477 478 return app.data; 479 } 480 481 /** Remove all mutations of kinds. 482 */ 483 void removeMutant(const Mutation.Kind[] kinds) @trusted { 484 const s = format!"DELETE FROM %s WHERE id IN (SELECT mp_id FROM %s WHERE kind IN (%(%s,%)))"( 485 mutationPointTable, mutationTable, kinds.map!(a => cast(int) a)); 486 auto stmt = db.prepare(s); 487 stmt.execute; 488 } 489 490 /** Reset all mutations of kinds with the status `st` to unknown. 491 */ 492 void resetMutant(const Mutation.Kind[] kinds, Mutation.Status st, Mutation.Status to_st) @trusted { 493 const s = format!"UPDATE %s SET status=%s WHERE status = %s AND id IN(SELECT st_id FROM %s WHERE kind IN (%(%s,%)))"( 494 mutationStatusTable, to_st.to!long, st.to!long, 495 mutationTable, kinds.map!(a => cast(int) a)); 496 auto stmt = db.prepare(s); 497 stmt.execute; 498 } 499 500 import dextool.plugin.mutate.backend.type; 501 502 alias aliveMutants = countMutants!([Mutation.Status.alive], false); 503 alias killedMutants = countMutants!([Mutation.Status.killed], false); 504 alias timeoutMutants = countMutants!([Mutation.Status.timeout], false); 505 506 /// Returns: Total that should be counted when calculating the mutation score. 507 alias totalMutants = countMutants!([ 508 Mutation.Status.alive, Mutation.Status.killed, Mutation.Status.timeout 509 ], false); 510 511 alias unknownMutants = countMutants!([Mutation.Status.unknown], false); 512 alias killedByCompilerMutants = countMutants!([ 513 Mutation.Status.killedByCompiler 514 ], false); 515 516 alias aliveSrcMutants = countMutants!([Mutation.Status.alive], true); 517 alias killedSrcMutants = countMutants!([Mutation.Status.killed], true); 518 alias timeoutSrcMutants = countMutants!([Mutation.Status.timeout], true); 519 520 /// Returns: Total that should be counted when calculating the mutation score. 521 alias totalSrcMutants = countMutants!([ 522 Mutation.Status.alive, Mutation.Status.killed, Mutation.Status.timeout 523 ], true); 524 525 alias unknownSrcMutants = countMutants!([Mutation.Status.unknown], true); 526 alias killedByCompilerSrcMutants = countMutants!([ 527 Mutation.Status.killedByCompiler 528 ], true); 529 530 /** Count the mutants with the nomut metadata. 531 * 532 * Params: 533 * status = status the mutants must be in to be counted. 534 * distinc = count based on unique source code changes. 535 * kinds = the kind of mutants to count. 536 * file = file to count mutants in. 537 */ 538 private MutationReportEntry countMutants(int[] status, bool distinct)( 539 const Mutation.Kind[] kinds, string file = null) @trusted { 540 import core.time : dur; 541 542 static if (distinct) { 543 auto qq = " 544 SELECT count(*),sum(time) 545 FROM ( 546 SELECT count(*),sum(t1.time) time 547 FROM %s t0, %s t1%s 548 WHERE 549 %s 550 t0.st_id = t1.id AND 551 t1.status IN (%(%s,%)) AND 552 t0.kind IN (%(%s,%)) 553 GROUP BY t1.id)"; 554 } else { 555 auto qq = " 556 SELECT count(*),sum(t1.time) time 557 FROM %s t0, %s t1%s 558 WHERE 559 %s 560 t0.st_id = t1.id AND 561 t1.status IN (%(%s,%)) AND 562 t0.kind IN (%(%s,%))"; 563 } 564 const query = () { 565 auto fq = file.length == 0 566 ? null : "t0.mp_id = t2.id AND t2.file_id = t3.id AND t3.path = :path AND"; 567 auto fq_from = file.length == 0 ? null : format(", %s t2, %s t3", 568 mutationPointTable, filesTable); 569 return format(qq, mutationTable, mutationStatusTable, fq_from, fq, 570 status, kinds.map!(a => cast(int) a)); 571 }(); 572 573 typeof(return) rval; 574 auto stmt = db.prepare(query); 575 if (file.length != 0) 576 stmt.bind(":path", file); 577 auto res = stmt.execute; 578 if (!res.empty) 579 rval = MutationReportEntry(res.front.peek!long(0), 580 res.front.peek!long(1).dur!"msecs"); 581 return rval; 582 } 583 584 /** Count the mutants with the nomut metadata. 585 * 586 * Params: 587 * status = status the mutants must be in to be counted. 588 * distinc = count based on unique source code changes. 589 * kinds = the kind of mutants to count. 590 * file = file to count mutants in. 591 */ 592 private MetadataNoMutEntry countNoMutMutants(int[] status, bool distinct)( 593 const Mutation.Kind[] kinds, string file = null) @trusted { 594 static if (distinct) { 595 auto sql_base = " 596 SELECT count(*) 597 FROM ( 598 SELECT count(*) 599 FROM %s t0, %s t1,%s t4%s 600 WHERE 601 %s 602 t0.st_id = t1.id AND 603 t0.st_id = t4.st_id AND 604 t4.nomut != 0 AND 605 t1.status IN (%(%s,%)) AND 606 t0.kind IN (%(%s,%)) 607 GROUP BY t1.id)"; 608 } else { 609 auto sql_base = " 610 SELECT count(*) 611 FROM %s t0, %s t1,%s t4%s 612 WHERE 613 %s 614 t0.st_id = t1.id AND 615 t0.st_id = t4.st_id AND 616 t4.nomut != 0 AND 617 t1.status IN (%(%s,%)) AND 618 t0.kind IN (%(%s,%))"; 619 } 620 const query = () { 621 auto fq = file.length == 0 622 ? null : "t0.mp_id = t2.id AND t2.file_id = t3.id AND t3.path = :path AND"; 623 auto fq_from = file.length == 0 ? null : format(", %s t2, %s t3", 624 mutationPointTable, filesTable); 625 return format(sql_base, mutationTable, mutationStatusTable, 626 srcMetadataTable, fq_from, fq, status, kinds.map!(a => cast(int) a)); 627 }(); 628 629 typeof(return) rval; 630 auto stmt = db.prepare(query); 631 if (file.length != 0) 632 stmt.bind(":path", file); 633 auto res = stmt.execute; 634 if (!res.empty) 635 rval = MetadataNoMutEntry(res.front.peek!long(0)); 636 return rval; 637 } 638 639 /// ditto. 640 alias aliveNoMutSrcMutants = countNoMutMutants!([Mutation.Status.alive], true); 641 642 /// Returns: mutants killed by the test case. 643 MutationStatusId[] testCaseKilledSrcMutants(const Mutation.Kind[] kinds, TestCase tc) @trusted { 644 const query = format(" 645 SELECT t1.id 646 FROM %s t0, %s t1, %s t2, %s t3 647 WHERE 648 t0.st_id = t1.id AND 649 t1.status = :st AND 650 t0.kind IN (%(%s,%)) AND 651 t2.name = :name AND 652 t2.id = t3.tc_id AND 653 t3.st_id = t1.id 654 GROUP BY t1.id", mutationTable, mutationStatusTable, 655 allTestCaseTable, killedTestCaseTable, kinds.map!(a => cast(int) a)); 656 657 auto stmt = db.prepare(query); 658 stmt.bind(":st", cast(long) Mutation.Status.killed); 659 stmt.bind(":name", tc.name); 660 661 auto app = appender!(MutationStatusId[])(); 662 foreach (res; stmt.execute) 663 app.put(MutationStatusId(res.peek!long(0))); 664 665 return app.data; 666 } 667 668 /// Returns: mutants at mutations points that the test case has killed mutants at. 669 alias testCaseMutationPointAliveSrcMutants = testCaseCountSrcMutants!([ 670 Mutation.Status.alive 671 ]); 672 /// ditto 673 alias testCaseMutationPointTimeoutSrcMutants = testCaseCountSrcMutants!( 674 [Mutation.Status.timeout]); 675 /// ditto 676 alias testCaseMutationPointKilledSrcMutants = testCaseCountSrcMutants!([ 677 Mutation.Status.killed 678 ]); 679 /// ditto 680 alias testCaseMutationPointUnknownSrcMutants = testCaseCountSrcMutants!( 681 [Mutation.Status.unknown]); 682 /// ditto 683 alias testCaseMutationPointKilledByCompilerSrcMutants = testCaseCountSrcMutants!( 684 [Mutation.Status.killedByCompiler]); 685 /// ditto 686 alias testCaseMutationPointTotalSrcMutants = testCaseCountSrcMutants!( 687 [ 688 Mutation.Status.alive, Mutation.Status.killed, Mutation.Status.timeout 689 ]); 690 691 private MutationStatusId[] testCaseCountSrcMutants(int[] status)( 692 const Mutation.Kind[] kinds, TestCase tc) @trusted { 693 const query = format(" 694 SELECT t1.id 695 FROM %s t0, %s t1 696 WHERE 697 t0.mp_id IN (SELECT t1.id 698 FROM %s t0,%s t1, %s t2, %s t3 699 WHERE 700 t0.mp_id = t1.id AND 701 t2.name = :name AND 702 t2.id = t3.tc_id AND 703 t3.st_id = t0.st_id 704 ) 705 AND 706 t0.st_id = t1.id AND 707 t1.status IN (%(%s,%)) AND 708 t0.kind IN (%(%s,%)) 709 GROUP BY t1.id", mutationTable, mutationStatusTable, mutationTable, mutationPointTable, 710 allTestCaseTable, killedTestCaseTable, status, kinds.map!(a => cast(int) a)); 711 712 auto stmt = db.prepare(query); 713 stmt.bind(":name", tc.name); 714 715 auto app = appender!(MutationStatusId[])(); 716 foreach (res; stmt.execute) 717 app.put(MutationStatusId(res.peek!long(0))); 718 719 return app.data; 720 } 721 722 void put(const Path p, Checksum cs, const Language lang) @trusted { 723 enum sql = format("INSERT OR IGNORE INTO %s (path, checksum0, checksum1, lang) VALUES (:path, :checksum0, :checksum1, :lang)", 724 filesTable); 725 auto stmt = db.prepare(sql); 726 stmt.bind(":path", cast(string) p); 727 stmt.bind(":checksum0", cast(long) cs.c0); 728 stmt.bind(":checksum1", cast(long) cs.c1); 729 stmt.bind(":lang", cast(long) lang); 730 stmt.execute; 731 } 732 733 /** Save line metadata to the database which is used to associate line 734 * metadata with mutants. 735 */ 736 void put(const LineMetadata[] mdata) { 737 import sumtype; 738 739 // TODO: convert to microrm 740 enum sql = format("INSERT OR IGNORE INTO %s 741 (file_id, line, nomut, tag, comment) 742 VALUES(:fid, :line, :nomut, :tag, :comment)", 743 rawSrcMetadataTable); 744 745 db.begin; 746 scope (failure) 747 db.rollback; 748 749 auto stmt = db.prepare(sql); 750 foreach (meta; mdata) { 751 auto nomut = meta.attr.match!((NoMetadata a) => NoMut.init, (NoMut a) => a); 752 stmt.bindAll(cast(long) meta.id, meta.line, meta.isNoMut, nomut.tag, nomut.comment); 753 stmt.execute; 754 stmt.reset; 755 } 756 757 db.commit; 758 } 759 760 /// Store all found mutants. 761 void put(MutationPointEntry2[] mps, AbsolutePath rel_dir) @trusted { 762 import std.algorithm : map, joiner; 763 import std.datetime : SysTime, Clock; 764 import std.path : relativePath; 765 766 db.begin; 767 scope (failure) 768 db.rollback; 769 770 enum insert_mp_sql = format("INSERT OR IGNORE INTO %s 771 (file_id, offset_begin, offset_end, line, column, line_end, column_end) 772 SELECT id,:begin,:end,:line,:column,:line_end,:column_end 773 FROM %s 774 WHERE 775 path = :path", mutationPointTable, filesTable); 776 auto mp_stmt = db.prepare(insert_mp_sql); 777 778 foreach (mp; mps) { 779 auto rel_file = relativePath(mp.file, rel_dir).Path; 780 mp_stmt.bind(":begin", mp.offset.begin); 781 mp_stmt.bind(":end", mp.offset.end); 782 mp_stmt.bind(":line", mp.sloc.line); 783 mp_stmt.bind(":column", mp.sloc.column); 784 mp_stmt.bind(":line_end", mp.slocEnd.line); 785 mp_stmt.bind(":column_end", mp.slocEnd.column); 786 mp_stmt.bind(":path", cast(string) rel_file); 787 mp_stmt.execute; 788 mp_stmt.reset; 789 } 790 791 enum insert_cmut_sql = format("INSERT OR IGNORE INTO %s 792 (status,test_cnt,update_ts,added_ts,checksum0,checksum1) 793 VALUES(:st,0,:update_ts,:added_ts,:c0,:c1)", 794 mutationStatusTable); 795 auto cmut_stmt = db.prepare(insert_cmut_sql); 796 const ts = Clock.currTime.toUTC.toSqliteDateTime; 797 cmut_stmt.bind(":st", Mutation.Status.unknown); 798 cmut_stmt.bind(":update_ts", ts); 799 cmut_stmt.bind(":added_ts", ts); 800 foreach (cm; mps.map!(a => a.cms).joiner) { 801 cmut_stmt.bind(":c0", cast(long) cm.id.c0); 802 cmut_stmt.bind(":c1", cast(long) cm.id.c1); 803 cmut_stmt.execute; 804 cmut_stmt.reset; 805 } 806 807 enum insert_m_sql = format("INSERT OR IGNORE INTO %s 808 (mp_id, st_id, kind) 809 SELECT t0.id,t1.id,:kind FROM %s t0, %s t1, %s t2 WHERE 810 t2.path = :path AND 811 t0.file_id = t2.id AND 812 t0.offset_begin = :off_begin AND 813 t0.offset_end = :off_end AND 814 t1.checksum0 = :c0 AND 815 t1.checksum1 = :c1", mutationTable, 816 mutationPointTable, mutationStatusTable, filesTable); 817 auto insert_m = db.prepare(insert_m_sql); 818 819 foreach (mp; mps) { 820 foreach (m; mp.cms) { 821 auto rel_file = relativePath(mp.file, rel_dir).Path; 822 insert_m.bind(":path", cast(string) rel_file); 823 insert_m.bind(":off_begin", mp.offset.begin); 824 insert_m.bind(":off_end", mp.offset.end); 825 insert_m.bind(":c0", cast(long) m.id.c0); 826 insert_m.bind(":c1", cast(long) m.id.c1); 827 insert_m.bind(":kind", m.mut.kind); 828 insert_m.execute; 829 insert_m.reset; 830 } 831 } 832 833 db.commit; 834 } 835 836 /** Remove all mutants points from the database. 837 * 838 * This removes all the mutants because of the cascade delete of the 839 * tables. But it will keep the mutation statuses and thus the checksums 840 * and the status of the code changes. 841 * 842 * This then mean that when mutations+mutation points are added back they 843 * may reconnect with a mutation status. 844 */ 845 void removeAllMutationPoints() @trusted { 846 enum del_mp_sql = format("DELETE FROM %s", mutationPointTable); 847 db.run(del_mp_sql); 848 } 849 850 /// ditto 851 void removeAllFiles() @trusted { 852 enum del_f_sql = format("DELETE FROM %s", filesTable); 853 db.run(del_f_sql); 854 } 855 856 /// Remove mutants that have no connection to a mutation point, orphened mutants. 857 void removeOrphanedMutants() @trusted { 858 enum del_orp_m_sql = format("DELETE FROM %s WHERE id NOT IN (SELECT st_id FROM %s)", 859 mutationStatusTable, mutationTable); 860 db.run(del_orp_m_sql); 861 } 862 863 /** Add a link between the mutation and what test case killed it. 864 * 865 * Params: 866 * id = ? 867 * tcs = test cases to add 868 */ 869 void updateMutationTestCases(const MutationId id, const(TestCase)[] tcs) @trusted { 870 if (tcs.length == 0) 871 return; 872 873 db.begin; 874 scope (failure) 875 db.rollback; 876 877 immutable st_id = () { 878 enum st_id_for_mutation_q = format("SELECT st_id FROM %s WHERE id=:id", mutationTable); 879 auto stmt = db.prepare(st_id_for_mutation_q); 880 stmt.bind(":id", cast(long) id); 881 return stmt.execute.oneValue!long; 882 }(); 883 884 try { 885 enum remove_old_sql = format("DELETE FROM %s WHERE st_id=:id", killedTestCaseTable); 886 auto stmt = db.prepare(remove_old_sql); 887 stmt.bind(":id", st_id); 888 stmt.execute; 889 } catch (Exception e) { 890 } 891 892 enum add_if_non_exist_tc_sql = format( 893 "INSERT INTO %s (name) SELECT :name1 WHERE NOT EXISTS (SELECT * FROM %s WHERE name = :name2)", 894 allTestCaseTable, allTestCaseTable); 895 auto stmt_insert_tc = db.prepare(add_if_non_exist_tc_sql); 896 897 enum add_new_sql = format( 898 "INSERT INTO %s (st_id, tc_id, location) SELECT :st_id,t1.id,:loc FROM %s t1 WHERE t1.name = :tc", 899 killedTestCaseTable, allTestCaseTable); 900 auto stmt_insert = db.prepare(add_new_sql); 901 foreach (const tc; tcs) { 902 try { 903 stmt_insert_tc.reset; 904 stmt_insert_tc.bind(":name1", tc.name); 905 stmt_insert_tc.bind(":name2", tc.name); 906 stmt_insert_tc.execute; 907 908 stmt_insert.reset; 909 stmt_insert.bind(":st_id", st_id); 910 stmt_insert.bind(":loc", tc.location); 911 stmt_insert.bind(":tc", tc.name); 912 stmt_insert.execute; 913 } catch (Exception e) { 914 logger.warning(e.msg); 915 } 916 } 917 918 db.commit; 919 } 920 921 /** Set detected test cases. 922 * 923 * This will replace those that where previously stored. 924 * 925 * Returns: ID of affected mutation statuses. 926 */ 927 MutationStatusId[] setDetectedTestCases(const(TestCase)[] tcs) @trusted { 928 auto mut_status_ids = appender!(MutationStatusId[])(); 929 930 if (tcs.length == 0) 931 return mut_status_ids.data; 932 933 db.begin; 934 scope (failure) 935 db.rollback; 936 937 immutable tmp_name = "tmp_new_tc_" ~ __LINE__.to!string; 938 internalAddDetectedTestCases(tcs, tmp_name); 939 940 enum mut_st_id = format("SELECT DISTINCT t1.st_id 941 FROM %s t0, %s t1 942 WHERE 943 t0.name NOT IN (SELECT name FROM %s) AND 944 t0.id = t1.tc_id", allTestCaseTable, 945 killedTestCaseTable, tmp_name); 946 foreach (res; db.prepare(mut_st_id).execute) 947 mut_status_ids.put(res.peek!long(0).MutationStatusId); 948 949 enum remove_old_sql = format("DELETE FROM %s WHERE name NOT IN (SELECT name FROM %s)", 950 allTestCaseTable, tmp_name); 951 db.run(remove_old_sql); 952 953 db.run(format!"DROP TABLE %s"(tmp_name)); 954 db.commit; 955 956 return mut_status_ids.data; 957 } 958 959 /** Add test cases to those that have been detected. 960 * 961 * They will be added if they are unique. 962 */ 963 void addDetectedTestCases(const(TestCase)[] tcs) @trusted { 964 if (tcs.length == 0) 965 return; 966 967 db.begin; 968 scope (failure) 969 db.rollback; 970 971 immutable tmp_name = "tmp_new_tc_" ~ __LINE__.to!string; 972 internalAddDetectedTestCases(tcs, tmp_name); 973 db.run(format!"DROP TABLE %s"(tmp_name)); 974 db.commit; 975 } 976 977 /// ditto. 978 private void internalAddDetectedTestCases(const(TestCase)[] tcs, string tmp_tbl) @trusted { 979 db.run(format!"CREATE TEMP TABLE %s (id INTEGER PRIMARY KEY, name TEXT NOT NULL)"( 980 tmp_tbl)); 981 982 immutable add_tc_sql = format!"INSERT INTO %s (name) VALUES(:name)"(tmp_tbl); 983 auto insert_s = db.prepare(add_tc_sql); 984 foreach (tc; tcs) { 985 insert_s.bind(":name", tc.name); 986 insert_s.execute; 987 insert_s.reset; 988 } 989 990 // https://stackoverflow.com/questions/2686254/how-to-select-all-records-from-one-table-that-do-not-exist-in-another-table 991 //Q: What is happening here? 992 // 993 //A: Conceptually, we select all rows from table1 and for each row we 994 //attempt to find a row in table2 with the same value for the name 995 //column. If there is no such row, we just leave the table2 portion of 996 //our result empty for that row. Then we constrain our selection by 997 //picking only those rows in the result where the matching row does not 998 //exist. Finally, We ignore all fields from our result except for the 999 //name column (the one we are sure that exists, from table1). 1000 // 1001 //While it may not be the most performant method possible in all cases, 1002 //it should work in basically every database engine ever that attempts 1003 //to implement ANSI 92 SQL 1004 immutable add_missing_sql = format!"INSERT INTO %s (name) SELECT t1.name FROM %s t1 LEFT JOIN %s t2 ON t2.name = t1.name WHERE t2.name IS NULL"( 1005 allTestCaseTable, tmp_tbl, allTestCaseTable); 1006 db.run(add_missing_sql); 1007 } 1008 1009 /// Returns: detected test cases. 1010 TestCase[] getDetectedTestCases() @trusted { 1011 import std.algorithm : copy; 1012 1013 auto rval = appender!(TestCase[])(); 1014 db.run(select!AllTestCaseTbl).map!(a => TestCase(a.name)).copy(rval); 1015 return rval.data; 1016 } 1017 1018 /// Returns: detected test cases. 1019 TestCaseId[] getDetectedTestCaseIds() @trusted { 1020 import std.algorithm : copy; 1021 1022 auto rval = appender!(TestCaseId[])(); 1023 db.run(select!AllTestCaseTbl).map!(a => TestCaseId(a.id)).copy(rval); 1024 return rval.data; 1025 } 1026 1027 /// Returns: test cases that has killed zero mutants. 1028 TestCase[] getTestCasesWithZeroKills() @trusted { 1029 enum sql = format("SELECT t1.name FROM %s t1 WHERE t1.id NOT IN (SELECT tc_id FROM %s)", 1030 allTestCaseTable, killedTestCaseTable); 1031 1032 auto rval = appender!(TestCase[])(); 1033 auto stmt = db.prepare(sql); 1034 foreach (a; stmt.execute) 1035 rval.put(TestCase(a.peek!string(0))); 1036 1037 return rval.data; 1038 } 1039 1040 /** Guarantees that the there are no duplications of `TestCaseId`. 1041 * 1042 * Returns: test cases that has killed at least one mutant. 1043 */ 1044 TestCaseId[] getTestCasesWithAtLeastOneKill(const Mutation.Kind[] kinds) @trusted { 1045 immutable sql = format!"SELECT DISTINCT t1.id 1046 FROM %s t1, %s t2, %s t3 1047 WHERE 1048 t1.id = t2.tc_id AND 1049 t2.st_id == t3.st_id AND 1050 t3.kind IN (%(%s,%))"(allTestCaseTable, 1051 killedTestCaseTable, mutationTable, kinds.map!(a => cast(int) a)); 1052 1053 auto rval = appender!(TestCaseId[])(); 1054 auto stmt = db.prepare(sql); 1055 foreach (a; stmt.execute) 1056 rval.put(TestCaseId(a.peek!long(0))); 1057 1058 return rval.data; 1059 } 1060 1061 /// Returns: the name of the test case. 1062 string getTestCaseName(const TestCaseId id) @trusted { 1063 enum sql = format!"SELECT name FROM %s WHERE id = :id"(allTestCaseTable); 1064 auto stmt = db.prepare(sql); 1065 stmt.bind(":id", cast(long) id); 1066 auto res = stmt.execute; 1067 return res.oneValue!string; 1068 } 1069 1070 /// Returns: the test case id. 1071 Nullable!TestCaseInfo getTestCaseInfo(const TestCase tc, const Mutation.Kind[] kinds) @trusted { 1072 import core.time : dur; 1073 1074 const sql = format("SELECT sum(t2.time),count(t1.st_id) 1075 FROM %s t0, %s t1, %s t2, %s t3 1076 WHERE 1077 t0.name = :name AND 1078 t0.id = t1.tc_id AND 1079 t1.st_id = t2.id AND 1080 t2.id = t3.st_id AND 1081 t3.kind IN (%(%s,%))", allTestCaseTable, 1082 killedTestCaseTable, mutationStatusTable, mutationTable, 1083 kinds.map!(a => cast(int) a)); 1084 auto stmt = db.prepare(sql); 1085 stmt.bind(":name", tc.name); 1086 1087 typeof(return) rval; 1088 foreach (a; stmt.execute) 1089 rval = TestCaseInfo(a.peek!long(0).dur!"msecs", a.peek!long(1)); 1090 return rval; 1091 } 1092 1093 /// Returns: the test case. 1094 Nullable!TestCase getTestCase(const TestCaseId id) @trusted { 1095 enum sql = format!"SELECT name FROM %s WHERE id = :id"(allTestCaseTable); 1096 auto stmt = db.prepare(sql); 1097 stmt.bind(":id", cast(long) id); 1098 1099 typeof(return) rval; 1100 foreach (res; stmt.execute) { 1101 rval = TestCase(res.peek!string(0)); 1102 } 1103 return rval; 1104 } 1105 1106 /// Returns: the test case id. 1107 Nullable!TestCaseId getTestCaseId(const TestCase tc) @trusted { 1108 enum sql = format!"SELECT id FROM %s WHERE name = :name"(allTestCaseTable); 1109 auto stmt = db.prepare(sql); 1110 stmt.bind(":name", tc.name); 1111 1112 typeof(return) rval; 1113 foreach (res; stmt.execute) { 1114 rval = TestCaseId(res.peek!long(0)); 1115 } 1116 return rval; 1117 } 1118 1119 /// The mutation ids are guaranteed to be sorted. 1120 /// Returns: the mutants the test case killed. 1121 MutationId[] getTestCaseMutantKills(const TestCaseId id, const Mutation.Kind[] kinds) @trusted { 1122 immutable sql = format!"SELECT t2.id 1123 FROM %s t1, %s t2 1124 WHERE 1125 t1.tc_id = :tid AND 1126 t1.st_id = t2.st_id AND 1127 t2.kind IN (%(%s,%)) 1128 ORDER BY 1129 t2.id"(killedTestCaseTable, 1130 mutationTable, kinds.map!(a => cast(int) a)); 1131 1132 auto rval = appender!(MutationId[])(); 1133 auto stmt = db.prepare(sql); 1134 stmt.bind(":tid", cast(long) id); 1135 foreach (a; stmt.execute) 1136 rval.put(MutationId(a.peek!long(0))); 1137 1138 return rval.data; 1139 } 1140 1141 /// Returns: test cases that killed the mutant. 1142 TestCase[] getTestCases(const MutationId id) @trusted { 1143 Appender!(TestCase[]) rval; 1144 1145 enum get_test_cases_sql = format!"SELECT t1.name,t2.location 1146 FROM %s t1, %s t2, %s t3 1147 WHERE 1148 t3.id = :id AND 1149 t3.st_id = t2.st_id AND 1150 t2.tc_id = t1.id"(allTestCaseTable, 1151 killedTestCaseTable, mutationTable); 1152 auto stmt = db.prepare(get_test_cases_sql); 1153 stmt.bind(":id", cast(long) id); 1154 foreach (a; stmt.execute) 1155 rval.put(TestCase(a.peek!string(0), a.peek!string(1))); 1156 1157 return rval.data; 1158 } 1159 1160 /** Returns: number of test cases 1161 */ 1162 long getNumOfTestCases() @trusted { 1163 enum num_test_cases_sql = format!"SELECT count(*) FROM %s"(allTestCaseTable); 1164 return db.execute(num_test_cases_sql).oneValue!long; 1165 } 1166 1167 /** Returns: test cases that killed other mutants at the same mutation point as `id`. 1168 */ 1169 TestCase[] getSurroundingTestCases(const MutationId id) @trusted { 1170 Appender!(TestCase[]) rval; 1171 1172 // TODO: optimize this. should be able to merge the two first queries to one. 1173 1174 // get the mutation point ID that id reside at 1175 long mp_id; 1176 { 1177 auto stmt = db.prepare(format!"SELECT mp_id FROM %s WHERE id=:id"(mutationTable)); 1178 stmt.bind(":id", cast(long) id); 1179 auto res = stmt.execute; 1180 if (res.empty) 1181 return null; 1182 mp_id = res.oneValue!long; 1183 } 1184 1185 // get all the mutation status ids at the mutation point 1186 long[] mut_st_ids; 1187 { 1188 auto stmt = db.prepare(format!"SELECT st_id FROM %s WHERE mp_id=:id"(mutationTable)); 1189 stmt.bind(":id", mp_id); 1190 auto res = stmt.execute; 1191 if (res.empty) 1192 return null; 1193 mut_st_ids = res.map!(a => a.peek!long(0)).array; 1194 } 1195 1196 // get all the test cases that are killed at the mutation point 1197 immutable get_test_cases_sql = format!"SELECT t2.name,t1.location FROM %s t1,%s t2 WHERE t1.tc_id == t2.id AND t0.st_id IN (%(%s,%))"( 1198 killedTestCaseTable, allTestCaseTable, mut_st_ids); 1199 auto stmt = db.prepare(get_test_cases_sql); 1200 foreach (a; stmt.execute) { 1201 rval.put(TestCase(a.peek!string(0), a.peek!string(1))); 1202 } 1203 1204 return rval.data; 1205 } 1206 1207 import std.regex : Regex; 1208 1209 void removeTestCase(const Regex!char rex, const(Mutation.Kind)[] kinds) @trusted { 1210 import std.regex : matchFirst; 1211 1212 immutable sql = format!"SELECT t1.id,t1.name FROM %s t1,%s t2, %s t3 WHERE t1.id = t2.tc_id AND t2.st_id = t3.st_id AND t3.kind IN (%(%s,%))"( 1213 allTestCaseTable, killedTestCaseTable, mutationTable, 1214 kinds.map!(a => cast(long) a)); 1215 auto stmt = db.prepare(sql); 1216 1217 auto del_stmt = db.prepare(format!"DELETE FROM %s WHERE id=:id"(allTestCaseTable)); 1218 foreach (row; stmt.execute) { 1219 string tc = row.peek!string(1); 1220 if (tc.matchFirst(rex).empty) 1221 continue; 1222 1223 long id = row.peek!long(0); 1224 del_stmt.reset; 1225 del_stmt.bind(":id", id); 1226 del_stmt.execute; 1227 } 1228 } 1229 } 1230 1231 /** This is only intended to be used when interacting with the SQLite database. 1232 * 1233 * It spins in a loop until the query stop throwing exceptions. 1234 */ 1235 auto spinSqlQuery(alias Callback)() nothrow { 1236 import core.time : dur; 1237 import std.exception : collectException; 1238 import dextool.plugin.mutate.backend.utility : rndSleep; 1239 1240 while (true) { 1241 try { 1242 return Callback(); 1243 } catch (Exception e) { 1244 logger.warning(e.msg).collectException; 1245 // even though the database have a builtin sleep it still result in too much spam. 1246 rndSleep(50.dur!"msecs", 100); 1247 } 1248 } 1249 1250 assert(0, "this should never happen"); 1251 } 1252 1253 SysTime fromSqLiteDateTime(string raw_dt) { 1254 import core.time : dur; 1255 import std.datetime : DateTime, UTC; 1256 import std.format : formattedRead; 1257 1258 int year, month, day, hour, minute, second, msecs; 1259 formattedRead(raw_dt, "%s-%s-%sT%s:%s:%s.%s", year, month, day, hour, minute, second, msecs); 1260 auto dt = DateTime(year, month, day, hour, minute, second); 1261 1262 return SysTime(dt, msecs.dur!"msecs", UTC()); 1263 } 1264 1265 string toSqliteDateTime(SysTime ts) { 1266 import std.format; 1267 1268 return format("%04s-%02s-%02sT%02s:%02s:%02s.%s", ts.year, 1269 cast(ushort) ts.month, ts.day, ts.hour, ts.minute, ts.second, 1270 ts.fracSecs.total!"msecs"); 1271 }