1 /++
2 Use a DB via plain SQL statements.
3 
4 Commands that are expected to return a result set - queries - have distinctive
5 methods that are enforced. That is it will be an error to call such a method
6 with an SQL command that does not produce a result set. So for commands like
7 SELECT, use the `query` functions. For other commands, like
8 INSERT/UPDATE/CREATE/etc, use `exec`.
9 +/
10 
11 module mysql.commands;
12 
13 import std.conv;
14 import std.exception;
15 import std.range;
16 import std.typecons;
17 import std.variant;
18 
19 import mysql.connection;
20 import mysql.exceptions;
21 import mysql.prepared;
22 import mysql.protocol.constants;
23 import mysql.protocol.extra_types;
24 import mysql.protocol.packets;
25 import mysql.result;
26 
27 /// This feature is not yet implemented. It currently has no effect.
28 /+
29 A struct to represent specializations of returned statement columns.
30 
31 If you are executing a query that will include result columns that are large objects,
32 it may be expedient to deal with the data as it is received rather than first buffering
33 it to some sort of byte array. These two variables allow for this. If both are provided
34 then the corresponding column will be fed to the stipulated delegate in chunks of
35 `chunkSize`, with the possible exception of the last chunk, which may be smaller.
36 The bool argument `finished` will be set to true when the last chunk is set.
37 
38 Be aware when specifying types for column specializations that for some reason the
39 field descriptions returned for a resultset have all of the types TINYTEXT, MEDIUMTEXT,
40 TEXT, LONGTEXT, TINYBLOB, MEDIUMBLOB, BLOB, and LONGBLOB lumped as type 0xfc
41 contrary to what it says in the protocol documentation.
42 +/
43 //TODO: I'm not sure this is tested
44 struct ColumnSpecialization
45 {
46 	size_t  cIndex;    // parameter number 0 - number of params-1
47 	ushort  type;
48 	uint    chunkSize;
49 	void delegate(const(ubyte)[] chunk, bool finished) chunkDelegate;
50 }
51 ///ditto
52 alias CSN = ColumnSpecialization;
53 
54 package struct ExecQueryImplInfo
55 {
56 	bool isPrepared;
57 
58 	// For non-prepared statements:
59 	string sql;
60 
61 	// For prepared statements:
62 	uint hStmt;
63 	PreparedStmtHeaders psh;
64 	Variant[] inParams;
65 	ParameterSpecialization[] psa;
66 }
67 
68 /++
69 Internal implementation for the exec and query functions.
70 
71 Execute a one-off SQL command.
72 
73 Any result set can be accessed via Connection.getNextRow(), but you should really be
74 using the query function for such queries.
75 
76 Params: ra = An out parameter to receive the number of rows affected.
77 Returns: true if there was a (possibly empty) result set.
78 +/
79 //TODO: All low-level commms should be moved into the mysql.protocol package.
80 package bool execQueryImpl(Connection conn, ExecQueryImplInfo info, out ulong ra)
81 {
82 	scope(failure) conn.kill();
83 
84 	// Send data
85 	if(info.isPrepared)
86 		ProtocolPrepared.sendCommand(conn, info.hStmt, info.psh, info.inParams, info.psa);
87 	else
88 	{
89 		conn.sendCmd(CommandType.QUERY, info.sql);
90 		conn._fieldCount = 0;
91 	}
92 
93 	// Handle response
94 	ubyte[] packet = conn.getPacket();
95 	bool rv;
96 	if (packet.front == ResultPacketMarker.ok || packet.front == ResultPacketMarker.error)
97 	{
98 		conn.resetPacket();
99 		auto okp = OKErrorPacket(packet);
100 		enforcePacketOK(okp);
101 		ra = okp.affected;
102 		conn._serverStatus = okp.serverStatus;
103 		conn._insertID = okp.insertID;
104 		rv = false;
105 	}
106 	else
107 	{
108 		// There was presumably a result set
109 		assert(packet.front >= 1 && packet.front <= 250); // Result set packet header should have this value
110 		conn._headersPending = conn._rowsPending = true;
111 		conn._binaryPending = info.isPrepared;
112 		auto lcb = packet.consumeIfComplete!LCB();
113 		assert(!lcb.isNull);
114 		assert(!lcb.isIncomplete);
115 		conn._fieldCount = cast(ushort)lcb.value;
116 		assert(conn._fieldCount == lcb.value);
117 		rv = true;
118 		ra = 0;
119 	}
120 	return rv;
121 }
122 
123 ///ditto
124 package bool execQueryImpl(Connection conn, ExecQueryImplInfo info)
125 {
126 	ulong rowsAffected;
127 	return execQueryImpl(conn, info, rowsAffected);
128 }
129 
130 /++
131 Execute an SQL command or prepared statement, such as INSERT/UPDATE/CREATE/etc.
132 
133 This method is intended for commands such as which do not produce a result set
134 (otherwise, use one of the `query` functions instead.) If the SQL command does
135 produces a result set (such as SELECT), `mysql.exceptions.MYXResultRecieved`
136 will be thrown.
137 
138 Only use the `string sql` overload when you are not going to be using the same
139 command repeatedly and you are CERTAIN all the data you're sending is properly
140 escaped. Otherwise, consider using overload that takes a `Prepared`.
141 
142 Type_Mappings: $(TYPE_MAPPINGS)
143 
144 Params:
145 conn = An open `mysql.connection.Connection` to the database.
146 sql = The SQL command to be run.
147 prepared = The prepared statement to be run.
148 
149 Returns: The number of rows affected.
150 +/
151 ulong exec(Connection conn, string sql)
152 {
153 	return execImpl(conn, ExecQueryImplInfo(false, sql));
154 }
155 
156 ///ditto
157 ulong exec(Connection conn, ref Prepared prepared)
158 {
159 	auto preparedInfo = conn.registerIfNeeded(prepared.sql);
160 	auto ra = execImpl(conn, prepared.getExecQueryImplInfo(preparedInfo.statementId));
161 	prepared._lastInsertID = conn.lastInsertID;
162 	return ra;
163 }
164 
165 ///ditto
166 ulong exec(Connection conn, ref BackwardCompatPrepared prepared)
167 {
168 	auto p = prepared.prepared;
169 	auto result = exec(conn, p);
170 	prepared._prepared = p;
171 	return result;
172 }
173 
174 /// Common implementation for `exec` overloads
175 package ulong execImpl(Connection conn, ExecQueryImplInfo info)
176 {
177 	ulong rowsAffected;
178 	bool receivedResultSet = execQueryImpl(conn, info, rowsAffected);
179 	if(receivedResultSet)
180 	{
181 		conn.purgeResult();
182 		throw new MYXResultRecieved();
183 	}
184 
185 	return rowsAffected;
186 }
187 
188 /++
189 Execute an SQL SELECT command or prepared statement.
190 
191 This returns an input range of `mysql.result.Row`, so if you need random access
192 to the `mysql.result.Row` elements, simply call
193 $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`)
194 on the result.
195 
196 If the SQL command does not produce a result set (such as INSERT/CREATE/etc),
197 then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use
198 `exec` instead for such commands.
199 
200 Only use the `string sql` overload when you are not going to be using the same
201 command repeatedly and you are CERTAIN all the data you're sending is properly
202 escaped. Otherwise, consider using overload that takes a `Prepared`.
203 
204 If there are long data items among the expected result columns you can use
205 the `csa` param to specify that they are to be subject to chunked transfer via a
206 delegate.
207 
208 Type_Mappings: $(TYPE_MAPPINGS)
209 
210 Params:
211 conn = An open `mysql.connection.Connection` to the database.
212 sql = The SQL command to be run.
213 prepared = The prepared statement to be run.
214 csa = Not yet implemented.
215 
216 Returns: A (possibly empty) `mysql.result.ResultRange`.
217 
218 Example:
219 ---
220 ResultRange oneAtATime = myConnection.query("SELECT * from myTable");
221 Row[]       allAtOnce  = myConnection.query("SELECT * from myTable").array;
222 ---
223 +/
224 /+
225 Future text:
226 If there are long data items among the expected result columns you can use
227 the `csa` param to specify that they are to be subject to chunked transfer via a
228 delegate.
229 
230 csa = An optional array of `ColumnSpecialization` structs. If you need to
231 use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`.
232 +/
233 ResultRange query(Connection conn, string sql, ColumnSpecialization[] csa = null)
234 {
235 	return queryImpl(csa, conn, ExecQueryImplInfo(false, sql));
236 }
237 
238 ///ditto
239 ResultRange query(Connection conn, ref Prepared prepared)
240 {
241 	auto preparedInfo = conn.registerIfNeeded(prepared.sql);
242 	auto result = queryImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId));
243 	prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled.
244 	return result;
245 }
246 
247 ///ditto
248 ResultRange query(Connection conn, ref BackwardCompatPrepared prepared)
249 {
250 	auto p = prepared.prepared;
251 	auto result = query(conn, p);
252 	prepared._prepared = p;
253 	return result;
254 }
255 
256 /// Common implementation for `query` overloads
257 package ResultRange queryImpl(ColumnSpecialization[] csa,
258 	Connection conn, ExecQueryImplInfo info)
259 {
260 	ulong ra;
261 	enforceEx!MYXNoResultRecieved(execQueryImpl(conn, info, ra));
262 
263 	conn._rsh = ResultSetHeaders(conn, conn._fieldCount);
264 	if (csa !is null)
265 		conn._rsh.addSpecializations(csa);
266 
267 	conn._headersPending = false;
268 	return ResultRange(conn, conn._rsh, conn._rsh.fieldNames);
269 }
270 
271 /++
272 Execute an SQL SELECT command or prepared statement where you only want the
273 first `mysql.result.Row`, if any.
274 
275 If the SQL command does not produce a result set (such as INSERT/CREATE/etc),
276 then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use
277 `exec` instead for such commands.
278 
279 Only use the `string sql` overload when you are not going to be using the same
280 command repeatedly and you are CERTAIN all the data you're sending is properly
281 escaped. Otherwise, consider using overload that takes a `Prepared`.
282 
283 If there are long data items among the expected result columns you can use
284 the `csa` param to specify that they are to be subject to chunked transfer via a
285 delegate.
286 
287 Type_Mappings: $(TYPE_MAPPINGS)
288 
289 Params:
290 conn = An open `mysql.connection.Connection` to the database.
291 sql = The SQL command to be run.
292 prepared = The prepared statement to be run.
293 csa = Not yet implemented.
294 
295 Returns: `Nullable!(mysql.result.Row)`: This will be null (check via `Nullable.isNull`) if the
296 query resulted in an empty result set.
297 +/
298 /+
299 Future text:
300 If there are long data items among the expected result columns you can use
301 the `csa` param to specify that they are to be subject to chunked transfer via a
302 delegate.
303 
304 csa = An optional array of `ColumnSpecialization` structs. If you need to
305 use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`.
306 +/
307 Nullable!Row queryRow(Connection conn, string sql, ColumnSpecialization[] csa = null)
308 {
309 	return queryRowImpl(csa, conn, ExecQueryImplInfo(false, sql));
310 }
311 
312 ///ditto
313 Nullable!Row queryRow(Connection conn, ref Prepared prepared)
314 {
315 	auto preparedInfo = conn.registerIfNeeded(prepared.sql);
316 	auto result = queryRowImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId));
317 	prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled.
318 	return result;
319 }
320 
321 ///ditto
322 Nullable!Row queryRow(Connection conn, ref BackwardCompatPrepared prepared)
323 {
324 	auto p = prepared.prepared;
325 	auto result = queryRow(conn, p);
326 	prepared._prepared = p;
327 	return result;
328 }
329 
330 /// Common implementation for `querySet` overloads.
331 package Nullable!Row queryRowImpl(ColumnSpecialization[] csa, Connection conn,
332 	ExecQueryImplInfo info)
333 {
334 	auto results = queryImpl(csa, conn, info);
335 	if(results.empty)
336 		return Nullable!Row();
337 	else
338 	{
339 		auto row = results.front;
340 		results.close();
341 		return Nullable!Row(row);
342 	}
343 }
344 
345 /++
346 Execute an SQL SELECT command or prepared statement where you only want the
347 first `mysql.result.Row`, and place result values into a set of D variables.
348 
349 This method will throw if any column type is incompatible with the corresponding D variable.
350 
351 Unlike the other query functions, queryRowTuple will throw
352 `mysql.exceptions.MYX` if the result set is empty
353 (and thus the reference variables passed in cannot be filled).
354 
355 If the SQL command does not produce a result set (such as INSERT/CREATE/etc),
356 then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use
357 `exec` instead for such commands.
358 
359 Only use the `string sql` overload when you are not going to be using the same
360 command repeatedly and you are CERTAIN all the data you're sending is properly
361 escaped. Otherwise, consider using overload that takes a `Prepared`.
362 
363 Type_Mappings: $(TYPE_MAPPINGS)
364 
365 Params:
366 conn = An open `mysql.connection.Connection` to the database.
367 sql = The SQL command to be run.
368 prepared = The prepared statement to be run.
369 args = The variables, taken by reference, to receive the values.
370 +/
371 void queryRowTuple(T...)(Connection conn, string sql, ref T args)
372 {
373 	return queryRowTupleImpl(conn, ExecQueryImplInfo(false, sql), args);
374 }
375 
376 ///ditto
377 void queryRowTuple(T...)(Connection conn, ref Prepared prepared, ref T args)
378 {
379 	auto preparedInfo = conn.registerIfNeeded(prepared.sql);
380 	queryRowTupleImpl(conn, prepared.getExecQueryImplInfo(preparedInfo.statementId), args);
381 	prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled.
382 }
383 
384 ///ditto
385 void queryRowTuple(T...)(Connection conn, ref BackwardCompatPrepared prepared, ref T args)
386 {
387 	auto p = prepared.prepared;
388 	queryRowTuple(conn, p, args);
389 	prepared._prepared = p;
390 }
391 
392 /// Common implementation for `queryRowTuple` overloads.
393 package void queryRowTupleImpl(T...)(Connection conn, ExecQueryImplInfo info, ref T args)
394 {
395 	ulong ra;
396 	enforceEx!MYXNoResultRecieved(execQueryImpl(conn, info, ra));
397 
398 	Row rr = conn.getNextRow();
399 	/+if (!rr._valid)   // The result set was empty - not a crime.
400 		return;+/
401 	enforceEx!MYX(rr._values.length == args.length, "Result column count does not match the target tuple.");
402 	foreach (size_t i, dummy; args)
403 	{
404 		enforceEx!MYX(typeid(args[i]).toString() == rr._values[i].type.toString(),
405 			"Tuple "~to!string(i)~" type and column type are not compatible.");
406 		args[i] = rr._values[i].get!(typeof(args[i]));
407 	}
408 	// If there were more rows, flush them away
409 	// Question: Should I check in purgeResult and throw if there were - it's very inefficient to
410 	// allow sloppy SQL that does not ensure just one row!
411 	conn.purgeResult();
412 }
413 
414 // Test what happends when queryRowTuple receives no rows
415 debug(MYSQLN_TESTS)
416 unittest
417 {
418 	import mysql.prepared;
419 	import mysql.test.common : scopedCn, createCn;
420 	mixin(scopedCn);
421 
422 	cn.exec("DROP TABLE IF EXISTS `queryRowTuple`");
423 	cn.exec("CREATE TABLE `queryRowTuple` (
424 		`val` INTEGER
425 	) ENGINE=InnoDB DEFAULT CHARSET=utf8");
426 
427 	immutable selectSQL = "SELECT * FROM `queryRowTuple`";
428 	int queryTupleResult;
429 	assertThrown!MYX(cn.queryRowTuple(selectSQL, queryTupleResult));
430 }
431 
432 /++
433 Execute an SQL SELECT command or prepared statement and return a single value:
434 the first column of the first row received.
435 
436 If the query did not produce any rows, or the rows it produced have zero columns,
437 this will return `Nullable!Variant()`, ie, null. Test for this with `result.isNull`.
438 
439 If the query DID produce a result, but the value actually received is NULL,
440 then `result.isNull` will be FALSE, and `result.get` will produce a Variant
441 which CONTAINS null. Check for this with `result.get.type == typeid(typeof(null))`.
442 
443 If the SQL command does not produce a result set (such as INSERT/CREATE/etc),
444 then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use
445 `exec` instead for such commands.
446 
447 Only use the `string sql` overload when you are not going to be using the same
448 command repeatedly and you are CERTAIN all the data you're sending is properly
449 escaped. Otherwise, consider using overload that takes a `Prepared`.
450 
451 If there are long data items among the expected result columns you can use
452 the `csa` param to specify that they are to be subject to chunked transfer via a
453 delegate.
454 
455 Type_Mappings: $(TYPE_MAPPINGS)
456 
457 Params:
458 conn = An open `mysql.connection.Connection` to the database.
459 sql = The SQL command to be run.
460 prepared = The prepared statement to be run.
461 csa = Not yet implemented.
462 
463 Returns: `Nullable!Variant`: This will be null (check via `Nullable.isNull`) if the
464 query resulted in an empty result set.
465 +/
466 /+
467 Future text:
468 If there are long data items among the expected result columns you can use
469 the `csa` param to specify that they are to be subject to chunked transfer via a
470 delegate.
471 
472 csa = An optional array of `ColumnSpecialization` structs. If you need to
473 use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`.
474 +/
475 Nullable!Variant queryValue(Connection conn, string sql, ColumnSpecialization[] csa = null)
476 {
477 	return queryValueImpl(csa, conn, ExecQueryImplInfo(false, sql));
478 }
479 
480 ///ditto
481 Nullable!Variant queryValue(Connection conn, ref Prepared prepared)
482 {
483 	auto preparedInfo = conn.registerIfNeeded(prepared.sql);
484 	auto result = queryValueImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId));
485 	prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled.
486 	return result;
487 }
488 
489 ///ditto
490 Nullable!Variant queryValue(Connection conn, ref BackwardCompatPrepared prepared)
491 {
492 	auto p = prepared.prepared;
493 	auto result = queryValue(conn, p);
494 	prepared._prepared = p;
495 	return result;
496 }
497 
498 /// Common implementation for `queryValue` overloads.
499 package Nullable!Variant queryValueImpl(ColumnSpecialization[] csa, Connection conn,
500 	ExecQueryImplInfo info)
501 {
502 	auto results = queryImpl(csa, conn, info);
503 	if(results.empty)
504 		return Nullable!Variant();
505 	else
506 	{
507 		auto row = results.front;
508 		results.close();
509 		
510 		if(row.length == 0)
511 			return Nullable!Variant();
512 		else
513 			return Nullable!Variant(row[0]);
514 	}
515 }