Mercurial > hg > orthanc-databases
comparison Framework/Odbc/OdbcResult.cpp @ 329:b5fb8b77ce4d
initial commit of ODBC framework
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 10 Aug 2021 20:08:53 +0200 |
parents | |
children | 16aac0287485 |
comparison
equal
deleted
inserted
replaced
328:6a49c495c940 | 329:b5fb8b77ce4d |
---|---|
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-2021 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 "OdbcResult.h" | |
23 | |
24 #include "../Common/BinaryStringValue.h" | |
25 #include "../Common/Integer64Value.h" | |
26 #include "../Common/NullValue.h" | |
27 #include "../Common/Utf8StringValue.h" | |
28 | |
29 #include <ChunkedBuffer.h> | |
30 #include <Logging.h> | |
31 #include <OrthancException.h> | |
32 #include <Toolbox.h> | |
33 | |
34 #include <boost/lexical_cast.hpp> | |
35 #include <sqlext.h> | |
36 | |
37 | |
38 namespace OrthancDatabases | |
39 { | |
40 static void ThrowCannotReadString() | |
41 { | |
42 throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot read text field"); | |
43 } | |
44 | |
45 | |
46 void OdbcResult::LoadFirst() | |
47 { | |
48 if (first_) | |
49 { | |
50 Next(); | |
51 first_ = false; | |
52 } | |
53 } | |
54 | |
55 | |
56 void OdbcResult::SetValue(size_t index, | |
57 IValue* value) | |
58 { | |
59 std::unique_ptr<IValue> raii(value); | |
60 | |
61 if (index >= values_.size()) | |
62 { | |
63 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); | |
64 } | |
65 else if (value == NULL) | |
66 { | |
67 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); | |
68 } | |
69 else | |
70 { | |
71 if (values_[index] != NULL) | |
72 { | |
73 delete values_[index]; | |
74 } | |
75 | |
76 values_[index] = raii.release(); | |
77 } | |
78 } | |
79 | |
80 | |
81 void OdbcResult::ReadString(std::string& target, | |
82 size_t column, | |
83 bool isBinary) | |
84 { | |
85 // https://docs.microsoft.com/en-us/sql/odbc/reference/develop-app/getting-long-data | |
86 | |
87 std::string buffer; | |
88 buffer.resize(1024 * 1024); | |
89 | |
90 const SQLSMALLINT targetType = (isBinary ? SQL_BINARY : SQL_C_CHAR); | |
91 | |
92 SQLLEN length; | |
93 SQLRETURN code = SQLGetData(statement_.GetHandle(), column + 1, targetType, &buffer[0], buffer.size(), &length); | |
94 if (code == SQL_NO_DATA) | |
95 { | |
96 target.clear(); | |
97 } | |
98 else if (code == SQL_SUCCESS) | |
99 { | |
100 if (length == -1) | |
101 { | |
102 target.clear(); // No data available | |
103 } | |
104 else | |
105 { | |
106 // The "buffer" was large enough to store the text value, plus the null termination | |
107 target.assign(buffer.c_str(), length); | |
108 } | |
109 } | |
110 else if (code == SQL_SUCCESS_WITH_INFO) | |
111 { | |
112 Orthanc::ChunkedBuffer chunks; | |
113 | |
114 if (isBinary) | |
115 { | |
116 chunks.AddChunk(buffer.c_str(), buffer.size()); | |
117 } | |
118 else | |
119 { | |
120 /** | |
121 * WARNING: At this point, in the MSSQL driver, "length" | |
122 * contains the number of Unicode characters! This is | |
123 * different from the actual number of **bytes** that are | |
124 * required to store the UTF-8 string. As a consequence, the | |
125 * "length" cannot be used to determine the final size of | |
126 * the "target" string. | |
127 **/ | |
128 chunks.AddChunk(buffer.c_str(), buffer.size() - 1); | |
129 } | |
130 | |
131 for (;;) | |
132 { | |
133 code = SQLGetData(statement_.GetHandle(), column + 1, targetType, &buffer[0], buffer.size(), &length); | |
134 | |
135 if (code == SQL_SUCCESS) | |
136 { | |
137 // This is the last chunk | |
138 if (length == 0 || | |
139 length > static_cast<SQLLEN>(buffer.size())) | |
140 { | |
141 ThrowCannotReadString(); | |
142 } | |
143 | |
144 chunks.AddChunk(buffer.c_str(), length); | |
145 break; | |
146 } | |
147 else if (code == SQL_SUCCESS_WITH_INFO) | |
148 { | |
149 // This is an intermediate chunk | |
150 if (isBinary) | |
151 { | |
152 chunks.AddChunk(buffer.c_str(), buffer.size()); | |
153 } | |
154 else | |
155 { | |
156 chunks.AddChunk(buffer.c_str(), buffer.size() - 1); | |
157 } | |
158 } | |
159 else | |
160 { | |
161 ThrowCannotReadString(); | |
162 } | |
163 } | |
164 | |
165 chunks.Flatten(target); | |
166 } | |
167 else | |
168 { | |
169 statement_.CheckCollision(dialect_); | |
170 ThrowCannotReadString(); | |
171 } | |
172 } | |
173 | |
174 | |
175 OdbcResult::OdbcResult(OdbcStatement& statement, | |
176 Dialect dialect) : | |
177 statement_(statement), | |
178 dialect_(dialect), | |
179 first_(true), | |
180 done_(false) | |
181 { | |
182 SQLSMALLINT count; | |
183 if (!SQL_SUCCEEDED(SQLNumResultCols(statement_.GetHandle(), &count))) | |
184 { | |
185 throw Orthanc::OrthancException(Orthanc::ErrorCode_Database); | |
186 } | |
187 | |
188 types_.resize(count); | |
189 typeNames_.resize(count); | |
190 values_.resize(count); | |
191 | |
192 for (size_t i = 0; i < values_.size(); i++) | |
193 { | |
194 /** | |
195 * NB: Don't use "SQLDescribeCol()", as it is less flexible | |
196 * (cf. OMSSQL-7: "SQLDescribeParam()" doesn't work with | |
197 * encrypted columns) | |
198 **/ | |
199 | |
200 if (!SQL_SUCCEEDED(SQLColAttribute(statement_.GetHandle(), i + 1, SQL_DESC_TYPE, NULL, -1, NULL, &types_[i]))) | |
201 { | |
202 throw Orthanc::OrthancException(Orthanc::ErrorCode_Database); | |
203 } | |
204 | |
205 SQLCHAR buffer[1024]; | |
206 SQLSMALLINT length; | |
207 | |
208 if (!SQL_SUCCEEDED(SQLColAttribute(statement_.GetHandle(), i + 1, SQL_DESC_TYPE_NAME, | |
209 buffer, sizeof(buffer) - 1, &length, NULL))) | |
210 { | |
211 throw Orthanc::OrthancException(Orthanc::ErrorCode_Database); | |
212 } | |
213 | |
214 std::string name(reinterpret_cast<const char*>(buffer), length); | |
215 Orthanc::Toolbox::ToLowerCase(typeNames_[i], name); | |
216 } | |
217 } | |
218 | |
219 | |
220 OdbcResult::~OdbcResult() | |
221 { | |
222 for (size_t i = 0; i < values_.size(); i++) | |
223 { | |
224 if (values_[i] != NULL) | |
225 { | |
226 delete values_[i]; | |
227 } | |
228 } | |
229 | |
230 if (!first_ && | |
231 !SQL_SUCCEEDED(SQLCloseCursor(statement_.GetHandle()))) | |
232 { | |
233 LOG(WARNING) << "Cannot close the ODBC cursor: " << std::endl << statement_.FormatError(); | |
234 } | |
235 } | |
236 | |
237 | |
238 void OdbcResult::SetExpectedType(size_t field, | |
239 ValueType type) | |
240 { | |
241 if (field >= types_.size()) | |
242 { | |
243 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); | |
244 } | |
245 else | |
246 { | |
247 // Ignore this information | |
248 } | |
249 } | |
250 | |
251 | |
252 bool OdbcResult::IsDone() const | |
253 { | |
254 const_cast<OdbcResult&>(*this).LoadFirst(); | |
255 return done_; | |
256 } | |
257 | |
258 | |
259 void OdbcResult::Next() | |
260 { | |
261 SQLRETURN code = SQLFetch(statement_.GetHandle()); | |
262 | |
263 if (code == SQL_NO_DATA) | |
264 { | |
265 done_ = true; | |
266 } | |
267 else if (code == SQL_SUCCESS) | |
268 { | |
269 done_ = false; | |
270 } | |
271 else | |
272 { | |
273 statement_.CheckCollision(dialect_); | |
274 throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot fetch new row"); | |
275 } | |
276 | |
277 assert(values_.size() == types_.size() && | |
278 values_.size() == typeNames_.size()); | |
279 | |
280 for (size_t i = 0; i < values_.size(); i++) | |
281 { | |
282 SQLLEN type = types_[i]; | |
283 const std::string& name = typeNames_[i]; | |
284 | |
285 if (done_) | |
286 { | |
287 SetValue(i, new NullValue); | |
288 } | |
289 else if (type == SQL_INTEGER) | |
290 { | |
291 int32_t value; | |
292 SQLLEN length; | |
293 if (SQL_SUCCEEDED(SQLGetData(statement_.GetHandle(), i + 1, SQL_INTEGER, &value, sizeof(value), &length))) | |
294 { | |
295 if (length == SQL_NULL_DATA) | |
296 { | |
297 SetValue(i, new NullValue); | |
298 } | |
299 else | |
300 { | |
301 SetValue(i, new Integer64Value(value)); | |
302 } | |
303 } | |
304 else | |
305 { | |
306 throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot get int32_t field"); | |
307 } | |
308 } | |
309 else if (type == SQL_BIGINT || | |
310 (dialect_ == Dialect_PostgreSQL && name == "bigserial")) | |
311 { | |
312 int64_t value; | |
313 SQLLEN length; | |
314 if (SQL_SUCCEEDED(SQLGetData(statement_.GetHandle(), i + 1, SQL_C_SBIGINT, &value, sizeof(value), &length))) | |
315 { | |
316 if (length == SQL_NULL_DATA) | |
317 { | |
318 SetValue(i, new NullValue); | |
319 } | |
320 else | |
321 { | |
322 SetValue(i, new Integer64Value(value)); | |
323 } | |
324 } | |
325 else | |
326 { | |
327 throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot get int64_t field"); | |
328 } | |
329 } | |
330 else if (type == SQL_VARCHAR || | |
331 name == "varchar" || | |
332 (dialect_ == Dialect_MSSQL && name == "nvarchar") || // This means UTF-16 | |
333 (dialect_ == Dialect_MySQL && name == "longtext") || | |
334 (dialect_ == Dialect_MySQL && name.empty() && type == -9) || // Seen in "SQLTables()" | |
335 (dialect_ == Dialect_PostgreSQL && name == "text") || | |
336 (dialect_ == Dialect_SQLite && name == "text") || | |
337 (dialect_ == Dialect_SQLite && name == "wvarchar")) // Seen on Windows with sqliteodbc-0.9998-win32.exe | |
338 { | |
339 std::string value; | |
340 ReadString(value, i, false /* not binary */); | |
341 SetValue(i, new Utf8StringValue(value)); | |
342 } | |
343 else if (type == SQL_NUMERIC) | |
344 { | |
345 /** | |
346 * SQL_NUMERIC_STRUCT could be used here, but is much more | |
347 * complex to deal with: | |
348 * https://stackoverflow.com/a/9188737/881731 | |
349 **/ | |
350 | |
351 std::string value; | |
352 ReadString(value, i, false /* not binary */); | |
353 SetValue(i, new Integer64Value(boost::lexical_cast<int64_t>(value))); | |
354 } | |
355 else if (type == SQL_BINARY || | |
356 (dialect_ == Dialect_PostgreSQL && name == "bytea") || | |
357 (dialect_ == Dialect_MySQL && name == "longblob") || | |
358 (dialect_ == Dialect_MSSQL && name == "varbinary")) | |
359 { | |
360 std::string value; | |
361 ReadString(value, i, true /* binary */); | |
362 SetValue(i, new BinaryStringValue(value)); | |
363 } | |
364 else | |
365 { | |
366 throw Orthanc::OrthancException( | |
367 Orthanc::ErrorCode_NotImplemented, | |
368 "Unknown type in result: " + name + " (" + boost::lexical_cast<std::string>(type) + ")"); | |
369 } | |
370 } | |
371 } | |
372 | |
373 | |
374 const IValue& OdbcResult::GetField(size_t field) const | |
375 { | |
376 if (field >= values_.size()) | |
377 { | |
378 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); | |
379 } | |
380 else if (IsDone()) | |
381 { | |
382 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
383 } | |
384 else if (values_[field] == NULL) | |
385 { | |
386 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | |
387 } | |
388 else | |
389 { | |
390 return *values_[field]; | |
391 } | |
392 } | |
393 } |