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