# HG changeset patch # User Sebastien Jodogne # Date 1628618933 -7200 # Node ID b5fb8b77ce4d9c7ef1492a27315271fff5052d13 # Parent 6a49c495c9408f5eea61a08ee0ffcff57d1ae787 initial commit of ODBC framework diff -r 6a49c495c940 -r b5fb8b77ce4d Framework/Odbc/OdbcDatabase.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Odbc/OdbcDatabase.cpp Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,635 @@ +/** + * 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 . + **/ + + +#include "OdbcDatabase.h" + +#include "../Common/ImplicitTransaction.h" +#include "../Common/RetryDatabaseFactory.h" +#include "../Common/Utf8StringValue.h" +#include "OdbcPreparedStatement.h" +#include "OdbcResult.h" + +#include +#include +#include + +#include +#include + + +namespace OrthancDatabases +{ + static void SetAutoCommitTransaction(SQLHDBC handle, + bool autocommit) + { + // Go to autocommit mode + SQLPOINTER value = (SQLPOINTER) (autocommit ? SQL_AUTOCOMMIT_ON : SQL_AUTOCOMMIT_OFF); + + if (!SQL_SUCCEEDED(SQLSetConnectAttr(handle, SQL_ATTR_AUTOCOMMIT, value, SQL_IS_UINTEGER))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, + "Cannot switch the autocommit mode"); + } + } + + + class OdbcDatabase::OdbcImplicitTransaction : public ImplicitTransaction + { + private: + OdbcDatabase& db_; + + protected: + virtual IResult* ExecuteInternal(IPrecompiledStatement& statement, + const Dictionary& parameters) ORTHANC_OVERRIDE + { + return dynamic_cast(statement).Execute(parameters); + } + + virtual void ExecuteWithoutResultInternal(IPrecompiledStatement& statement, + const Dictionary& parameters) ORTHANC_OVERRIDE + { + std::unique_ptr result(Execute(statement, parameters)); + } + + public: + OdbcImplicitTransaction(OdbcDatabase& db) : + db_(db) + { + SetAutoCommitTransaction(db_.GetHandle(), true); + } + + virtual bool DoesTableExist(const std::string& name) ORTHANC_OVERRIDE + { + return db_.DoesTableExist(name.c_str()); + } + + virtual bool DoesTriggerExist(const std::string& name) ORTHANC_OVERRIDE + { + return false; + } + + virtual void ExecuteMultiLines(const std::string& query) ORTHANC_OVERRIDE + { + db_.ExecuteMultiLines(query); + } + }; + + + class OdbcDatabase::OdbcExplicitTransaction : public ITransaction + { + private: + OdbcDatabase& db_; + bool isOpen_; + + void EndTransaction(SQLSMALLINT completionType) + { + if (!isOpen_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Transaction is already finalized"); + } + else if (SQL_SUCCEEDED(SQLEndTran(SQL_HANDLE_DBC, db_.GetHandle(), completionType))) + { + isOpen_ = false; + } + else + { + SQLCHAR stateBuf[SQL_SQLSTATE_SIZE + 1]; + SQLSMALLINT stateLength = 0; + + const SQLSMALLINT recNum = 1; + + if (SQL_SUCCEEDED(SQLGetDiagField(SQL_HANDLE_DBC, db_.GetHandle(), + recNum, SQL_DIAG_SQLSTATE, &stateBuf, sizeof(stateBuf), &stateLength))) + { + const std::string state(reinterpret_cast(stateBuf)); + + if (state == "40001") + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_DatabaseCannotSerialize); + } + } + + switch (completionType) + { + case SQL_COMMIT: + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot commit transaction"); + + case SQL_ROLLBACK: + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot rollback transaction"); + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + } + + public: + OdbcExplicitTransaction(OdbcDatabase& db) : + db_(db), + isOpen_(true) + { + SetAutoCommitTransaction(db_.GetHandle(), false); + } + + virtual ~OdbcExplicitTransaction() + { + if (isOpen_) + { + LOG(INFO) << "An active ODBC transaction was dismissed"; + if (!SQL_SUCCEEDED(SQLEndTran(SQL_HANDLE_DBC, db_.GetHandle(), SQL_ROLLBACK))) + { + LOG(ERROR) << "Cannot rollback transaction"; + } + } + } + + virtual bool IsImplicit() const ORTHANC_OVERRIDE + { + return false; + } + + virtual void Commit() ORTHANC_OVERRIDE + { + EndTransaction(SQL_COMMIT); + } + + virtual void Rollback() ORTHANC_OVERRIDE + { + EndTransaction(SQL_ROLLBACK); + } + + virtual bool DoesTableExist(const std::string& name) ORTHANC_OVERRIDE + { + return db_.DoesTableExist(name.c_str()); + } + + virtual bool DoesTriggerExist(const std::string& name) ORTHANC_OVERRIDE + { + return false; + } + + virtual void ExecuteMultiLines(const std::string& query) ORTHANC_OVERRIDE + { + db_.ExecuteMultiLines(query); + } + + virtual IResult* Execute(IPrecompiledStatement& statement, + const Dictionary& parameters) ORTHANC_OVERRIDE + { + return dynamic_cast(statement).Execute(parameters); + } + + virtual void ExecuteWithoutResult(IPrecompiledStatement& statement, + const Dictionary& parameters) ORTHANC_OVERRIDE + { + std::unique_ptr result(Execute(statement, parameters)); + } + }; + + + static bool ParseThreePartsVersion(unsigned int& majorVersion, + const std::string& version) + { + std::vector tokens; + Orthanc::Toolbox::TokenizeString(tokens, version, '.'); + + try + { + if (tokens.size() == 3u) + { + int tmp = boost::lexical_cast(tokens[0]); + if (tmp >= 0) + { + majorVersion = static_cast(tmp); + return true; + } + } + } + catch (boost::bad_lexical_cast&) + { + } + + return false; + } + + + OdbcDatabase::OdbcDatabase(OdbcEnvironment& environment, + const std::string& connectionString) : + dbmsMajorVersion_(0) + { + LOG(INFO) << "Creating an ODBC connection: " << connectionString; + + /* Allocate a connection handle */ + if (!SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_DBC, environment.GetHandle(), &handle_))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_DatabaseUnavailable, + "Cannot create ODBC connection"); + } + + /* Connect to the DSN mydsn */ + SQLCHAR* tmp = const_cast(reinterpret_cast(connectionString.c_str())); + SQLCHAR outBuffer[2048]; + SQLSMALLINT outSize = 0; + + bool success = true; + + if (SQL_SUCCEEDED(SQLDriverConnect(handle_, NULL, tmp, SQL_NTS /* null-terminated string */, + outBuffer, sizeof(outBuffer), &outSize, SQL_DRIVER_COMPLETE))) + { + LOG(INFO) << "Returned connection string: " << outBuffer; + } + else + { + success = false; + } + + if (!SQL_SUCCEEDED(SQLSetConnectAttr(handle_, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER) SQL_TXN_SERIALIZABLE, SQL_NTS))) + { + /** + * Switch to the "serializable" isolation level that is expected + * by Orthanc. This is already the default for MySQL and MSSQL, + * but is needed for PostgreSQL. + * https://docs.microsoft.com/en-us/sql/odbc/reference/develop-app/transaction-isolation-levels + **/ + success = false; + } + + SQLCHAR versionBuffer[2048]; + SQLSMALLINT versionSize; + + if (success && + SQL_SUCCEEDED(SQLGetInfo(handle_, SQL_DBMS_NAME, outBuffer, sizeof(outBuffer) - 1, &outSize)) && + SQL_SUCCEEDED(SQLGetInfo(handle_, SQL_DBMS_VER, versionBuffer, sizeof(versionBuffer) - 1, &versionSize))) + { + std::string dbms(reinterpret_cast(outBuffer), outSize); + std::string version(reinterpret_cast(versionBuffer), versionSize); + + LOG(WARNING) << "DBMS Name: " << dbms; + LOG(WARNING) << "DBMS Version: " << version; + + if (dbms == "PostgreSQL") + { + dialect_ = Dialect_PostgreSQL; + } + else if (dbms == "SQLite") + { + dialect_ = Dialect_SQLite; + ExecuteMultiLines("PRAGMA FOREIGN_KEYS=ON"); // Necessary for cascaded delete to work + ExecuteMultiLines("PRAGMA ENCODING=\"UTF-8\""); + + // The following lines speed up SQLite + + /*ExecuteMultiLines("PRAGMA SYNCHRONOUS=NORMAL;"); + ExecuteMultiLines("PRAGMA JOURNAL_MODE=WAL;"); + ExecuteMultiLines("PRAGMA LOCKING_MODE=EXCLUSIVE;"); + ExecuteMultiLines("PRAGMA WAL_AUTOCHECKPOINT=1000;");*/ + } + else if (dbms == "MySQL") + { + dialect_ = Dialect_MySQL; + + if (!ParseThreePartsVersion(dbmsMajorVersion_, version)) + { + SQLFreeHandle(SQL_HANDLE_DBC, handle_); + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot parse the version of MySQL: " + version); + } + } + else if (dbms == "Microsoft SQL Server") + { + dialect_ = Dialect_MSSQL; + + if (!ParseThreePartsVersion(dbmsMajorVersion_, version)) + { + SQLFreeHandle(SQL_HANDLE_DBC, handle_); + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot parse the version of SQL Server: " + version); + } + } + else + { + SQLFreeHandle(SQL_HANDLE_DBC, handle_); + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Unknown SQL dialect for DBMS: " + dbms); + } + } + else + { + success = false; + } + + if (!success) + { + std::string error = FormatError(); + SQLFreeHandle(SQL_HANDLE_DBC, handle_); // Cannot call FormatError() below this point + + throw Orthanc::OrthancException(Orthanc::ErrorCode_DatabaseUnavailable, "Error in SQLDriverConnect():\n" + error); + } + } + + + OdbcDatabase::~OdbcDatabase() + { + LOG(INFO) << "Destructing an ODBC connection"; + + if (!SQL_SUCCEEDED(SQLDisconnect(handle_))) + { + LOG(ERROR) << "Cannot disconnect from driver"; + } + + if (!SQL_SUCCEEDED(SQLFreeHandle(SQL_HANDLE_DBC, handle_))) + { + LOG(ERROR) << "Cannot destruct the ODBC connection"; + } + } + + + std::string OdbcDatabase::FormatError() + { + return OdbcEnvironment::FormatError(handle_, SQL_HANDLE_DBC); + } + + + void OdbcDatabase::ListTables(std::set& target) + { + target.clear(); + + OdbcStatement statement(GetHandle()); + + if (SQL_SUCCEEDED(SQLTables(statement.GetHandle(), NULL, 0, NULL, 0, NULL, 0, + const_cast(reinterpret_cast("'TABLE'")), SQL_NTS))) + { + OdbcResult result(statement, dialect_); + + while (!result.IsDone()) + { + if (result.GetFieldsCount() < 5) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Invalid result for SQLTables()"); + } + else + { + if (result.GetField(2).GetType() == ValueType_Utf8String && + result.GetField(3).GetType() == ValueType_Utf8String && + dynamic_cast(result.GetField(3)).GetContent() == "TABLE") + { + std::string name = dynamic_cast(result.GetField(2)).GetContent(); + Orthanc::Toolbox::ToLowerCase(name); + target.insert(name); + } + } + + result.Next(); + } + } + } + + + bool OdbcDatabase::DoesTableExist(const std::string& name) + { + std::set tables; + ListTables(tables); + return (tables.find(name) != tables.end()); + } + + + void OdbcDatabase::ExecuteMultiLines(const std::string& query) + { + OdbcStatement statement(GetHandle()); + + std::vector lines; + Orthanc::Toolbox::TokenizeString(lines, query, ';'); + + for (size_t i = 0; i < lines.size(); i++) + { + std::string line = Orthanc::Toolbox::StripSpaces(lines[i]); + if (!line.empty()) + { + LOG(INFO) << "Running ODBC SQL: " << line; + SQLCHAR* tmp = const_cast(reinterpret_cast(line.c_str())); + + SQLRETURN code = SQLExecDirect(statement.GetHandle(), tmp, SQL_NTS); + + if (code != SQL_NO_DATA && + code != SQL_SUCCESS && + code != SQL_SUCCESS_WITH_INFO) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, + "Cannot execute multi-line SQL:\n" + statement.FormatError()); + } + } + } + } + + + IPrecompiledStatement* OdbcDatabase::Compile(const Query& query) + { + return new OdbcPreparedStatement(GetHandle(), GetDialect(), query); + } + + + ITransaction* OdbcDatabase::CreateTransaction(TransactionType type) + { + /** + * In ODBC, there is no "START TRANSACTION". A transaction is + * automatically created with each connection, and the "READ + * ONLY" status can only be set at the statement level + * (cf. SQL_CONCUR_READ_ONLY). One can only control the + * autocommit: https://stackoverflow.com/a/35351267/881731 + **/ + switch (type) + { + case TransactionType_Implicit: + return new OdbcImplicitTransaction(*this); + + case TransactionType_ReadWrite: + case TransactionType_ReadOnly: + return new OdbcExplicitTransaction(*this); + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + unsigned int OdbcDatabase::GetDbmsMajorVersion() const + { + return dbmsMajorVersion_; + } + + + IDatabaseFactory* OdbcDatabase::CreateDatabaseFactory(unsigned int maxConnectionRetries, + unsigned int connectionRetryInterval, + const std::string& connectionString, + bool checkEncodings) + { + class Factory : public RetryDatabaseFactory + { + private: + OdbcEnvironment environment_; + std::string connectionString_; + bool checkEncodings_; + + bool LookupConnectionOption(std::string& value, + const std::string& option) const + { + std::vector tokens; + Orthanc::Toolbox::TokenizeString(tokens, connectionString_, ';'); + + for (size_t i = 0; i < tokens.size(); i++) + { + if (boost::starts_with(tokens[i], option + "=")) + { + value = tokens[i]; + return true; + } + } + + return false; + } + + + void CheckMSSQLEncodings(OdbcDatabase& db) + { + // https://en.wikipedia.org/wiki/History_of_Microsoft_SQL_Server + if (db.GetDbmsMajorVersion() <= 14) + { + // Microsoft SQL Server up to 2017 + + std::string value; + if (LookupConnectionOption(value, "AutoTranslate")) + { + if (value != "AutoTranslate=no") + { + LOG(WARNING) << "For UTF-8 to work properly, it is strongly advised to set \"AutoTranslate=no\" in the " + << "ODBC connection string when connecting to Microsoft SQL Server with version <= 2017"; + } + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, + "Your Microsoft SQL Server has version <= 2017, and thus doesn't support UTF-8; " + "Please upgrade or add \"AutoTranslate=no\" to your ODBC connection string"); + } + } + else + { + std::string value; + if (LookupConnectionOption(value, "AutoTranslate") && + value != "AutoTranslate=yes") + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, + "Your Microsoft SQL Server has version >= 2019, and thus fully supports UTF-8; " + "Please set \"AutoTranslate=yes\" in your ODBC connection string"); + } + } + } + + + void CheckMySQLEncodings(OdbcDatabase& db) + { + // https://dev.mysql.com/doc/connector-odbc/en/connector-odbc-configuration-connection-parameters.html + + std::string value; + if (LookupConnectionOption(value, "charset")) + { + if (value != "charset=utf8") + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, + "For compatibility with UTF-8 in Orthanc, your connection string to MySQL " + "must *not* set the \"charset\" option to another value than \"utf8\""); + } + } + else if (db.GetDbmsMajorVersion() < 8) + { + // MySQL up to 5.7 + LOG(WARNING) << "It is advised to set the \"charset=utf8\" option in your connection string if using MySQL <= 5.7"; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, + "For compatibility with UTF-8 in Orthanc, your connection string to MySQL >= 8.0 " + "*must* set the \"charset=utf8\" in your connection string"); + } + } + + + protected: + IDatabase* TryOpen() + { + std::unique_ptr db(new OdbcDatabase(environment_, connectionString_)); + + if (checkEncodings_) + { + switch (db->GetDialect()) + { + case Dialect_MSSQL: + CheckMSSQLEncodings(*db); + break; + + case Dialect_MySQL: + CheckMySQLEncodings(*db); + break; + + case Dialect_SQLite: + case Dialect_PostgreSQL: + // Nothing specific to be checked wrt. encodings + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + + if (db->GetDbmsMajorVersion() >= 15) + { + /** + * SQL Server 2019 introduces support for UTF-8. Note that + * "ALTER" cannot be run inside a transaction, and must be + * done *before* the creation of the tables. + * https://docs.microsoft.com/en-US/sql/relational-databases/collations/collation-and-unicode-support#utf8 + * + * Furthermore, this call must be done by both + * "odbc-index" and "odbc-storage" plugins, because + * altering collation is an operation that requires + * exclusive lock: If "odbc-storage" is the first plugin + * to be loaded and doesn't set the UTF-8 collation, + * "odbc-index" cannot start because it doesn't have + * exclusive access. + **/ + db->ExecuteMultiLines("ALTER DATABASE CURRENT COLLATE LATIN1_GENERAL_100_CI_AS_SC_UTF8"); + } + + return db.release(); + } + + public: + Factory(unsigned int maxConnectionRetries, + unsigned int connectionRetryInterval, + const std::string& connectionString, + bool checkEncodings) : + RetryDatabaseFactory(maxConnectionRetries, connectionRetryInterval), + connectionString_(connectionString), + checkEncodings_(checkEncodings) + { + } + }; + + return new Factory(maxConnectionRetries, connectionRetryInterval, connectionString, checkEncodings); + } +} diff -r 6a49c495c940 -r b5fb8b77ce4d Framework/Odbc/OdbcDatabase.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Odbc/OdbcDatabase.h Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,82 @@ +/** + * 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 . + **/ + + +#pragma once + +#include "OdbcEnvironment.h" + +#include "../Common/IDatabaseFactory.h" + +#include + +#include + + +namespace OrthancDatabases +{ + class OdbcDatabase : public IDatabase + { + private: + class OdbcImplicitTransaction; + class OdbcExplicitTransaction; + + SQLHDBC handle_; + Dialect dialect_; + unsigned int dbmsMajorVersion_; + + public: + OdbcDatabase(OdbcEnvironment& environment, + const std::string& connectionString); + + virtual ~OdbcDatabase(); + + SQLHDBC GetHandle() + { + return handle_; + } + + std::string FormatError(); + + void ListTables(std::set& target); + + // "name" must be in lower-case + bool DoesTableExist(const std::string& name); + + void ExecuteMultiLines(const std::string& query); + + virtual Dialect GetDialect() const ORTHANC_OVERRIDE + { + return dialect_; + } + + virtual IPrecompiledStatement* Compile(const Query& query) ORTHANC_OVERRIDE; + + virtual ITransaction* CreateTransaction(TransactionType type) ORTHANC_OVERRIDE; + + // https://en.wikipedia.org/wiki/History_of_Microsoft_SQL_Server + unsigned int GetDbmsMajorVersion() const; + + static IDatabaseFactory* CreateDatabaseFactory(unsigned int maxConnectionRetries, + unsigned int connectionRetryInterval, + const std::string& connectionString, + bool checkEncodings); + }; +} diff -r 6a49c495c940 -r b5fb8b77ce4d Framework/Odbc/OdbcEnvironment.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Odbc/OdbcEnvironment.cpp Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,97 @@ +/** + * 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 . + **/ + + +#include "OdbcEnvironment.h" + +#include +#include + +#include +#include + + +namespace OrthancDatabases +{ + OdbcEnvironment::OdbcEnvironment() + { + LOG(INFO) << "Creating the ODBC environment"; + + /* Allocate an environment handle */ + if (!SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &handle_))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, + "Cannot create ODBC environment"); + } + + /* We want ODBC 3 support */ + if (!SQL_SUCCEEDED(SQLSetEnvAttr(handle_, SQL_ATTR_ODBC_VERSION, (void *) SQL_OV_ODBC3, 0))) + { + SQLFreeHandle(SQL_HANDLE_ENV, handle_); + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, + "Your environment doesn't support ODBC 3.x"); + } + } + + + OdbcEnvironment::~OdbcEnvironment() + { + LOG(INFO) << "Destructing the ODBC environment"; + + if (!SQL_SUCCEEDED(SQLFreeHandle(SQL_HANDLE_ENV, handle_))) + { + LOG(ERROR) << "Cannot tear down ODBC environment"; + } + } + + + std::string OdbcEnvironment::FormatError(SQLHANDLE handle, + SQLSMALLINT type) + { + SQLINTEGER i = 0; + SQLINTEGER native; + SQLCHAR state[SQL_SQLSTATE_SIZE + 1]; + SQLCHAR text[256]; + SQLSMALLINT len; + + std::string s; + + for (;;) + { + SQLRETURN ret = SQLGetDiagRec(type, handle, ++i, state, &native, text, sizeof(text), &len); + if (SQL_SUCCEEDED(ret)) + { + if (i >= 2) + { + s += "\n"; + } + + s += (std::string(reinterpret_cast(state)) + " : " + + boost::lexical_cast(i) + "/" + + boost::lexical_cast(native) + " " + + std::string(reinterpret_cast(text))); + } + else + { + return s; + } + } + } +} diff -r 6a49c495c940 -r b5fb8b77ce4d Framework/Odbc/OdbcEnvironment.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Odbc/OdbcEnvironment.h Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,53 @@ +/** + * 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 . + **/ + + +#pragma once + +#if defined(_WIN32) +# include // Used in "sql.h" +#endif + +#include +#include +#include + + +namespace OrthancDatabases +{ + class OdbcEnvironment : public boost::noncopyable + { + private: + SQLHENV handle_; + + public: + OdbcEnvironment(); + + virtual ~OdbcEnvironment(); + + SQLHENV GetHandle() + { + return handle_; + } + + static std::string FormatError(SQLHANDLE handle, + SQLSMALLINT type); + }; +} diff -r 6a49c495c940 -r b5fb8b77ce4d Framework/Odbc/OdbcPreparedStatement.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Odbc/OdbcPreparedStatement.cpp Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,252 @@ +/** + * 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 . + **/ + + +#include "OdbcPreparedStatement.h" + +#include "../Common/InputFileValue.h" +#include "../Common/Integer64Value.h" +#include "../Common/Utf8StringValue.h" +#include "OdbcResult.h" + +#include +#include + +#include + + +namespace OrthancDatabases +{ + void OdbcPreparedStatement::Setup(const Query& query) + { + formatter_.SetNamedDialect(Dialect_MSSQL); /* ODBC uses "?" to name its parameters */ + + std::string sql; + query.Format(sql, formatter_); + + LOG(INFO) << "Preparing ODBC statement: " << sql; + SQLCHAR* s = const_cast(reinterpret_cast(sql.c_str())); + + if (!SQL_SUCCEEDED(SQLPrepare(statement_.GetHandle(), s, SQL_NTS))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, + "Cannot prepare ODBC statement: " + sql); + } + + paramsIndex_.resize(formatter_.GetParametersCount()); + + size_t countInt64 = 0; + size_t countStrings = 0; + + for (size_t i = 0; i < paramsIndex_.size(); i++) + { + switch (formatter_.GetParameterType(i)) + { + case ValueType_Integer64: + paramsIndex_[i] = countInt64; + countInt64++; + break; + + case ValueType_InputFile: + case ValueType_Utf8String: + paramsIndex_[i] = countStrings; + countStrings++; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + + paramsInt64_.resize(countInt64); + paramsString_.resize(countStrings); + } + + + OdbcPreparedStatement::OdbcPreparedStatement(SQLHSTMT databaseHandle, + Dialect dialect, + const Query& query) : + statement_(databaseHandle), + formatter_(dialect) + { + Setup(query); + } + + + OdbcPreparedStatement::OdbcPreparedStatement(SQLHSTMT databaseHandle, + Dialect dialect, + const std::string& sql) : + statement_(databaseHandle), + formatter_(dialect) + { + Query query(sql); + Setup(query); + } + + + IResult* OdbcPreparedStatement::Execute() + { + Dictionary parameters; + return Execute(parameters); + } + + + IResult* OdbcPreparedStatement::Execute(const Dictionary& parameters) + { + /** + * NB: This class creates a copy of all the string parameters from + * "parameters", because "SQLBindParameter()" assumes that the + * lifetime of the bound values must be larger than the lifetime + * of the cursor. This is no problem for the index plugin, but + * might be problematic if storing large files in the storage area + * (as this doubles RAM requirements). + **/ + + for (size_t i = 0; i < formatter_.GetParametersCount(); i++) + { + const std::string& name = formatter_.GetParameterName(i); + + if (!parameters.HasKey(name)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem, + "Missing parameter to SQL prepared statement: " + name); + } + else + { + const IValue& value = parameters.GetValue(name); + if (value.GetType() == ValueType_Null) + { + SQLSMALLINT cType, sqlType; + + switch (formatter_.GetParameterType(i)) + { + case ValueType_Integer64: + cType = SQL_C_SBIGINT; + sqlType = SQL_BIGINT; + break; + + case ValueType_Utf8String: + cType = SQL_C_CHAR; + sqlType = SQL_VARCHAR; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + SQLLEN null = SQL_NULL_DATA; + if (!SQL_SUCCEEDED(SQLBindParameter(statement_.GetHandle(), i + 1, SQL_PARAM_INPUT, cType, sqlType, + 0 /* ignored */, 0 /* ignored */, NULL, 0, &null))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, + "Cannot bind NULL parameter: " + statement_.FormatError()); + } + } + else if (value.GetType() != formatter_.GetParameterType(i)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType, + "Parameter \"" + name + "\" should be of type \"" + + EnumerationToString(formatter_.GetParameterType(i)) + + "\", found \"" + EnumerationToString(value.GetType()) + "\""); + } + else + { + assert(i < paramsIndex_.size()); + size_t index = paramsIndex_[i]; + + switch (value.GetType()) + { + case ValueType_Integer64: + assert(index < paramsInt64_.size()); + paramsInt64_[index] = dynamic_cast(value).GetValue(); + + if (!SQL_SUCCEEDED(SQLBindParameter(statement_.GetHandle(), i + 1, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_BIGINT, + 0 /* ignored */, 0 /* ignored */, ¶msInt64_[index], + sizeof(int64_t), NULL))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, + "Cannot bind integer parameter: " + statement_.FormatError()); + } + + break; + + case ValueType_Utf8String: + { + assert(index < paramsString_.size()); + paramsString_[index] = dynamic_cast(value).GetContent(); + + const char* content = (paramsString_[index].empty() ? "" : paramsString_[index].c_str()); + + if (!SQL_SUCCEEDED(SQLBindParameter( + statement_.GetHandle(), i + 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, + 0 /* ignored */, 0 /* ignored */, const_cast(content), paramsString_[index].size(), NULL))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, + "Cannot bind UTF-8 parameter: " + statement_.FormatError()); + } + + break; + } + + case ValueType_InputFile: + { + assert(index < paramsString_.size()); + paramsString_[index] = dynamic_cast(value).GetContent(); + + const char* content = (paramsString_[index].empty() ? NULL : paramsString_[index].c_str()); + + SQLLEN a = paramsString_[index].size(); + if (!SQL_SUCCEEDED(SQLBindParameter( + statement_.GetHandle(), i + 1, SQL_PARAM_INPUT, SQL_C_BINARY, SQL_LONGVARBINARY, + paramsString_[index].size() /* only used by MSSQL */, + 0 /* ignored */, const_cast(content), 0, &a))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, + "Cannot bind binary parameter: " + statement_.FormatError()); + } + + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + } + } + + const Dialect dialect = formatter_.GetAutoincrementDialect(); + + SQLRETURN code = SQLExecute(statement_.GetHandle()); + + if (code == SQL_SUCCESS || + code == SQL_NO_DATA /* this is the case of DELETE in PostgreSQL and MSSQL */) + { + return new OdbcResult(statement_, dialect); + } + else + { + statement_.CheckCollision(dialect); + + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, + "Cannot execute ODBC statement:\n" + statement_.FormatError()); + } + } +} diff -r 6a49c495c940 -r b5fb8b77ce4d Framework/Odbc/OdbcPreparedStatement.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Odbc/OdbcPreparedStatement.h Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,58 @@ +/** + * 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 . + **/ + + +#pragma once + +#include "OdbcStatement.h" + +#include "../Common/Dictionary.h" +#include "../Common/GenericFormatter.h" +#include "../Common/IPrecompiledStatement.h" +#include "../Common/IResult.h" + + +namespace OrthancDatabases +{ + class OdbcPreparedStatement : public IPrecompiledStatement + { + private: + OdbcStatement statement_; + GenericFormatter formatter_; + std::vector paramsInt64_; + std::vector paramsString_; + std::vector paramsIndex_; + + void Setup(const Query& query); + + public: + OdbcPreparedStatement(SQLHSTMT databaseHandle, + Dialect dialect, + const Query& query); + + OdbcPreparedStatement(SQLHSTMT databaseHandle, + Dialect dialect, + const std::string& sql); + + IResult* Execute(); + + IResult* Execute(const Dictionary& parameters); + }; +} diff -r 6a49c495c940 -r b5fb8b77ce4d Framework/Odbc/OdbcResult.cpp --- /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 . + **/ + + +#include "OdbcResult.h" + +#include "../Common/BinaryStringValue.h" +#include "../Common/Integer64Value.h" +#include "../Common/NullValue.h" +#include "../Common/Utf8StringValue.h" + +#include +#include +#include +#include + +#include +#include + + +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 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(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(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(*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(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(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]; + } + } +} diff -r 6a49c495c940 -r b5fb8b77ce4d Framework/Odbc/OdbcResult.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Odbc/OdbcResult.h Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,76 @@ +/** + * 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 . + **/ + + +#pragma once + +#include "OdbcStatement.h" + +#include "../Common/IResult.h" + +#include + +#include +#include + + +namespace OrthancDatabases +{ + class OdbcResult : public IResult + { + private: + OdbcStatement& statement_; + Dialect dialect_; + bool first_; + bool done_; + std::vector types_; + std::vector typeNames_; + std::vector values_; + + void LoadFirst(); + + void SetValue(size_t index, + IValue* value); + + void ReadString(std::string& target, + size_t column, + bool isBinary); + + public: + OdbcResult(OdbcStatement& statement, + Dialect dialect); + + virtual ~OdbcResult(); + + virtual void SetExpectedType(size_t field, + ValueType type) ORTHANC_OVERRIDE; + + virtual bool IsDone() const ORTHANC_OVERRIDE; + + virtual void Next() ORTHANC_OVERRIDE; + + virtual size_t GetFieldsCount() const ORTHANC_OVERRIDE + { + return values_.size(); + } + + virtual const IValue& GetField(size_t field) const ORTHANC_OVERRIDE; + }; +} diff -r 6a49c495c940 -r b5fb8b77ce4d Framework/Odbc/OdbcStatement.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Odbc/OdbcStatement.cpp Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,86 @@ +/** + * 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 . + **/ + + +#include "OdbcStatement.h" + +#include "OdbcEnvironment.h" + +#include +#include + +#include + + +namespace OrthancDatabases +{ + OdbcStatement::OdbcStatement(SQLHSTMT databaseHandle) + { + if (!SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_STMT, databaseHandle, &handle_))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot allocate statement"); + } + } + + + OdbcStatement::~OdbcStatement() + { + if (!SQL_SUCCEEDED(SQLFreeHandle(SQL_HANDLE_STMT, handle_))) + { + LOG(ERROR) << "Cannot destruct statement"; + } + } + + + std::string OdbcStatement::FormatError() + { + return OdbcEnvironment::FormatError(handle_, SQL_HANDLE_STMT); + } + + + void OdbcStatement::CheckCollision(Dialect dialect) + { + SQLINTEGER native = -1; + SQLCHAR stateBuf[SQL_SQLSTATE_SIZE + 1]; + SQLSMALLINT stateLength = 0; + + for (SQLSMALLINT recNum = 1; ; recNum++) + { + if (SQL_SUCCEEDED(SQLGetDiagField(SQL_HANDLE_STMT, handle_, + recNum, SQL_DIAG_NATIVE, &native, SQL_IS_INTEGER, 0)) && + SQL_SUCCEEDED(SQLGetDiagField(SQL_HANDLE_STMT, handle_, + recNum, SQL_DIAG_SQLSTATE, &stateBuf, sizeof(stateBuf), &stateLength))) + { + const std::string state(reinterpret_cast(stateBuf)); + + if (state == "40001" || + (dialect == Dialect_MySQL && native == 1213) || + (dialect == Dialect_MSSQL && native == 1205)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_DatabaseCannotSerialize); + } + } + else + { + return; + } + } + } +} diff -r 6a49c495c940 -r b5fb8b77ce4d Framework/Odbc/OdbcStatement.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Odbc/OdbcStatement.h Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,56 @@ +/** + * 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 . + **/ + + +#pragma once + +#if defined(_WIN32) +# include // Used in "sql.h" +#endif + +#include "../Common/DatabasesEnumerations.h" + +#include +#include +#include + + +namespace OrthancDatabases +{ + class OdbcStatement : public boost::noncopyable + { + private: + SQLHSTMT handle_; + + public: + OdbcStatement(SQLHSTMT databaseHandle); + + ~OdbcStatement(); + + SQLHSTMT GetHandle() + { + return handle_; + } + + std::string FormatError(); + + void CheckCollision(Dialect dialect); + }; +} diff -r 6a49c495c940 -r b5fb8b77ce4d MySQL/CMakeLists.txt --- a/MySQL/CMakeLists.txt Thu Jul 22 20:20:26 2021 +0200 +++ b/MySQL/CMakeLists.txt Tue Aug 10 20:08:53 2021 +0200 @@ -1,3 +1,22 @@ +# 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 . + + cmake_minimum_required(VERSION 2.8) project(OrthancMySQL) diff -r 6a49c495c940 -r b5fb8b77ce4d Odbc/CMakeLists.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Odbc/CMakeLists.txt Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,158 @@ +# 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 . + + +cmake_minimum_required(VERSION 2.8) +project(OrthancODBC) + +set(ORTHANC_PLUGIN_VERSION "mainline") + +set(ORTHANC_OPTIMAL_VERSION_MAJOR 1) +set(ORTHANC_OPTIMAL_VERSION_MINOR 9) +set(ORTHANC_OPTIMAL_VERSION_REVISION 2) + +if (ORTHANC_PLUGIN_VERSION STREQUAL "mainline") + set(ORTHANC_FRAMEWORK_VERSION "mainline") + set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg") +else() + set(ORTHANC_FRAMEWORK_VERSION "1.9.6") + set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web") +endif() + +include(${CMAKE_SOURCE_DIR}/../Resources/CMake/DatabasesPluginParameters.cmake) + +set(ENABLE_ODBC_BACKEND ON) + +include(${CMAKE_SOURCE_DIR}/../Resources/CMake/DatabasesPluginConfiguration.cmake) + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${ORTHANC_FRAMEWORK_ROOT}/../Resources/WindowsResources.py + ${ORTHANC_PLUGIN_VERSION} "ODBC storage area plugin" OrthancODBCStorage.dll + "ODBC as a database back-end to Orthanc (storage area)" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/StorageVersion.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${ORTHANC_FRAMEWORK_ROOT}/../Resources/WindowsResources.py + ${ORTHANC_PLUGIN_VERSION} "ODBC index plugin" OrthancODBCIndex.dll + "ODBC as a database back-end to Orthanc (index)" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/IndexVersion.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + set(INDEX_RESOURCES ${AUTOGENERATED_DIR}/IndexVersion.rc) + set(STORAGE_RESOURCES ${AUTOGENERATED_DIR}/StorageVersion.rc) +endif() + + +EmbedResources( + ODBC_PREPARE_INDEX ${CMAKE_SOURCE_DIR}/Plugins/PrepareIndex.sql + ODBC_PREPARE_STORAGE ${CMAKE_SOURCE_DIR}/Plugins/PrepareStorage.sql + ) + +add_custom_target( + AutogeneratedTarget + DEPENDS + ${AUTOGENERATED_SOURCES} + ) + +add_library(FrameworkForPlugins STATIC + ${AUTOGENERATED_SOURCES} + ${DATABASES_SOURCES} + ${LTDL_SOURCES} + ${UNIX_ODBC_SOURCES} + + ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/PluginInitialization.cpp + Plugins/OdbcIndex.cpp + ) + +set_target_properties(FrameworkForPlugins PROPERTIES + POSITION_INDEPENDENT_CODE ON + COMPILE_FLAGS -DORTHANC_ENABLE_LOGGING_PLUGIN=1 + ) + +add_library(OrthancODBCIndex SHARED + ${INDEX_RESOURCES} + Plugins/IndexPlugin.cpp + ) + +add_library(OrthancODBCStorage SHARED + ${STORAGE_RESOURCES} + Plugins/StoragePlugin.cpp + ) + +add_dependencies(FrameworkForPlugins AutogeneratedTarget) + +target_link_libraries(OrthancODBCIndex FrameworkForPlugins) +target_link_libraries(OrthancODBCStorage FrameworkForPlugins) + +message("Setting the version of the libraries to ${ORTHANC_PLUGIN_VERSION}") + +add_definitions( + -DORTHANC_PLUGIN_VERSION="${ORTHANC_PLUGIN_VERSION}" + ) + +set_target_properties(OrthancODBCStorage PROPERTIES + VERSION ${ORTHANC_PLUGIN_VERSION} + SOVERSION ${ORTHANC_PLUGIN_VERSION} + COMPILE_FLAGS -DORTHANC_ENABLE_LOGGING_PLUGIN=1 + ) + +set_target_properties(OrthancODBCIndex PROPERTIES + VERSION ${ORTHANC_PLUGIN_VERSION} + SOVERSION ${ORTHANC_PLUGIN_VERSION} + COMPILE_FLAGS -DORTHANC_ENABLE_LOGGING_PLUGIN=1 + ) + +install( + TARGETS OrthancODBCIndex OrthancODBCStorage + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) + + +add_executable(UnitTests + ${AUTOGENERATED_SOURCES} + ${DATABASES_SOURCES} + ${GOOGLE_TEST_SOURCES} + ${LTDL_SOURCES} + ${UNIX_ODBC_SOURCES} + + Plugins/OdbcIndex.cpp + UnitTests/UnitTestsMain.cpp + ) + +add_dependencies(UnitTests AutogeneratedTarget) + +target_link_libraries(UnitTests ${GOOGLE_TEST_LIBRARIES}) +set_target_properties(UnitTests PROPERTIES + COMPILE_FLAGS -DORTHANC_ENABLE_LOGGING_PLUGIN=0 + ) diff -r 6a49c495c940 -r b5fb8b77ce4d Odbc/Plugins/IndexPlugin.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Odbc/Plugins/IndexPlugin.cpp Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,138 @@ +/** + * 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 . + **/ + + +#include "OdbcIndex.h" + +#include "../../Framework/Plugins/PluginInitialization.h" +#include "../../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" + +#include + + +#if defined(_WIN32) +# warning Strings have not been tested on Windows (UTF-16 issues ahead)! +# include +#else +# include +# include +#endif + + +static const char* const KEY_ODBC = "Odbc"; + + +extern "C" +{ +#if !defined(_WIN32) + extern lt_dlvtable *dlopen_LTX_get_vtable(lt_user_data loader_data); +#endif + + + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) + { + if (!OrthancDatabases::InitializePlugin(context, "ODBC", true)) + { + return -1; + } + +#if !defined(_WIN32) + lt_dlinit(); + + /** + * The following call is necessary for "libltdl" to access the + * "dlopen()" primitives if statically linking. Otherwise, only the + * "preopen" primitives are available. + **/ + lt_dlloader_add(dlopen_LTX_get_vtable(NULL)); +#endif + + OrthancPlugins::OrthancConfiguration configuration; + + if (!configuration.IsSection(KEY_ODBC)) + { + LOG(WARNING) << "No available configuration for the ODBC index plugin"; + return 0; + } + + OrthancPlugins::OrthancConfiguration odbc; + configuration.GetSection(odbc, KEY_ODBC); + + bool enable; + if (!odbc.LookupBooleanValue(enable, "EnableIndex") || + !enable) + { + LOG(WARNING) << "The ODBC index is currently disabled, set \"EnableIndex\" " + << "to \"true\" in the \"" << KEY_ODBC << "\" section of the configuration file of Orthanc"; + return 0; + } + + try + { + const std::string connectionString = odbc.GetStringValue("IndexConnectionString", ""); + const unsigned int countConnections = odbc.GetUnsignedIntegerValue("IndexConnectionsCount", 1); + const unsigned int maxConnectionRetries = odbc.GetUnsignedIntegerValue("MaxConnectionRetries", 10); + const unsigned int connectionRetryInterval = odbc.GetUnsignedIntegerValue("ConnectionRetryInterval", 5); + + if (connectionString.empty()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "No connection string provided for the ODBC index"); + } + + std::unique_ptr index(new OrthancDatabases::OdbcIndex(context, connectionString)); + index->SetMaxConnectionRetries(maxConnectionRetries); + index->SetConnectionRetryInterval(connectionRetryInterval); + + OrthancDatabases::IndexBackend::Register(index.release(), countConnections, maxConnectionRetries); + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << e.What(); + return -1; + } + catch (...) + { + LOG(ERROR) << "Native exception while initializing the plugin"; + return -1; + } + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + LOG(WARNING) << "ODBC index is finalizing"; + OrthancDatabases::IndexBackend::Finalize(); + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return "odbc-index"; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return ORTHANC_PLUGIN_VERSION; + } +} diff -r 6a49c495c940 -r b5fb8b77ce4d Odbc/Plugins/OdbcIndex.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Odbc/Plugins/OdbcIndex.cpp Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,694 @@ +/** + * 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 . + **/ + + +#include "OdbcIndex.h" + +#include "../../Framework/Common/Integer64Value.h" +#include "../../Framework/Odbc/OdbcDatabase.h" +#include "../../Framework/Plugins/GlobalProperties.h" + +#include // Autogenerated file + +#include +#include +#include + +#include + + +// Some aliases for internal properties +static const Orthanc::GlobalProperty GlobalProperty_LastChange = Orthanc::GlobalProperty_DatabaseInternal0; + + +namespace OrthancDatabases +{ + static int64_t GetSQLiteLastInsert(DatabaseManager& manager) + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, "SELECT LAST_INSERT_ROWID()"); + + statement.Execute(); + + return statement.ReadInteger64(0); + } + + + static int64_t GetMySQLLastInsert(DatabaseManager& manager) + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, "SELECT LAST_INSERT_ID()"); + + statement.Execute(); + + return statement.ReadInteger64(0); + } + + + static int64_t GetMSSQLLastInsert(DatabaseManager& manager) + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, "SELECT @@IDENTITY"); + + statement.Execute(); + + return statement.ReadInteger64(0); + } + + + static void AddPatientToRecyclingOrder(DatabaseManager& manager, + int64_t patient) + { + // In the other database plugins, this is done with a trigger + + std::unique_ptr statement; + + switch (manager.GetDialect()) + { + case Dialect_SQLite: + case Dialect_MySQL: + statement.reset( + new DatabaseManager::CachedStatement( + STATEMENT_FROM_HERE, manager, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ${patient})")); + break; + + case Dialect_PostgreSQL: + statement.reset( + new DatabaseManager::CachedStatement( + STATEMENT_FROM_HERE, manager, "INSERT INTO PatientRecyclingOrder VALUES(DEFAULT, ${patient})")); + break; + + case Dialect_MSSQL: + statement.reset( + new DatabaseManager::CachedStatement( + STATEMENT_FROM_HERE, manager, "INSERT INTO PatientRecyclingOrder VALUES(${patient})")); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + statement->SetParameterType("patient", ValueType_Integer64); + + Dictionary args; + args.SetIntegerValue("patient", patient); + statement->Execute(args); + } + + + static OrthancPluginResourceType GetParentType(OrthancPluginResourceType level) + { + switch (level) + { + case OrthancPluginResourceType_Study: + return OrthancPluginResourceType_Patient; + + case OrthancPluginResourceType_Series: + return OrthancPluginResourceType_Study; + + case OrthancPluginResourceType_Instance: + return OrthancPluginResourceType_Series; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + OdbcIndex::OdbcIndex(OrthancPluginContext* context, + const std::string& connectionString) : + IndexBackend(context), + maxConnectionRetries_(10), + connectionRetryInterval_(5), + connectionString_(connectionString) + { + } + + + void OdbcIndex::SetConnectionRetryInterval(unsigned int seconds) + { + if (seconds == 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + connectionRetryInterval_ = seconds; + } + } + + + IDatabaseFactory* OdbcIndex::CreateDatabaseFactory() + { + return OdbcDatabase::CreateDatabaseFactory(maxConnectionRetries_, connectionRetryInterval_, connectionString_, true); + } + + + void OdbcIndex::ConfigureDatabase(DatabaseManager& manager) + { + uint32_t expectedVersion = 6; + + if (GetContext()) // "GetContext()" can possibly be NULL in the unit tests + { + expectedVersion = OrthancPluginGetExpectedDatabaseVersion(GetContext()); + } + + // Check the expected version of the database + if (expectedVersion != 6) + { + LOG(ERROR) << "This database plugin is incompatible with your version of Orthanc " + << "expecting the DB schema version " << expectedVersion + << ", but this plugin is only compatible with version 6"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin); + } + + OdbcDatabase& db = dynamic_cast(manager.GetDatabase()); + + if (!db.DoesTableExist("resources")) + { + std::string sql; + Orthanc::EmbeddedResources::GetFileResource(sql, Orthanc::EmbeddedResources::ODBC_PREPARE_INDEX); + + switch (db.GetDialect()) + { + case Dialect_SQLite: + boost::replace_all(sql, "${LONGTEXT}", "TEXT"); + boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT"); + boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "NULL, "); + break; + + case Dialect_PostgreSQL: + boost::replace_all(sql, "${LONGTEXT}", "TEXT"); + boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGSERIAL NOT NULL PRIMARY KEY"); + boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "DEFAULT, "); + break; + + case Dialect_MySQL: + boost::replace_all(sql, "${LONGTEXT}", "LONGTEXT"); + boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY"); + boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "NULL, "); + break; + + case Dialect_MSSQL: + /** + * cf. OMSSQL-5: Use VARCHAR(MAX) instead of TEXT: (1) + * Microsoft issued a warning stating that "ntext, text, and + * image data types will be removed in a future version of + * SQL Server" + * (https://msdn.microsoft.com/en-us/library/ms187993.aspx), + * and (2) SQL Server does not support comparison of TEXT + * with '=' operator (e.g. in WHERE statements such as + * IndexBackend::LookupIdentifier())." + **/ + boost::replace_all(sql, "${LONGTEXT}", "VARCHAR(MAX)"); + boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGINT IDENTITY NOT NULL PRIMARY KEY"); + boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", ""); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + { + DatabaseManager::Transaction t(manager, TransactionType_ReadWrite); + + db.ExecuteMultiLines(sql); + + if (db.GetDialect() == Dialect_MySQL) + { + // Switch to the collation that is the default since MySQL + // 8.0.1. This must be *after* the creation of the tables. + db.ExecuteMultiLines("ALTER DATABASE CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); + } + + t.Commit(); + } + } + } + + + int64_t OdbcIndex::CreateResource(DatabaseManager& manager, + const char* publicId, + OrthancPluginResourceType type) + { + Dictionary args; + args.SetUtf8Value("id", publicId); + args.SetIntegerValue("type", static_cast(type)); + + switch (manager.GetDatabase().GetDialect()) + { + case Dialect_SQLite: + { + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, "INSERT INTO Resources VALUES(NULL, ${type}, ${id}, NULL)"); + + statement.SetParameterType("id", ValueType_Utf8String); + statement.SetParameterType("type", ValueType_Integer64); + statement.Execute(args); + } + + // Must be out of the scope of "DatabaseManager::CachedStatement" + const int64_t id = GetSQLiteLastInsert(manager); + + if (type == OrthancPluginResourceType_Patient) + { + AddPatientToRecyclingOrder(manager, id); + } + + return id; + } + + case Dialect_PostgreSQL: + { + int64_t id; + + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "INSERT INTO Resources VALUES(DEFAULT, ${type}, ${id}, NULL) RETURNING internalId"); + + statement.SetParameterType("id", ValueType_Utf8String); + statement.SetParameterType("type", ValueType_Integer64); + statement.Execute(args); + id = statement.ReadInteger64(0); + } + + if (type == OrthancPluginResourceType_Patient) + { + AddPatientToRecyclingOrder(manager, id); + } + + return id; + } + + case Dialect_MySQL: + { + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, "INSERT INTO Resources VALUES(NULL, ${type}, ${id}, NULL)"); + + statement.SetParameterType("id", ValueType_Utf8String); + statement.SetParameterType("type", ValueType_Integer64); + statement.Execute(args); + } + + // Must be out of the scope of "DatabaseManager::CachedStatement" + const int64_t id = GetMySQLLastInsert(manager); + + if (type == OrthancPluginResourceType_Patient) + { + AddPatientToRecyclingOrder(manager, id); + } + + return id; + } + + case Dialect_MSSQL: + { + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, "INSERT INTO Resources VALUES(${type}, ${id}, NULL)"); + + statement.SetParameterType("id", ValueType_Utf8String); + statement.SetParameterType("type", ValueType_Integer64); + statement.Execute(args); + } + + // Must be out of the scope of "DatabaseManager::CachedStatement" + const int64_t id = GetMSSQLLastInsert(manager); + + if (type == OrthancPluginResourceType_Patient) + { + AddPatientToRecyclingOrder(manager, id); + } + + return id; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + + + void OdbcIndex::DeleteResource(IDatabaseBackendOutput& output, + DatabaseManager& manager, + int64_t id) + { + /** + * Contrarily to PostgreSQL and SQLite, the MySQL dialect + * doesn't support cascaded delete inside the same + * table. Furthermore, for maximum portability, we don't use + * triggers in the ODBC plugins. We therefore implement a custom + * version of this deletion. + **/ + + ClearDeletedFiles(manager); + ClearDeletedResources(manager); + + OrthancPluginResourceType type; + bool hasParent; + int64_t parentId; + + { + DatabaseManager::CachedStatement lookupResource( + STATEMENT_FROM_HERE, manager, + "SELECT resourceType, parentId FROM Resources WHERE internalId=${id}"); + lookupResource.SetParameterType("id", ValueType_Integer64); + + Dictionary args; + args.SetIntegerValue("id", id); + lookupResource.Execute(args); + + if (lookupResource.IsDone()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + + type = static_cast(lookupResource.ReadInteger32(0)); + + if (lookupResource.GetResultField(1).GetType() == ValueType_Null) + { + hasParent = false; + } + else + { + hasParent = true; + parentId = lookupResource.ReadInteger64(1); + } + } + + { + DatabaseManager::CachedStatement scheduleRootDeletion( + STATEMENT_FROM_HERE, manager, + "INSERT INTO DeletedResources SELECT internalId, resourceType, publicId " + "FROM Resources WHERE Resources.internalId = ${id}"); + scheduleRootDeletion.SetParameterType("id", ValueType_Integer64); + + Dictionary args; + args.SetIntegerValue("id", id); + scheduleRootDeletion.Execute(args); + } + + { + const std::string scheduleChildrenDeletion = + "INSERT INTO DeletedResources SELECT Resources.internalId, Resources.resourceType, Resources.publicId " + "FROM Resources INNER JOIN DeletedResources ON Resources.parentId = DeletedResources.internalId " + "WHERE Resources.resourceType = ${level}"; + + switch (type) + { + /** + * WARNING: Don't add "break" or reorder cases below. + **/ + + case OrthancPluginResourceType_Patient: + { + DatabaseManager::CachedStatement statement(STATEMENT_FROM_HERE, manager, scheduleChildrenDeletion); + statement.SetParameterType("level", ValueType_Integer64); + + Dictionary args; + args.SetIntegerValue("level", OrthancPluginResourceType_Study); + statement.Execute(args); + } + + case OrthancPluginResourceType_Study: + { + DatabaseManager::CachedStatement statement(STATEMENT_FROM_HERE, manager, scheduleChildrenDeletion); + statement.SetParameterType("level", ValueType_Integer64); + + Dictionary args; + args.SetIntegerValue("level", OrthancPluginResourceType_Series); + statement.Execute(args); + } + + case OrthancPluginResourceType_Series: + { + DatabaseManager::CachedStatement statement(STATEMENT_FROM_HERE, manager, scheduleChildrenDeletion); + statement.SetParameterType("level", ValueType_Integer64); + + Dictionary args; + args.SetIntegerValue("level", OrthancPluginResourceType_Instance); + statement.Execute(args); + } + + case OrthancPluginResourceType_Instance: + // No child + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + bool hasRemainingAncestor = false; + std::string remainingAncestor; + OrthancPluginResourceType ancestorType; + + if (hasParent) + { + int64_t currentAncestor = parentId; + int64_t currentResource = id; + OrthancPluginResourceType currentType = type; + + for (;;) + { + bool hasSiblings; + + { + std::string suffix; + if (manager.GetDialect() == Dialect_MSSQL) + { + suffix = "ORDER BY internalId OFFSET 0 ROWS FETCH FIRST 1 ROWS ONLY"; + } + else + { + suffix = "LIMIT 1"; + } + + DatabaseManager::CachedStatement lookupSiblings( + STATEMENT_FROM_HERE, manager, + "SELECT internalId FROM Resources WHERE parentId = ${parent} AND internalId <> ${id} " + suffix); + + lookupSiblings.SetParameterType("parent", ValueType_Integer64); + lookupSiblings.SetParameterType("id", ValueType_Integer64); + + Dictionary args; + args.SetIntegerValue("parent", currentAncestor); + args.SetIntegerValue("id", currentResource); + lookupSiblings.Execute(args); + + hasSiblings = !lookupSiblings.IsDone(); + } + + if (hasSiblings) + { + // There remains some sibling: Signal this remaining ancestor + hasRemainingAncestor = true; + remainingAncestor = GetPublicId(manager, currentAncestor); + ancestorType = GetParentType(currentType); + break; + } + else + { + // No sibling remaining: This parent resource must be deleted + { + DatabaseManager::CachedStatement addDeletedResource( + STATEMENT_FROM_HERE, manager, + "INSERT INTO DeletedResources SELECT internalId, resourceType, publicId " + "FROM Resources WHERE internalId=${id}"); + addDeletedResource.SetParameterType("id", ValueType_Integer64); + + Dictionary args; + args.SetIntegerValue("id", currentAncestor); + addDeletedResource.Execute(args); + } + + int64_t tmp; + if (LookupParent(tmp, manager, currentAncestor)) + { + currentResource = currentAncestor; + currentAncestor = tmp; + currentType = GetParentType(currentType); + } + else + { + assert(currentType == OrthancPluginResourceType_Study); + break; + } + } + } + } + + { + // This is implemented by triggers in the PostgreSQL and MySQL plugins + DatabaseManager::CachedStatement lookupDeletedAttachments( + STATEMENT_FROM_HERE, manager, + "INSERT INTO DeletedFiles SELECT AttachedFiles.* FROM AttachedFiles " + "INNER JOIN DeletedResources ON AttachedFiles.id = DeletedResources.internalId"); + lookupDeletedAttachments.Execute(); + } + + { + // Note that the attachments are automatically deleted by DELETE CASCADE + DatabaseManager::CachedStatement applyResourcesDeletion( + STATEMENT_FROM_HERE, manager, + "DELETE FROM Resources WHERE internalId IN (SELECT internalId FROM DeletedResources)"); + applyResourcesDeletion.Execute(); + } + + SignalDeletedResources(output, manager); + SignalDeletedFiles(output, manager); + + if (hasRemainingAncestor) + { + assert(!remainingAncestor.empty()); + output.SignalRemainingAncestor(remainingAncestor, ancestorType); + } + } + + + static void ExecuteLogChange(DatabaseManager::CachedStatement& statement, + const Dictionary& args) + { + statement.SetParameterType("changeType", ValueType_Integer64); + statement.SetParameterType("id", ValueType_Integer64); + statement.SetParameterType("resourceType", ValueType_Integer64); + statement.SetParameterType("date", ValueType_Utf8String); + statement.Execute(args); + } + + + void OdbcIndex::LogChange(DatabaseManager& manager, + int32_t changeType, + int64_t resourceId, + OrthancPluginResourceType resourceType, + const char* date) + { + Dictionary args; + args.SetIntegerValue("changeType", changeType); + args.SetIntegerValue("id", resourceId); + args.SetIntegerValue("resourceType", resourceType); + args.SetUtf8Value("date", date); + + int64_t seq; + + switch (manager.GetDatabase().GetDialect()) + { + case Dialect_SQLite: + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "INSERT INTO Changes VALUES(NULL, ${changeType}, ${id}, ${resourceType}, ${date})"); + ExecuteLogChange(statement, args); + seq = GetSQLiteLastInsert(manager); + break; + } + + case Dialect_PostgreSQL: + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "INSERT INTO Changes VALUES(DEFAULT, ${changeType}, ${id}, ${resourceType}, ${date}) RETURNING seq"); + ExecuteLogChange(statement, args); + seq = statement.ReadInteger64(0); + break; + } + + case Dialect_MySQL: + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "INSERT INTO Changes VALUES(NULL, ${changeType}, ${id}, ${resourceType}, ${date})"); + ExecuteLogChange(statement, args); + seq = GetMySQLLastInsert(manager); + break; + } + + case Dialect_MSSQL: + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "INSERT INTO Changes VALUES(${changeType}, ${id}, ${resourceType}, ${date})"); + ExecuteLogChange(statement, args); + seq = GetMSSQLLastInsert(manager); + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + std::string value = boost::lexical_cast(seq); + SetGlobalProperty(manager, MISSING_SERVER_IDENTIFIER, GlobalProperty_LastChange, value.c_str()); + } + + + int64_t OdbcIndex::GetLastChangeIndex(DatabaseManager& manager) + { + std::string value; + + if (LookupGlobalProperty(value, manager, MISSING_SERVER_IDENTIFIER, GlobalProperty_LastChange)) + { + return boost::lexical_cast(value); + } + else + { + return 0; + } + } + + + void OdbcIndex::DeleteAttachment(IDatabaseBackendOutput& output, + DatabaseManager& manager, + int64_t id, + int32_t attachment) + { + ClearDeletedFiles(manager); + + Dictionary args; + args.SetIntegerValue("id", id); + args.SetIntegerValue("type", static_cast(attachment)); + + { + // This is implemented by triggers in the PostgreSQL and MySQL plugins + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "INSERT INTO DeletedFiles SELECT * FROM AttachedFiles WHERE id=${id} AND fileType=${type}"); + + statement.SetParameterType("id", ValueType_Integer64); + statement.SetParameterType("type", ValueType_Integer64); + statement.Execute(args); + } + + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "DELETE FROM AttachedFiles WHERE id=${id} AND fileType=${type}"); + + statement.SetParameterType("id", ValueType_Integer64); + statement.SetParameterType("type", ValueType_Integer64); + statement.Execute(args); + } + + SignalDeletedFiles(output, manager); + } +} diff -r 6a49c495c940 -r b5fb8b77ce4d Odbc/Plugins/OdbcIndex.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Odbc/Plugins/OdbcIndex.h Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,86 @@ +/** + * 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 . + **/ + + +#pragma once + +#include "../../Framework/Plugins/IndexBackend.h" + +namespace OrthancDatabases +{ + class OdbcIndex : public IndexBackend + { + private: + unsigned int maxConnectionRetries_; + unsigned int connectionRetryInterval_; + std::string connectionString_; + + public: + OdbcIndex(OrthancPluginContext* context, + const std::string& connectionString); + + unsigned int GetMaxConnectionRetries() const + { + return maxConnectionRetries_; + } + + void SetMaxConnectionRetries(unsigned int retries) + { + maxConnectionRetries_ = retries; + } + + unsigned int GetConnectionRetryInterval() const + { + return connectionRetryInterval_; + } + + void SetConnectionRetryInterval(unsigned int seconds); + + virtual IDatabaseFactory* CreateDatabaseFactory() ORTHANC_OVERRIDE; + + virtual void ConfigureDatabase(DatabaseManager& manager) ORTHANC_OVERRIDE; + + virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE + { + return true; + } + + virtual int64_t CreateResource(DatabaseManager& manager, + const char* publicId, + OrthancPluginResourceType type) ORTHANC_OVERRIDE; + + virtual void DeleteResource(IDatabaseBackendOutput& output, + DatabaseManager& manager, + int64_t id) ORTHANC_OVERRIDE; + + virtual void LogChange(DatabaseManager& manager, + int32_t changeType, + int64_t resourceId, + OrthancPluginResourceType resourceType, + const char* date) ORTHANC_OVERRIDE; + + virtual int64_t GetLastChangeIndex(DatabaseManager& manager) ORTHANC_OVERRIDE; + + virtual void DeleteAttachment(IDatabaseBackendOutput& output, + DatabaseManager& manager, + int64_t id, + int32_t attachment) ORTHANC_OVERRIDE; + }; +} diff -r 6a49c495c940 -r b5fb8b77ce4d Odbc/Plugins/PrepareIndex.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Odbc/Plugins/PrepareIndex.sql Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,124 @@ +CREATE TABLE GlobalProperties( + property INTEGER PRIMARY KEY, + value ${LONGTEXT} + ); + +CREATE TABLE ServerProperties( + server VARCHAR(64) NOT NULL, + property INTEGER NOT NULL, + value ${LONGTEXT}, + PRIMARY KEY(server, property) + ); + +CREATE TABLE Resources( + internalId ${AUTOINCREMENT_TYPE}, + resourceType INTEGER NOT NULL, + publicId VARCHAR(64) NOT NULL, + parentId BIGINT + ); + +CREATE TABLE MainDicomTags( + id BIGINT NOT NULL, + tagGroup INTEGER NOT NULL, + tagElement INTEGER NOT NULL, + value VARCHAR(255), + PRIMARY KEY(id, tagGroup, tagElement), + CONSTRAINT MainDicomTags1 FOREIGN KEY (id) REFERENCES Resources(internalId) ON DELETE CASCADE + ); + +CREATE TABLE DicomIdentifiers( + id BIGINT NOT NULL, + tagGroup INTEGER NOT NULL, + tagElement INTEGER NOT NULL, + value VARCHAR(255), + PRIMARY KEY(id, tagGroup, tagElement), + CONSTRAINT DicomIdentifiers1 FOREIGN KEY (id) REFERENCES Resources(internalId) ON DELETE CASCADE + ); + +CREATE TABLE Metadata( + id BIGINT NOT NULL, + type INTEGER NOT NULL, + value ${LONGTEXT}, + revision INTEGER, + PRIMARY KEY(id, type), + CONSTRAINT Metadata1 FOREIGN KEY (id) REFERENCES Resources(internalId) ON DELETE CASCADE + ); + +CREATE TABLE AttachedFiles( + id BIGINT NOT NULL, + fileType INTEGER, + uuid VARCHAR(64) NOT NULL, + compressedSize BIGINT, + uncompressedSize BIGINT, + compressionType INTEGER, + uncompressedHash VARCHAR(40), + compressedHash VARCHAR(40), + revision INTEGER, + PRIMARY KEY(id, fileType), + CONSTRAINT AttachedFiles1 FOREIGN KEY (id) REFERENCES Resources(internalId) ON DELETE CASCADE + ); + +CREATE TABLE Changes( + seq ${AUTOINCREMENT_TYPE}, + changeType INTEGER, + internalId BIGINT NOT NULL, + resourceType INTEGER, + date VARCHAR(64), + CONSTRAINT Changes1 FOREIGN KEY (internalId) REFERENCES Resources(internalId) ON DELETE CASCADE + ); + +CREATE TABLE ExportedResources( + seq ${AUTOINCREMENT_TYPE}, + resourceType INTEGER, + publicId VARCHAR(64), + remoteModality VARCHAR(64), + patientId VARCHAR(64), + studyInstanceUid VARCHAR(128), + seriesInstanceUid VARCHAR(128), + sopInstanceUid VARCHAR(128), + date VARCHAR(64) + ); + +CREATE TABLE PatientRecyclingOrder( + seq ${AUTOINCREMENT_TYPE}, + patientId BIGINT NOT NULL, + CONSTRAINT PatientRecyclingOrder1 FOREIGN KEY (patientId) REFERENCES Resources(internalId) ON DELETE CASCADE + ); + +CREATE INDEX ChildrenIndex ON Resources(parentId); +CREATE INDEX PublicIndex ON Resources(publicId); +CREATE INDEX ResourceTypeIndex ON Resources(resourceType); +CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId); + +CREATE INDEX MainDicomTagsIndex ON MainDicomTags(id); +CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id); +CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement); +CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value); + +CREATE INDEX ChangesIndex ON Changes(internalId); + + + +-- New tables wrt. Orthanc core +CREATE TABLE DeletedFiles( -- Same structure as AttachedFiles + id BIGINT NOT NULL, + fileType INTEGER, + uuid VARCHAR(64) NOT NULL, + compressedSize BIGINT, + uncompressedSize BIGINT, + compressionType INTEGER, + uncompressedHash VARCHAR(40), + compressedHash VARCHAR(40), + revision INTEGER + ); + +CREATE TABLE DeletedResources( + internalId BIGINT NOT NULL PRIMARY KEY, + resourceType INTEGER NOT NULL, + publicId VARCHAR(64) NOT NULL + ); + + + +-- Set version of database to 6 +INSERT INTO GlobalProperties VALUES(1, '6'); diff -r 6a49c495c940 -r b5fb8b77ce4d Odbc/Plugins/PrepareStorage.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Odbc/Plugins/PrepareStorage.sql Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,4 @@ +CREATE TABLE StorageArea( + uuid VARCHAR(64) NOT NULL PRIMARY KEY, + content ${BINARY} NOT NULL, + type INTEGER NOT NULL); diff -r 6a49c495c940 -r b5fb8b77ce4d Odbc/Plugins/StoragePlugin.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Odbc/Plugins/StoragePlugin.cpp Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,199 @@ +/** + * 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 . + **/ + + +#include "../../Framework/Odbc/OdbcDatabase.h" +#include "../../Framework/Plugins/PluginInitialization.h" +#include "../../Framework/Plugins/StorageBackend.h" +#include "../../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" + +#include // Autogenerated file + +#include + + +namespace OrthancDatabases +{ + class OdbcStorageArea : public StorageBackend + { + protected: + virtual bool HasReadRange() const ORTHANC_OVERRIDE + { + // Read range is only available in native PostgreSQL/MySQL plugins + return false; + } + + public: + OdbcStorageArea(unsigned int maxConnectionRetries, + unsigned int connectionRetryInterval, + const std::string& connectionString) : + StorageBackend(OdbcDatabase::CreateDatabaseFactory( + maxConnectionRetries, connectionRetryInterval, connectionString, false), + maxConnectionRetries) + { + { + AccessorBase accessor(*this); + OdbcDatabase& db = dynamic_cast(accessor.GetManager().GetDatabase()); + + if (!db.DoesTableExist("storagearea")) + { + std::string sql; + Orthanc::EmbeddedResources::GetFileResource(sql, Orthanc::EmbeddedResources::ODBC_PREPARE_STORAGE); + + switch (db.GetDialect()) + { + case Dialect_SQLite: + boost::replace_all(sql, "${BINARY}", "BLOB"); + break; + + case Dialect_PostgreSQL: + boost::replace_all(sql, "${BINARY}", "BYTEA"); + break; + + case Dialect_MySQL: + boost::replace_all(sql, "${BINARY}", "LONGBLOB"); + break; + + case Dialect_MSSQL: + boost::replace_all(sql, "${BINARY}", "VARBINARY(MAX)"); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + { + DatabaseManager::Transaction t(accessor.GetManager(), TransactionType_ReadWrite); + db.ExecuteMultiLines(sql); + t.Commit(); + } + } + } + } + }; +} + + + +#if defined(_WIN32) +# include +#else +# include +# include +#endif + + +static const char* const KEY_ODBC = "Odbc"; + + +extern "C" +{ +#if !defined(_WIN32) + extern lt_dlvtable *dlopen_LTX_get_vtable(lt_user_data loader_data); +#endif + + + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) + { + if (!OrthancDatabases::InitializePlugin(context, "ODBC", false)) + { + return -1; + } + +#if !defined(_WIN32) + lt_dlinit(); + + /** + * The following call is necessary for "libltdl" to access the + * "dlopen()" primitives if statically linking. Otherwise, only the + * "preopen" primitives are available. + **/ + lt_dlloader_add(dlopen_LTX_get_vtable(NULL)); +#endif + + OrthancPlugins::OrthancConfiguration configuration; + + if (!configuration.IsSection(KEY_ODBC)) + { + LOG(WARNING) << "No available configuration for the ODBC storage area plugin"; + return 0; + } + + OrthancPlugins::OrthancConfiguration odbc; + configuration.GetSection(odbc, KEY_ODBC); + + bool enable; + if (!odbc.LookupBooleanValue(enable, "EnableStorage") || + !enable) + { + LOG(WARNING) << "The ODBC storage area is currently disabled, set \"EnableStorage\" " + << "to \"true\" in the \"" << KEY_ODBC << "\" section of the configuration file of Orthanc"; + return 0; + } + + try + { + const std::string connectionString = odbc.GetStringValue("StorageConnectionString", ""); + const unsigned int maxConnectionRetries = odbc.GetUnsignedIntegerValue("MaxConnectionRetries", 10); + const unsigned int connectionRetryInterval = odbc.GetUnsignedIntegerValue("ConnectionRetryInterval", 5); + + if (connectionString.empty()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "No connection string provided for the ODBC storage area"); + } + + OrthancDatabases::StorageBackend::Register( + context, new OrthancDatabases::OdbcStorageArea( + maxConnectionRetries, connectionRetryInterval, connectionString)); + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << e.What(); + return -1; + } + catch (...) + { + LOG(ERROR) << "Native exception while initializing the plugin"; + return -1; + } + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + LOG(WARNING) << "ODBC storage area is finalizing"; + OrthancDatabases::StorageBackend::Finalize(); + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return "odbc-storage"; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return ORTHANC_PLUGIN_VERSION; + } +} diff -r 6a49c495c940 -r b5fb8b77ce4d Odbc/UnitTests/UnitTestsMain.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Odbc/UnitTests/UnitTestsMain.cpp Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,93 @@ +/** + * 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 . + **/ + + +#include "../Plugins/OdbcIndex.h" + +static std::string connectionString_; + +#include "../../Framework/Plugins/IndexUnitTests.h" + +#include + + +#if defined(_WIN32) +# warning Strings have not been tested on Windows (UTF16 issues ahead)! +# include +#else +# include +# include +#endif + + +#if !defined(_WIN32) +extern "C" +{ + extern lt_dlvtable *dlopen_LTX_get_vtable(lt_user_data loader_data); +} +#endif + + +int main(int argc, char **argv) +{ + if (argc < 2) + { + std::cerr + << std::endl + << "Usage: " << argv[0] << " " + << std::endl << std::endl + << "Example: " << argv[0] << " \"DSN=test\"" + << std::endl << std::endl; + return -1; + } + + for (int i = 1; i < argc; i++) + { + // Ignore arguments beginning with "-" to allow passing arguments + // to Google Test such as "--gtest_filter=" + if (argv[i] != NULL && + argv[i][0] != '-') + { + connectionString_ = argv[i]; + } + } + +#if !defined(_WIN32) + lt_dlinit(); + + /** + * The following call is necessary for "libltdl" to access the + * "dlopen()" primitives if statically linking. Otherwise, only the + * "preopen" primitives are available. + **/ + lt_dlloader_add(dlopen_LTX_get_vtable(NULL)); +#endif + + ::testing::InitGoogleTest(&argc, argv); + Orthanc::Logging::Initialize(); + Orthanc::Logging::EnableInfoLevel(true); + //Orthanc::Logging::EnableTraceLevel(true); + + int result = RUN_ALL_TESTS(); + + Orthanc::Logging::Finalize(); + + return result; +} diff -r 6a49c495c940 -r b5fb8b77ce4d PostgreSQL/CMakeLists.txt --- a/PostgreSQL/CMakeLists.txt Thu Jul 22 20:20:26 2021 +0200 +++ b/PostgreSQL/CMakeLists.txt Tue Aug 10 20:08:53 2021 +0200 @@ -1,3 +1,22 @@ +# 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 . + + cmake_minimum_required(VERSION 2.8) project(OrthancPostgreSQL) diff -r 6a49c495c940 -r b5fb8b77ce4d Resources/CMake/DatabasesFrameworkConfiguration.cmake --- a/Resources/CMake/DatabasesFrameworkConfiguration.cmake Thu Jul 22 20:20:26 2021 +0200 +++ b/Resources/CMake/DatabasesFrameworkConfiguration.cmake Tue Aug 10 20:08:53 2021 +0200 @@ -47,6 +47,10 @@ endif() endif() +if (ENABLE_ODBC_BACKEND) +endif() + + ##################################################################### ## Configure the Orthanc Framework @@ -171,3 +175,26 @@ unset(USE_SYSTEM_LIBPQ CACHE) add_definitions(-DORTHANC_ENABLE_POSTGRESQL=0) endif() + + + +##################################################################### +## Configure ODBC if need be +##################################################################### + +if (ENABLE_ODBC_BACKEND) + include(${CMAKE_CURRENT_LIST_DIR}/UnixOdbcConfiguration.cmake) + add_definitions(-DORTHANC_ENABLE_ODBC=1) + list(APPEND DATABASES_SOURCES + ${ORTHANC_DATABASES_ROOT}/Framework/Odbc/OdbcDatabase.cpp + ${ORTHANC_DATABASES_ROOT}/Framework/Odbc/OdbcEnvironment.cpp + ${ORTHANC_DATABASES_ROOT}/Framework/Odbc/OdbcPreparedStatement.cpp + ${ORTHANC_DATABASES_ROOT}/Framework/Odbc/OdbcResult.cpp + ${ORTHANC_DATABASES_ROOT}/Framework/Odbc/OdbcStatement.cpp + ${LIBPQ_SOURCES} + ) +else() + unset(USE_SYSTEM_UNIX_ODBC) + unset(USE_SYSTEM_LTDL) + add_definitions(-DORTHANC_ENABLE_ODBC=0) +endif() diff -r 6a49c495c940 -r b5fb8b77ce4d Resources/CMake/DatabasesFrameworkParameters.cmake --- a/Resources/CMake/DatabasesFrameworkParameters.cmake Thu Jul 22 20:20:26 2021 +0200 +++ b/Resources/CMake/DatabasesFrameworkParameters.cmake Tue Aug 10 20:08:53 2021 +0200 @@ -36,6 +36,11 @@ set(USE_SYSTEM_LIBPQ ON CACHE BOOL "Use the system version of the PostgreSQL client library") set(USE_SYSTEM_MYSQL_CLIENT ON CACHE BOOL "Use the system version of the MySQL client library") +if (NOT CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(USE_SYSTEM_UNIX_ODBC ON CACHE BOOL "Use the system version of unixODBC") + set(USE_SYSTEM_LTDL ON CACHE BOOL "Use the system version of libltdl") +endif() + ##################################################################### ## Internal CMake parameters to enable the optional subcomponents of @@ -43,5 +48,6 @@ ##################################################################### set(ENABLE_MYSQL_BACKEND OFF) +set(ENABLE_ODBC_BACKEND OFF) set(ENABLE_POSTGRESQL_BACKEND OFF) set(ENABLE_SQLITE_BACKEND OFF) diff -r 6a49c495c940 -r b5fb8b77ce4d Resources/CMake/UnixOdbcConfiguration.cmake --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/CMake/UnixOdbcConfiguration.cmake Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,248 @@ +# 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 . + + +if (STATIC_BUILD OR NOT USE_SYSTEM_UNIX_ODBC) + include(CheckFunctionExists) + include(CheckTypeSize) + + set(VERSION "2.3.9") # Used in "config.h.in" + set(UNIX_ODBC_SOURCES_DIR ${CMAKE_BINARY_DIR}/unixODBC-${VERSION}) + set(UNIX_ODBC_MD5 "06f76e034bb41df5233554abe961a16f") + set(UNIX_ODBC_URL "http://orthanc.osimis.io/ThirdPartyDownloads/unixODBC-${VERSION}.tar.gz") + + DownloadPackage(${UNIX_ODBC_MD5} ${UNIX_ODBC_URL} "${UNIX_ODBC_SOURCES_DIR}") + + if (STATIC_BUILD OR NOT USE_SYSTEM_LTDL) + add_definitions( + -DLT_OBJDIR=".libs/" + -DLTDL # Necessary for LT_SCOPE to be properly defined + #-DLT_DEBUG_LOADERS # Get debug messages + ) + + include_directories( + ${UNIX_ODBC_SOURCES_DIR}/libltdl + ${UNIX_ODBC_SOURCES_DIR}/libltdl/libltdl/ + ) + + list(APPEND LTDL_SOURCES + ${UNIX_ODBC_SOURCES_DIR}/libltdl/loaders/dlopen.c + ${UNIX_ODBC_SOURCES_DIR}/libltdl/loaders/preopen.c + ${UNIX_ODBC_SOURCES_DIR}/libltdl/lt__alloc.c + ${UNIX_ODBC_SOURCES_DIR}/libltdl/lt__strl.c + ${UNIX_ODBC_SOURCES_DIR}/libltdl/lt_dlloader.c + ${UNIX_ODBC_SOURCES_DIR}/libltdl/lt_error.c + ${UNIX_ODBC_SOURCES_DIR}/libltdl/ltdl.c + ${UNIX_ODBC_SOURCES_DIR}/libltdl/slist.c + ) + else() + check_include_file("libltdl/lt_dlloader.h" HAVE_LT_DLLOADER_H) + if (NOT HAVE_LT_DLLOADER_H) + message(FATAL_ERROR "Please install the libltdl-dev package") + endif() + + link_libraries(ltdl) + endif() + + + include_directories( + ${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED + ${UNIX_ODBC_SOURCES_DIR}/include + ${UNIX_ODBC_SOURCES_DIR}/DriverManager + ) + + file(GLOB UNIX_ODBC_SOURCES + ${UNIX_ODBC_SOURCES_DIR}/cur/*.c + ${UNIX_ODBC_SOURCES_DIR}/DriverManager/*.c + ${UNIX_ODBC_SOURCES_DIR}/odbcinst/*.c + ${UNIX_ODBC_SOURCES_DIR}/ini/*.c + ${UNIX_ODBC_SOURCES_DIR}/log/*.c + ${UNIX_ODBC_SOURCES_DIR}/lst/*.c + ) + + list(REMOVE_ITEM UNIX_ODBC_SOURCES + ${UNIX_ODBC_SOURCES_DIR}/cur/SQLConnect.c + ${UNIX_ODBC_SOURCES_DIR}/cur/SQLGetDiagRec.c + ) + + + set(ASCII_ENCODING "auto-search") + set(SYSTEM_FILE_PATH "/etc") + set(DEFLIB_PATH "/usr/lib") + set(ENABLE_DRIVER_ICONV ON) # Enables support for encodings + + set(STDC_HEADERS 1) + set(UNIXODBC ON) + set(UNIXODBC_SOURCE ON) # This makes "intptr_t" to be defined + set(ICONV_CONST ON) + set(STRICT_ODBC_ERROR ON) + + if (CMAKE_SIZEOF_VOID_P EQUAL 8) + set(PLATFORM64 1) + endif() + + list(GET CMAKE_FIND_LIBRARY_SUFFIXES 0 SHLIBEXT) + + check_include_file("alloca.h" HAVE_ALLOCA_H) + check_include_file("argz.h" HAVE_ARGZ_H) + check_include_file("crypt.h" HAVE_CRYPT_H) + check_include_file("dirent.h" HAVE_DIRENT_H) + check_include_file("dlfcn.h" HAVE_DLFCN_H) + check_include_file("inttypes.h" HAVE_INTTYPES_H) + check_include_file("langinfo.h" HAVE_LANGINFO_H) + check_include_file("crypt.h" HAVE_CRYPT_H) + check_include_file("limits.h" HAVE_LIMITS_H) + check_include_file("locale.h" HAVE_LOCALE_H) + check_include_file("malloc.h" HAVE_MALLOC_H) + check_include_file("memory.h" HAVE_MEMORY_H) + check_include_file("pwd.h" HAVE_PWD_H) + check_include_file("stdarg.h" HAVE_STDARG_H) + check_include_file("stdlib.h" HAVE_STDLIB_H) + check_include_file("string.h" HAVE_STRING_H) + check_include_file("strings.h" HAVE_STRINGS_H) + check_include_file("time.h" HAVE_TIME_H) + check_include_file("sys/sem.h" HAVE_SYS_SEM_H) + check_include_file("sys/stat.h" HAVE_SYS_STAT_H) + check_include_file("sys/time.h" HAVE_SYS_TIME_H) + check_include_file("sys/timeb.h" HAVE_SYS_TIMEB_H) + check_include_file("unistd.h" HAVE_UNISTD_H) + check_include_file("readline/readline.h" HAVE_READLINE_H) + check_include_file("readline/history.h" HAVE_READLINE_HISTORY_H) + + check_symbol_exists(alloca "alloca.h" HAVE_ALLOCA) + check_symbol_exists(argz_add "argz.h" HAVE_ARGZ_ADD) + check_symbol_exists(argz_append "argz.h" HAVE_ARGZ_APPEND) + check_symbol_exists(argz_count "argz.h" HAVE_ARGZ_COUNT) + check_symbol_exists(argz_create_sep "argz.h" HAVE_ARGZ_CREATE_SEP) + check_symbol_exists(argz_insert "argz.h" HAVE_ARGZ_INSERT) + check_symbol_exists(argz_next "argz.h" HAVE_ARGZ_NEXT) + check_symbol_exists(argz_stringify "argz.h" HAVE_ARGZ_STRINGIFY) + + check_function_exists(atoll HAVE_ATOLL) + check_function_exists(closedir HAVE_CLOSEDIR) + check_function_exists(endpwent HAVE_ENDPWENT) + + if (HAVE_ARGZ_H) + set(HAVE_WORKING_ARGZ 1) + endif() + + find_package(Threads) + if (Threads_FOUND) + set(HAVE_LIBPTHREAD 1) + endif () + + set(CMAKE_REQUIRED_LIBRARIES) + if (HAVE_DLFCN_H) + list(APPEND CMAKE_REQUIRED_LIBRARIES "dl") + endif() + if (HAVE_CRYPT_H) + list(APPEND CMAKE_REQUIRED_LIBRARIES "crypt") + endif() + if (HAVE_READLINE_H) + list(APPEND CMAKE_REQUIRED_LIBRARIES "readline") + endif() + if (HAVE_LT_DLLOADER_H) + set(HAVE_LIBDLLOADER 0) # to improve + set(HAVE_LTDL 1) # to improve + endif() + + check_function_exists(dlerror HAVE_DLERROR) + check_function_exists(dlloader_init HAVE_LIBDLLOADER) + check_function_exists(dlopen HAVE_LIBDL) + check_function_exists(encrypt HAVE_LIBCRYPT) + check_function_exists(ftime HAVE_FTIME) + check_function_exists(getpwuid HAVE_GETPWUID) + check_function_exists(gettimeofday HAVE_GETTIMEOFDAY) + check_function_exists(gettimeofday HAVE_GETTIMEOFDAY) + check_function_exists(getuid HAVE_GETUID) + check_function_exists(iconv HAVE_ICONV) + check_function_exists(localtime_r HAVE_LOCALTIME_R) + check_function_exists(opendir HAVE_OPENDIR) + check_function_exists(putenv HAVE_PUTENV) + check_function_exists(readdir HAVE_READDIR) + check_function_exists(readline HAVE_READLINE) + check_function_exists(setenv HAVE_SETENV) + check_function_exists(setlocale HAVE_SETLOCALE) + check_function_exists(socket HAVE_SOCKET) + check_function_exists(strcasecmp HAVE_STRCASECMP) + check_function_exists(strchr HAVE_STRCHR) + check_function_exists(strdup HAVE_STRDUP) + check_function_exists(strncasecmp HAVE_STRNCASECMP) + check_function_exists(strstr HAVE_STRSTR) + check_function_exists(strtol HAVE_STRTOL) + check_function_exists(strtoll HAVE_STRTOLL) + check_function_exists(time HAVE_TIME) + check_function_exists(vprintf HAVE_VPRINTF) + check_function_exists(vsnprintf HAVE_VSNPRINTF) + + set(CMAKE_EXTRA_INCLUDE_FILES) + if (HAVE_ARGZ_H) + list(APPEND CMAKE_EXTRA_INCLUDE_FILES "argz.h") + endif() + + check_type_size("long" SIZEOF_LONG) + check_type_size("long int" SIZEOF_LONG_INT) + + check_type_size("error_t" HAVE_ERROR_T) + if (DEFINED HAVE_ERROR_T) + set(HAVE_ERROR_T 1) + endif() + + check_type_size("long long" HAVE_LONG_LONG) + if (DEFINED HAVE_LONG_LONG) + set(HAVE_LONG_LONG 1) + endif() + + check_type_size("nl_langinfo" HAVE_LANGINFO_CODESET) + if (DEFINED HAVE_LANGINFO_CODESET) + set(HAVE_LANGINFO_CODESET 1) # to improve + set(HAVE_NL_LANGINFO 1) + endif() + + check_type_size("ptrdiff_t" HAVE_PTRDIFF_T) + if (DEFINED HAVE_PTRDIFF_T) + set(HAVE_PTRDIFF_T 1) + endif() + + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/../Odbc/config.h.in + ${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED/config.h + ) + + configure_file( + ${CMAKE_CURRENT_LIST_DIR}/../Odbc/config.h.in + ${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED/unixodbc_conf.h + ) + + add_definitions( + -DHAVE_CONFIG_H=1 + ) + +else() + check_include_file("sqlext.h" HAVE_UNIX_ODBC_H) + if (NOT HAVE_UNIX_ODBC_H) + message(FATAL_ERROR "Please install the unixodbc-dev package") + endif() + + check_include_file("libltdl/lt_dlloader.h" HAVE_LT_DLLOADER_H) + if (NOT HAVE_LT_DLLOADER_H) + message(FATAL_ERROR "Please install the libltdl-dev package") + endif() + + link_libraries(odbc ltdl) +endif() diff -r 6a49c495c940 -r b5fb8b77ce4d Resources/Odbc/config.h.in --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Odbc/config.h.in Tue Aug 10 20:08:53 2021 +0200 @@ -0,0 +1,507 @@ +/* config.h.in. Generated from configure.ac by autoheader. */ + +/* Encoding to use for CHAR */ +#cmakedefine ASCII_ENCODING "@ASCII_ENCODING@" + +/* Install bindir */ +#cmakedefine BIN_PREFIX + +/* Use a semaphore to allow ODBCConfig to display running counts */ +#cmakedefine COLLECT_STATS + +/* Define to one of `_getb67', `GETB67', `getb67' for Cray-2 and Cray-YMP + systems. This function is required for `alloca.c' support on those systems. + */ +#cmakedefine CRAY_STACKSEG_END + +/* Define to 1 if using `alloca.c'. */ +#cmakedefine C_ALLOCA + +/* Lib directory */ +#define DEFLIB_PATH "@DEFLIB_PATH@" + +/* Using perdriver iconv */ +#cmakedefine ENABLE_DRIVER_ICONV + +/* Using ini cacheing */ +#cmakedefine ENABLE_INI_CACHING + +/* Install exec_prefix */ +#cmakedefine EXEC_PREFIX + +/* Disable the precise but slow checking of the validity of handles */ +#cmakedefine FAST_HANDLE_VALIDATE + +/* Define to 1 if you have `alloca', as a function or macro. */ +#cmakedefine HAVE_ALLOCA @HAVE_ALLOCA@ + +/* Define to 1 if you have and it should be used (not on Ultrix). + */ +#cmakedefine HAVE_ALLOCA_H @HAVE_ALLOCA_H@ + +/* Define to 1 if you have the `argz_add' function. */ +#cmakedefine HAVE_ARGZ_ADD @HAVE_ARGZ_ADD@ + +/* Define to 1 if you have the `argz_append' function. */ +#cmakedefine HAVE_ARGZ_APPEND @HAVE_ARGZ_APPEND@ + +/* Define to 1 if you have the `argz_count' function. */ +#cmakedefine HAVE_ARGZ_COUNT @HAVE_ARGZ_COUNT@ + +/* Define to 1 if you have the `argz_create_sep' function. */ +#cmakedefine HAVE_ARGZ_CREATE_SEP @HAVE_ARGZ_CREATE_SEP@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_ARGZ_H @HAVE_ARGZ_H@ + +/* Define to 1 if you have the `argz_insert' function. */ +#cmakedefine HAVE_ARGZ_INSERT @HAVE_ARGZ_INSERT@ + +/* Define to 1 if you have the `argz_next' function. */ +#cmakedefine HAVE_ARGZ_NEXT @HAVE_ARGZ_NEXT@ + +/* Define to 1 if you have the `argz_stringify' function. */ +#cmakedefine HAVE_ARGZ_STRINGIFY @HAVE_ARGZ_STRINGIFY@ + +/* Define to 1 if you have the `atoll' function. */ +#cmakedefine HAVE_ATOLL @HAVE_ATOLL@ + +/* Define to 1 if you have the `closedir' function. */ +#cmakedefine HAVE_CLOSEDIR @HAVE_CLOSEDIR@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_CRYPT_H @HAVE_CRYPT_H@ + +/* Define to 1 if you have the declaration of `cygwin_conv_path', and to 0 if + you don't. */ +#cmakedefine HAVE_DECL_CYGWIN_CONV_PATH @HAVE_DECL_CYGWIN_CONV_PATH@ + +/* Define to 1 if you have the header file, and it defines `DIR'. + */ +#cmakedefine HAVE_DIRENT_H @HAVE_DIRENT_H@ + +/* Define if you have the GNU dld library. */ +#cmakedefine HAVE_DLD @HAVE_DLD@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_DLD_H @HAVE_DLD_H@ + +/* Define to 1 if you have the `dlerror' function. */ +#cmakedefine HAVE_DLERROR @HAVE_DLERROR@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_DLFCN_H @HAVE_DLFCN_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_DL_H @HAVE_DL_H@ + +/* Define to 1 if you don't have `vprintf' but do have `_doprnt.' */ +#cmakedefine HAVE_DOPRNT @HAVE_DOPRNT@ + +/* Define if you have the _dyld_func_lookup function. */ +#cmakedefine HAVE_DYLD @HAVE_DYLD@ + +/* Add editline support */ +#cmakedefine HAVE_EDITLINE @HAVE_EDITLINE@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_EDITLINE_READLINE_H @HAVE_EDITLINE_READLINE_H@ + +/* Define to 1 if you have the `endpwent' function. */ +#cmakedefine HAVE_ENDPWENT @HAVE_ENDPWENT@ + +/* Define to 1 if the system has the type `error_t'. */ +#cmakedefine HAVE_ERROR_T @HAVE_ERROR_T@ + +/* Define to 1 if you have the `ftime' function. */ +#cmakedefine HAVE_FTIME @HAVE_FTIME@ + +/* Define to 1 if you have the `ftok' function. */ +#cmakedefine HAVE_FTOK @HAVE_FTOK@ + +/* Define to 1 if you have the `getpwuid' function. */ +#cmakedefine HAVE_GETPWUID @HAVE_GETPWUID@ + +/* Define to 1 if you have the `gettimeofday' function. */ +#cmakedefine HAVE_GETTIMEOFDAY @HAVE_GETTIMEOFDAY@ + +/* Define to 1 if you have the `getuid' function. */ +#cmakedefine HAVE_GETUID @HAVE_GETUID@ + +/* Define if you have the iconv() function. */ +#cmakedefine HAVE_ICONV @HAVE_ICONV@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_INTTYPES_H @HAVE_INTTYPES_H@ + +/* Define if you have and nl_langinfo(CODESET). */ +#cmakedefine HAVE_LANGINFO_CODESET @HAVE_LANGINFO_CODESET@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_LANGINFO_H @HAVE_LANGINFO_H@ + +/* Add -lcrypt to lib list */ +#cmakedefine HAVE_LIBCRYPT @HAVE_LIBCRYPT@ + +/* Define if you have the libdl library or equivalent. */ +#cmakedefine HAVE_LIBDL @HAVE_LIBDL@ + +/* Define if libdlloader will be built on this platform */ +#cmakedefine HAVE_LIBDLLOADER @HAVE_LIBDLLOADER@ + +/* Use the -lpth thread library */ +#cmakedefine HAVE_LIBPTH @HAVE_LIBPTH@ + +/* Use -lpthread threading lib */ +#cmakedefine HAVE_LIBPTHREAD @HAVE_LIBPTHREAD@ + +/* Use the -lthread threading lib */ +#cmakedefine HAVE_LIBTHREAD @HAVE_LIBTHREAD@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_LIMITS_H @HAVE_LIMITS_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_LOCALE_H @HAVE_LOCALE_H@ + +/* Use rentrant version of localtime */ +#cmakedefine HAVE_LOCALTIME_R @HAVE_LOCALTIME_R@ + +/* Define if you have long long */ +#cmakedefine HAVE_LONG_LONG @HAVE_LONG_LONG@ + +/* Define this if a modern libltdl is already installed */ +#cmakedefine HAVE_LTDL @HAVE_LTDL@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_MACH_O_DYLD_H @HAVE_MACH_O_DYLD_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_MALLOC_H @HAVE_MALLOC_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_MEMORY_H @HAVE_MEMORY_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_MSQL_H @HAVE_MSQL_H@ + +/* Define to 1 if you have the header file, and it defines `DIR'. */ +#cmakedefine HAVE_NDIR_H @HAVE_NDIR_H@ + +/* Define to 1 if you have the `nl_langinfo' function. */ +#cmakedefine HAVE_NL_LANGINFO @HAVE_NL_LANGINFO@ + +/* Define to 1 if you have the `opendir' function. */ +#cmakedefine HAVE_OPENDIR @HAVE_OPENDIR@ + +/* Define if libtool can extract symbol lists from object files. */ +#cmakedefine HAVE_PRELOADED_SYMBOLS @HAVE_PRELOADED_SYMBOLS@ + +/* Define to 1 if the system has the type `ptrdiff_t'. */ +#cmakedefine HAVE_PTRDIFF_T @HAVE_PTRDIFF_T@ + +/* Define to 1 if you have the `putenv' function. */ +#cmakedefine HAVE_PUTENV @HAVE_PUTENV@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_PWD_H @HAVE_PWD_H@ + +/* Define to 1 if you have the `readdir' function. */ +#cmakedefine HAVE_READDIR @HAVE_READDIR@ + +/* Add readline support */ +#cmakedefine HAVE_READLINE @HAVE_READLINE@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_READLINE_HISTORY_H @HAVE_READLINE_HISTORY_H@ + +/* Use the scandir lib */ +#cmakedefine HAVE_SCANDIR @HAVE_SCANDIR@ + +/* Define to 1 if you have the `semget' function. */ +#cmakedefine HAVE_SEMGET @HAVE_SEMGET@ + +/* Define to 1 if you have the `semop' function. */ +#cmakedefine HAVE_SEMOP @HAVE_SEMOP@ + +/* Define to 1 if you have the `setenv' function. */ +#cmakedefine HAVE_SETENV @HAVE_SETENV@ + +/* Define to 1 if you have the `setlocale' function. */ +#cmakedefine HAVE_SETLOCALE @HAVE_SETLOCALE@ + +/* Define if you have the shl_load function. */ +#cmakedefine HAVE_SHL_LOAD @HAVE_SHL_LOAD@ + +/* Define to 1 if you have the `shmget' function. */ +#cmakedefine HAVE_SHMGET @HAVE_SHMGET@ + +/* Define to 1 if you have the `snprintf' function. */ +#cmakedefine HAVE_SNPRINTF @HAVE_SNPRINTF@ + +/* Define to 1 if you have the `socket' function. */ +#cmakedefine HAVE_SOCKET @HAVE_SOCKET@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_STDARG_H @HAVE_STDARG_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_STDDEF_H @HAVE_STDDEF_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_STDINT_H @HAVE_STDINT_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_STDLIB_H @HAVE_STDLIB_H@ + +/* Define to 1 if you have the `strcasecmp' function. */ +#cmakedefine HAVE_STRCASECMP @HAVE_STRCASECMP@ + +/* Define to 1 if you have the `strchr' function. */ +#cmakedefine HAVE_STRCHR @HAVE_STRCHR@ + +/* Define to 1 if you have the `strdup' function. */ +#cmakedefine HAVE_STRDUP @HAVE_STRDUP@ + +/* Define to 1 if you have the `stricmp' function. */ +#cmakedefine HAVE_STRICMP @HAVE_STRICMP@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_STRINGS_H @HAVE_STRINGS_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_STRING_H @HAVE_STRING_H@ + +/* Define to 1 if you have the `strlcat' function. */ +#cmakedefine HAVE_STRLCAT @HAVE_STRLCAT@ + +/* Define to 1 if you have the `strlcpy' function. */ +#cmakedefine HAVE_STRLCPY @HAVE_STRLCPY@ + +/* Define to 1 if you have the `strncasecmp' function. */ +#cmakedefine HAVE_STRNCASECMP @HAVE_STRNCASECMP@ + +/* Define to 1 if you have the `strnicmp' function. */ +#cmakedefine HAVE_STRNICMP @HAVE_STRNICMP@ + +/* Define to 1 if you have the `strstr' function. */ +#cmakedefine HAVE_STRSTR @HAVE_STRSTR@ + +/* Define to 1 if you have the `strtol' function. */ +#cmakedefine HAVE_STRTOL @HAVE_STRTOL@ + +/* Define to 1 if you have the `strtoll' function. */ +#cmakedefine HAVE_STRTOLL @HAVE_STRTOLL@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYNCH_H @HAVE_SYNCH_H@ + +/* Define to 1 if you have the header file, and it defines `DIR'. + */ +#cmakedefine HAVE_SYS_DIR_H @HAVE_SYS_DIR_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYS_DL_H @HAVE_SYS_DL_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYS_MALLOC_H @HAVE_SYS_MALLOC_H@ + +/* Define to 1 if you have the header file, and it defines `DIR'. + */ +#cmakedefine HAVE_SYS_NDIR_H @HAVE_SYS_NDIR_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYS_SEM_H @HAVE_SYS_SEM_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYS_STAT_H @HAVE_SYS_STAT_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYS_TIMEB_H @HAVE_SYS_TIMEB_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYS_TIME_H @HAVE_SYS_TIME_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYS_TYPES_H @HAVE_SYS_TYPES_H@ + +/* Define to 1 if you have the `time' function. */ +#cmakedefine HAVE_TIME @HAVE_TIME@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_TIME_H @HAVE_TIME_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_UNISTD_H @HAVE_UNISTD_H@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_VARARGS_H @HAVE_VARARGS_H@ + +/* Define to 1 if you have the `vprintf' function. */ +#cmakedefine HAVE_VPRINTF @HAVE_VPRINTF@ + +/* Define to 1 if you have the `vsnprintf' function. */ +#cmakedefine HAVE_VSNPRINTF @HAVE_VSNPRINTF@ + +/* This value is set to 1 to indicate that the system argz facility works */ +#cmakedefine HAVE_WORKING_ARGZ @HAVE_WORKING_ARGZ@ + +/* Define as const if the declaration of iconv() needs const. */ +#cmakedefine ICONV_CONST + +/* Install includedir */ +#cmakedefine INCLUDE_PREFIX + +/* Lib directory */ +#cmakedefine LIB_PREFIX + +/* Define if the OS needs help to load dependent libraries for dlopen(). */ +#cmakedefine LTDL_DLOPEN_DEPLIBS + +/* Define to the system default library search path. */ +#cmakedefine LT_DLSEARCH_PATH + +/* The archive extension */ +#cmakedefine LT_LIBEXT + +/* The archive prefix */ +#cmakedefine LT_LIBPREFIX + +/* Define to the extension used for runtime loadable modules, say, ".so". */ +#cmakedefine LT_MODULE_EXT + +/* Define to the name of the environment variable that determines the run-time + module search path. */ +#cmakedefine LT_MODULE_PATH_VAR + +/* Define to the sub-directory where libtool stores uninstalled libraries. */ +#cmakedefine LT_OBJDIR + +/* Define to the shared library suffix, say, ".dylib". */ +#cmakedefine LT_SHARED_EXT + +/* Define to the shared archive member specification, say "(shr.o)". */ +#cmakedefine LT_SHARED_LIB_MEMBER + +/* Define if you need semundo union */ +#cmakedefine NEED_SEMUNDO_UNION + +/* Define if dlsym() requires a leading underscore in symbol names. */ +#cmakedefine NEED_USCORE + +/* Using OSX */ +#cmakedefine OSXHEADER + +/* Name of package */ +#cmakedefine PACKAGE + +/* Define to the address where bug reports for this package should be sent. */ +#cmakedefine PACKAGE_BUGREPORT + +/* Define to the full name of this package. */ +#cmakedefine PACKAGE_NAME + +/* Define to the full name and version of this package. */ +#cmakedefine PACKAGE_STRING + +/* Define to the one symbol short name of this package. */ +#cmakedefine PACKAGE_TARNAME + +/* Define to the home page for this package. */ +#cmakedefine PACKAGE_URL + +/* Define to the version of this package. */ +#cmakedefine PACKAGE_VERSION + +/* Platform is 64 bit */ +#cmakedefine PLATFORM64 + +/* Install prefix */ +#cmakedefine PREFIX + +/* Using QNX */ +#cmakedefine QNX_LIBLTDL + +/* Shared lib extension */ +#define SHLIBEXT "@SHLIBEXT@" + +/* The size of `long', as computed by sizeof. */ +#define SIZEOF_LONG @SIZEOF_LONG@ + +/* The size of `long int', as computed by sizeof. */ +#define SIZEOF_LONG_INT @SIZEOF_LONG_INT@ + +/* If using the C implementation of alloca, define if you know the + direction of stack growth for your system; otherwise it will be + automatically deduced at runtime. + STACK_DIRECTION > 0 => grows toward higher addresses + STACK_DIRECTION < 0 => grows toward lower addresses + STACK_DIRECTION = 0 => direction of growth unknown */ +#cmakedefine STACK_DIRECTION + +/* Filename to use for ftok */ +#cmakedefine STATS_FTOK_NAME + +/* Define to 1 if you have the ANSI C header files. */ +#cmakedefine STDC_HEADERS @STDC_HEADERS@ + +/* don't include unixODBC prefix in driver error messages */ +#cmakedefine STRICT_ODBC_ERROR + +/* System file path */ +#cmakedefine SYSTEM_FILE_PATH "@SYSTEM_FILE_PATH@" + +/* Lib path */ +#cmakedefine SYSTEM_LIB_PATH "@SYSTEM_LIB_PATH@" + +/* Define to 1 if you can safely include both and . */ +#cmakedefine TIME_WITH_SYS_TIME + +/* Define to 1 if your declares `struct tm'. */ +#cmakedefine TM_IN_SYS_TIME + +/* Encoding to use for UNICODE */ +#cmakedefine UNICODE_ENCODING + +/* Flag that we are not using another DM */ +#cmakedefine UNIXODBC + +/* We are building inside the unixODBC source tree */ +#cmakedefine UNIXODBC_SOURCE + +/* Version number of package */ +#define VERSION "@VERSION@" + +/* Work with IBM drivers that use 32 bit handles on 64 bit platforms */ +#cmakedefine WITH_HANDLE_REDIRECT + +/* Define to 1 if `lex' declares `yytext' as a `char *' by default, not a + `char[]'. */ +#cmakedefine YYTEXT_POINTER + +/* Build flag for AIX */ +#cmakedefine _ALL_SOURCE + +/* Build flag for AIX */ +#cmakedefine _LONG_LONG + +/* Build flag for AIX */ +#cmakedefine _THREAD_SAFE + +/* Define so that glibc/gnulib argp.h does not typedef error_t. */ +#cmakedefine __error_t_defined + +/* Define to empty if `const' does not conform to ANSI C. */ +#undef const + +/* Define to a type to use for 'error_t' if it is not otherwise available. */ +#undef error_t + +/* Define to `int' if doesn't define. */ +#undef gid_t + +/* Define to `unsigned int' if does not define. */ +#undef size_t + +/* Define to `int' if doesn't define. */ +#undef uid_t diff -r 6a49c495c940 -r b5fb8b77ce4d SQLite/CMakeLists.txt --- a/SQLite/CMakeLists.txt Thu Jul 22 20:20:26 2021 +0200 +++ b/SQLite/CMakeLists.txt Tue Aug 10 20:08:53 2021 +0200 @@ -1,3 +1,22 @@ +# 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 . + + cmake_minimum_required(VERSION 2.8) project(OrthancSQLite)