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