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 }