diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Odbc/OdbcResult.cpp	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,393 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OdbcResult.h"
+
+#include "../Common/BinaryStringValue.h"
+#include "../Common/Integer64Value.h"
+#include "../Common/NullValue.h"
+#include "../Common/Utf8StringValue.h"
+
+#include <ChunkedBuffer.h>
+#include <Logging.h>
+#include <OrthancException.h>
+#include <Toolbox.h>
+
+#include <boost/lexical_cast.hpp>
+#include <sqlext.h>
+
+
+namespace OrthancDatabases
+{
+  static void ThrowCannotReadString()
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot read text field");
+  }
+
+
+  void OdbcResult::LoadFirst()
+  {
+    if (first_)
+    {
+      Next();
+      first_ = false;
+    }
+  }
+
+    
+  void OdbcResult::SetValue(size_t index,
+                            IValue* value)
+  {
+    std::unique_ptr<IValue> raii(value);
+        
+    if (index >= values_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else if (value == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    else
+    {
+      if (values_[index] != NULL)
+      {
+        delete values_[index];
+      }
+          
+      values_[index] = raii.release();
+    }
+  }
+      
+
+  void OdbcResult::ReadString(std::string& target,
+                              size_t column,
+                              bool isBinary)
+  {
+    // https://docs.microsoft.com/en-us/sql/odbc/reference/develop-app/getting-long-data
+
+    std::string buffer;
+    buffer.resize(1024 * 1024);
+
+    const SQLSMALLINT targetType = (isBinary ? SQL_BINARY : SQL_C_CHAR);
+
+    SQLLEN length;
+    SQLRETURN code = SQLGetData(statement_.GetHandle(), column + 1, targetType, &buffer[0], buffer.size(), &length);
+    if (code == SQL_NO_DATA)
+    {
+      target.clear();
+    }
+    else if (code == SQL_SUCCESS)
+    {
+      if (length == -1)
+      {
+        target.clear();  // No data available
+      }
+      else
+      {
+        // The "buffer" was large enough to store the text value, plus the null termination
+        target.assign(buffer.c_str(), length);
+      }
+    }
+    else if (code == SQL_SUCCESS_WITH_INFO)
+    {
+      Orthanc::ChunkedBuffer chunks;
+      
+      if (isBinary)
+      {
+        chunks.AddChunk(buffer.c_str(), buffer.size());
+      }
+      else
+      {
+        /**
+         * WARNING: At this point, in the MSSQL driver, "length"
+         * contains the number of Unicode characters! This is
+         * different from the actual number of **bytes** that are
+         * required to store the UTF-8 string. As a consequence, the
+         * "length" cannot be used to determine the final size of
+         * the "target" string.
+         **/
+        chunks.AddChunk(buffer.c_str(), buffer.size() - 1);
+      }
+
+      for (;;)
+      {
+        code = SQLGetData(statement_.GetHandle(), column + 1, targetType, &buffer[0], buffer.size(), &length);
+            
+        if (code == SQL_SUCCESS)
+        {
+          // This is the last chunk
+          if (length == 0 ||
+              length > static_cast<SQLLEN>(buffer.size()))
+          {
+            ThrowCannotReadString();
+          }
+                  
+          chunks.AddChunk(buffer.c_str(), length);
+          break;
+        }
+        else if (code == SQL_SUCCESS_WITH_INFO)
+        {
+          // This is an intermediate chunk
+          if (isBinary)
+          {
+            chunks.AddChunk(buffer.c_str(), buffer.size());
+          }
+          else
+          {
+            chunks.AddChunk(buffer.c_str(), buffer.size() - 1);
+          }
+        }
+        else
+        {
+          ThrowCannotReadString();
+        }
+      }
+          
+      chunks.Flatten(target);
+    }
+    else
+    {
+      statement_.CheckCollision(dialect_);
+      ThrowCannotReadString();
+    }
+  }
+      
+
+  OdbcResult::OdbcResult(OdbcStatement& statement,
+                         Dialect dialect) :
+    statement_(statement),
+    dialect_(dialect),
+    first_(true),
+    done_(false)
+  {
+    SQLSMALLINT count;
+    if (!SQL_SUCCEEDED(SQLNumResultCols(statement_.GetHandle(), &count)))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
+    }
+
+    types_.resize(count);
+    typeNames_.resize(count);
+    values_.resize(count);
+        
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      /**
+       * NB: Don't use "SQLDescribeCol()", as it is less flexible
+       * (cf. OMSSQL-7: "SQLDescribeParam()" doesn't work with
+       * encrypted columns)
+       **/
+          
+      if (!SQL_SUCCEEDED(SQLColAttribute(statement_.GetHandle(), i + 1, SQL_DESC_TYPE, NULL, -1, NULL, &types_[i])))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
+      }
+
+      SQLCHAR buffer[1024];
+      SQLSMALLINT length;
+
+      if (!SQL_SUCCEEDED(SQLColAttribute(statement_.GetHandle(), i + 1, SQL_DESC_TYPE_NAME,
+                                         buffer, sizeof(buffer) - 1, &length, NULL)))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
+      }
+
+      std::string name(reinterpret_cast<const char*>(buffer), length);
+      Orthanc::Toolbox::ToLowerCase(typeNames_[i], name);
+    }
+  }
+
+    
+  OdbcResult::~OdbcResult()
+  {
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      if (values_[i] != NULL)
+      {
+        delete values_[i];
+      }
+    }
+
+    if (!first_ &&
+        !SQL_SUCCEEDED(SQLCloseCursor(statement_.GetHandle())))
+    {
+      LOG(WARNING) << "Cannot close the ODBC cursor: " << std::endl << statement_.FormatError();
+    }
+  }
+    
+      
+  void OdbcResult::SetExpectedType(size_t field,
+                                   ValueType type) 
+  {
+    if (field >= types_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      // Ignore this information
+    }
+  }
+
+
+  bool OdbcResult::IsDone() const 
+  {
+    const_cast<OdbcResult&>(*this).LoadFirst();
+    return done_;
+  }
+    
+
+  void OdbcResult::Next() 
+  {
+    SQLRETURN code = SQLFetch(statement_.GetHandle());
+
+    if (code == SQL_NO_DATA)
+    {
+      done_ = true;
+    }
+    else if (code == SQL_SUCCESS)
+    {
+      done_ = false;
+    }
+    else
+    {
+      statement_.CheckCollision(dialect_);
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot fetch new row");
+    }
+
+    assert(values_.size() == types_.size() &&
+           values_.size() == typeNames_.size());
+
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      SQLLEN type = types_[i];
+      const std::string& name = typeNames_[i];
+          
+      if (done_)
+      {
+        SetValue(i, new NullValue);
+      }
+      else if (type == SQL_INTEGER)
+      {
+        int32_t value;
+        SQLLEN length;
+        if (SQL_SUCCEEDED(SQLGetData(statement_.GetHandle(), i + 1, SQL_INTEGER, &value, sizeof(value), &length)))
+        {
+          if (length == SQL_NULL_DATA)
+          {
+            SetValue(i, new NullValue);
+          }
+          else
+          {
+            SetValue(i, new Integer64Value(value));
+          }
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot get int32_t field");
+        }
+      }
+      else if (type == SQL_BIGINT ||
+               (dialect_ == Dialect_PostgreSQL && name == "bigserial"))
+      {
+        int64_t value;
+        SQLLEN length;
+        if (SQL_SUCCEEDED(SQLGetData(statement_.GetHandle(), i + 1, SQL_C_SBIGINT, &value, sizeof(value), &length)))
+        {
+          if (length == SQL_NULL_DATA)
+          {
+            SetValue(i, new NullValue);
+          }
+          else
+          {
+            SetValue(i, new Integer64Value(value));
+          }
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot get int64_t field");
+        }
+      }
+      else if (type == SQL_VARCHAR ||
+               name == "varchar" ||
+               (dialect_ == Dialect_MSSQL && name == "nvarchar") ||  // This means UTF-16
+               (dialect_ == Dialect_MySQL && name == "longtext") ||
+               (dialect_ == Dialect_MySQL && name.empty() && type == -9) ||  // Seen in "SQLTables()"
+               (dialect_ == Dialect_PostgreSQL && name == "text") ||
+               (dialect_ == Dialect_SQLite && name == "text") ||
+               (dialect_ == Dialect_SQLite && name == "wvarchar"))  // Seen on Windows with sqliteodbc-0.9998-win32.exe
+      {
+        std::string value;
+        ReadString(value, i, false /* not binary */);
+        SetValue(i, new Utf8StringValue(value));
+      }
+      else if (type == SQL_NUMERIC)
+      {
+        /**
+         * SQL_NUMERIC_STRUCT could be used here, but is much more
+         * complex to deal with:
+         * https://stackoverflow.com/a/9188737/881731
+         **/
+        
+        std::string value;
+        ReadString(value, i, false /* not binary */);
+        SetValue(i, new Integer64Value(boost::lexical_cast<int64_t>(value)));
+      }
+      else if (type == SQL_BINARY ||
+               (dialect_ == Dialect_PostgreSQL && name == "bytea") ||
+               (dialect_ == Dialect_MySQL && name == "longblob") ||
+               (dialect_ == Dialect_MSSQL && name == "varbinary"))
+      {
+        std::string value;
+        ReadString(value, i, true /* binary */);
+        SetValue(i, new BinaryStringValue(value));
+      }
+      else
+      {
+        throw Orthanc::OrthancException(
+          Orthanc::ErrorCode_NotImplemented,
+          "Unknown type in result: " + name + " (" + boost::lexical_cast<std::string>(type) + ")");
+      }
+    }
+  }
+
+
+  const IValue& OdbcResult::GetField(size_t field) const 
+  {
+    if (field >= values_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else if (IsDone())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else if (values_[field] == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    else
+    {
+      return *values_[field];
+    }
+  }
+}