Mercurial > hg > orthanc-databases
annotate Framework/MySQL/MySQLStatement.cpp @ 44:95f0f57f8920
fix mainline version
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 17 Jul 2018 08:50:53 +0200 |
parents | b2ff1cd2907a |
children | 6a574d810b98 |
rev | line source |
---|---|
0 | 1 /** |
2 * Orthanc - A Lightweight, RESTful DICOM Store | |
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics | |
4 * Department, University Hospital of Liege, Belgium | |
5 * Copyright (C) 2017-2018 Osimis S.A., Belgium | |
6 * | |
7 * This program is free software: you can redistribute it and/or | |
8 * modify it under the terms of the GNU Affero General Public License | |
9 * as published by the Free Software Foundation, either version 3 of | |
10 * the License, or (at your option) any later version. | |
11 * | |
12 * This program is distributed in the hope that it will be useful, but | |
13 * WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
15 * Affero General Public License for more details. | |
16 * | |
17 * You should have received a copy of the GNU Affero General Public License | |
18 * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 **/ | |
20 | |
21 | |
22 #include "MySQLStatement.h" | |
23 | |
24 #include "../Common/BinaryStringValue.h" | |
25 #include "../Common/FileValue.h" | |
26 #include "../Common/Integer64Value.h" | |
27 #include "../Common/NullValue.h" | |
28 #include "../Common/Utf8StringValue.h" | |
29 #include "MySQLResult.h" | |
30 | |
31 #include <Core/Logging.h> | |
32 #include <Core/OrthancException.h> | |
33 | |
34 #include <list> | |
35 #include <memory> | |
36 | |
37 namespace OrthancDatabases | |
38 { | |
39 class MySQLStatement::ResultField : public boost::noncopyable | |
40 { | |
41 private: | |
42 IValue* CreateIntegerValue(MYSQL_BIND& bind) const | |
43 { | |
44 if (length_ != buffer_.size()) | |
45 { | |
46 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | |
47 } | |
48 | |
49 switch (mysqlType_) | |
50 { | |
51 case MYSQL_TYPE_TINY: | |
52 if (bind.is_unsigned) | |
53 { | |
54 return new Integer64Value(*reinterpret_cast<const uint8_t*>(&buffer_[0])); | |
55 } | |
56 else | |
57 { | |
58 return new Integer64Value(*reinterpret_cast<const int8_t*>(&buffer_[0])); | |
59 } | |
60 | |
61 case MYSQL_TYPE_SHORT: | |
62 if (bind.is_unsigned) | |
63 { | |
64 return new Integer64Value(*reinterpret_cast<const uint16_t*>(&buffer_[0])); | |
65 } | |
66 else | |
67 { | |
68 return new Integer64Value(*reinterpret_cast<const int16_t*>(&buffer_[0])); | |
69 } | |
70 | |
71 break; | |
72 | |
73 case MYSQL_TYPE_LONG: | |
74 if (bind.is_unsigned) | |
75 { | |
76 return new Integer64Value(*reinterpret_cast<const uint32_t*>(&buffer_[0])); | |
77 } | |
78 else | |
79 { | |
80 return new Integer64Value(*reinterpret_cast<const int32_t*>(&buffer_[0])); | |
81 } | |
82 | |
83 break; | |
84 | |
85 case MYSQL_TYPE_LONGLONG: | |
86 if (bind.is_unsigned) | |
87 { | |
88 uint64_t value = *reinterpret_cast<const uint64_t*>(&buffer_[0]); | |
89 if (static_cast<uint64_t>(static_cast<int64_t>(value)) != value) | |
90 { | |
91 LOG(WARNING) << "Overflow in a 64 bit integer"; | |
92 } | |
93 | |
94 return new Integer64Value(static_cast<int64_t>(value)); | |
95 } | |
96 else | |
97 { | |
98 return new Integer64Value(*reinterpret_cast<const int64_t*>(&buffer_[0])); | |
99 } | |
100 | |
101 break; | |
102 | |
103 default: | |
104 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | |
105 } | |
106 } | |
107 | |
108 | |
109 enum enum_field_types mysqlType_; | |
110 ValueType orthancType_; | |
111 std::string buffer_; | |
112 my_bool isNull_; | |
113 my_bool isError_; | |
114 unsigned long length_; | |
115 | |
116 | |
117 public: | |
118 ResultField(const MYSQL_FIELD& field) : | |
119 mysqlType_(field.type) | |
120 { | |
121 // https://dev.mysql.com/doc/refman/8.0/en/c-api-data-structures.html | |
122 // https://dev.mysql.com/doc/refman/8.0/en/mysql-stmt-fetch.html => size of "buffer_" | |
123 switch (field.type) | |
124 { | |
125 case MYSQL_TYPE_TINY: | |
126 orthancType_ = ValueType_Integer64; | |
127 buffer_.resize(1); | |
128 break; | |
129 | |
130 case MYSQL_TYPE_SHORT: | |
131 orthancType_ = ValueType_Integer64; | |
132 buffer_.resize(2); | |
133 break; | |
134 | |
135 case MYSQL_TYPE_LONG: | |
136 orthancType_ = ValueType_Integer64; | |
137 buffer_.resize(4); | |
138 break; | |
139 | |
140 case MYSQL_TYPE_LONGLONG: | |
141 orthancType_ = ValueType_Integer64; | |
142 buffer_.resize(8); | |
143 break; | |
144 | |
145 case MYSQL_TYPE_STRING: | |
146 case MYSQL_TYPE_VAR_STRING: | |
147 case MYSQL_TYPE_BLOB: | |
148 // https://medium.com/@adamhooper/in-mysql-never-use-utf8-use-utf8mb4-11761243e434 | |
149 switch (field.charsetnr) | |
150 { | |
151 case 45: // utf8mb4_general_ci | |
152 case 46: // utf8mb4_bin | |
153 case 224: // utf8mb4_unicode_ci => RECOMMENDED collation | |
154 // https://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci | |
155 orthancType_ = ValueType_Utf8String; | |
156 break; | |
157 | |
158 case 63: | |
159 orthancType_ = ValueType_BinaryString; | |
160 break; | |
161 | |
162 default: | |
163 LOG(ERROR) << "Unsupported MySQL charset: " << field.charsetnr; | |
164 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); | |
165 } | |
166 | |
167 if (field.max_length > 0) | |
168 { | |
169 buffer_.resize(field.max_length); | |
170 } | |
171 | |
172 break; | |
173 | |
174 default: | |
175 LOG(ERROR) << "MYSQL_TYPE not implemented: " << field.type; | |
176 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); | |
177 } | |
178 } | |
179 | |
180 enum enum_field_types GetMysqlType() const | |
181 { | |
182 return mysqlType_; | |
183 } | |
184 | |
185 ValueType GetOrthancType() const | |
186 { | |
187 return orthancType_; | |
188 } | |
189 | |
190 void PrepareBind(MYSQL_BIND& bind) | |
191 { | |
192 memset(&bind, 0, sizeof(bind)); | |
193 | |
9
eff482803d30
fix uninitialized values
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
0
diff
changeset
|
194 isNull_ = false; |
eff482803d30
fix uninitialized values
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
0
diff
changeset
|
195 isError_ = false; |
0 | 196 length_ = 0; |
197 | |
198 bind.buffer_length = buffer_.size(); | |
199 bind.buffer_type = mysqlType_; | |
200 bind.is_null = &isNull_; | |
201 bind.length = &length_; | |
202 | |
203 if (buffer_.empty()) | |
204 { | |
205 // Only fetches the actual size of the field (*): | |
206 // mysql_stmt_fetch_column() must be invoked afterward | |
207 bind.buffer = 0; | |
208 } | |
209 else | |
210 { | |
211 bind.buffer = &buffer_[0]; | |
212 bind.error = &isError_; | |
213 } | |
214 } | |
215 | |
216 | |
217 IValue* FetchValue(MySQLDatabase& database, | |
218 MYSQL_STMT& statement, | |
219 MYSQL_BIND& bind, | |
220 unsigned int column) const | |
221 { | |
222 if (isError_) | |
223 { | |
224 throw Orthanc::OrthancException(Orthanc::ErrorCode_Database); | |
225 } | |
226 else if (isNull_) | |
227 { | |
228 return new NullValue; | |
229 } | |
230 else if (orthancType_ == ValueType_Integer64) | |
231 { | |
232 return CreateIntegerValue(bind); | |
233 } | |
234 else if (orthancType_ == ValueType_Utf8String || | |
235 orthancType_ == ValueType_BinaryString) | |
236 { | |
237 std::string tmp; | |
238 tmp.resize(length_); | |
239 | |
240 if (!tmp.empty()) | |
241 { | |
242 if (buffer_.empty()) | |
243 { | |
244 bind.buffer = &tmp[0]; | |
245 bind.buffer_length = tmp.size(); | |
246 | |
247 database.CheckErrorCode(mysql_stmt_fetch_column(&statement, &bind, column, 0)); | |
248 } | |
249 else if (tmp.size() <= buffer_.size()) | |
250 { | |
251 memcpy(&tmp[0], &buffer_[0], length_); | |
252 } | |
253 else | |
254 { | |
255 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | |
256 } | |
257 } | |
258 | |
259 if (orthancType_ == ValueType_Utf8String) | |
260 { | |
261 return new Utf8StringValue(tmp); | |
262 } | |
263 else | |
264 { | |
265 return new BinaryStringValue(tmp); | |
266 } | |
267 } | |
268 else | |
269 { | |
270 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | |
271 } | |
272 } | |
273 }; | |
274 | |
275 | |
276 class MySQLStatement::ResultMetadata : public boost::noncopyable | |
277 { | |
278 private: | |
279 MYSQL_RES* metadata_; | |
280 | |
281 public: | |
282 ResultMetadata(MySQLDatabase& db, | |
283 MySQLStatement& statement) : | |
284 metadata_(NULL) | |
285 { | |
286 metadata_ = mysql_stmt_result_metadata(statement.GetObject()); | |
287 } | |
288 | |
289 ~ResultMetadata() | |
290 { | |
291 if (metadata_ != NULL) | |
292 { | |
293 mysql_free_result(metadata_); | |
294 } | |
295 } | |
296 | |
297 bool HasFields() const | |
298 { | |
299 return metadata_ != NULL; | |
300 } | |
301 | |
302 size_t GetFieldsCount() | |
303 { | |
304 if (HasFields()) | |
305 { | |
306 return mysql_num_fields(metadata_); | |
307 } | |
308 else | |
309 { | |
310 return 0; | |
311 } | |
312 } | |
313 | |
314 MYSQL_RES* GetObject() | |
315 { | |
316 return metadata_; | |
317 } | |
318 }; | |
319 | |
320 | |
321 void MySQLStatement::Close() | |
322 { | |
323 for (size_t i = 0; i < result_.size(); i++) | |
324 { | |
325 if (result_[i] != NULL) | |
326 { | |
327 delete result_[i]; | |
328 } | |
329 } | |
330 | |
331 if (statement_ != NULL) | |
332 { | |
333 mysql_stmt_close(statement_); | |
334 statement_ = NULL; | |
335 } | |
336 } | |
337 | |
338 | |
339 MySQLStatement::MySQLStatement(MySQLDatabase& db, | |
340 const Query& query) : | |
341 db_(db), | |
342 readOnly_(query.IsReadOnly()), | |
343 statement_(NULL), | |
344 formatter_(Dialect_MySQL) | |
345 { | |
346 std::string sql; | |
347 query.Format(sql, formatter_); | |
348 | |
349 statement_ = mysql_stmt_init(db.GetObject()); | |
350 if (statement_ == NULL) | |
351 { | |
352 db.LogError(); | |
353 throw Orthanc::OrthancException(Orthanc::ErrorCode_Database); | |
354 } | |
355 | |
356 LOG(INFO) << "Preparing MySQL statement: " << sql; | |
357 | |
358 db_.CheckErrorCode(mysql_stmt_prepare(statement_, sql.c_str(), sql.size())); | |
359 | |
360 if (mysql_stmt_param_count(statement_) != formatter_.GetParametersCount()) | |
361 { | |
362 Close(); | |
363 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | |
364 } | |
365 | |
366 try | |
367 { | |
368 ResultMetadata result(db, *this); | |
369 | |
370 if (result.HasFields()) | |
371 { | |
372 MYSQL_FIELD *field; | |
373 while ((field = mysql_fetch_field(result.GetObject()))) | |
374 { | |
375 result_.push_back(new ResultField(*field)); | |
376 } | |
377 } | |
378 | |
379 if (result_.size() != result.GetFieldsCount()) | |
380 { | |
381 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | |
382 } | |
383 } | |
384 catch (Orthanc::OrthancException&) | |
385 { | |
386 Close(); | |
387 throw; | |
388 } | |
389 | |
390 if (query.IsReadOnly()) | |
391 { | |
392 unsigned long type = (unsigned long) CURSOR_TYPE_READ_ONLY; | |
393 mysql_stmt_attr_set(statement_, STMT_ATTR_CURSOR_TYPE, (void*) &type); | |
394 } | |
395 } | |
396 | |
397 | |
398 MYSQL_STMT* MySQLStatement::GetObject() | |
399 { | |
400 if (statement_ == NULL) | |
401 { | |
402 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
403 } | |
404 else | |
405 { | |
406 return statement_; | |
407 } | |
408 } | |
409 | |
410 | |
411 IValue* MySQLStatement::FetchResultField(size_t i) | |
412 { | |
413 if (i >= result_.size()) | |
414 { | |
415 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); | |
416 } | |
417 else | |
418 { | |
419 assert(result_[i] != NULL); | |
420 return result_[i]->FetchValue(db_, *statement_, outputs_[i], i); | |
421 } | |
422 } | |
423 | |
424 | |
23
b2ff1cd2907a
handling of implicit transactions in DatabaseManager
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
9
diff
changeset
|
425 IResult* MySQLStatement::Execute(ITransaction& transaction, |
0 | 426 const Dictionary& parameters) |
427 { | |
428 std::list<int> intParameters; | |
429 std::list<long long int> int64Parameters; | |
430 | |
431 std::vector<MYSQL_BIND> inputs(formatter_.GetParametersCount()); | |
432 | |
433 for (size_t i = 0; i < inputs.size(); i++) | |
434 { | |
435 memset(&inputs[i], 0, sizeof(MYSQL_BIND)); | |
436 | |
437 const std::string& name = formatter_.GetParameterName(i); | |
438 if (!parameters.HasKey(name)) | |
439 { | |
440 LOG(ERROR) << "Missing required parameter in a SQL query: " << name; | |
441 throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem); | |
442 } | |
443 | |
444 ValueType type = formatter_.GetParameterType(i); | |
445 | |
446 const IValue& value = parameters.GetValue(name); | |
447 if (value.GetType() != type) | |
448 { | |
449 LOG(ERROR) << "Bad type of argument provided to a SQL query: " << name; | |
450 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType); | |
451 } | |
452 | |
453 // https://dev.mysql.com/doc/refman/8.0/en/c-api-prepared-statement-type-codes.html | |
454 switch (type) | |
455 { | |
456 case ValueType_Integer64: | |
457 { | |
458 int64Parameters.push_back(dynamic_cast<const Integer64Value&>(value).GetValue()); | |
459 inputs[i].buffer = &int64Parameters.back(); | |
460 inputs[i].buffer_type = MYSQL_TYPE_LONGLONG; | |
461 break; | |
462 } | |
463 | |
464 case ValueType_Utf8String: | |
465 { | |
466 const std::string& utf8 = dynamic_cast<const Utf8StringValue&>(value).GetContent(); | |
467 inputs[i].buffer = const_cast<char*>(utf8.c_str()); | |
468 inputs[i].buffer_length = utf8.size(); | |
469 inputs[i].buffer_type = MYSQL_TYPE_STRING; | |
470 break; | |
471 } | |
472 | |
473 case ValueType_BinaryString: | |
474 { | |
475 const std::string& content = dynamic_cast<const BinaryStringValue&>(value).GetContent(); | |
476 inputs[i].buffer = const_cast<char*>(content.c_str()); | |
477 inputs[i].buffer_length = content.size(); | |
478 inputs[i].buffer_type = MYSQL_TYPE_BLOB; | |
479 break; | |
480 } | |
481 | |
482 case ValueType_File: | |
483 { | |
484 const std::string& content = dynamic_cast<const FileValue&>(value).GetContent(); | |
485 inputs[i].buffer = const_cast<char*>(content.c_str()); | |
486 inputs[i].buffer_length = content.size(); | |
487 inputs[i].buffer_type = MYSQL_TYPE_BLOB; | |
488 break; | |
489 } | |
490 | |
491 case ValueType_Null: | |
492 { | |
493 inputs[i].buffer = NULL; | |
494 inputs[i].buffer_type = MYSQL_TYPE_NULL; | |
495 break; | |
496 } | |
497 | |
498 default: | |
499 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); | |
500 } | |
501 } | |
502 | |
503 if (!inputs.empty()) | |
504 { | |
505 db_.CheckErrorCode(mysql_stmt_bind_param(statement_, &inputs[0])); | |
506 } | |
507 | |
508 db_.CheckErrorCode(mysql_stmt_execute(statement_)); | |
509 | |
510 outputs_.resize(result_.size()); | |
511 | |
512 for (size_t i = 0; i < result_.size(); i++) | |
513 { | |
514 assert(result_[i] != NULL); | |
515 result_[i]->PrepareBind(outputs_[i]); | |
516 } | |
517 | |
518 if (!outputs_.empty()) | |
519 { | |
520 db_.CheckErrorCode(mysql_stmt_bind_result(statement_, &outputs_[0])); | |
521 db_.CheckErrorCode(mysql_stmt_store_result(statement_)); | |
522 } | |
523 | |
524 return new MySQLResult(db_, *this); | |
525 } | |
526 | |
527 | |
23
b2ff1cd2907a
handling of implicit transactions in DatabaseManager
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
9
diff
changeset
|
528 void MySQLStatement::ExecuteWithoutResult(ITransaction& transaction, |
0 | 529 const Dictionary& parameters) |
530 { | |
531 std::auto_ptr<IResult> dummy(Execute(transaction, parameters)); | |
532 } | |
533 } |