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