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 }