1 module dpq.relationproxy;
2 
3 import dpq.attributes;
4 import dpq.connection;
5 import dpq.querybuilder;
6 import dpq.value : Value;
7 
8 import std.algorithm : map;
9 import std.array;
10 import std.meta : Alias;
11 import std.typecons : Nullable;
12 
13 /**
14     Verifies that the given type can be used a relation
15  */
16 template IsValidRelation(T)
17 {
18    import std.traits : hasUDA;
19 
20    // Class or struct, and has a RelationAttribute.
21    enum IsValidRelation = (is(T == struct) || is(T == class)) &&
22       hasUDA!(T, RelationAttribute);
23 }
24 
25 
26 /**
27    A structure proxying to the actual Postgres table.
28 
29    Allows short and readable querying, returning actual records or arrays of
30    them. first, last, and all will be cached if appropriate, the rest will
31    execute a query whenever a result is needed.
32    Most functions return a reference to the same object, allowing you to
33    chain them. Builder pattern, really.
34 
35    Can be implicitly converted to an array of the requested structure.
36 
37    Mostly meant to be used with RelationMixin, but can be used manually.
38 
39    Examples:
40    ---------------
41    struct User
42    {
43       // provides where, find, findBy, etc on the User struct
44       mixin RelationMixin;
45 
46       @PK @serial int id'
47       string username;
48    }
49 
50    auto user1 = User.where(["id": 1]).first;
51    // Or better yet. Search directly by PK
52    auto betterUser1 = User.find(1);
53    // Delete some users
54    long nDeleted = User.where("username LIKE 'somethi%'").removeAll();
55    ---------------
56  */
57 struct RelationProxy(T)
58    if (IsValidRelation!T)
59 {
60    private
61    {
62       /**
63          The actual content, if any at all.
64        */
65       T[] _content;
66 
67       Connection _connection;
68 
69       /**
70          QueryBuilder used to generate the required queries for content.
71 
72          Sensible defaults.
73        */
74       QueryBuilder _queryBuilder = QueryBuilder().select(AttributeList!T).from!T;
75 
76       /**
77          Signifies whether the content is fresh. The content is not considered
78          fresh once any filters change or new ones are applied.
79        */
80       bool _contentFresh = false;
81 
82       /**
83          Update the content, does not check for freshness
84        */
85       void _updateContent()
86       {
87          auto result = _queryBuilder.query(_connection).run();
88 
89          _content = result.map!(deserialise!T).array;
90          _markFresh();
91       }
92 
93       /**
94          Mark the content stale, meaning it will be reloaded when next needed.
95        */
96       void _markStale()
97       {
98          _contentFresh = false;
99       }
100 
101       /**
102          Mark the content fresh, it should not be reloaded unless filters change.
103        */
104       void _markFresh()
105       {
106          _contentFresh = true;
107       }
108    }
109 
110    /**
111       Basic constructor. RelationProxy requires a connection to operate on.
112     */
113    this(ref Connection connection)
114    {
115       _connection = connection;
116    }
117 
118 
119    /**
120       Makes a copy of just the filters, not any content;
121     */
122    @property RelationProxy!T dup()
123    {
124       auto copy = RelationProxy!T(_connection);
125       copy._queryBuilder = _queryBuilder;
126 
127       return copy;
128    }
129 
130    /**
131       Reloads the content with existing filters.
132     */
133    ref auto reload()
134    {
135       _updateContent();
136       return this;
137    }
138 
139    /**
140        Returns the actual content, executing the query if data has not yet been
141        fetched from the database.
142     */
143    @property T[] all()
144    {
145       // Update the content if it's not currently fresh or has not been fetched yet
146       if (!_contentFresh)
147          _updateContent();
148 
149       return _content;
150    }
151 
152    alias all this;
153 
154    /**
155       Specifies filters according to the given AA.
156       Filters will be joined with AND.
157     */
158    ref auto where(U)(U[string] filters)
159    {
160       _markStale();
161       _queryBuilder.where(filters);
162       return this;
163    }
164 
165    /**
166       Same as above, but allowing you to specify a completely custom filter.
167       The supplied string will be wrapped in (), can be called multiple times
168       to specify multiple filters.
169     */
170    ref auto where(U...)(string filter, U params)
171    {
172       _markStale();
173       _queryBuilder.where(filter, params);
174       return this;
175    }
176 
177    /**
178       Convenience alias, allows us to do proxy.where(..).and(...)
179     */
180    alias and = where;
181 
182    /**
183       Inserts an OR seperator between the filters.
184 
185       Examples:
186       ------------------
187       proxy.where("something").and("something2").or("something3");
188       // will produce "WHERE ((something) AND (something2)) OR (something3)"
189       ------------------
190     */
191    @property ref auto or()
192    {
193       _queryBuilder.or();
194       return this;
195    }
196 
197    /**
198       Fetches the first record matching the filters.
199 
200       Will return a Nullable null value if no matches.
201 
202       If data is already cached and not marked stale/unfresh, it will reuse it,
203       meaning that calling this after calling all will not generate an additional
204       query, even if called multiple times.
205       Will not cache its own result, only reuse existing data.
206 
207       Params:
208          by = optional, specifies the column to order by, defaults to PK name
209          order = Order to sort by, (Order.asc, Order.desc), optional, defaults to asc
210          filters = optional filters to apply
211 
212       Example:
213       -----------------
214       auto p = RelationProxy!User();
215       auto user = p.where(["something": 123]).first;
216       -----------------
217     */
218    @property Nullable!T first()
219    {
220       alias RT = Nullable!T;
221 
222       // If the content is fresh, we do not have to fetch anything
223       if (_contentFresh)
224       {
225          if (_content.length == 0)
226             return RT.init;
227          return RT(_content[0]);
228       }
229 
230       // Make a copy of the builder, as to not ruin the query in case of reuse
231       auto qb = _queryBuilder;
232       qb.limit(1).order(primaryKeyAttributeName!T, Order.asc);
233 
234       auto result = qb.query(_connection).run();
235 
236       if (result.rows == 0)
237          return RT.init;
238 
239       return RT(result[0].deserialise!T);
240    }
241 
242    /**
243       Same as first, but defaults to desceding order, giving you the last match.
244 
245       Caching acts the same as with first.
246     */
247    @property Nullable!T last(string by = primaryKeyAttributeName!T)
248    {
249       alias RT = Nullable!T;
250 
251       // If the content is fresh, we do not have to fetch anything
252       if (_contentFresh)
253       {
254          if (_content.length == 0)
255             return RT.init;
256          return RT(_content[$ - 1]);
257       }
258 
259       // Make a copy of the builder, as to not ruin the query in case of reuse
260       auto qb = _queryBuilder;
261       qb.limit(1).order(primaryKeyAttributeName!T, Order.desc);
262 
263       auto result = qb.query(_connection).run();
264 
265       if (result.rows == 0)
266          return RT.init;
267 
268       return RT(result[result.rows - 1].deserialise!T);
269    }
270 
271    /**
272       Finds a single record matching the filters. Equivalent to where(...).first.
273 
274       Does not change the filters for the RelationProxy it was called on.
275     */
276    Nullable!T findBy(U)(U[string] filters)
277    {
278       // Make a copy so we do not destroy our filters in the case of reuse.
279       return this.dup.where(filters).first;
280    }
281 
282    /**
283       Same as above, but always searches by the primary key
284     */
285    Nullable!T find(U)(U param)
286    {
287       return findBy([primaryKeyAttributeName!T: param]);
288    }
289 
290 
291    /**
292       Update all the records matching filters to new values from the AA.
293 
294       Does not use IDs of any existing cached data, simply uses the specified
295       filter in an UPDATE query.
296 
297       Examples:
298       ----------------
299       auto p = RelationProxy!User(db);
300       p.where("posts > 500").updateAll(["rank": "Frequent Poster]);
301       ----------------
302     */
303    auto updateAll(U)(U[string] updates)
304    {
305       _markStale();
306 
307       auto result = _queryBuilder.dup
308          .update!T
309          .set(updates)
310          .query(_connection).run();
311 
312       return result.rows;
313    }
314 
315    auto update(U, Tpk)(Tpk id, U[string] values)
316    {
317       _markStale();
318 
319       auto qb = QueryBuilder()
320          .update!T
321          .set(values)
322          .where([primaryKeyAttributeName!T: Value(id)]);
323 
324       return qb.query(_connection).run().rows;
325    }
326 
327    /**
328       Simply deletes all the records matching the filters.
329 
330       Even if any data is already cached, does not filter by IDs, but simply
331       uses the specified filters in a DELETE query.
332     */
333    auto removeAll()
334    {
335       _markStale();
336 
337       auto result = _queryBuilder.dup
338          .remove()
339          .query(_connection).run();
340 
341       return result.rows;
342    }
343 
344    /**
345       Removes a single record, filtering it by the primary key's value.
346     */
347    auto remove(Tpk)(Tpk id)
348    {
349       _markStale();
350 
351       auto qb = QueryBuilder()
352          .remove()
353          .from!T
354          .where([primaryKeyAttributeName!T: Value(id)]);
355 
356 
357       return qb.query(_connection).run().rows;
358    }
359 
360    /**
361       Inserts a new record, takes the record as a reference in order to update
362       its PK value. Does not update any other values.
363 
364       Does not check if the record is already in the database, simply creates
365       an insert without the PK and can therefore be used to insert the same
366       record multiple times.
367     */
368    ref T insert(ref T record)
369    {
370       _markStale();
371 
372       enum pk = primaryKeyName!T;
373       enum pkAttr = primaryKeyAttributeName!T;
374 
375       auto qb = QueryBuilder()
376             .insert(relationName!T, AttributeList!(T, true, true))
377             .returning(pkAttr)
378             .addValues!T(record);
379 
380       alias pkMem = Alias!(__traits(getMember, record, pk));
381       auto result = qb.query(_connection).run();
382       __traits(getMember, record, pk) = result[0][pkAttr].as!(typeof(pkMem));
383 
384       return record;
385    }
386 
387    /**
388       Updates the given record in the DB with all the current values.
389 
390       Updates by ID. Assumes the record is already in the DB. Does not insert
391       under any circumstance.
392 
393       Examples:
394       -----------------
395       User user = User.first;
396       user.posts = posts - 2;
397       user.save();
398       -----------------
399     */
400    bool save(T record)
401    {
402       _markStale();
403 
404       enum pkName = primaryKeyName!T;
405 
406       auto qb = QueryBuilder()
407          .update(relationName!T)
408          .where([pkName: __traits(getMember, record, pkName)]);
409 
410       foreach (member; serialisableMembers!T)
411          qb.set(
412                attributeName!(__traits(getMember, record, member)),
413                __traits(getMember, record, member));
414 
415       return qb.query(_connection).run().rows > 0;
416    }
417 
418    /**
419       Selects the count of records with current filters.
420       Default is *, but any column can be specified as a parameter. The column
421       name is not further escaped.
422 
423       Does not cache the result or use existing data's length. Call .length to
424       get local data's length.
425     */
426    long count(string col = "*")
427    {
428       import std..string : format;
429 
430       auto qb = _queryBuilder.dup
431          .select("count(%s)".format(col));
432 
433       auto result = qb.query(_connection).run();
434       return result[0][0].as!long;
435    }
436 
437    /**
438       A basic toString implementation, mostly here to prevent querying when
439       the object is implicitly converted to a string.
440     */
441    @property string toString()
442    {
443       return "<RelationProxy!" ~ T.stringof ~ "::`" ~ _queryBuilder.command ~ "`>";
444    }
445 }
446 
447 
448 /**************************************************/
449 /* Methods meant to be used on records themselves */
450 /**************************************************/
451 
452 /**
453    Reloads the record from the DB, overwrites it. Returns a reference to the
454    same object.
455 
456    Examples:
457    ---------------------
458    User user = User.first;
459    writeln("My user is: ", user);
460    user.update(["username": "Oopdated Ooser"]);
461    writeln("My user is: ", user);
462    writeln("My user from DB is: ", User.find(user.id));
463    writeln("Reloaded user ", user.reload); // will be same as above
464    ---------------------
465  */
466 ref T reload(T)(ref T record)
467    if (IsValidRelation!T)
468 {
469    enum pkName = primaryKeyName!T;
470    record = T.find(__traits(getMember, record, pkName));
471 
472    return record;
473 }
474 
475 /**
476    Updates a single record with the new values, does not set them on the record
477    itself.
478 
479    Examples:
480    --------------------
481    User user = User.first;
482    user.update(["username": "Some new name"]); // will run an UPDATE query
483    user.reload(); // Can be reloaded after to fetch new data from DB if needed
484    --------------------
485 
486  */
487 auto update(T, U)(T record, U[string] values)
488    if (IsValidRelation!T)
489 {
490    enum pkName = primaryKeyName!T;
491 
492    return T.updateOne(__traits(getMember, record, pkName), values);
493 }
494 
495 /**
496    Removes a record from the DB, filtering by the primary key.
497  */
498 auto remove(T)(T record)
499    if (IsValidRelation!T)
500 {
501    enum pkName = primaryKeyName!T;
502 
503    return T.removeOne(__traits(getMember, record, pkName));
504 }
505 
506 /**
507    See: RelationProxy's save method
508  */
509 bool save(T)(T record)
510    if (IsValidRelation!T)
511 {
512    return T.saveRecord(record);
513 }