1 /// Structures for MySQL types not built-in to D/Phobos.
2 module mysql.types;
3 import taggedalgebraic.taggedalgebraic;
4 import std.datetime : DateTime, TimeOfDay, Date;
5 import std.typecons : Nullable;
6 
7 /++
8 A simple struct to represent time difference.
9 
10 D's std.datetime does not have a type that is closely compatible with the MySQL
11 interpretation of a time difference, so we define a struct here to hold such
12 values.
13 +/
14 struct TimeDiff
15 {
16 	bool negative;
17 	int days;
18 	ubyte hours, minutes, seconds;
19 }
20 
21 /++
22 A D struct to stand for a TIMESTAMP
23 
24 It is assumed that insertion of TIMESTAMP values will not be common, since in general,
25 such columns are used for recording the time of a row insertion, and are filled in
26 automatically by the server. If you want to force a timestamp value in a prepared insert,
27 set it into a timestamp struct as an unsigned long in the format YYYYMMDDHHMMSS
28 and use that for the appropriate parameter. When TIMESTAMPs are retrieved as part of
29 a result set it will be as DateTime structs.
30 +/
31 struct Timestamp
32 {
33 	ulong rep;
34 }
35 
36 private union _MYTYPE
37 {
38 @safeOnly:
39 	// blobs are const because of the indirection. In this case, it's not
40 	// important because nobody is going to use MySQLVal to maintain their
41 	// ubyte array.
42 	ubyte[] Blob;
43 	const(ubyte)[] CBlob;
44 
45 	typeof(null) Null;
46 	bool Bit;
47 	ubyte UByte;
48 	byte Byte;
49 	ushort UShort;
50 	short Short;
51 	uint UInt;
52 	int Int;
53 	ulong ULong;
54 	long Long;
55 	float Float;
56 	double Double;
57 	.DateTime DateTime;
58 	TimeOfDay Time;
59 	.Timestamp Timestamp;
60 	.Date Date;
61 
62 	@disableIndex string Text;
63 	@disableIndex const(char)[] CText;
64 
65 	// pointers
66 	const(bool)* BitRef;
67 	const(ubyte)* UByteRef;
68 	const(byte)* ByteRef;
69 	const(ushort)* UShortRef;
70 	const(short)* ShortRef;
71 	const(uint)* UIntRef;
72 	const(int)* IntRef;
73 	const(ulong)* ULongRef;
74 	const(long)* LongRef;
75 	const(float)* FloatRef;
76 	const(double)* DoubleRef;
77 	const(.DateTime)* DateTimeRef;
78 	const(TimeOfDay)* TimeRef;
79 	const(.Date)* DateRef;
80 	const(string)* TextRef;
81 	const(char[])* CTextRef;
82 	const(ubyte[])* BlobRef;
83 	const(.Timestamp)* TimestampRef;
84 }
85 
86 /++
87 MySQLVal is mysql-native's tagged algebraic type that supports only @safe usage
88 (see $(LINK2 http://code.dlang.org/packages/taggedalgebraic, TaggedAlgebraic)
89 for more information on the features of this type). Note that TaggedAlgebraic
90 has UFCS methods that are not available without importing that module in your
91 code.
92 
93 The type can hold any possible type that MySQL can use or return. The _MYTYPE
94 union, which is a private union for the project, defines the names of the types
95 that can be stored. These names double as the names for the MySQLVal.Kind
96 enumeration. To that end, this is the entire union definition:
97 
98 ------
99 private union _MYTYPE
100 {
101 	ubyte[] Blob;
102 	const(ubyte)[] CBlob;
103 
104 	typeof(null) Null;
105 	bool Bit;
106 	ubyte UByte;
107 	byte Byte;
108 	ushort UShort;
109 	short Short;
110 	uint UInt;
111 	int Int;
112 	ulong ULong;
113 	long Long;
114 	float Float;
115 	double Double;
116 	std.datetime.DateTime DateTime;
117 	std.datetime.TimeOfDay Time;
118 	mysql.types.Timestamp Timestamp;
119 	std.datetime.Date Date;
120 
121 	string Text;
122 	const(char)[] CText;
123 
124 	// pointers
125 	const(bool)* BitRef;
126 	const(ubyte)* UByteRef;
127 	const(byte)* ByteRef;
128 	const(ushort)* UShortRef;
129 	const(short)* ShortRef;
130 	const(uint)* UIntRef;
131 	const(int)* IntRef;
132 	const(ulong)* ULongRef;
133 	const(long)* LongRef;
134 	const(float)* FloatRef;
135 	const(double)* DoubleRef;
136 	const(DateTime)* DateTimeRef;
137 	const(TimeOfDay)* TimeRef;
138 	const(Date)* DateRef;
139 	const(string)* TextRef;
140 	const(char[])* CTextRef;
141 	const(ubyte[])* BlobRef;
142 	const(Timestamp)* TimestampRef;
143 }
144 ------
145 
146 Note that the pointers are all const, as the only use case in mysql-native for them is as rebindable parameters to a Prepared struct.
147 
148 MySQLVal allows operations, field, and member function access for each of the supported types without unwrapping the MySQLVal value. For example:
149 
150 ------
151 import mysql.safe;
152 
153 // support for comparison is valid for any type that supports it
154 assert(conn.queryValue("SELECT COUNT(*) FROM sometable") > 20);
155 
156 // access members of supporting types without unwrapping or verifying type first
157 assert(conn.queryValue("SELECT updated_date FROM someTable WHERE id=5").year == 2020);
158 
159 // arithmetic is supported, return type may vary
160 auto val = conn.queryValue("SELECT some_integer FROM sometable WHERE id=5") + 100;
161 static assert(is(typeof(val) == MySQLVal));
162 assert(val.kind == MySQLVal.Kind.Int);
163 
164 // this will be a double and not a MySQLVal, because all types that support
165 // addition with a double result in a double.
166 auto val2 = conn.queryValue("SELECT some_float FROM sometable WHERE id=5") + 100.0;
167 static assert(is(typeof(val2) == double));
168 ------
169 
170 Note that per [TaggedAlgebraic's API](https://vibed.org/api/taggedalgebraic.taggedalgebraic/TaggedAlgebraic),
171 using operators or members of a MySQLVal that aren't valid for the currently
172 held type will throw an assertion error. If you wish to avoid this, and are not
173 sure of the actual type, first validate the type is as you expect using the
174 `kind` member.
175 
176 MySQLVal is used in all operations interally for mysql-native, and explicitly
177 for all safe API calls. Version 3.0.x and earlier of the mysql-native library
178 used Variant, so this module provides multiple shims to allow code to "just
179 work", and also provides conversion back to Variant.
180 
181 $(SAFE_MIGRATION)
182 +/
183 alias MySQLVal = TaggedAlgebraic!_MYTYPE;
184 
185 // helper to convert variants to MySQLVal. Used wherever variant is still used.
186 import std.variant : Variant;
187 package MySQLVal _toVal(Variant v)
188 {
189 	int x;
190 	// unfortunately, we need to use a giant switch. But hopefully people will stop using Variant, and this will go away.
191 	string ts = v.type.toString();
192 	bool isRef;
193 	if (ts[$-1] == '*')
194 	{
195 		ts.length = ts.length-1;
196 		isRef= true;
197 	}
198 
199 	import std.meta;
200 	import std.traits;
201 	import mysql.exceptions;
202 	alias BasicTypes = AliasSeq!(bool, byte, ubyte, short, ushort, int, uint, long, ulong, float, double, DateTime, TimeOfDay, Date, Timestamp);
203 	alias ArrayTypes = AliasSeq!(char[], const(char)[], ubyte[], const(ubyte)[], immutable(ubyte)[]);
204 	switch (ts)
205 	{
206 		static foreach(Type; BasicTypes)
207 		{
208 		case fullyQualifiedName!Type:
209 		case "const(" ~ fullyQualifiedName!Type ~ ")":
210 		case "immutable(" ~ fullyQualifiedName!Type ~ ")":
211 		case "shared(immutable(" ~ fullyQualifiedName!Type ~ "))":
212 			if(isRef)
213 				return MySQLVal(v.get!(const(Type*)));
214 			else
215 				return MySQLVal(v.get!(const(Type)));
216 		}
217 		static foreach(Type; ArrayTypes)
218 		{
219 		case Type.stringof:
220 			{
221 				alias ET = Unqual!(typeof(Type.init[0]));
222 				if(isRef)
223 					return MySQLVal(v.get!(const(ET[]*)));
224 				else
225 					return MySQLVal(v.get!(Type));
226 			}
227 		}
228 	case "immutable(char)[]":
229 		// have to do this separately, because everything says "string" but
230 		// Variant says "immutable(char)[]"
231 		if(isRef)
232 			return MySQLVal(v.get!(const(char[]*)));
233 		else
234 			return MySQLVal(v.get!(string));
235 	case "typeof(null)":
236 		return MySQLVal(null);
237 	default:
238 		throw new MYX("Unsupported Database Variant Type: " ~ ts);
239 	}
240 }
241 
242 /++
243 Convert a MySQLVal into a Variant. This provides a backwards-compatible shim to use if necessary when transitioning to the safe API.
244 
245 $(SAFE_MIGRATION)
246 +/
247 Variant asVariant(MySQLVal v)
248 {
249 	return v.apply!((a) => Variant(a));
250 }
251 
252 /// ditto
253 Nullable!Variant asVariant(Nullable!MySQLVal v)
254 {
255 	if(v.isNull)
256 		return Nullable!Variant();
257 	return Nullable!Variant(v.get.asVariant);
258 }
259 
260 /++
261 Compatibility layer for MySQLVal. These functions provide methods that
262 $(LINK2 http://code.dlang.org/packages/taggedalgebraic, TaggedAlgebraic)
263 does not provide in order to keep functionality that was available with Variant.
264 
265 Notes:
266 
267 The `type` shim should be avoided in favor of using the `kind` property of
268 TaggedAlgebraic.
269 
270 The `get` shim works differently than the TaggedAlgebraic version, as the
271 Variant get function would provide implicit type conversions, but the
272 TaggedAlgebraic version does not.
273 
274 All shims other than `type` will likely remain as convenience features.
275 
276 Note that `peek` is inferred @system because it returns a pointer to the
277 provided value.
278 
279 $(SAFE_MIGRATION)
280 +/
281 bool convertsTo(T)(ref MySQLVal val)
282 {
283 	return val.apply!((a) => is(typeof(a) : T));
284 }
285 
286 /// ditto
287 T get(T)(auto ref MySQLVal val)
288 {
289 	static T convert(V)(ref V v)
290 	{
291 		static if(is(V : T))
292 			return v;
293 		else
294 		{
295 			import mysql.exceptions;
296 			throw new MYX("Cannot get type " ~ T.stringof ~ " from MySQLVal storing type " ~ V.stringof);
297 		}
298 	}
299 	return val.apply!convert();
300 }
301 
302 /// ditto
303 T coerce(T)(auto ref MySQLVal val)
304 {
305 	import std.conv : to;
306 	static T convert(V)(ref V v)
307 	{
308 		static if(is(V : T))
309 		{
310 			return v;
311 		}
312 		else static if(is(typeof(v.to!T())))
313 		{
314 			return v.to!T;
315 		}
316 		else
317 		{
318 			import mysql.exceptions;
319 			throw new MYX("Cannot coerce type " ~ V.stringof ~ " into type " ~ T.stringof);
320 		}
321 	}
322 	return val.apply!convert();
323 }
324 
325 /// ditto
326 TypeInfo type(MySQLVal val) @safe pure nothrow
327 {
328 	return val.apply!((ref v) => typeid(v));
329 }
330 
331 /// ditto
332 T *peek(T)(ref MySQLVal val)
333 {
334 	// use exact type.
335 	import taggedalgebraic.taggedalgebraic : get;
336 	if(val.hasType!T)
337 		return &val.get!T;
338 	return null;
339 }