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 }