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