1 module dorm.types.relations;
2 
3 import dorm.declarative.conversion;
4 import dorm.model;
5 import dorm.types.patches;
6 
7 version(none) static struct ManyToManyField(alias idOrModel)
8 {
9 	alias T = ModelFromIdOrModel!idOrModel;
10 	alias primaryKeyAlias = IdAliasFromIdOrModel!idOrModel;
11 	enum primaryKeyField = IdFieldFromIdOrModel!idOrModel;
12 	alias PrimaryKeyType = typeof(primaryKeyAlias);
13 
14 	bool toClear;
15 	PrimaryKeyType[] toAdd;
16 	PrimaryKeyType[] toRemove;
17 
18 	private T[] cached;
19 	private bool resolved;
20 
21 	T[] populated()
22 	{
23 		assert(resolved, "ManyToManyField reference is not populated! Call "
24 			~ "`db.populate!(Model.manyToManyFieldName)(modelInstance)` or query "
25 			~ "data with the recursion flag set!");
26 		return cached;
27 	}
28 
29 	void setCachedPopulated(T[] populated)
30 	{
31 		cached = populated;
32 		resolved = true;
33 	}
34 
35 	void add(T other)
36 	{
37 		auto refField = __traits(child, other, primaryKeyAlias);
38 		toRemove = toRemove.remove!(refField);
39 		toAdd ~= refField;
40 	}
41 
42 	void add(PrimaryKeyType primaryKey)
43 	{
44 		toRemove = toRemove.remove!(primaryKey);
45 		toAdd ~= primaryKey;
46 	}
47 
48 	void add(Range)(Range range)
49 	if (!is(Range == T)
50 	&& !is(Range == PrimaryKeyType))
51 	{
52 		foreach (item; range)
53 			add(item);
54 	}
55 
56 	void remove(T other)
57 	{
58 		auto refField = __traits(child, other, primaryKeyAlias);
59 		toAdd = toAdd.remove!(refField);
60 		toRemove ~= refField;
61 	}
62 
63 	void add(PrimaryKeyType primaryKey)
64 	{
65 		toRemove = toRemove.remove!(primaryKey);
66 		toAdd ~= primaryKey;
67 	}
68 
69 	void remove(Range)(Range range)
70 	if (!is(Range == T)
71 	&& !is(Range == PrimaryKeyType))
72 	{
73 		foreach (item; range)
74 			remove(item);
75 	}
76 
77 	void clear()
78 	{
79 		toAdd.length = 0;
80 		toRemove.length = 0;
81 		toClear = true;
82 	}
83 }
84 
85 /**
86  * DORM field type representing a referenced model through a foreign key in SQL.
87  *
88  * The actual data stored in the DB as foreign key is what's stored in the
89  * `foreignKey` member. The `populated` property is simply a cache member for
90  * supporting proactively fetched joined data from the database. Trying to
91  * access the populated data without having it populated will result in a
92  * program crash through `assert(false)`.
93  *
94  * Bugs: `db.populate(ModelRef)` is not yet implemented
95  */
96 static template ModelRef(alias idOrPatch)
97 {
98 	static if (is(idOrPatch : Model)
99 		&& !__traits(compiles, typeof(IdAliasFromIdOrPatch!idOrPatch))
100 		&& !__traits(compiles, LogicalFields!idOrPatch))
101 		pragma(msg, __FILE__ ~ "(" ~ __LINE__.stringof ~ "): Hint: if this field fails because of a circular reference, "
102 			~ "refer to the field directly instead of refering to a model:\n"
103 			~ "\tModelRef!(" ~ __traits(identifier, idOrPatch) ~ ".remotePrimaryKey) field;\n"
104 			~ "\n"
105 			~ "\tThis is a currently known limitation that's unlikely to be fixed soon.\n"
106 			~ "\t--------------------------");
107 	alias primaryKeyAlias = IdAliasFromIdOrPatch!idOrPatch;
108 	alias TPatch = PatchFromIdOrPatch!idOrPatch;
109 	alias T = ModelFromSomePatch!TPatch;
110 	alias ModelRef = ModelRefImpl!(primaryKeyAlias, T, TPatch);
111 }
112 
113 /// ditto
114 static struct ModelRefImpl(alias id, _TModel, _TSelect)
115 {
116 	alias TModel = _TModel;
117 	alias TSelect = _TSelect;
118 	alias primaryKeyAlias = id;
119 	enum primaryKeySourceName = __traits(identifier, id);
120 	enum primaryKeyColumnName = DormColumnNameImpl!primaryKeyAlias;
121 	alias PrimaryKeyType = typeof(primaryKeyAlias);
122 
123 	/// The actual data stored in the DB field. Can be manipulated manually to
124 	/// perform special operations not supported otherwise.
125 	PrimaryKeyType foreignKey;
126 
127 	private TSelect cached;
128 	private bool resolved;
129 
130 	/// Returns: `true` if populated can be called, `false` otherwise.
131 	bool isPopulated() const @property
132 	{
133 		return resolved;
134 	}
135 
136 	/**
137 	 * Returns: the value that was fetched from the database. Only works if the
138 	 * value was either requested to be included using `select.populate` or by
139 	 * calling `db.populate(thisObject)`.
140 	 *
141 	 * When trying to call this function with an unpopulate object, the program
142 	 * will crash with an AssertError.
143 	 */
144 	TSelect populated()
145 	{
146 		if (!resolved)
147 		{
148 			assert(false, "ModelRef reference is not populated! Call "
149 				~ "`db.populate!(Model.referenceFieldName)(modelInstance)` or query "
150 				~ "data with `select!T.populate(o => o.fieldNameToPopulate.yes)`!");
151 		}
152 		return cached;
153 	}
154 
155 	/**
156 	 * Sets the populated value as well as the foreign key for saving in the DB.
157 	 */
158 	auto opAssign(TSelect value)
159 	{
160 		resolved = true;
161 		cached = value;
162 		foreignKey = __traits(child, value, primaryKeyAlias);
163 		return value;
164 	}
165 
166 	/// Returns true if `other`'s primary key is equal to the foreign key stored
167 	/// in this ModelRef instance. Does not check any other fields. Does not
168 	/// require this ModelRef to be populated.
169 	bool refersTo(const TModel other) const
170 	{
171 		return foreignKey == mixin("other.", primaryKeySourceName);
172 	}
173 
174 	static if (!is(TModel == TSelect))
175 	{
176 		/// ditto
177 		bool refersTo(const TSelect other) const
178 		{
179 			return foreignKey == __traits(child, other, primaryKeyAlias);
180 		}
181 	}
182 }
183 
184 // TODO: need to figure out how to make BackRefs
185 version (none)
186 static struct BackRef(alias foreignField)
187 {
188 	static assert(is(__traits(parent, foreignField) : Model),
189 		"Invalid foreign key field `" ~ foreignField.stringof
190 		~ "`! Change to `BackRef!(OtherModel.foreignKeyReferencingThis)`");
191 
192 	alias T = __traits(parent, foreignField);
193 
194 	private T[] cached;
195 	private bool resolved;
196 
197 	T[] populated()
198 	{
199 		assert(resolved, "BackRef value is not populated! Call "
200 			~ "`db.populate!(Model.otherFieldReferencingThis)(modelInstance)` or query "
201 			~ "data with the recursion flag set!");
202 		return cached;
203 	}
204 }