1 module mysql.data_object;
2 
3 import std.string;
4 import mysql.database;
5 
6 /*
7     This is like a result set
8 
9 
10     DataObject res = [...];
11 
12     res.name = "Something";
13 
14     res.commit; // runs the actual update or insert
15 
16 
17     res = new DataObject(fields, tables
18 
19 
20 
21 
22 
23 
24 
25     when doing a select, we need to figure out all the tables and modify the query to include the ids we need
26 
27 
28     search for FROM and JOIN
29     the next token is the table name
30 
31     right before the FROM, add the ids of each table
32 
33 
34     given:
35         SELECT name, phone FROM customers LEFT JOIN phones ON customer.id = phones.cust_id
36 
37     we want:
38         SELECT name, phone, customers.id AS id_from_customers, phones.id AS id_from_phones FROM customers LEFT JOIN phones ON customer.id[...];
39 
40 */
41 
42 mixin template DataObjectConstructors() {
43     this(Database db, string[string] res, Tuple!(string, string)[string] mappings) {
44         super(db, res, mappings);
45     }
46 }
47 
48 string yield(string what) { return `if(auto result = dg(`~what~`)) return result;`; }
49 
50 import std.typecons;
51 import std.json; // for json value making
52 
53 class DataObject {
54     // lets you just free-form set fields, assuming they all come from the given table
55     // note it doesn't try to handle joins for new rows. you've gotta do that yourself
56     this(Database db, string table) {
57         assert(db !is null);
58         this.db = db;
59         this.table = table;
60 
61         mode = UpdateOrInsertMode.CheckForMe;
62     }
63 
64     JSONValue makeJsonValue() {
65         JSONValue val;
66         JSONValue[string] valo;
67         //val.type = JSON_TYPE.OBJECT;
68         foreach(k, v; fields) {
69             JSONValue s;
70             //s.type = JSON_TYPE.STRING;
71             s.str = v;
72             valo[k] = s;
73             //val.object[k] = s;
74         }
75         val = valo;
76         return val;
77     }
78 
79     this(Database db, string[string] res, Tuple!(string, string)[string] mappings) {
80         this.db = db;
81         this.mappings = mappings;
82         this.fields = res;
83 
84         mode = UpdateOrInsertMode.AlwaysUpdate;
85     }
86 
87     string table;
88     //     table,  column  [alias]
89     Tuple!(string, string)[string] mappings;
90 
91     // value [field] [table]
92     string[string][string] multiTableKeys; // note this is not set internally tight now
93                         // but it can be set manually to do multi table mappings for automatic update
94 
95 
96     string opDispatch(string field, string file = __FILE__, size_t line = __LINE__)()
97         if((field.length < 8 || field[0..8] != "id_from_") && field != "popFront")
98     {
99         if(field !in fields)
100             throw new Exception("no such field " ~ field, file, line);
101 
102         return fields[field];
103     }
104 
105     string opDispatch(string field, T)(T t)
106         if((field.length < 8 || field[0..8] != "id_from_") && field != "popFront")
107     {
108         static if(__traits(compiles, t is null)) {
109             if(t is null)
110                 setImpl(field, null);
111             else
112                 setImpl(field, to!string(t));
113         } else
114             setImpl(field, to!string(t));
115 
116         return fields[field];
117     }
118 
119 
120     // vararg hack so property assignment works right, even with null
121     version(none)
122     string opDispatch(string field, string file = __FILE__, size_t line = __LINE__)(...)
123         if((field.length < 8 || field[0..8] != "id_from_") && field != "popFront")
124     {
125         if(_arguments.length == 0) {
126             if(field !in fields)
127                 throw new Exception("no such field " ~ field, file, line);
128 
129             return fields[field];
130         } else if(_arguments.length == 1) {
131             auto arg = _arguments[0];
132 
133             string a;
134             if(arg == typeid(string) || arg == typeid(immutable(string)) || arg == typeid(const(immutable(char)[]))) {
135                 a = va_arg!(string)(_argptr);
136             } else if (arg == typeid(int) || arg == typeid(immutable(int)) || arg == typeid(const(int))) {
137                 auto e = va_arg!(int)(_argptr);
138                 a = to!string(e);
139             } else if (arg == typeid(char) || arg == typeid(immutable(char))) {
140                 auto e = va_arg!(char)(_argptr);
141                 a = to!string(e);
142             } else if (arg == typeid(long) || arg == typeid(const(long)) || arg == typeid(immutable(long))) {
143                 auto e = va_arg!(long)(_argptr);
144                 a = to!string(e);
145             } else if (arg == typeid(null)) {
146                 a = null;
147             } else assert(0, "invalid type " ~ arg.toString );
148 
149 
150             auto setTo = a;
151             setImpl(field, setTo);
152 
153             return setTo;
154 
155         } else assert(0, "too many arguments");
156 
157         assert(0); // should never be reached
158     }
159 
160     private void setImpl(string field, string value) {
161         if(field in fields) {
162             if(fields[field] != value)
163                 changed[field] = true;
164         } else {
165             changed[field] = true;
166         }
167 
168         fields[field] = value;
169     }
170 
171     public void setWithoutChange(string field, string value) {
172         fields[field] = value;
173     }
174 
175     int opApply(int delegate(ref string) dg) {
176         foreach(a; fields)
177             mixin(yield("a"));
178 
179         return 0;
180     }
181 
182     int opApply(int delegate(ref string, ref string) dg) {
183         foreach(a, b; fields)
184             mixin(yield("a, b"));
185 
186         return 0;
187     }
188 
189 
190     string opIndex(string field, string file = __FILE__, size_t line = __LINE__) {
191         if(field !in fields)
192             throw new DatabaseException("No such field in data object: " ~ field, file, line);
193         return fields[field];
194     }
195 
196     string opIndexAssign(string value, string field) {
197         setImpl(field, value);
198         return value;
199     }
200 
201     string* opBinary(string op)(string key)  if(op == "in") {
202         return key in fields;
203     }
204 
205     string[string] fields;
206     bool[string] changed;
207 
208     void commitChanges() {
209         commitChanges(cast(string) null, null);
210     }
211 
212     void commitChanges(string key, string keyField) {
213         commitChanges(key is null ? null : [key], keyField is null ? null : [keyField]);
214     }
215 
216     void commitChanges(string[] keys, string[] keyFields = null) {
217         string[string][string] toUpdate;
218         int updateCount = 0;
219         foreach(field, c; changed) {
220             if(c) {
221                 string tbl, col;
222                 if(mappings is null) {
223                     tbl = this.table;
224                     col = field;
225                 } else {
226                     if(field !in mappings)
227                         assert(0, "no such mapping for " ~ field);
228                     auto m = mappings[field];
229                     tbl = m[0];
230                     col = m[1];
231                 }
232 
233                 toUpdate[tbl][col] = fields[field];
234                 updateCount++;
235             }
236         }
237 
238         if(updateCount) {
239             db.startTransaction();
240             scope(success) db.query("COMMIT");
241             scope(failure) db.query("ROLLBACK");
242 
243             foreach(tbl, values; toUpdate) {
244                 string where, keyFieldToPass;
245 
246                 if(keys is null) {
247                     keys = [null];
248                 }
249 
250                 if(multiTableKeys is null || tbl !in multiTableKeys)
251                 foreach(i, key; keys) {
252                     string keyField;
253 
254                     if(key is null) {
255                         key = "id_from_" ~ tbl;
256                         if(key !in fields)
257                             key = "id";
258                     }
259 
260                     if(i >= keyFields.length || keyFields[i] is null) {
261                         if(key == "id_from_" ~ tbl)
262                             keyField = "id";
263                         else
264                             keyField = key;
265                     } else {
266                         keyField = keyFields[i];
267                     }
268 
269 
270                     if(where.length)
271                         where ~= " AND ";
272 
273                     auto f = key in fields ? fields[key] : null;
274                     if(f is null)
275                         where ~= keyField ~ " = NULL";
276                     else
277                         where ~= keyField ~ " = '"~db.escape(f)~"'" ;
278                     if(keyFieldToPass.length)
279                         keyFieldToPass ~= ", ";
280 
281                     keyFieldToPass ~= keyField;
282                 }
283                 else {
284                     foreach(keyField, v; multiTableKeys[tbl]) {
285                         if(where.length)
286                             where ~= " AND ";
287 
288                         where ~= keyField ~ " = '"~db.escape(v)~"'" ;
289                         if(keyFieldToPass.length)
290                             keyFieldToPass ~= ", ";
291 
292                         keyFieldToPass ~= keyField;
293                     }
294                 }
295 
296 
297 
298                 updateOrInsert(db, tbl, values, where, mode, keyFieldToPass);
299             }
300 
301             changed = null;
302         }
303     }
304 
305     void commitDelete() {
306         if(mode == UpdateOrInsertMode.AlwaysInsert)
307             throw new Exception("Cannot delete an item not in the database");
308 
309         assert(table.length); // FIXME, should work with fancy items too
310 
311         // FIXME: escaping and primary key questions
312         db.query("DELETE FROM " ~ table ~ " WHERE id = '" ~ db.escape(fields["id"]) ~ "'");
313     }
314 
315     string getAlias(string table, string column) {
316         string ali;
317         if(mappings is null) {
318             if(this.table is null) {
319                 mappings[column] = tuple(table, column);
320                 return column;
321             } else {
322                 assert(table == this.table);
323                 ali = column;
324             }
325         } else {
326             foreach(a, what; mappings)
327                 if(what[0] == table && what[1] == column
328                   && a.indexOf("id_from_") == -1) {
329                     ali = a;
330                     break;
331                 }
332         }
333 
334         return ali;
335     }
336 
337     void set(string table, string column, string value) {
338         string ali = getAlias(table, column);
339         //assert(ali in fields);
340         setImpl(ali, value);
341     }
342 
343     string select(string table, string column) {
344         string ali = getAlias(table, column);
345         //assert(ali in fields);
346         if(ali in fields)
347             return fields[ali];
348         return null;
349     }
350 
351     DataObject addNew() {
352         auto n = new DataObject(db, null);
353 
354         n.db = this.db;
355         n.table = this.table;
356         n.mappings = this.mappings;
357 
358         foreach(k, v; this.fields)
359             if(k.indexOf("id_from_") == -1)
360                 n.fields[k] = v;
361             else
362                 n.fields[k] = null; // don't copy ids
363 
364         n.mode = UpdateOrInsertMode.AlwaysInsert;
365 
366         return n;
367     }
368 
369     Database db;
370     UpdateOrInsertMode mode;
371 }
372 
373 /**
374     You can subclass DataObject if you want to
375     get some compile time checks or better types.
376 
377     You'll want to disable opDispatch, then forward your
378     properties to the super opDispatch.
379 */
380 
381 /*mixin*/ string DataObjectField(T, string table, string column, string aliasAs = null)() {
382     string aliasAs_;
383     if(aliasAs is null)
384         aliasAs_ = column;
385     else
386         aliasAs_ = aliasAs;
387     return `
388         @property void `~aliasAs_~`(`~T.stringof~` setTo) {
389             super.set("`~table~`", "`~column~`", to!string(setTo));
390         }
391 
392         @property `~T.stringof~` `~aliasAs_~` () {
393             return to!(`~T.stringof~`)(super.select("`~table~`", "`~column~`"));
394         }
395     `;
396 }
397 
398 mixin template StrictDataObject() {
399     // disable opdispatch
400     string opDispatch(string name)(...) if (0) {}
401 }
402 
403 
404 string createDataObjectFieldsFromAlias(string table, fieldsToUse)() {
405     string ret;
406 
407     fieldsToUse f;
408     foreach(member; __traits(allMembers, fieldsToUse)) {
409         ret ~= DataObjectField!(typeof(__traits(getMember, f, member)), table, member);
410     }
411 
412     return ret;
413 }
414 
415 
416 /**
417     This creates an editable data object out of a simple struct.
418 
419     struct MyFields {
420         int id;
421         string name;
422     }
423 
424     alias SimpleDataObject!("my_table", MyFields) User;
425 
426 
427     User a = new User(db);
428 
429     a.id = 30;
430     a.name = "hello";
431     a.commitChanges(); // tries an update or insert on the my_table table
432 
433 
434     Unlike the base DataObject class, this template provides compile time
435     checking for types and names, based on the struct you pass in:
436 
437     a.id = "aa"; // compile error
438 
439     a.notAField; // compile error
440 */
441 class SimpleDataObject(string tableToUse, fieldsToUse) : DataObject {
442     mixin StrictDataObject!();
443 
444     mixin(createDataObjectFieldsFromAlias!(tableToUse, fieldsToUse)());
445 
446     this(Database db) {
447         super(db, tableToUse);
448     }
449 }