view Framework/Odbc/OdbcResult.cpp @ 522:c49136b34891 large-queries

use a prepared statement for InsertOrUpdateMetadata
author Alain Mazy <am@orthanc.team>
date Fri, 05 Jul 2024 09:15:54 +0200
parents 54d518dcd74a
children
line wrap: on
line source

/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2023 Osimis S.A., Belgium
 * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
 * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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];
    }
  }
}