1 /** 2 * Copyright: Copyright Jason White, 2016 3 * License: MIT 4 * Authors: Jason White 5 * 6 * Description: 7 * Stores the persistent state of the build. 8 */ 9 module button.state; 10 11 import button.command; 12 import button.exceptions; 13 import button.resource; 14 import button.task; 15 import button.edge, button.edgedata; 16 import util.sqlite3; 17 18 import std.typecons : tuple, Tuple; 19 20 /** 21 * Table of resource vertices. 22 * 23 * The first entry in this table will always be the build description. 24 */ 25 private immutable resourcesTable = q"{ 26 CREATE TABLE IF NOT EXISTS resource ( 27 id INTEGER NOT NULL, 28 path TEXT NOT NULL, 29 status INTEGER NOT NULL, 30 checksum BLOB NOT NULL, 31 PRIMARY KEY (id), 32 UNIQUE (path) 33 )}"; 34 35 /** 36 * Table of task vertices. 37 */ 38 private immutable tasksTable = q"{ 39 CREATE TABLE IF NOT EXISTS task ( 40 id INTEGER, 41 commands TEXT NOT NULL, 42 workDir TEXT NOT NULL, 43 display TEXT, 44 lastExecuted INTEGER NOT NULL, 45 PRIMARY KEY(id), 46 UNIQUE(commands, workDir) 47 )}"; 48 49 /** 50 * Table of outgoing edges from resources. 51 */ 52 private immutable resourceEdgesTable = q"{ 53 CREATE TABLE IF NOT EXISTS resourceEdge ( 54 id INTEGER PRIMARY KEY, 55 "from" INTEGER NOT NULL REFERENCES resource(id) ON DELETE CASCADE, 56 "to" INTEGER NOT NULL REFERENCES task(id) ON DELETE CASCADE, 57 type INTEGER NOT NULL, 58 UNIQUE("from", "to", type) 59 )}"; 60 61 /** 62 * Table of outgoing edges from tasks. 63 */ 64 private immutable taskEdgesTable = q"{ 65 CREATE TABLE IF NOT EXISTS taskEdge ( 66 id INTEGER PRIMARY KEY, 67 "from" INTEGER NOT NULL REFERENCES task(id) ON DELETE CASCADE, 68 "to" INTEGER NOT NULL REFERENCES resource(id) ON DELETE CASCADE, 69 type INTEGER NOT NULL, 70 UNIQUE ("from", "to", type) 71 )}"; 72 73 /** 74 * Table of pending resources. 75 */ 76 private immutable pendingResourcesTable = q"{ 77 CREATE TABLE IF NOT EXISTS pendingResources ( 78 resid INTEGER NOT NULL REFERENCES resource(id) ON DELETE CASCADE, 79 PRIMARY KEY (resid), 80 UNIQUE (resid) 81 )}"; 82 83 /** 84 * Table of pending tasks. 85 */ 86 private immutable pendingTasksTable = q"{ 87 CREATE TABLE IF NOT EXISTS pendingTasks ( 88 taskid INTEGER NOT NULL REFERENCES task(id) ON DELETE CASCADE, 89 PRIMARY KEY (taskid), 90 UNIQUE (taskid) 91 )}"; 92 93 /** 94 * Index on vertex keys to speed up searches. 95 */ 96 private immutable resourceIndex = q"{ 97 CREATE INDEX IF NOT EXISTS resourceIndex ON resource(path) 98 }"; 99 100 /// Ditto 101 private immutable taskIndex = q"{ 102 CREATE INDEX IF NOT EXISTS taskIndex ON task(commands,workDir) 103 }"; 104 105 /** 106 * Index on edges to speed up finding neighbors. 107 */ 108 private immutable resourceEdgeIndex = q"{ 109 CREATE INDEX IF NOT EXISTS resourceEdgeIndex ON resourceEdge("from","to") 110 }"; 111 112 /// Ditto 113 private immutable taskEdgeIndex = q"{ 114 CREATE INDEX IF NOT EXISTS taskEdgeIndex ON taskEdge("from","to") 115 }"; 116 117 /** 118 * List of SQL statements to run in order to initialize the database. 119 */ 120 private immutable initializeStatements = [ 121 // Create tables 122 resourcesTable, 123 tasksTable, 124 resourceEdgesTable, 125 taskEdgesTable, 126 pendingResourcesTable, 127 pendingTasksTable, 128 129 // Indiees 130 resourceEdgeIndex, 131 taskEdgeIndex, 132 resourceIndex, 133 taskIndex, 134 ]; 135 136 /** 137 * Simple type to leverage the type system to help to differentiate between 138 * storage indices. 139 */ 140 struct Index(T) 141 { 142 ulong index; 143 alias index this; 144 145 /// An invalid index. 146 enum Invalid = Index!T(0); 147 } 148 149 /** 150 * Convenience type for an edge composed of two indices. 151 */ 152 alias Index(A, B) = Edge!(Index!A, Index!B); 153 154 /** 155 * Convenience type for an index of the edge itself. 156 */ 157 alias EdgeIndex(A, B) = Index!(Edge!(A, B)); 158 159 /** 160 * An edge row in the database. 161 */ 162 alias EdgeRow(A, B, Data=EdgeType) = Edge!(Index!A, Index!B, Data); 163 164 /** 165 * A vertex paired with some data. This is useful for representing a neighbor. 166 */ 167 struct Neighbor(Vertex, Data) 168 { 169 Vertex vertex; 170 Data data; 171 } 172 173 /** 174 * Convenience templates to get the other type of vertex from the given vertex. 175 */ 176 alias Other(A : Resource) = Task; 177 alias Other(A : Task) = Resource; /// Ditto 178 179 /** 180 * Convenience template to construct an edge from the starting vertex. 181 */ 182 alias NeighborIndex(V : Index!V) = EdgeIndex!(V, Other!V); 183 184 /** 185 * Deserializes a vertex from a SQLite statement. This assumes that the 186 * statement has every column of the vertex except the row ID. 187 */ 188 Vertex parse(Vertex : Resource)(SQLite3.Statement s) 189 { 190 return Resource( 191 s.get!string(0), // Path 192 cast(Resource.Status)s.get!long(1), // Status 193 cast(ubyte[])s.get!(void[])(2) // Checksum 194 ); 195 } 196 197 /// Ditto 198 Vertex parse(Vertex : Task)(SQLite3.Statement s) 199 { 200 import std.conv : to; 201 import std.datetime : SysTime; 202 import std.algorithm.iteration : map; 203 import std.array : array; 204 import std.exception : assumeUnique; 205 206 return Task( 207 s.get!string(0) 208 .to!(string[][]) 209 .map!(x => Command(x.assumeUnique)) 210 .array 211 .assumeUnique, 212 s.get!string(1), 213 s.get!string(2), 214 SysTime(s.get!long(3)), 215 ); 216 } 217 218 /** 219 * Deserializes an edge from a SQLite statement. This assumes that the 220 * statement has every column of the vertex except the row ID. 221 */ 222 E parse(E : EdgeRow!(Resource, Task, EdgeType))(SQLite3.Statement s) 223 { 224 return E( 225 Index!Resource(s.get!ulong(0)), 226 Index!Task(s.get!ulong(1)), 227 cast(EdgeType)s.get!int(2) 228 ); 229 } 230 231 /// Ditto 232 E parse(E : EdgeRow!(Task, Resource, EdgeType))(SQLite3.Statement s) 233 { 234 return E( 235 Index!Task(s.get!ulong(0)), 236 Index!Resource(s.get!ulong(1)), 237 cast(EdgeType)s.get!int(2) 238 ); 239 } 240 241 /// Ditto 242 E parse(E : EdgeRow!(Resource, Task, EdgeIndex!(Resource, Task)))(SQLite3.Statement s) 243 { 244 return E( 245 Index!Resource(s.get!ulong(0)), 246 Index!Task(s.get!ulong(1)), 247 cast(EdgeIndex!(Resource, Task))s.get!int(2) 248 ); 249 } 250 251 /// Ditto 252 E parse(E : EdgeRow!(Task, Resource, EdgeIndex!(Task, Resource)))(SQLite3.Statement s) 253 { 254 return E( 255 Index!Task(s.get!ulong(0)), 256 Index!Resource(s.get!ulong(1)), 257 cast(EdgeIndex!(Task, Resource))s.get!int(2) 258 ); 259 } 260 261 /** 262 * Parses an edge without the associated data. 263 */ 264 E parse(E : Index!(Resource, Task))(SQLite3.Statement s) 265 { 266 return E(Index!Resource(s.get!ulong(0)), Index!Task(s.get!ulong(1))); 267 } 268 269 /// Ditto 270 E parse(E : Index!(Task, Resource))(SQLite3.Statement s) 271 { 272 return E(Index!Task(s.get!ulong(0)), Index!Resource(s.get!ulong(1))); 273 } 274 275 /** 276 * Deserializes edge data. 277 */ 278 E parse(E : EdgeType)(SQLite3.Statement s) 279 { 280 return cast(EdgeType)s.get!int(0); 281 } 282 283 /** 284 * Deserializes a neighbor. 285 */ 286 E parse(E : Neighbor!(Index!Resource, EdgeType))(SQLite3.Statement s) 287 { 288 return E( 289 Index!Resource(s.get!ulong(0)), 290 cast(EdgeType)s.get!int(1) 291 ); 292 } 293 294 /// Ditto 295 E parse(E : Neighbor!(Index!Task, EdgeType))(SQLite3.Statement s) 296 { 297 return E( 298 Index!Task(s.get!ulong(0)), 299 cast(EdgeType)s.get!int(1) 300 ); 301 } 302 303 /// Ditto 304 E parse(E : Neighbor!(Index!Resource, EdgeIndex!(Task, Resource)))(SQLite3.Statement s) 305 { 306 return E( 307 Index!Resource(s.get!ulong(0)), 308 EdgeIndex!(Task, Resource)(s.get!ulong(1)) 309 ); 310 } 311 312 /// Ditto 313 E parse(E : Neighbor!(Index!Task, EdgeIndex!(Resource, Task)))(SQLite3.Statement s) 314 { 315 return E( 316 Index!Task(s.get!ulong(0)), 317 EdgeIndex!(Resource, Task)(s.get!ulong(1)) 318 ); 319 } 320 321 /** 322 * Parses a vertex key. 323 */ 324 E parse(E : ResourceKey)(SQLite3.Statement s) 325 { 326 return E( 327 s.get!string(0) 328 ); 329 } 330 331 /// Ditto 332 E parse(E : TaskKey)(SQLite3.Statement s) 333 { 334 import std.conv : to; 335 import std.algorith.iteration : map; 336 import std.array : array; 337 import std.exception : assumeUnique; 338 339 return E( 340 s.get!string(0) 341 .to!(string[][]) 342 .map!(x => Command(x.assumeUnique)) 343 .array 344 .assumeUnique, 345 s.get!string(1) 346 ); 347 } 348 349 /** 350 * Stores the current state of the build. 351 */ 352 class BuildState : SQLite3 353 { 354 // The build description is always the first entry in the database. 355 static immutable buildDescId = Index!Resource(1); 356 357 private 358 { 359 Statement sqlInsertResource; 360 Statement sqlInsertTask; 361 362 Statement sqlAddResource; 363 Statement sqlAddTask; 364 365 Statement sqlRemoveResourceByIndex; 366 Statement sqlRemoveTaskByIndex; 367 368 Statement sqlRemoveResourceByKey; 369 Statement sqlRemoveTaskByKey; 370 371 Statement sqlFindResourceByKey; 372 Statement sqlFindTaskByKey; 373 374 Statement sqlResourceByIndex; 375 Statement sqlTaskByIndex; 376 377 Statement sqlResourceByKey; 378 Statement sqlTaskByKey; 379 380 Statement sqlUpdateResource; 381 Statement sqlUpdateTask; 382 383 Statement sqlInsertTaskEdge; 384 Statement sqlInsertResourceEdge; 385 Statement sqlRemoveTaskEdge; 386 Statement sqlRemoveResourceEdge; 387 388 Statement sqlResourceDegreeIn; 389 Statement sqlTaskDegreeIn; 390 Statement sqlResourceDegreeOut; 391 Statement sqlTaskDegreeOut; 392 393 Statement sqlResourceEdgeExists; 394 Statement sqlTaskEdgeExists; 395 396 Statement sqlAddPendingResource; 397 Statement sqlAddPendingTask; 398 Statement sqlRemovePendingResource; 399 Statement sqlRemovePendingTask; 400 Statement sqlIsPendingResource; 401 Statement sqlIsPendingTask; 402 } 403 404 /** 405 * Open or create the build state file. 406 */ 407 this(string fileName = ":memory:") 408 { 409 super(fileName); 410 411 execute("PRAGMA foreign_keys = ON"); 412 413 initialize(); 414 } 415 416 /** 417 * Creates the tables if they don't already exist. 418 */ 419 private void initialize() 420 { 421 begin(); 422 scope (success) commit(); 423 scope (failure) rollback(); 424 425 foreach (statement; initializeStatements) 426 execute(statement); 427 428 // Add the build description resource if it doesn't already exist. 429 execute( 430 `INSERT OR IGNORE INTO resource` ~ 431 ` (id,path,status,checksum)` ~ 432 ` VALUES (?,?,?,?)` 433 , buildDescId, "", 0, 0 434 ); 435 436 // Prepare SQL statements. This is much faster than preparing + 437 // executing statements for every query. For queries that only run once, 438 // it is not necessary to prepare them here. 439 sqlInsertResource = new Statement( 440 `INSERT INTO resource(path, status, checksum)` ~ 441 ` VALUES(?, ?, ?)` 442 ); 443 sqlInsertTask = new Statement( 444 `INSERT INTO task (commands, workDir, display, lastExecuted)` ~ 445 ` VALUES(?, ?, ?, ?)` 446 ); 447 sqlAddResource = new Statement( 448 `INSERT OR IGNORE INTO resource` ~ 449 ` (path, status, checksum)` ~ 450 ` VALUES(?, ?, ?)` 451 ); 452 sqlAddTask = new Statement( 453 `INSERT OR IGNORE INTO task` ~ 454 ` (commands, workDir, display, lastExecuted)` ~ 455 ` VALUES(?, ?, ?, ?)` 456 ); 457 sqlRemoveResourceByIndex = new Statement( 458 `DELETE FROM resource WHERE id=?` 459 ); 460 sqlRemoveTaskByIndex = new Statement( 461 `DELETE FROM task WHERE id=?` 462 ); 463 sqlRemoveResourceByKey = new Statement( 464 `DELETE FROM resource WHERE path=?` 465 ); 466 sqlRemoveTaskByKey = new Statement( 467 `DELETE FROM task WHERE commands=? AND workDir=?` 468 ); 469 sqlFindResourceByKey = new Statement( 470 `SELECT id FROM resource WHERE path=?` 471 ); 472 sqlFindTaskByKey = new Statement( 473 `SELECT id FROM task WHERE commands=? AND workDir=?` 474 ); 475 sqlResourceByIndex = new Statement( 476 `SELECT path,status,checksum` ~ 477 ` FROM resource WHERE id=?` 478 ); 479 sqlTaskByIndex = new Statement( 480 `SELECT commands,workDir,display,lastExecuted` ~ 481 ` FROM task WHERE id=?` 482 ); 483 sqlResourceByKey = new Statement( 484 `SELECT path,status,checksum` ~ 485 ` FROM resource WHERE path=?` 486 ); 487 sqlTaskByKey = new Statement( 488 `SELECT commands,workDir,display,lastExecuted FROM task` ~ 489 ` WHERE commands=? AND workDir=?` 490 ); 491 sqlUpdateResource = new Statement( 492 `UPDATE resource` ~ 493 ` SET path=?,status=?,checksum=?` ~ 494 ` WHERE id=?` 495 ); 496 sqlUpdateTask = new Statement( 497 `UPDATE task` ~ 498 ` SET commands=?,workDir=?,display=?,lastExecuted=?` ~ 499 ` WHERE id=?` 500 ); 501 sqlInsertTaskEdge = new Statement( 502 `INSERT INTO taskEdge("from", "to", type) VALUES(?, ?, ?)` 503 ); 504 sqlInsertResourceEdge = new Statement( 505 `INSERT INTO resourceEdge("from", "to", type)` ~ 506 ` VALUES(?, ?, ?)` 507 ); 508 sqlRemoveTaskEdge = new Statement( 509 `DELETE FROM taskEdge WHERE "from"=? AND "to"=? AND type=?` 510 ); 511 sqlRemoveResourceEdge = new Statement( 512 `DELETE FROM resourceEdge WHERE "from"=? AND "to"=? AND type=?` 513 ); 514 sqlResourceDegreeIn = new Statement( 515 `SELECT COUNT("to") FROM taskEdge WHERE "to"=?` 516 ); 517 sqlTaskDegreeIn = new Statement( 518 `SELECT COUNT("to") FROM resourceEdge WHERE "to"=?` 519 ); 520 sqlResourceDegreeOut = new Statement( 521 `SELECT COUNT("to") FROM resourceEdge WHERE "from"=?` 522 ); 523 sqlTaskDegreeOut = new Statement( 524 `SELECT COUNT("to") FROM taskEdge WHERE "from"=?` 525 ); 526 sqlResourceEdgeExists = new Statement( 527 `SELECT "type" FROM resourceEdge` ~ 528 ` WHERE "from"=? AND "to"=? AND type=?` 529 ); 530 sqlTaskEdgeExists = new Statement( 531 `SELECT "type" FROM taskEdge WHERE` ~ 532 ` "from"=? AND "to"=? AND type=?` 533 ); 534 sqlAddPendingResource = new Statement( 535 `INSERT OR IGNORE INTO pendingResources(resid) VALUES(?)` 536 ); 537 sqlAddPendingTask = new Statement( 538 `INSERT OR IGNORE INTO pendingTasks(taskid) VALUES(?)` 539 ); 540 sqlRemovePendingResource = new Statement( 541 `DELETE FROM pendingResources WHERE resid=?` 542 ); 543 sqlRemovePendingTask = new Statement( 544 `DELETE FROM pendingTasks WHERE taskid=?` 545 ); 546 sqlIsPendingResource = new Statement( 547 `SELECT EXISTS(` ~ 548 ` SELECT 1 FROM pendingResources WHERE resid=? LIMIT 1` ~ 549 `)` 550 ); 551 sqlIsPendingTask = new Statement( 552 `SELECT EXISTS(` ~ 553 ` SELECT 1 FROM pendingTasks WHERE taskid=? LIMIT 1`~ 554 `)` 555 ); 556 } 557 558 /** 559 * Returns the number of vertices in the database. 560 */ 561 ulong length(Vertex : Resource)() 562 { 563 import std.exception : enforce; 564 auto s = prepare(`SELECT COUNT(*) FROM resource WHERE id > 1`); 565 enforce(s.step(), "Failed to find number of resources"); 566 return s.get!ulong(0); 567 } 568 569 /// Dito 570 ulong length(Vertex : Task)() 571 { 572 import std.exception : enforce; 573 auto s = prepare(`SELECT COUNT(*) FROM task`); 574 enforce(s.step(), "Failed to find number of tasks"); 575 return s.get!ulong(0); 576 } 577 578 /** 579 * Inserts a vertex into the database. An exception is thrown if the vertex 580 * already exists. Otherwise, the vertex's ID is returned. 581 */ 582 Index!Resource put(in Resource resource) 583 { 584 alias s = sqlInsertResource; 585 586 synchronized (s) 587 { 588 scope (exit) s.reset(); 589 590 s.bind(resource.path, 591 resource.status, 592 resource.checksum); 593 s.step(); 594 } 595 596 return Index!Resource(lastInsertId); 597 } 598 599 /// Ditto 600 Index!Task put(in Task task) 601 { 602 import std.conv : to; 603 604 alias s = sqlInsertTask; 605 606 synchronized (s) 607 { 608 scope (exit) s.reset(); 609 610 s.bind(task.commands.to!string(), 611 task.workingDirectory, 612 task.display, 613 task.lastExecuted.stdTime); 614 s.step(); 615 } 616 617 return Index!Task(lastInsertId); 618 } 619 620 unittest 621 { 622 auto state = new BuildState; 623 624 { 625 immutable vertex = Resource("foo.c", Resource.Status.file); 626 627 auto id = state.put(vertex); 628 assert(state[id] == vertex); 629 } 630 631 { 632 immutable vertex = Task([Command(["foo", "test", "test test"])]); 633 634 immutable id = state.put(vertex); 635 assert(state[id] == vertex); 636 } 637 } 638 639 /** 640 * Inserts a vertex into the database unless it already exists. 641 */ 642 void add(in Resource resource) 643 { 644 alias s = sqlAddResource; 645 646 synchronized (s) 647 { 648 scope (exit) s.reset(); 649 650 s.bind(resource.path, 651 resource.status, 652 resource.checksum); 653 s.step(); 654 } 655 } 656 657 // Ditto 658 void add(in Task task) 659 { 660 import std.conv : to; 661 662 alias s = sqlAddTask; 663 664 synchronized (s) 665 { 666 scope (exit) s.reset(); 667 668 s.bind(task.commands.to!string(), 669 task.workingDirectory, 670 task.display, 671 task.lastExecuted.stdTime); 672 s.step(); 673 } 674 } 675 676 /** 677 * Removes a vertex by the given index. If the vertex does not exist, an 678 * exception is thrown. 679 */ 680 void remove(Index!Resource index) 681 { 682 alias s = sqlRemoveResourceByIndex; 683 684 synchronized (s) 685 { 686 scope (exit) s.reset(); 687 s.bind(index); 688 s.step(); 689 } 690 } 691 692 /// Ditto 693 void remove(Index!Task index) 694 { 695 alias s = sqlRemoveTaskByIndex; 696 697 synchronized (s) 698 { 699 scope (exit) s.reset(); 700 s.bind(index); 701 s.step(); 702 } 703 } 704 705 /// Ditto 706 void remove(ResourceId path) 707 { 708 alias s = sqlRemoveResourceByKey; 709 710 synchronized (s) 711 { 712 scope (exit) s.reset(); 713 s.bind(path); 714 s.step(); 715 } 716 } 717 718 /// Ditto 719 void remove(TaskKey key) 720 { 721 import std.conv : to; 722 723 alias s = sqlRemoveTaskByKey; 724 725 synchronized (s) 726 { 727 scope (exit) s.reset(); 728 s.bind(key.commands.to!string, key.workingDirectory); 729 s.step(); 730 } 731 } 732 733 /** 734 * Returns the index of the given vertex. 735 */ 736 Index!Resource find(const(char)[] key) 737 { 738 alias s = sqlFindResourceByKey; 739 740 synchronized (s) 741 { 742 scope (exit) s.reset(); 743 744 s.bind(key); 745 746 if (s.step()) 747 return typeof(return)(s.get!ulong(0)); 748 } 749 750 return typeof(return).Invalid; 751 } 752 753 /// Ditto 754 Index!Task find(TaskKey id) 755 { 756 import std.conv : to; 757 758 alias s = sqlFindTaskByKey; 759 760 synchronized (s) 761 { 762 scope (exit) s.reset(); 763 764 s.bind(id.commands.to!string, id.workingDirectory); 765 766 if (s.step()) 767 return typeof(return)(s.get!ulong(0)); 768 } 769 770 return typeof(return).Invalid; 771 } 772 773 /** 774 * Returns the vertex state at the given index. 775 */ 776 Resource opIndex(Index!Resource index) 777 { 778 import std.exception : enforce; 779 780 alias s = sqlResourceByIndex; 781 782 synchronized (s) 783 { 784 scope (exit) s.reset(); 785 s.bind(index); 786 enforce(s.step(), "Vertex does not exist."); 787 return s.parse!Resource(); 788 } 789 } 790 791 /// Ditto 792 Task opIndex(Index!Task index) 793 { 794 import std.exception : enforce; 795 796 alias s = sqlTaskByIndex; 797 798 synchronized (s) 799 { 800 scope (exit) s.reset(); 801 s.bind(index); 802 enforce(s.step(), "Vertex does not exist."); 803 return s.parse!Task(); 804 } 805 } 806 807 /** 808 * Returns the vertex state for the given vertex name. Throws an exception if 809 * the vertex does not exist. 810 * 811 * TODO: Only return the vertex's value. 812 */ 813 Resource opIndex(ResourceId path) 814 { 815 import std.exception : enforce; 816 817 alias s = sqlResourceByKey; 818 819 synchronized (s) 820 { 821 scope (exit) s.reset(); 822 s.bind(path); 823 enforce(s.step(), "Vertex does not exist."); 824 return s.parse!Resource(); 825 } 826 } 827 828 /// Ditto 829 Task opIndex(TaskKey key) 830 { 831 import std.exception : enforce; 832 import std.conv : to; 833 834 alias s = sqlTaskByKey; 835 836 synchronized (s) 837 { 838 scope (exit) s.reset(); 839 s.bind(key.commands.to!string, key.workingDirectory); 840 enforce(s.step(), "Vertex does not exist."); 841 return s.parse!Task(); 842 } 843 } 844 845 /** 846 * Changes the state of the vertex at the given index. Throws an exception if 847 * the vertex does not exist. 848 */ 849 void opIndexAssign(in Resource v, Index!Resource index) 850 { 851 alias s = sqlUpdateResource; 852 853 synchronized (s) 854 { 855 scope (exit) s.reset(); 856 s.bind(v.path, v.status, v.checksum, index); 857 s.step(); 858 } 859 } 860 861 /// Ditto 862 void opIndexAssign(in Task v, Index!Task index) 863 { 864 import std.conv : to; 865 866 alias s = sqlUpdateTask; 867 868 synchronized (s) 869 { 870 scope (exit) s.reset(); 871 s.bind(v.commands.to!string, 872 v.workingDirectory, 873 v.display, 874 v.lastExecuted.stdTime, index); 875 s.step(); 876 } 877 } 878 879 /** 880 * Returns an input range that iterates over all resources. The order is 881 * guaranteed to be the same as the order they were inserted in. 882 */ 883 @property auto enumerate(T : Resource)() 884 { 885 return prepare( 886 `SELECT path,status,checksum FROM resource WHERE id>1` 887 ).rows!(parse!T); 888 } 889 890 unittest 891 { 892 import std.algorithm : equal; 893 894 auto state = new BuildState; 895 896 immutable vertices = [ 897 Resource("foo.o", Resource.Status.unknown), 898 Resource("foo.c", Resource.Status.file), 899 Resource("bar.c", Resource.Status.missing), 900 Resource("bar.o", Resource.Status.missing), 901 ]; 902 903 foreach (vertex; vertices) 904 state.put(vertex); 905 906 assert(equal(vertices, state.enumerate!Resource)); 907 } 908 909 /** 910 * Returns an input range that iterates over all tasks. The order is 911 * guaranteed to be the same as the order they were inserted in. 912 */ 913 @property auto enumerate(T : Task)() 914 { 915 return prepare(`SELECT commands,workDir,display,lastExecuted FROM task`) 916 .rows!(parse!T); 917 } 918 919 unittest 920 { 921 import std.algorithm : equal, sort; 922 923 auto state = new BuildState; 924 925 immutable tasks = [ 926 Task([Command(["foo", "arg 1", "arg 2"])]), 927 Task([Command(["bar", "arg 1"])]), 928 Task([Command(["baz", "arg 1", "arg 2", "arg 3"])]), 929 ]; 930 931 foreach (task; tasks) 932 state.put(task); 933 934 assert(equal(tasks, state.enumerate!Task)); 935 } 936 937 /** 938 * Returns a range of vertex keys. The returned range is not guaranteed to 939 * be sorted. 940 */ 941 @property auto enumerate(T : ResourceKey)() 942 { 943 return prepare(`SELECT path FROM resource WHERE id>1`) 944 .rows!(parse!T); 945 } 946 947 /// Ditto 948 @property auto enumerate(T : TaskKey)() 949 { 950 return prepare(`SELECT commands,workDir FROM task`) 951 .rows!(parse!TaskKey); 952 } 953 954 /** 955 * Returns a range of row indices. 956 */ 957 @property auto enumerate(T : Index!Resource)() 958 { 959 return prepare(`SELECT id FROM resource WHERE id>1`) 960 .rows!((SQLite3.Statement s) => T(s.get!ulong(0))); 961 } 962 963 /// Ditto 964 @property auto enumerate(T : Index!Task)() 965 { 966 return prepare(`SELECT id FROM task`) 967 .rows!((SQLite3.Statement s) => T(s.get!ulong(0))); 968 } 969 970 /** 971 * Adds an edge. Throws an exception if the edge already exists. Returns the 972 * index of the edge. 973 */ 974 void put(Index!Task from, Index!Resource to, EdgeType type) 975 { 976 alias s = sqlInsertTaskEdge; 977 978 synchronized (s) 979 { 980 scope (exit) s.reset(); 981 s.bind(from, to, type); 982 s.step(); 983 } 984 } 985 986 /// Ditto 987 void put(Index!Resource from, Index!Task to, EdgeType type) 988 { 989 alias s = sqlInsertResourceEdge; 990 991 synchronized (s) 992 { 993 scope (exit) s.reset(); 994 s.bind(from, to, type); 995 s.step(); 996 } 997 } 998 999 /// Ditto 1000 void put(ResourceId a, TaskKey b, EdgeType type) 1001 { 1002 put(find(a), find(b), type); 1003 } 1004 1005 /// Ditto 1006 void put(TaskKey a, ResourceId b, EdgeType type) 1007 { 1008 put(find(a), find(b), type); 1009 } 1010 1011 /** 1012 * Removes an edge. Throws an exception if the edge does not exist. 1013 */ 1014 void remove(Index!Resource from, Index!Task to, EdgeType type) 1015 { 1016 alias s = sqlRemoveResourceEdge; 1017 1018 synchronized (s) 1019 { 1020 scope (exit) s.reset(); 1021 s.bind(from, to, type); 1022 s.step(); 1023 } 1024 } 1025 1026 /// Ditto 1027 void remove(Index!Task from, Index!Resource to, EdgeType type) 1028 { 1029 alias s = sqlRemoveTaskEdge; 1030 1031 synchronized (s) 1032 { 1033 scope (exit) s.reset(); 1034 s.bind(from, to, type); 1035 s.step(); 1036 } 1037 } 1038 1039 /// Ditto 1040 void remove(TaskKey from, ResourceId to, EdgeType type) 1041 { 1042 remove(find(from), find(to), type); 1043 } 1044 1045 /// Ditto 1046 void remove(ResourceId from, TaskKey to, EdgeType type) 1047 { 1048 remove(find(from), find(to), type); 1049 } 1050 1051 unittest 1052 { 1053 auto state = new BuildState; 1054 1055 immutable resId = state.put(Resource("foo.c")); 1056 immutable taskId = state.put(Task([Command(["gcc", "foo.c"])])); 1057 state.remove(resId); 1058 state.remove(taskId); 1059 } 1060 1061 /** 1062 * Returns the number of incoming edges to the given vertex. 1063 */ 1064 size_t degreeIn(Index!Resource index) 1065 { 1066 import std.exception : enforce; 1067 1068 alias s = sqlResourceDegreeIn; 1069 1070 synchronized (s) 1071 { 1072 scope (exit) s.reset(); 1073 s.bind(index); 1074 enforce(s.step(), "Failed to count incoming edges to resource"); 1075 return s.get!(typeof(return))(0); 1076 } 1077 } 1078 1079 /// Ditto 1080 size_t degreeIn(Index!Task index) 1081 { 1082 import std.exception : enforce; 1083 1084 alias s = sqlTaskDegreeIn; 1085 1086 synchronized (s) 1087 { 1088 scope (exit) s.reset(); 1089 s.bind(index); 1090 enforce(s.step(), "Failed to count incoming edges to resource"); 1091 return s.get!(typeof(return))(0); 1092 } 1093 } 1094 1095 /// Ditto 1096 size_t degreeOut(Index!Resource index) 1097 { 1098 import std.exception : enforce; 1099 1100 alias s = sqlResourceDegreeOut; 1101 1102 synchronized (s) 1103 { 1104 scope (exit) s.reset(); 1105 s.bind(index); 1106 enforce(s.step(), "Failed to count outgoing edges from resource"); 1107 return s.get!(typeof(return))(0); 1108 } 1109 } 1110 1111 /// Ditto 1112 size_t degreeOut(Index!Task index) 1113 { 1114 import std.exception : enforce; 1115 1116 alias s = sqlTaskDegreeOut; 1117 1118 synchronized (s) 1119 { 1120 scope (exit) s.reset(); 1121 s.bind(index); 1122 enforce(s.step(), "Failed to count outgoing edges from task"); 1123 return s.get!(typeof(return))(0); 1124 } 1125 } 1126 1127 unittest 1128 { 1129 auto state = new BuildState(); 1130 1131 auto resources = [ 1132 state.put(Resource("foo")), 1133 state.put(Resource("bar")), 1134 ]; 1135 1136 auto tasks = [ 1137 state.put(Task([Command(["test"])])), 1138 state.put(Task([Command(["foobar", "foo", "bar"])])), 1139 ]; 1140 1141 state.put(tasks[0], resources[0], EdgeType.explicit); 1142 state.put(tasks[0], resources[1], EdgeType.explicit); 1143 1144 state.put(resources[0], tasks[1], EdgeType.explicit); 1145 state.put(resources[1], tasks[1], EdgeType.explicit); 1146 1147 assert(state.degreeIn(tasks[0]) == 0); 1148 assert(state.degreeIn(tasks[1]) == 2); 1149 assert(state.degreeIn(resources[0]) == 1); 1150 assert(state.degreeIn(resources[1]) == 1); 1151 1152 assert(state.degreeOut(tasks[0]) == 2); 1153 assert(state.degreeOut(tasks[1]) == 0); 1154 assert(state.degreeOut(resources[0]) == 1); 1155 assert(state.degreeOut(resources[1]) == 1); 1156 } 1157 1158 /** 1159 * Lists all outgoing task edges. 1160 */ 1161 @property auto edges(From : Task, To : Resource, Data : EdgeType)() 1162 { 1163 return prepare(`SELECT "from","to","type" FROM taskEdge`) 1164 .rows!(parse!(EdgeRow!(From, To, Data))); 1165 } 1166 1167 /// Ditto 1168 @property auto edges(From : Task, To : Resource, Data : EdgeType) 1169 (EdgeType type) 1170 { 1171 return prepare(`SELECT "from","to","type" FROM taskEdge WHERE type=?`, 1172 type) 1173 .rows!(parse!(EdgeRow!(From, To, Data))); 1174 } 1175 1176 /** 1177 * Checks if an edge exists between two vertices. 1178 */ 1179 bool edgeExists(Index!Task from, Index!Resource to, EdgeType type) 1180 { 1181 alias s = sqlTaskEdgeExists; 1182 1183 synchronized (s) 1184 { 1185 scope (exit) s.reset(); 1186 s.bind(from, to, type); 1187 return s.step(); 1188 } 1189 } 1190 1191 /// Ditto 1192 bool edgeExists(Index!Resource from, Index!Task to, EdgeType type) 1193 { 1194 alias s = sqlResourceEdgeExists; 1195 1196 synchronized (s) 1197 { 1198 scope (exit) s.reset(); 1199 s.bind(from, to, type); 1200 return s.step(); 1201 } 1202 } 1203 1204 /** 1205 * Lists all outgoing resource edges. 1206 */ 1207 @property auto edges(From : Resource, To : Task, Data : EdgeType)() 1208 { 1209 return prepare(`SELECT "from","to","type" FROM resourceEdge`) 1210 .rows!(parse!(EdgeRow!(From, To, Data))); 1211 } 1212 1213 /// Ditto 1214 @property auto edges(From : Resource, To : Task, Data : EdgeType) 1215 (EdgeType type) 1216 { 1217 return prepare( 1218 `SELECT "from","to","type" FROM resourceEdge WHERE type=?`, 1219 type) 1220 .rows!(parse!(EdgeRow!(From, To, Data))); 1221 } 1222 1223 /** 1224 * Returns the outgoing neighbors of the given node. 1225 */ 1226 @property auto outgoing(Index!Resource v) 1227 { 1228 return prepare(`SELECT "to" FROM resourceEdge WHERE "from"=?`, v) 1229 .rows!((SQLite3.Statement s) => Index!Task(s.get!ulong(0))); 1230 } 1231 1232 /// Ditto 1233 @property auto outgoing(Index!Task v) 1234 { 1235 return prepare(`SELECT "to" FROM taskEdge WHERE "from"=?`, v) 1236 .rows!((SQLite3.Statement s) => Index!Resource(s.get!ulong(0))); 1237 } 1238 1239 /// Ditto 1240 @property auto outgoing(Data : EdgeType)(Index!Resource v) 1241 { 1242 return prepare(`SELECT "to",type FROM resourceEdge WHERE "from"=?`, v) 1243 .rows!(parse!(Neighbor!(Index!Task, Data))); 1244 } 1245 1246 /// Ditto 1247 @property auto outgoing(Data : EdgeType)(Index!Task v) 1248 { 1249 return prepare(`SELECT "to",type FROM taskEdge WHERE "from"=?`, v) 1250 .rows!(parse!(Neighbor!(Index!Resource, Data))); 1251 } 1252 1253 /// Ditto 1254 @property auto outgoing(Data : EdgeIndex!(Resource, Task))(Index!Resource v) 1255 { 1256 return prepare(`SELECT "to",id FROM resourceEdge WHERE "from"=?`, v) 1257 .rows!(parse!(Neighbor!(Index!Task, Data))); 1258 } 1259 1260 /// Ditto 1261 @property auto outgoing(Data : EdgeIndex!(Task, Resource))(Index!Task v) 1262 { 1263 return prepare(`SELECT "to",id FROM taskEdge WHERE "from"=?`, v) 1264 .rows!(parse!(Neighbor!(Index!Resource, Data))); 1265 } 1266 1267 /// Ditto 1268 @property auto outgoing(Data : ResourceId)(Index!Task v) 1269 { 1270 return prepare( 1271 `SELECT resource.path` ~ 1272 ` FROM taskEdge AS e` ~ 1273 ` JOIN resource ON e."to"=resource.id` ~ 1274 ` WHERE e."from"=?`, v 1275 ) 1276 .rows!((SQLite3.Statement s) => s.get!string(0)); 1277 } 1278 1279 /// Ditto 1280 @property auto outgoing(Data : Resource)(Index!Task v, EdgeType type) 1281 { 1282 return prepare( 1283 `SELECT resource.path, resource.status, resource.checksum` ~ 1284 ` FROM taskEdge AS e` ~ 1285 ` JOIN resource ON e."to"=resource.id` ~ 1286 ` WHERE e."from"=? AND type=?`, v, type 1287 ) 1288 .rows!(parse!Resource); 1289 } 1290 1291 /** 1292 * Returns the incoming neighbors of the given node. 1293 */ 1294 @property auto incoming(Data : EdgeType)(Index!Resource v) 1295 { 1296 return prepare(`SELECT "from",type FROM taskEdge WHERE "to"=?`, v) 1297 .rows!(parse!(Neighbor!(Index!Task, Data))); 1298 } 1299 1300 /// Ditto 1301 @property auto incoming(Data : EdgeType)(Index!Task v) 1302 { 1303 return prepare(`SELECT "from",type FROM resourceEdge WHERE "to"=?`, v) 1304 .rows!(parse!(Neighbor!(Index!Resource, Data))); 1305 } 1306 1307 /// Ditto 1308 @property auto incoming(Data : EdgeIndex!(Resource, Task))(Index!Resource v) 1309 { 1310 return prepare(`SELECT "from",id FROM taskEdge WHERE "to"=?`, v) 1311 .rows!(parse!(Neighbor!(Index!Task, Data))); 1312 } 1313 1314 /// Ditto 1315 @property auto incoming(Data : EdgeIndex!(Task, Resource))(Index!Task v) 1316 { 1317 return prepare(`SELECT "from",id FROM resourceEdge WHERE "to"=?`, v) 1318 .rows!(parse!(Neighbor!(Index!Resource, Data))); 1319 } 1320 1321 /// Ditto 1322 @property auto incoming(Data : ResourceId)(Index!Task v) 1323 { 1324 return prepare( 1325 `SELECT resource.path` ~ 1326 ` FROM resourceEdge AS e` ~ 1327 ` JOIN resource ON e."from"=resource.id` ~ 1328 ` WHERE e."to"=?`, v 1329 ) 1330 .rows!((SQLite3.Statement s) => s.get!string(0)); 1331 } 1332 1333 /// Ditto 1334 @property auto incoming(Data : Resource)(Index!Task v, EdgeType type) 1335 { 1336 return prepare( 1337 `SELECT resource.path, resource.status, resource.checksum` ~ 1338 ` FROM resourceEdge AS e` ~ 1339 ` JOIN resource ON e."from"=resource.id` ~ 1340 ` WHERE e."to"=? AND type=?`, v, type 1341 ) 1342 .rows!(parse!Resource); 1343 } 1344 1345 /** 1346 * Adds a vertex to the list of pending vertices. If the vertex is already 1347 * pending, nothing is done. 1348 */ 1349 void addPending(Index!Resource v) 1350 { 1351 alias s = sqlAddPendingResource; 1352 1353 synchronized (s) 1354 { 1355 scope (exit) s.reset(); 1356 s.bind(v); 1357 s.step(); 1358 } 1359 } 1360 1361 /// Ditto 1362 void addPending(Index!Task v) 1363 { 1364 alias s = sqlAddPendingTask; 1365 1366 synchronized (s) 1367 { 1368 scope (exit) s.reset(); 1369 s.bind(v); 1370 s.step(); 1371 } 1372 } 1373 1374 /** 1375 * Removes a pending vertex. 1376 */ 1377 void removePending(Index!Resource v) 1378 { 1379 alias s = sqlRemovePendingResource; 1380 1381 synchronized (s) 1382 { 1383 scope (exit) s.reset(); 1384 s.bind(v); 1385 s.step(); 1386 } 1387 } 1388 1389 /// Ditto 1390 void removePending(Index!Task v) 1391 { 1392 alias s = sqlRemovePendingTask; 1393 1394 synchronized (s) 1395 { 1396 scope (exit) s.reset(); 1397 s.bind(v); 1398 s.step(); 1399 } 1400 } 1401 1402 /// Ditto 1403 void removePending(ResourceId v) 1404 { 1405 removePending(find(v)); 1406 } 1407 1408 /// Ditto 1409 void removePending(TaskKey v) 1410 { 1411 removePending(find(v)); 1412 } 1413 1414 /** 1415 * Returns true if a given vertex is pending. 1416 */ 1417 bool isPending(Index!Resource v) 1418 { 1419 import std.exception : enforce; 1420 1421 alias s = sqlIsPendingResource; 1422 1423 synchronized (s) 1424 { 1425 scope (exit) s.reset(); 1426 s.bind(v); 1427 enforce(s.step(), "Failed to check if resource is pending"); 1428 return s.get!uint(0) == 1; 1429 } 1430 } 1431 1432 /// Ditto 1433 bool isPending(Index!Task v) 1434 { 1435 import std.exception : enforce; 1436 1437 alias s = sqlIsPendingTask; 1438 1439 synchronized (s) 1440 { 1441 scope (exit) s.reset(); 1442 s.bind(v); 1443 enforce(s.step(), "Failed to check if task is pending"); 1444 return s.get!uint(0) == 1; 1445 } 1446 } 1447 1448 /** 1449 * Lists the pending vertices. 1450 */ 1451 @property auto pending(Vertex : Resource)() 1452 { 1453 return prepare("SELECT resid FROM pendingResources") 1454 .rows!((SQLite3.Statement s) => Index!Vertex(s.get!ulong(0))); 1455 } 1456 1457 /// Ditto 1458 @property auto pending(Vertex : Task)() 1459 { 1460 return prepare("SELECT taskid FROM pendingTasks") 1461 .rows!((SQLite3.Statement s) => Index!Vertex(s.get!ulong(0))); 1462 } 1463 1464 unittest 1465 { 1466 import std.algorithm : map, equal; 1467 import std.array : array; 1468 1469 auto state = new BuildState; 1470 1471 assert(state.pending!Resource.empty); 1472 assert(state.pending!Task.empty); 1473 1474 immutable resources = ["a", "b", "c"]; 1475 auto resourceIds = resources.map!(r => state.put(Resource(r))).array; 1476 1477 immutable tasks = [[Command(["foo"])], [Command(["bar"])]]; 1478 auto taskIds = tasks.map!(t => state.put(Task(t))).array; 1479 1480 foreach (immutable id; resourceIds) 1481 state.addPending(id); 1482 1483 foreach (immutable id; taskIds) 1484 state.addPending(id); 1485 1486 assert(equal(state.pending!Resource, resourceIds)); 1487 assert(equal(state.pending!Task, taskIds)); 1488 1489 state.removePending(resourceIds[0]); 1490 state.removePending(taskIds[1]); 1491 1492 assert(equal(state.pending!Resource, [3, 4].map!(x => Index!Resource(x)))); 1493 assert(equal(state.pending!Task, [Index!Task(1)])); 1494 } 1495 1496 /** 1497 * Finds vertices with no incoming and no outgoing edges. 1498 */ 1499 @property auto islands(Vertex : Resource)() 1500 { 1501 return prepare( 1502 `SELECT id FROM resource` ~ 1503 ` WHERE id>1 AND` ~ 1504 ` resource.id` 1505 ).rows!((SQLite3.Statement s) => Index!Vertex(s.get!string(0))); 1506 } 1507 }