1 module mysql.protocol.extra_types;
2 
3 import std.algorithm;
4 import std.conv;
5 import std.datetime;
6 import std.digest.sha;
7 import std.exception;
8 import std.range;
9 import std.socket;
10 import std.stdio;
11 import std.string;
12 import std.traits;
13 import std.variant;
14 
15 import mysql.common;
16 
17 /++
18 A simple struct to represent time difference.
19 
20 D's std.datetime does not have a type that is closely compatible with the MySQL
21 interpretation of a time difference, so we define a struct here to hold such
22 values.
23 +/
24 struct TimeDiff
25 {
26 	bool negative;
27 	int days;
28 	ubyte hours, minutes, seconds;
29 }
30 
31 /++
32 A D struct to stand for a TIMESTAMP
33 
34 It is assumed that insertion of TIMESTAMP values will not be common, since in general,
35 such columns are used for recording the time of a row insertion, and are filled in
36 automatically by the server. If you want to force a timestamp value in a prepared insert,
37 set it into a timestamp struct as an unsigned long in the format YYYYMMDDHHMMSS
38 and use that for the approriate parameter. When TIMESTAMPs are retrieved as part of
39 a result set it will be as DateTime structs.
40 +/
41 struct Timestamp
42 {
43 	ulong rep;
44 }
45 
46 struct SQLValue
47 {
48 	bool isNull;
49 	bool isIncomplete;
50 	Variant _value;
51 
52 	// empty template as a template and non-template won't be added to the same overload set
53 	@property inout(Variant) value()() inout
54 	{
55 		enforceEx!MYX(!isNull, "SQL value is null");
56 		enforceEx!MYX(!isIncomplete, "SQL value not complete");
57 		return _value;
58 	}
59 
60 	@property void value(T)(T value)
61 	{
62 		enforceEx!MYX(!isNull, "SQL value is null");
63 		enforceEx!MYX(!isIncomplete, "SQL value not complete");
64 		_value = value;
65 	}
66 
67 	pure const nothrow invariant()
68 	{
69 		isNull && assert(!isIncomplete);
70 		isIncomplete && assert(!isNull);
71 	}
72 }
73 
74 
75 /// Length Coded Binary Value
76 struct LCB
77 {
78 	/// True if the LCB contains a null value
79 	bool isNull;
80 
81 	/// True if the packet that created this LCB didn't have enough bytes
82 	/// to store a value of the size specified. More bytes have to be fetched from the server
83 	bool isIncomplete;
84 
85 	/// Number of bytes needed to store the value (Extracted from the LCB header. The header byte is not included)
86 	ubyte numBytes;
87 
88 	/// Number of bytes total used for this LCB
89 	@property ubyte totalBytes() pure const nothrow
90 	{
91 		return cast(ubyte)(numBytes <= 1 ? 1 : numBytes+1);
92 	}
93 
94 	/// The decoded value. This is always 0 if isNull or isIncomplete is set.
95 	ulong value;
96 
97 	pure const nothrow invariant()
98 	{
99 		if(isIncomplete)
100 		{
101 			assert(!isNull);
102 			assert(value == 0);
103 			assert(numBytes > 0);
104 		}
105 		else if(isNull)
106 		{
107 			assert(!isIncomplete);
108 			assert(value == 0);
109 			assert(numBytes == 0);
110 		}
111 		else
112 		{
113 			assert(!isNull);
114 			assert(!isIncomplete);
115 			assert(numBytes > 0);
116 		}
117 	}
118 }
119 
120 /// Length Coded String
121 struct LCS
122 {
123 	// dummy struct just to tell what value we are using
124 	// we don't need to store anything here as the result is always a string
125 }
126 
127 /++
128 A struct to represent specializations of prepared statement parameters.
129 
130 Strongly considering the removal of the isNull field, now that Prepared
131 can handle `null` as a value just fine.
132 
133 There are two specializations. First you can set an isNull flag to indicate that the
134 parameter is to have the SQL NULL value.
135 
136 Second, if you need to send large objects to the database it might be convenient to
137 send them in pieces. These two variables allow for this. If both are provided
138 then the corresponding column will be populated by calling the delegate repeatedly.
139 the source should fill the indicated slice with data and arrange for the delegate to
140 return the length of the data supplied. Af that is less than the chunkSize
141 then the chunk will be assumed to be the last one.
142 +/
143 struct ParameterSpecialization
144 {
145 	import mysql.protocol.constants;
146 	
147 	size_t pIndex;    //parameter number 0 - number of params-1
148 	SQLType type = SQLType.INFER_FROM_D_TYPE;
149 	uint chunkSize;
150 	uint delegate(ubyte[]) chunkDelegate;
151 }
152 alias PSN = ParameterSpecialization;
153 
154 /++
155 A struct to represent specializations of prepared statement parameters.
156 
157 If you are executing a query that will include result columns that are large objects
158 it may be expedient to deal with the data as it is received rather than first buffering
159 it to some sort of byte array. These two variables allow for this. If both are provided
160 then the corresponding column will be fed to the stipulated delegate in chunks of
161 chunkSize, with the possible exception of the last chunk, which may be smaller.
162 The 'finished' argument will be set to true when the last chunk is set.
163 
164 Be aware when specifying types for column specializations that for some reason the
165 field descriptions returned for a resultset have all of the types TINYTEXT, MEDIUMTEXT,
166 TEXT, LONGTEXT, TINYBLOB, MEDIUMBLOB, BLOB, and LONGBLOB lumped as type 0xfc
167 contrary to what it says in the protocol documentation.
168 +/
169 struct ColumnSpecialization
170 {
171 	size_t  cIndex;    // parameter number 0 - number of params-1
172 	ushort  type;
173 	uint    chunkSize;
174 	void delegate(const(ubyte)[] chunk, bool finished) chunkDelegate;
175 }
176 alias CSN = ColumnSpecialization;
177 
178 /// A struct to hold column metadata
179 struct ColumnInfo
180 {
181 	/// The database that the table having this column belongs to.
182 	string schema;
183 	/// The table that this column belongs to.
184 	string table;
185 	/// The name of the column.
186 	string name;
187 	/// Zero based index of the column within a table row.
188 	size_t index;
189 	/// Is the default value NULL?
190 	bool defaultNull;
191 	/// The default value as a string if not NULL
192 	string defaultValue;
193 	/// Can the column value be set to NULL
194 	bool nullable;
195 	/// What type is the column - tinyint, char, varchar, blob, date etc
196 	string type;
197 	/// Capacity in characters, -1L if not applicable
198 	long charsMax;
199 	/// Capacity in bytes - same as chars if not a unicode table definition, -1L if not applicable.
200 	long octetsMax;
201 	/// Presentation information for numerics, -1L if not applicable.
202 	short numericPrecision;
203 	/// Scale information for numerics or NULL, -1L if not applicable.
204 	short numericScale;
205 	/// Character set, "<NULL>" if not applicable.
206 	string charSet;
207 	/// Collation, "<NULL>" if not applicable.
208 	string collation;
209 	/// More detail about the column type, e.g. "int(10) unsigned".
210 	string colType;
211 	/// Information about the column's key status, blank if none.
212 	string key;
213 	/// Extra information.
214 	string extra;
215 	/// Privileges for logged in user.
216 	string privileges;
217 	/// Any comment that was set at table definition time.
218 	string comment;
219 }
220 
221 /// A struct to hold stored function metadata
222 struct MySQLProcedure
223 {
224 	string db;
225 	string name;
226 	string type;
227 	string definer;
228 	DateTime modified;
229 	DateTime created;
230 	string securityType;
231 	string comment;
232 	string charSetClient;
233 	string collationConnection;
234 	string collationDB;
235 }
236 
237 /++
238 Facilities to recover meta-data from a connection
239 
240 It is important to bear in mind that the methods provided will only return the
241 information that is available to the connected user. This may well be quite limited.
242 +/
243 struct MetaData
244 {
245 	import mysql.connection;
246 	
247 private:
248 	Connection _con;
249 
250 	MySQLProcedure[] stored(bool procs)
251 	{
252 		enforceEx!MYX(_con.currentDB.length, "There is no selected database");
253 		string query = procs ? "SHOW PROCEDURE STATUS WHERE db='": "SHOW FUNCTION STATUS WHERE db='";
254 		query ~= _con.currentDB ~ "'";
255 
256 		auto cmd = Command(_con, query);
257 		auto rs = cmd.execSQLResult();
258 		MySQLProcedure[] pa;
259 		pa.length = rs.length;
260 		foreach (size_t i; 0..rs.length)
261 		{
262 			MySQLProcedure foo;
263 			Row r = rs[i];
264 			foreach (int j; 0..11)
265 			{
266 				if (r.isNull(j))
267 					continue;
268 				auto value = r[j].toString();
269 				switch (j)
270 				{
271 					case 0:
272 						foo.db = value;
273 						break;
274 					case 1:
275 						foo.name = value;
276 						break;
277 					case 2:
278 						foo.type = value;
279 						break;
280 					case 3:
281 						foo.definer = value;
282 						break;
283 					case 4:
284 						foo.modified = r[j].get!(DateTime);
285 						break;
286 					case 5:
287 						foo.created = r[j].get!(DateTime);
288 						break;
289 					case 6:
290 						foo.securityType = value;
291 						break;
292 					case 7:
293 						foo.comment = value;
294 						break;
295 					case 8:
296 						foo.charSetClient = value;
297 						break;
298 					case 9:
299 						foo.collationConnection = value;
300 						break;
301 					case 10:
302 						foo.collationDB = value;
303 						break;
304 					default:
305 						assert(0);
306 				}
307 			}
308 			pa[i] = foo;
309 		}
310 		return pa;
311 	}
312 
313 public:
314 	this(Connection con)
315 	{
316 		_con = con;
317 	}
318 
319 	/++
320 	List the available databases
321 	
322 	Note that if you have connected using the credentials of a user with
323 	limited permissions you may not get many results.
324 	
325 	Returns:
326 		An array of strings
327 	+/
328 	string[] databases()
329 	{
330 		auto cmd = Command(_con, "SHOW DATABASES");
331 		auto rs = cmd.execSQLResult();
332 		string[] dbNames;
333 		dbNames.length = rs.length;
334 		foreach (size_t i; 0..rs.length)
335 			dbNames[i] = rs[i][0].toString();
336 		return dbNames;
337 	}
338 
339 	/++
340 	List the tables in the current database
341 	
342 	Returns:
343 		An array of strings
344 	+/
345 	string[] tables()
346 	{
347 		auto cmd = Command(_con, "SHOW TABLES");
348 		auto rs = cmd.execSQLResult();
349 		string[] tblNames;
350 		tblNames.length = rs.length;
351 		foreach (size_t i; 0..rs.length)
352 			tblNames[i] = rs[i][0].toString();
353 		return tblNames;
354 	}
355 
356 	/++
357 	Get column metadata for a table in the current database
358 	
359 	Params:
360 		table = The table name
361 	Returns:
362 		An array of ColumnInfo structs
363 	+/
364 	ColumnInfo[] columns(string table)
365 	{
366 		// Manually specify all fields to avoid problems when newer versions of
367 		// the server add or rearrange fields. (Issue #45)
368 		string query =
369 			"SELECT " ~
370 			" TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME," ~
371 			" COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT," ~
372 			" IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH," ~
373 			" CHARACTER_OCTET_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE," ~
374 			" CHARACTER_SET_NAME, COLLATION_NAME, COLUMN_TYPE," ~
375 			" COLUMN_KEY, EXTRA, PRIVILEGES, COLUMN_COMMENT" ~
376 			" FROM information_schema.COLUMNS WHERE" ~
377 			" table_schema='" ~ _con.currentDB ~ "' AND table_name='" ~ table ~ "'";
378 		auto cmd = Command(_con, query);
379 		auto rs = cmd.execSQLResult();
380 		ColumnInfo[] ca;
381 		ca.length = rs.length;
382 		foreach (size_t i; 0..rs.length)
383 		{
384 			ColumnInfo col;
385 			Row r = rs[i];
386 			for (int j = 1; j < 19; j++)
387 			{
388 				string t;
389 				bool isNull = r.isNull(j);
390 				if (!isNull)
391 					t = to!string(r[j]);
392 				switch (j)
393 				{
394 					case 1:
395 						col.schema = t;
396 						break;
397 					case 2:
398 						col.table = t;
399 						break;
400 					case 3:
401 						col.name = t;
402 						break;
403 					case 4:
404 						if(isNull)
405 							col.index = -1;
406 						else
407 							col.index = cast(size_t)(r[j].coerce!ulong() - 1);
408 						//col.index = cast(size_t)(r[j].get!(ulong)-1);
409 						break;
410 					case 5:
411 						if (isNull)
412 							col.defaultNull = true;
413 						else
414 							col.defaultValue = t;
415 						break;
416 					case 6:
417 						if (t == "YES")
418 						col.nullable = true;
419 						break;
420 					case 7:
421 						col.type = t;
422 						break;
423 					case 8:
424 						col.charsMax = cast(long)(isNull? -1L: r[j].coerce!(ulong));
425 						break;
426 					case 9:
427 						col.octetsMax = cast(long)(isNull? -1L: r[j].coerce!(ulong));
428 						break;
429 					case 10:
430 						col.numericPrecision = cast(short) (isNull? -1: r[j].coerce!(ulong));
431 						break;
432 					case 11:
433 						col.numericScale = cast(short) (isNull? -1: r[j].coerce!(ulong));
434 						break;
435 					case 12:
436 						col.charSet = isNull? "<NULL>": t;
437 						break;
438 					case 13:
439 						col.collation = isNull? "<NULL>": t;
440 						break;
441 					case 14:
442 						col.colType = r[j].get!string();
443 						break;
444 					case 15:
445 						col.key = t;
446 						break;
447 					case 16:
448 						col.extra = t;
449 						break;
450 					case 17:
451 						col.privileges = t;
452 						break;
453 					case 18:
454 						col.comment = t;
455 						break;
456 					default:
457 						break;
458 				}
459 			}
460 			ca[i] = col;
461 		}
462 		return ca;
463 	}
464 
465 	/// Get list of stored functions in the current database, and their properties
466 	MySQLProcedure[] functions()
467 	{
468 		return stored(false);
469 	}
470 
471 	/// Get list of stored procedures in the current database, and their properties
472 	MySQLProcedure[] procedures()
473 	{
474 		return stored(true);
475 	}
476 }