1 ///
2 module microrm.api;
3 
4 import std.array : Appender;
5 import std.range;
6 
7 import microrm.queries;
8 import microrm.util;
9 import microrm.exception;
10 
11 import d2sqlite3;
12 
13 debug (microrm) import std.stdio : stderr;
14 
15 //version = microrm_cache_stmt;
16 
17 ///
18 class MDatabase
19 {
20     version (microrm_cache_stmt)
21         private Statement[string] cachedStmt;
22 
23     private Appender!(char[]) buf;
24 
25     ///
26     Database db;
27 
28     ///
29     this(string path,
30          int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
31          size_t queryBufferInitReserve=512)
32     {
33         db = Database(path, flags);
34         buf.reserve(queryBufferInitReserve);
35     }
36 
37     void run(string sql, scope bool delegate(ResultRange) dg = null)
38     { db.run(sql, dg); }
39 
40     void close() { db.close(); }
41 
42     ///
43     auto select(T)() @property
44     {
45         buf.clear();
46         return Select!(T, typeof(buf))(&db, &buf);
47     }
48 
49     ///
50     auto count(T)() @property
51     {
52         buf.clear();
53         return Count!(T, typeof(buf))(&db, &buf);
54     }
55 
56     ///
57     void insert(bool all=false, T)(T[] arr...) if (!isInputRange!T)
58     { procInsert!all(false, arr); }
59     ///
60     void insertOrReplace(bool all=false, T)(T[] arr...) if (!isInputRange!T)
61     { procInsert!all(true, arr); }
62 
63     ///
64     void insert(bool all=false, R)(R rng)
65         if (isInputRange!R && ((all && hasLength!R) || !all))
66     { procInsert!all(false, rng); }
67     ///
68     void insertOrReplace(bool all=false, R)(R rng)
69         if (isInputRange!R && ((all && hasLength!R) || !all))
70     { procInsert!all(true, rng); }
71 
72     private auto procInsert(bool all=false, R)(bool replace, R rng)
73         if ((all && hasLength!R) || !all)
74     {
75         buf.clear;
76         alias T = ElementType!R;
77         static if (all)
78             buf.buildInsertOrReplace!T(replace, rng.length);
79         else
80             buf.buildInsertOrReplace!T(replace);
81 
82         auto sql = buf.data.idup;
83 
84         debug (microrm) stderr.writeln(sql);
85 
86         version (microrm_cache_stmt)
87         {
88             if (sql !in cachedStmt)
89                 cachedStmt[sql] = db.prepare(sql);
90             auto stmt = cachedStmt[sql];
91         }
92         else auto stmt = db.prepare(sql);
93 
94         int n;
95         static if (all)
96         {
97             foreach (v; rng)
98                 bindStruct(stmt, v, replace, n);
99             stmt.execute();
100             stmt.reset();
101         }
102         else foreach (v; rng)
103         {
104             n = 0;
105             bindStruct(stmt, v, replace, n);
106             stmt.execute();
107             stmt.reset();
108         }
109 
110         version (microrm_cache_stmt) {}
111         else stmt.finalize();
112 
113         return true;
114     }
115 
116     ///
117     auto del(T)()
118     {
119         buf.clear();
120         return Delete!(T, typeof(buf))(&db, &buf);
121     }
122 
123     ///
124     auto lastInsertId() @property
125     {
126         return db.
127             executeCheck("SELECT last_insert_rowid()").
128             front.front.as!ulong;
129     }
130 
131     private static int bindStruct(T)(ref Statement stmt, T v, bool replace, ref int n)
132     {
133         foreach (i, f; v.tupleof)
134         {
135             enum name = __traits(identifier, v.tupleof[i]);
136             alias F = typeof(f);
137             static if (is(F==struct))
138                 bindStruct(stmt, f, replace, n);
139             else
140             {
141                 if (name == IDNAME && !replace) continue;
142                 stmt.bind(n+1, f);
143                 n++;
144             }
145         }
146         return n;
147     }
148 }
149 
150 version (unittest)
151 {
152     import microrm.schema;
153 
154     import std.conv : text, to;
155     import std.range;
156     import std.algorithm;
157     import std.datetime;
158     import std.array;
159     import std.stdio;
160 }
161 
162 unittest
163 {
164     struct One
165     {
166         ulong id;
167         string text;
168     }
169 
170     import std.experimental.allocator;
171     import std.experimental.allocator.mallocator;
172     import std.experimental.allocator.building_blocks.scoped_allocator;
173 
174     MDatabase db;
175     ScopedAllocator!Mallocator scalloc;
176     db = scalloc.make!MDatabase(":memory:");
177     scope (exit) db.close();
178     db.run(buildSchema!One);
179 
180     assert(db.count!One.run == 0);
181     db.insert(iota(0,10).map!(i=>One(i*100,"hello" ~ text(i))));
182     assert(db.count!One.run == 10);
183 
184     auto ones = db.select!One.run.array;
185     assert(ones.length == 10);
186     assert(ones.all!(a=>a.id < 100));
187     assert(db.lastInsertId == ones[$-1].id);
188     db.del!One.run;
189     assert(db.count!One.run == 0);
190     db.insertOrReplace(iota(0,499).map!(i=>One((i+1)*100,"hello" ~ text(i))));
191     assert(ones.length == 10);
192     ones = db.select!One.run.array;
193     assert(ones.length == 499);
194     assert(ones.all!(a=>a.id >= 100));
195     assert(db.lastInsertId == ones[$-1].id);
196 }
197 
198 unittest
199 {
200     struct One
201     {
202         ulong id;
203         string text;
204     }
205 
206     auto db = new MDatabase(":memory:");
207     scope (exit) db.close();
208     db.run(buildSchema!One);
209 
210     assert(db.count!One.run == 0);
211     db.insert!true(iota(0,10).map!(i=>One(i*100,"hello" ~ text(i))));
212     assert(db.count!One.run == 10);
213 
214     auto ones = db.select!One.run.array;
215     assert(ones.length == 10);
216     assert(ones.all!(a=>a.id < 100));
217     assert(db.lastInsertId == ones[$-1].id);
218     db.del!One.run;
219     assert(db.count!One.run == 0);
220     import std.datetime;
221     import std.conv : to;
222     db.insertOrReplace!true(iota(0,499).map!(i=>One((i+1)*100,"hello" ~ text(i))));
223     assert(ones.length == 10);
224     ones = db.select!One.run.array;
225     assert(ones.length == 499);
226     assert(ones.all!(a=>a.id >= 100));
227     assert(db.lastInsertId == ones[$-1].id);
228 }
229 
230 unittest
231 {
232     struct Limit { int min, max; }
233     struct Limits { Limit volt, curr; }
234     struct Settings
235     {
236         ulong id;
237         Limits limits;
238     }
239 
240     auto db = new MDatabase(":memory:");
241     scope (exit) db.close();
242     db.run(buildSchema!Settings);
243     assert(db.count!Settings.run == 0);
244     db.insertOrReplace(Settings(10, Limits(Limit(0,12), Limit(-10, 10))));
245     assert(db.count!Settings.run == 1);
246 
247     db.insertOrReplace(Settings(10, Limits(Limit(0,2), Limit(-3, 3))));
248     db.insertOrReplace(Settings(11, Limits(Limit(0,11), Limit(-11, 11))));
249     db.insertOrReplace(Settings(12, Limits(Limit(0,12), Limit(-12, 12))));
250 
251     assert(db.count!Settings.run == 3);
252     assert(db.count!Settings.where(`"limits.volt.max" = `, 2).run == 1);
253     assert(db.count!Settings.where(`"limits.volt.max" > `, 10).run == 2);
254     db.del!Settings.where(`"limits.volt.max" < `, 10).run;
255     assert(db.count!Settings.run == 2);
256 }
257 
258 unittest
259 {
260     struct Settings
261     {
262         ulong id;
263         int[5] data;
264     }
265 
266     auto db = new MDatabase(":memory:");
267     scope (exit) db.close();
268     db.run(buildSchema!Settings);
269 
270     db.insert(Settings(0, [1,2,3,4,5]));
271 
272     assert(db.count!Settings.run == 1);
273     auto s = db.select!Settings.run.front;
274     assert(s.data == [1,2,3,4,5]);
275 }