changeset 5219:802e2e3d9bfc

integration db-protobuf->mainline
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 03 Apr 2023 17:00:51 +0200
parents 1fa3bfa86f8e (current diff) afa96af2eb5a (diff)
children 5874e5dd9a38
files
diffstat 22 files changed, 3006 insertions(+), 132 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Fri Mar 31 16:32:02 2023 +0200
+++ b/NEWS	Mon Apr 03 17:00:51 2023 +0200
@@ -5,18 +5,20 @@
 --------
 
 * API version upgraded to 20
-* /system: added UserMetadata
+* /system: added "UserMetadata"
+
+Plugins
+-------
+
+* Added "OrthancPluginRegisterDatabaseBackendV4()" to communicate using Google
+  Protocol Buffers between the Orthanc core and database plugins
 
 Maintenance
 -----------
 
 * Enforce the existence of the patient/study/instance while creating its archive
 * Security: New configuration option "RestApiWriteToFileSystemEnabled"
-  to allow "/instances/../export" that is now disabled by default.
-
-Bug Fixes
----------
-
+  to allow "/instances/../export" that is now disabled by default
 * Fix issue 214: VOILUTSequence is not returned in Wado-RS
 * Fix /tools/reset crashing when ExtraMainDicomTags were defined
 
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Fri Mar 31 16:32:02 2023 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Mon Apr 03 17:00:51 2023 +0200
@@ -141,6 +141,11 @@
   unset(ENABLE_DCMTK_LOG CACHE)
 endif()
 
+if (NOT ENABLE_PROTOBUF)
+  unset(USE_SYSTEM_PROTOBUF CACHE)
+  add_definitions(-DORTHANC_ENABLE_PROTOBUF=0)
+endif()
+
 
 #####################################################################
 ## List of source files
@@ -476,6 +481,16 @@
 endif()
 
 
+##
+## Google Protocol Buffers
+##
+
+if (ENABLE_PROTOBUF)
+  include(${CMAKE_CURRENT_LIST_DIR}/ProtobufConfiguration.cmake)
+  add_definitions(-DORTHANC_ENABLE_PROTOBUF=1)
+endif()
+
+
 
 #####################################################################
 ## Inclusion of mandatory third-party dependencies
@@ -712,6 +727,7 @@
   ${LUA_SOURCES}
   ${MONGOOSE_SOURCES}
   ${OPENSSL_SOURCES}
+  ${PROTOBUF_LIBRARY_SOURCES}
   ${PUGIXML_SOURCES}
   ${SQLITE_SOURCES}
   ${UUID_SOURCES}
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Fri Mar 31 16:32:02 2023 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Mon Apr 03 17:00:51 2023 +0200
@@ -70,6 +70,7 @@
 set(USE_SYSTEM_LUA ON CACHE BOOL "Use the system version of Lua")
 set(USE_SYSTEM_MONGOOSE ON CACHE BOOL "Use the system version of Mongoose")
 set(USE_SYSTEM_OPENSSL ON CACHE BOOL "Use the system version of OpenSSL")
+set(USE_SYSTEM_PROTOBUF ON CACHE BOOL "Use the system version of Google Protocol Buffers")
 set(USE_SYSTEM_PUGIXML ON CACHE BOOL "Use the system version of Pugixml")
 set(USE_SYSTEM_SQLITE ON CACHE BOOL "Use the system version of SQLite")
 set(USE_SYSTEM_UUID ON CACHE BOOL "Use the system version of the uuid library from e2fsprogs")
@@ -123,6 +124,8 @@
 set(ENABLE_LOCALE OFF CACHE INTERNAL "Enable support for locales (notably in Boost)")
 set(ENABLE_LUA OFF CACHE INTERNAL "Enable support of Lua scripting")
 set(ENABLE_PNG OFF CACHE INTERNAL "Enable support of PNG")
+set(ENABLE_PROTOBUF OFF CACHE INTERNAL "Enable support for Google Protocol Buffers' library")
+set(ENABLE_PROTOBUF_COMPILER OFF CACHE INTERNAL "Enable support for Google Protocol Buffers' compiler")
 set(ENABLE_PUGIXML OFF CACHE INTERNAL "Enable support of XML through Pugixml")
 set(ENABLE_SQLITE OFF CACHE INTERNAL "Enable support of SQLite databases")
 set(ENABLE_ZLIB OFF CACHE INTERNAL "Enable support of zlib")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/ProtobufConfiguration.cmake	Mon Apr 03 17:00:51 2023 +0200
@@ -0,0 +1,85 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2023 Osimis S.A., Belgium
+# Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# <http://www.gnu.org/licenses/>.
+
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_PROTOBUF)
+  if (ENABLE_PROTOBUF_COMPILER)
+    include(ExternalProject)
+    externalproject_add(ProtobufCompiler
+      SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../ProtocolBuffers"
+      BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/ProtobufCompiler-build"
+      # this helps triggering build when changing the external project
+      BUILD_ALWAYS 1
+      CMAKE_ARGS
+      -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
+      -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}
+      )
+
+    # The "protoc" compiler is built using "externalproject_add",
+    # which builds for the host platform, not for the target platform
+    if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
+      set(Suffix ".exe")
+    else()
+      set(Suffix "")
+    endif()
+
+    set(PROTOC_EXECUTABLE ${CMAKE_CURRENT_BINARY_DIR}/protoc${Suffix})
+  endif()
+
+  include(${CMAKE_CURRENT_LIST_DIR}/../ProtocolBuffers/ProtobufLibrary.cmake)  
+  source_group(ThirdParty\\Protobuf REGULAR_EXPRESSION ${PROTOBUF_SOURCE_DIR}/.*)
+
+else()
+  if (CMAKE_CROSSCOMPILING)
+    message(FATAL_ERROR "If cross-compiling, the static version of Protocol Buffers should be used to avoid version mismatch")
+  endif()
+  
+  if (ENABLE_PROTOBUF_COMPILER)
+    find_program(PROTOC_EXECUTABLE protoc)
+    if (${PROTOC_EXECUTABLE} MATCHES "PROTOC_EXECUTABLE-NOTFOUND")
+      message(FATAL_ERROR "Please install the 'protoc' compiler for Protocol Buffers (package 'protobuf-compiler' on Debian/Ubuntu)")
+    endif()
+    add_custom_target(ProtobufCompiler)
+  endif()
+  
+  check_include_file_cxx(google/protobuf/any.h HAVE_PROTOBUF_H)
+  if (NOT HAVE_PROTOBUF_H)
+    message(FATAL_ERROR "Please install the libprotobuf-dev package")
+  endif()
+
+  set(CMAKE_REQUIRED_LIBRARIES "protobuf")
+
+  include(CheckCXXSourceCompiles) 
+  check_cxx_source_compiles(
+    "
+#include <google/protobuf/descriptor.h>
+int main()
+{
+  google::protobuf::FieldDescriptor::TypeName(google::protobuf::FieldDescriptor::TYPE_FLOAT);
+}
+"  HAVE_PROTOBUF_LIB)
+  if (NOT HAVE_PROTOBUF_LIB)
+    message(FATAL_ERROR "Cannot find the protobuf library")
+  endif()
+  
+  unset(CMAKE_REQUIRED_LIBRARIES)
+
+  link_libraries(protobuf)
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/protobuf-3.5.1.patch	Mon Apr 03 17:00:51 2023 +0200
@@ -0,0 +1,17 @@
+diff -urEb protobuf-3.5.1.orig/src/google/protobuf/stubs/io_win32.cc protobuf-3.5.1/src/google/protobuf/stubs/io_win32.cc
+--- protobuf-3.5.1.orig/src/google/protobuf/stubs/io_win32.cc	2023-03-26 20:13:45.095021011 +0200
++++ protobuf-3.5.1/src/google/protobuf/stubs/io_win32.cc	2023-03-26 20:19:19.932920102 +0200
+@@ -91,7 +91,12 @@
+ 
+ template <typename char_type>
+ bool null_or_empty(const char_type* s) {
+-  return s == nullptr || *s == 0;
++  /**
++   * "nullptr" is not known to Visual Studio 2008, because this is a
++   * C++11 construction, which shouldn't be present in protobuf 3.5.1
++   * that is supposed to comply with C++98.
++   **/
++  return s == NULL || *s == 0;
+ }
+ 
+ // Returns true if the path starts with a drive letter, e.g. "c:".
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ProtocolBuffers/CMakeLists.txt	Mon Apr 03 17:00:51 2023 +0200
@@ -0,0 +1,149 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2023 Osimis S.A., Belgium
+# Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# <http://www.gnu.org/licenses/>.
+
+
+cmake_minimum_required(VERSION 2.8.3)
+
+project(ProtocolBuffers)
+
+set(ALLOW_DOWNLOADS ON)
+
+include(${CMAKE_SOURCE_DIR}/../CMake/DownloadPackage.cmake)
+include(${CMAKE_SOURCE_DIR}/../CMake/Compiler.cmake)
+
+include(${CMAKE_SOURCE_DIR}/ProtobufLibrary.cmake)
+
+set(PROTOBUF_COMPILER_SOURCES
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/code_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/command_line_interface.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_enum.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_extension.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_file.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_helpers.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_map_field.cc  
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_padding_optimizer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_primitive_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_service.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/cpp/cpp_string_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_doc_comment.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_enum.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_field_base.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_helpers.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_map_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_primitive_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_reflection_class.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_repeated_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_repeated_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_repeated_primitive_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_source_generator_base.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_wrapper_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/importer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_context.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_doc_comment.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_enum.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_enum_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_enum_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_extension.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_extension_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_file.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_generator_factory.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_helpers.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_lazy_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_lazy_message_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_map_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_map_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message_builder.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message_builder_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_message_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_name_resolver.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_primitive_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_primitive_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_service.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_shared_code_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_string_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/java/java_string_field_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_enum.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_extension.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_file.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_helpers.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_map_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/javanano/javanano_primitive_field.cc
+  #${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/js/embed.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/js/js_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/js/well_known_types_embed.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/main.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_enum.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_enum_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_extension.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_file.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_helpers.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_map_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_message_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_oneof.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_primitive_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/parser.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/php/php_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/plugin.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/plugin.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/python/python_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/ruby/ruby_generator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/subprocess.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/compiler/zip_writer.cc
+  )
+
+if (NOT CMAKE_SYSTEM_NAME STREQUAL "Windows")
+  set_property(
+    SOURCE ${PROTOBUF_COMPILER_SOURCES}
+    PROPERTY COMPILE_DEFINITIONS "HAVE_PTHREAD=1"
+    )
+endif()
+
+add_executable(protoc
+  ${PROTOBUF_LIBRARY_SOURCES}
+  ${PROTOBUF_COMPILER_SOURCES}
+  )
+
+install(
+  TARGETS protoc
+  RUNTIME DESTINATION .
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ProtocolBuffers/NOTES.txt	Mon Apr 03 17:00:51 2023 +0200
@@ -0,0 +1,29 @@
+
+Version
+=======
+
+We use Google's Protocol Buffers version 3.5.1, as this is the last
+release to be compatible with C++98, which is mandatory for Visual
+Studio 2008 and Linux Standard Base.
+
+References:
+https://github.com/protocolbuffers/protobuf/releases/tag/v3.5.1
+https://github.com/protocolbuffers/protobuf/issues/2780
+
+
+Linux Standard Base
+===================
+
+$ mkdir lsb
+$ cd lsb
+$ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON -DCMAKE_TOOLCHAIN_FILE=../../Toolchains/LinuxStandardBaseToolchain.cmake -G Ninja
+$ ninja
+
+
+MinGW for 32bits
+================
+
+$ mkdir w32
+$ cd w32
+$ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Release -DALLOW_DOWNLOADS=ON -DCMAKE_TOOLCHAIN_FILE=../../Toolchains/MinGW-W64-Toolchain32.cmake -G Ninja
+$ ninja
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ProtocolBuffers/ProtobufLibrary.cmake	Mon Apr 03 17:00:51 2023 +0200
@@ -0,0 +1,144 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2023 Osimis S.A., Belgium
+# Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# <http://www.gnu.org/licenses/>.
+
+
+set(PROTOBUF_SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/protobuf-3.5.1)
+
+if (IS_DIRECTORY "${PROTOBUF_SOURCE_DIR}")
+  set(FirstRun OFF)
+else()
+  set(FirstRun ON)
+endif()
+
+DownloadPackage(
+  "ca0d9b243e649d398a6b419acd35103a"
+  "http://orthanc.uclouvain.be/third-party-downloads/protobuf-cpp-3.5.1.tar.gz"
+  "${CMAKE_CURRENT_BINARY_DIR}/protobuf-3.5.1")
+
+if (FirstRun)
+  # Apply the patches
+  execute_process(
+    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+    ${CMAKE_CURRENT_LIST_DIR}/../Patches/protobuf-3.5.1.patch
+    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+    RESULT_VARIABLE Failure
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while patching a file")
+  endif()
+endif()
+
+include_directories(
+  ${PROTOBUF_SOURCE_DIR}/src
+  )
+  
+set(PROTOBUF_LIBRARY_SOURCES
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/any.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/any.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/api.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/arena.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/arenastring.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/descriptor.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/descriptor.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/descriptor_database.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/duration.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/dynamic_message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/empty.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/extension_set.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/extension_set_heavy.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/field_mask.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/generated_message_reflection.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/generated_message_table_driven.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/generated_message_table_driven_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/generated_message_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/coded_stream.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/gzip_stream.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/printer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/strtod.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/tokenizer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/zero_copy_stream.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/zero_copy_stream_impl.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/io/zero_copy_stream_impl_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/map_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/message.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/message_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/reflection_ops.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/repeated_field.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/service.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/source_context.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/struct.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_arm64_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_arm_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_generic_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_mips_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_ppc_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_x86_gcc.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_x86_gcc.h
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/atomicops_internals_x86_msvc.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/common.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/int128.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/io_win32.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/mathlimits.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/once.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/status.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/statusor.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/stringpiece.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/stringprintf.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/structurally_valid.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/strutil.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/substitute.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/time.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/stubs/bytestream.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/text_format.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/timestamp.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/type.pb.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/unknown_field_set.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/delimited_message_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/field_comparator.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/field_mask_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/datapiece.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/default_value_objectwriter.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/error_listener.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/field_mask_utility.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/json_escaping.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/json_objectwriter.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/json_stream_parser.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/object_writer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/proto_writer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/protostream_objectsource.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/protostream_objectwriter.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/type_info.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/internal/utility.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/json_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/message_differencer.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/time_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/util/type_resolver_util.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/wire_format.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/wire_format_lite.cc
+  ${PROTOBUF_SOURCE_DIR}/src/google/protobuf/wrappers.pb.cc
+  )
+
+if (NOT CMAKE_SYSTEM_NAME STREQUAL "Windows")
+  set_property(
+    SOURCE ${PROTOBUF_LIBRARY_SOURCES}
+    PROPERTY COMPILE_DEFINITIONS "HAVE_PTHREAD=1"
+    )
+endif()
--- a/OrthancServer/CMakeLists.txt	Fri Mar 31 16:32:02 2023 +0200
+++ b/OrthancServer/CMakeLists.txt	Mon Apr 03 17:00:51 2023 +0200
@@ -69,6 +69,11 @@
 ## Configuration of the Orthanc framework
 #####################################################################
 
+if (ENABLE_PLUGINS)
+  set(ENABLE_PROTOBUF ON)
+  set(ENABLE_PROTOBUF_COMPILER ON)
+endif()
+
 include(${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/CMake/VisualStudioPrecompiledHeaders.cmake)
 include(${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake)
 
@@ -181,6 +186,7 @@
   list(APPEND ORTHANC_SERVER_SOURCES
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabase.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabaseV3.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabaseV4.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPlugins.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsEnumerations.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsErrorDictionary.cpp
@@ -361,8 +367,13 @@
 ## Build the core of Orthanc
 #####################################################################
 
+add_custom_target(AutogeneratedTarget
+  DEPENDS
+  ${AUTOGENERATED_SOURCES}
+  )
+
 # "CoreLibrary" contains all the third-party dependencies and the
-# content of the "Core" folder
+# content of the "OrthancFramework" folder
 add_library(CoreLibrary
   STATIC
   ${ORTHANC_CORE_PCH}
@@ -371,6 +382,8 @@
   ${AUTOGENERATED_SOURCES}
   )
 
+add_dependencies(CoreLibrary AutogeneratedTarget)
+
 if (LIBICU_LIBRARIES)
   target_link_libraries(CoreLibrary ${LIBICU_LIBRARIES})
 endif()
@@ -380,6 +393,30 @@
 ## Build the Orthanc server
 #####################################################################
 
+if (ENABLE_PLUGINS)
+  add_custom_command(
+    COMMAND
+    ${PROTOC_EXECUTABLE} ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancDatabasePlugin.proto --cpp_out=${AUTOGENERATED_DIR} -I${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc
+    DEPENDS
+    ProtobufCompiler
+    ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancDatabasePlugin.proto
+    OUTPUT
+    ${AUTOGENERATED_DIR}/OrthancDatabasePlugin.pb.cc
+    ${AUTOGENERATED_DIR}/OrthancDatabasePlugin.pb.h
+    )
+  
+  add_custom_target(OrthancDatabaseProtobuf
+    DEPENDS
+    ${AUTOGENERATED_DIR}/OrthancDatabasePlugin.pb.h
+    )
+
+  list(APPEND ORTHANC_SERVER_SOURCES
+    ${AUTOGENERATED_DIR}/OrthancDatabasePlugin.pb.cc
+    )
+else()
+  add_custom_target(OrthancDatabaseProtobuf)
+endif()
+
 add_library(ServerLibrary
   STATIC
   ${ORTHANC_SERVER_PCH}
@@ -387,7 +424,7 @@
   )
 
 # Ensure autogenerated code is built before building ServerLibrary
-add_dependencies(ServerLibrary CoreLibrary)
+add_dependencies(ServerLibrary CoreLibrary OrthancDatabaseProtobuf)
 
 add_executable(Orthanc
   ${CMAKE_SOURCE_DIR}/Sources/main.cpp
@@ -546,7 +583,6 @@
 
 if (ENABLE_PLUGINS AND (BUILD_DELAYED_DELETION OR BUILD_CONNECTIVITY_CHECKS))
   include(ExternalProject)
-
 endif()
 
 
@@ -825,8 +861,9 @@
 if (ENABLE_PLUGINS)
   install(
     FILES
-    ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancCPlugin.h 
-    ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancCDatabasePlugin.h 
+    ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancCPlugin.h
+    ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancCDatabasePlugin.h
+    ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancDatabasePlugin.proto
     DESTINATION include/orthanc
     )
 endif()
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Fri Mar 31 16:32:02 2023 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Mon Apr 03 17:00:51 2023 +0200
@@ -565,7 +565,7 @@
                                       std::list<std::string>* instancesId,
                                       const std::vector<DatabaseConstraint>& lookup,
                                       ResourceType queryLevel,
-                                      size_t limit) ORTHANC_OVERRIDE
+                                      uint32_t limit) ORTHANC_OVERRIDE
     {
       if (that_.extensions_.lookupResources == NULL)
       {
@@ -800,8 +800,8 @@
 
     virtual void GetAllPublicIds(std::list<std::string>& target,
                                  ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit) ORTHANC_OVERRIDE
+                                 int64_t since,
+                                 uint32_t limit) ORTHANC_OVERRIDE
     {
       if (that_.extensions_.getAllPublicIdsWithLimit != NULL)
       {
@@ -825,7 +825,7 @@
         std::list<std::string> tmp;
         GetAllPublicIds(tmp, resourceType);
     
-        if (tmp.size() <= since)
+        if (tmp.size() <= static_cast<size_t>(since))
         {
           // Not enough results => empty answer
           return;
@@ -847,14 +847,14 @@
     virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
                             bool& done /*out*/,
                             int64_t since,
-                            uint32_t maxResults) ORTHANC_OVERRIDE
+                            uint32_t limit) ORTHANC_OVERRIDE
     {
       ResetAnswers();
       answerChanges_ = &target;
       answerDone_ = &done;
       done = false;
 
-      CheckSuccess(that_.backend_.getChanges(that_.GetContext(), that_.payload_, since, maxResults));
+      CheckSuccess(that_.backend_.getChanges(that_.GetContext(), that_.payload_, since, limit));
     }
 
 
@@ -897,14 +897,14 @@
     virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
                                       bool& done /*out*/,
                                       int64_t since,
-                                      uint32_t maxResults) ORTHANC_OVERRIDE
+                                      uint32_t limit) ORTHANC_OVERRIDE
     {
       ResetAnswers();
       answerExportedResources_ = &target;
       answerDone_ = &done;
       done = false;
 
-      CheckSuccess(that_.backend_.getExportedResources(that_.GetContext(), that_.payload_, since, maxResults));
+      CheckSuccess(that_.backend_.getExportedResources(that_.GetContext(), that_.payload_, since, limit));
     }
 
 
@@ -1021,14 +1021,6 @@
     }
 
 
-    virtual bool IsExistingResource(int64_t internalId) ORTHANC_OVERRIDE
-    {
-      int32_t existing;
-      CheckSuccess(that_.backend_.isExistingResource(&existing, that_.payload_, internalId));
-      return (existing != 0);
-    }
-
-
     virtual bool IsProtectedPatient(int64_t internalId) ORTHANC_OVERRIDE
     {
       int32_t isProtected;
@@ -1063,15 +1055,18 @@
     }
 
 
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change) ORTHANC_OVERRIDE
+    virtual void LogChange(ChangeType changeType,
+                           ResourceType resourceType,
+                           int64_t internalId,
+                           const std::string& publicId,
+                           const std::string& date) ORTHANC_OVERRIDE
     {
       OrthancPluginChange tmp;
-      tmp.seq = change.GetSeq();
-      tmp.changeType = static_cast<int32_t>(change.GetChangeType());
-      tmp.resourceType = Plugins::Convert(change.GetResourceType());
-      tmp.publicId = change.GetPublicId().c_str();
-      tmp.date = change.GetDate().c_str();
+      tmp.seq = -1;  // Unused (it is attributed by the database engine)
+      tmp.changeType = static_cast<int32_t>(changeType);
+      tmp.resourceType = Plugins::Convert(resourceType);
+      tmp.publicId = publicId.c_str();
+      tmp.date = date.c_str();
 
       CheckSuccess(that_.backend_.logChange(that_.payload_, &tmp));
     }
@@ -1370,12 +1365,12 @@
                it = content.GetListTags().begin(); it != content.GetListTags().end(); ++it)
         {
           OrthancPluginResourcesContentTags tmp;
-          tmp.resource = it->resourceId_;
-          tmp.group = it->tag_.GetGroup();
-          tmp.element = it->tag_.GetElement();
-          tmp.value = it->value_.c_str();
+          tmp.resource = it->GetResourceId();
+          tmp.group = it->GetTag().GetGroup();
+          tmp.element = it->GetTag().GetElement();
+          tmp.value = it->GetValue().c_str();
 
-          if (it->isIdentifier_)
+          if (it->IsIdentifier())
           {
             identifierTags.push_back(tmp);
           }
@@ -1389,9 +1384,9 @@
                it = content.GetListMetadata().begin(); it != content.GetListMetadata().end(); ++it)
         {
           OrthancPluginResourcesContentMetadata tmp;
-          tmp.resource = it->resourceId_;
-          tmp.metadata = it->metadata_;
-          tmp.value = it->value_.c_str();
+          tmp.resource = it->GetResourceId();
+          tmp.metadata = it->GetType();
+          tmp.value = it->GetValue().c_str();
           metadata.push_back(tmp);
         }
 
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Fri Mar 31 16:32:02 2023 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Mon Apr 03 17:00:51 2023 +0200
@@ -388,8 +388,8 @@
     
     virtual void GetAllPublicIds(std::list<std::string>& target,
                                  ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit) ORTHANC_OVERRIDE
+                                 int64_t since,
+                                 uint32_t limit) ORTHANC_OVERRIDE
     {
       CheckSuccess(that_.backend_.getAllPublicIdsWithLimit(
                      transaction_, Plugins::Convert(resourceType),
@@ -403,10 +403,10 @@
     virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
                             bool& done /*out*/,
                             int64_t since,
-                            uint32_t maxResults) ORTHANC_OVERRIDE
+                            uint32_t limit) ORTHANC_OVERRIDE
     {
       uint8_t tmpDone = true;
-      CheckSuccess(that_.backend_.getChanges(transaction_, &tmpDone, since, maxResults));
+      CheckSuccess(that_.backend_.getChanges(transaction_, &tmpDone, since, limit));
       CheckNoEvent();
 
       done = (tmpDone != 0);
@@ -454,10 +454,10 @@
     virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
                                       bool& done /*out*/,
                                       int64_t since,
-                                      uint32_t maxResults) ORTHANC_OVERRIDE
+                                      uint32_t limit) ORTHANC_OVERRIDE
     {
       uint8_t tmpDone = true;
-      CheckSuccess(that_.backend_.getExportedResources(transaction_, &tmpDone, since, maxResults));
+      CheckSuccess(that_.backend_.getExportedResources(transaction_, &tmpDone, since, limit));
       CheckNoEvent();
 
       done = (tmpDone != 0);
@@ -594,15 +594,6 @@
     }
 
     
-    virtual bool IsExistingResource(int64_t internalId) ORTHANC_OVERRIDE
-    {
-      uint8_t b;
-      CheckSuccess(that_.backend_.isExistingResource(transaction_, &b, internalId));
-      CheckNoEvent();
-      return (b != 0);
-    }
-
-    
     virtual bool IsProtectedPatient(int64_t internalId) ORTHANC_OVERRIDE
     {
       uint8_t b;
@@ -631,12 +622,15 @@
     }
 
     
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change) ORTHANC_OVERRIDE
+    virtual void LogChange(ChangeType changeType,
+                           ResourceType resourceType,
+                           int64_t internalId,
+                           const std::string& /* publicId - unused */,
+                           const std::string& date) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.logChange(transaction_, static_cast<int32_t>(change.GetChangeType()),
-                                            internalId, Plugins::Convert(change.GetResourceType()),
-                                            change.GetDate().c_str()));
+      CheckSuccess(that_.backend_.logChange(transaction_, static_cast<int32_t>(changeType),
+                                            internalId, Plugins::Convert(resourceType),
+                                            date.c_str()));
       CheckNoEvent();
     }
 
@@ -806,7 +800,7 @@
                                       std::list<std::string>* instancesId, // Can be NULL if not needed
                                       const std::vector<DatabaseConstraint>& lookup,
                                       ResourceType queryLevel,
-                                      size_t limit) ORTHANC_OVERRIDE
+                                      uint32_t limit) ORTHANC_OVERRIDE
     {
       std::vector<OrthancPluginDatabaseConstraint> constraints;
       std::vector< std::vector<const char*> > constraintsValues;
@@ -910,12 +904,12 @@
              it = content.GetListTags().begin(); it != content.GetListTags().end(); ++it)
       {
         OrthancPluginResourcesContentTags tmp;
-        tmp.resource = it->resourceId_;
-        tmp.group = it->tag_.GetGroup();
-        tmp.element = it->tag_.GetElement();
-        tmp.value = it->value_.c_str();
+        tmp.resource = it->GetResourceId();
+        tmp.group = it->GetTag().GetGroup();
+        tmp.element = it->GetTag().GetElement();
+        tmp.value = it->GetValue().c_str();
 
-        if (it->isIdentifier_)
+        if (it->IsIdentifier())
         {
           identifierTags.push_back(tmp);
         }
@@ -929,9 +923,9 @@
              it = content.GetListMetadata().begin(); it != content.GetListMetadata().end(); ++it)
       {
         OrthancPluginResourcesContentMetadata tmp;
-        tmp.resource = it->resourceId_;
-        tmp.metadata = it->metadata_;
-        tmp.value = it->value_.c_str();
+        tmp.resource = it->GetResourceId();
+        tmp.metadata = it->GetType();
+        tmp.value = it->GetValue().c_str();
         metadata.push_back(tmp);
       }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Mon Apr 03 17:00:51 2023 +0200
@@ -0,0 +1,1310 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../Sources/PrecompiledHeadersServer.h"
+#include "OrthancPluginDatabaseV4.h"
+
+#if ORTHANC_ENABLE_PLUGINS != 1
+#  error The plugin support is disabled
+#endif
+
+#include "../../../OrthancFramework/Sources/Logging.h"
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../Sources/Database/ResourcesContent.h"
+#include "../../Sources/Database/VoidDatabaseListener.h"
+#include "PluginsEnumerations.h"
+
+#include "OrthancDatabasePlugin.pb.h"  // Auto-generated file
+
+#include <cassert>
+
+
+namespace Orthanc
+{
+  static void CheckSuccess(PluginsErrorDictionary& errorDictionary,
+                           OrthancPluginErrorCode code)
+  {
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      errorDictionary.LogError(code, true);
+      throw OrthancException(static_cast<ErrorCode>(code));
+    }
+  }
+
+
+  static ResourceType Convert(DatabasePluginMessages::ResourceType type)
+  {
+    switch (type)
+    {
+      case DatabasePluginMessages::RESOURCE_PATIENT:
+        return ResourceType_Patient;
+
+      case DatabasePluginMessages::RESOURCE_STUDY:
+        return ResourceType_Study;
+
+      case DatabasePluginMessages::RESOURCE_SERIES:
+        return ResourceType_Series;
+
+      case DatabasePluginMessages::RESOURCE_INSTANCE:
+        return ResourceType_Instance;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+    
+  static DatabasePluginMessages::ResourceType Convert(ResourceType type)
+  {
+    switch (type)
+    {
+      case ResourceType_Patient:
+        return DatabasePluginMessages::RESOURCE_PATIENT;
+
+      case ResourceType_Study:
+        return DatabasePluginMessages::RESOURCE_STUDY;
+
+      case ResourceType_Series:
+        return DatabasePluginMessages::RESOURCE_SERIES;
+
+      case ResourceType_Instance:
+        return DatabasePluginMessages::RESOURCE_INSTANCE;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+    
+  static FileInfo Convert(const DatabasePluginMessages::FileInfo& source)
+  {
+    return FileInfo(source.uuid(),
+                    static_cast<FileContentType>(source.content_type()),
+                    source.uncompressed_size(),
+                    source.uncompressed_hash(),
+                    static_cast<CompressionType>(source.compression_type()),
+                    source.compressed_size(),
+                    source.compressed_hash());
+  }
+
+
+  static ServerIndexChange Convert(const DatabasePluginMessages::ServerIndexChange& source)
+  {
+    return ServerIndexChange(source.seq(),
+                             static_cast<ChangeType>(source.change_type()),
+                             Convert(source.resource_type()),
+                             source.public_id(),
+                             source.date());
+  }
+
+
+  static ExportedResource Convert(const DatabasePluginMessages::ExportedResource& source)
+  {
+    return ExportedResource(source.seq(),
+                            Convert(source.resource_type()),
+                            source.public_id(),
+                            source.modality(),
+                            source.date(),
+                            source.patient_id(),
+                            source.study_instance_uid(),
+                            source.series_instance_uid(),
+                            source.sop_instance_uid());
+  }
+
+
+  static void Execute(DatabasePluginMessages::Response& response,
+                      const OrthancPluginDatabaseV4& database,
+                      const DatabasePluginMessages::Request& request)
+  {
+    std::string requestSerialized;
+    request.SerializeToString(&requestSerialized);
+
+    OrthancPluginMemoryBuffer64 responseSerialized;
+    CheckSuccess(database.GetErrorDictionary(), database.GetDefinition().operations(
+                   &responseSerialized, database.GetDefinition().backend,
+                   requestSerialized.empty() ? NULL : requestSerialized.c_str(),
+                   requestSerialized.size()));
+
+    bool success = response.ParseFromArray(responseSerialized.data, responseSerialized.size);
+
+    if (responseSerialized.size > 0)
+    {
+      free(responseSerialized.data);
+    }
+
+    if (!success)
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin, "Cannot unserialize protobuf originating from the database plugin");
+    }
+  }
+  
+
+  static void ExecuteDatabase(DatabasePluginMessages::DatabaseResponse& response,
+                              const OrthancPluginDatabaseV4& database,
+                              DatabasePluginMessages::DatabaseOperation operation,
+                              const DatabasePluginMessages::DatabaseRequest& request)
+  {
+    DatabasePluginMessages::Request fullRequest;
+    fullRequest.set_type(DatabasePluginMessages::REQUEST_DATABASE);
+    fullRequest.mutable_database_request()->CopyFrom(request);
+    fullRequest.mutable_database_request()->set_operation(operation);
+
+    DatabasePluginMessages::Response fullResponse;
+    Execute(fullResponse, database, fullRequest);
+    
+    response.CopyFrom(fullResponse.database_response());
+  }
+
+  
+  class OrthancPluginDatabaseV4::Transaction : public IDatabaseWrapper::ITransaction
+  {
+  private:
+    OrthancPluginDatabaseV4&  database_;
+    IDatabaseListener&        listener_;
+    void*                     transaction_;
+    
+    void ExecuteTransaction(DatabasePluginMessages::TransactionResponse& response,
+                            DatabasePluginMessages::TransactionOperation operation,
+                            const DatabasePluginMessages::TransactionRequest& request)
+    {
+      DatabasePluginMessages::Request fullRequest;
+      fullRequest.set_type(DatabasePluginMessages::REQUEST_TRANSACTION);
+      fullRequest.mutable_transaction_request()->CopyFrom(request);
+      fullRequest.mutable_transaction_request()->set_transaction(reinterpret_cast<intptr_t>(transaction_));
+      fullRequest.mutable_transaction_request()->set_operation(operation);
+
+      DatabasePluginMessages::Response fullResponse;
+      Execute(fullResponse, database_, fullRequest);
+    
+      response.CopyFrom(fullResponse.transaction_response());
+    }
+    
+    
+    void ExecuteTransaction(DatabasePluginMessages::TransactionResponse& response,
+                            DatabasePluginMessages::TransactionOperation operation)
+    {
+      DatabasePluginMessages::TransactionRequest request;    // Ignored
+      ExecuteTransaction(response, operation, request);
+    }
+    
+    
+    void ExecuteTransaction(DatabasePluginMessages::TransactionOperation operation,
+                            const DatabasePluginMessages::TransactionRequest& request)
+    {
+      DatabasePluginMessages::TransactionResponse response;  // Ignored
+      ExecuteTransaction(response, operation, request);
+    }
+    
+    
+    void ExecuteTransaction(DatabasePluginMessages::TransactionOperation operation)
+    {
+      DatabasePluginMessages::TransactionResponse response;  // Ignored
+      DatabasePluginMessages::TransactionRequest request;    // Ignored
+      ExecuteTransaction(response, operation, request);
+    }
+
+
+  public:
+    Transaction(OrthancPluginDatabaseV4& database,
+                IDatabaseListener& listener,
+                TransactionType type) :
+      database_(database),
+      listener_(listener),
+      transaction_(NULL)
+    {
+      DatabasePluginMessages::DatabaseRequest request;
+
+      switch (type)
+      {
+        case TransactionType_ReadOnly:
+          request.mutable_start_transaction()->set_type(DatabasePluginMessages::TRANSACTION_READ_ONLY);
+          break;
+
+        case TransactionType_ReadWrite:
+          request.mutable_start_transaction()->set_type(DatabasePluginMessages::TRANSACTION_READ_WRITE);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      DatabasePluginMessages::DatabaseResponse response;
+      ExecuteDatabase(response, database, DatabasePluginMessages::OPERATION_START_TRANSACTION, request);
+
+      transaction_ = reinterpret_cast<void*>(response.start_transaction().transaction());
+
+      if (transaction_ == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+    }
+
+    
+    virtual ~Transaction()
+    {
+      try
+      {
+        DatabasePluginMessages::DatabaseRequest request;
+        request.mutable_finalize_transaction()->set_transaction(reinterpret_cast<intptr_t>(transaction_));
+
+        DatabasePluginMessages::DatabaseResponse response;
+        ExecuteDatabase(response, database_, DatabasePluginMessages::OPERATION_FINALIZE_TRANSACTION, request);
+      }
+      catch (OrthancException& e)
+      {
+        // Destructors must not throw exceptions
+        LOG(ERROR) << "Cannot finalize the database engine: " << e.What();
+      }
+    }
+
+
+    void* GetTransactionObject()
+    {
+      return transaction_;
+    }
+    
+
+    virtual void Rollback() ORTHANC_OVERRIDE
+    {
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_ROLLBACK);
+    }
+    
+
+    virtual void Commit(int64_t fileSizeDelta) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_commit()->set_file_size_delta(fileSizeDelta);
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_COMMIT, request);
+    }
+
+    
+    virtual void AddAttachment(int64_t id,
+                               const FileInfo& attachment,
+                               int64_t revision) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_add_attachment()->set_id(id);
+      request.mutable_add_attachment()->mutable_attachment()->set_uuid(attachment.GetUuid());
+      request.mutable_add_attachment()->mutable_attachment()->set_content_type(attachment.GetContentType());
+      request.mutable_add_attachment()->mutable_attachment()->set_uncompressed_size(attachment.GetUncompressedSize());
+      request.mutable_add_attachment()->mutable_attachment()->set_uncompressed_hash(attachment.GetUncompressedMD5());
+      request.mutable_add_attachment()->mutable_attachment()->set_compression_type(attachment.GetCompressionType());
+      request.mutable_add_attachment()->mutable_attachment()->set_compressed_size(attachment.GetCompressedSize());
+      request.mutable_add_attachment()->mutable_attachment()->set_compressed_hash(attachment.GetCompressedMD5());        
+      request.mutable_add_attachment()->set_revision(revision);
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_ADD_ATTACHMENT, request);
+    }
+
+
+    virtual void ClearChanges() ORTHANC_OVERRIDE
+    {
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_CLEAR_CHANGES);
+    }
+
+    
+    virtual void ClearExportedResources() ORTHANC_OVERRIDE
+    {
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_CLEAR_EXPORTED_RESOURCES);
+    }
+
+
+    virtual void DeleteAttachment(int64_t id,
+                                  FileContentType attachment) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_delete_attachment()->set_id(id);
+      request.mutable_delete_attachment()->set_type(attachment);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_DELETE_ATTACHMENT, request);
+
+      listener_.SignalAttachmentDeleted(Convert(response.delete_attachment().deleted_attachment()));
+    }
+
+    
+    virtual void DeleteMetadata(int64_t id,
+                                MetadataType type) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_delete_metadata()->set_id(id);
+      request.mutable_delete_metadata()->set_type(type);
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_DELETE_METADATA, request);
+    }
+
+    
+    virtual void DeleteResource(int64_t id) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_delete_resource()->set_id(id);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_DELETE_RESOURCE, request);
+
+      for (int i = 0; i < response.delete_resource().deleted_attachments().size(); i++)
+      {
+        listener_.SignalAttachmentDeleted(Convert(response.delete_resource().deleted_attachments(i)));
+      }
+
+      for (int i = 0; i < response.delete_resource().deleted_resources().size(); i++)
+      {
+        listener_.SignalResourceDeleted(Convert(response.delete_resource().deleted_resources(i).level()),
+                                        response.delete_resource().deleted_resources(i).public_id());
+      }
+
+      if (response.delete_resource().is_remaining_ancestor())
+      {
+        listener_.SignalRemainingAncestor(Convert(response.delete_resource().remaining_ancestor().level()),
+                                          response.delete_resource().remaining_ancestor().public_id());
+      }
+    }
+
+    
+    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                int64_t id) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_all_metadata()->set_id(id);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_ALL_METADATA, request);
+
+      target.clear();
+      for (int i = 0; i < response.get_all_metadata().metadata().size(); i++)
+      {
+        MetadataType key = static_cast<MetadataType>(response.get_all_metadata().metadata(i).type());
+          
+        if (target.find(key) == target.end())
+        {
+          target[key] = response.get_all_metadata().metadata(i).value();
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+      }
+    }
+
+    
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_all_public_ids()->set_resource_type(Convert(resourceType));
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_ALL_PUBLIC_IDS, request);
+
+      target.clear();
+      for (int i = 0; i < response.get_all_public_ids().ids().size(); i++)
+      {
+        target.push_back(response.get_all_public_ids().ids(i));
+      }
+    }
+
+    
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType,
+                                 int64_t since,
+                                 uint32_t limit) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_all_public_ids_with_limits()->set_resource_type(Convert(resourceType));
+      request.mutable_get_all_public_ids_with_limits()->set_since(since);
+      request.mutable_get_all_public_ids_with_limits()->set_limit(limit);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_ALL_PUBLIC_IDS_WITH_LIMITS, request);
+
+      target.clear();
+      for (int i = 0; i < response.get_all_public_ids_with_limits().ids().size(); i++)
+      {
+        target.push_back(response.get_all_public_ids_with_limits().ids(i));
+      }
+    }
+
+    
+    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                            bool& done /*out*/,
+                            int64_t since,
+                            uint32_t limit) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_changes()->set_since(since);
+      request.mutable_get_changes()->set_limit(limit);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_CHANGES, request);
+
+      done = response.get_changes().done();
+        
+      target.clear();
+      for (int i = 0; i < response.get_changes().changes().size(); i++)
+      {
+        target.push_back(Convert(response.get_changes().changes(i)));
+      }
+    }
+
+    
+    virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                       int64_t id) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_children_internal_id()->set_id(id);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_CHILDREN_INTERNAL_ID, request);
+
+      target.clear();
+      for (int i = 0; i < response.get_children_internal_id().ids().size(); i++)
+      {
+        target.push_back(response.get_children_internal_id().ids(i));
+      }
+    }
+
+    
+    virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                     int64_t id) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_children_public_id()->set_id(id);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_CHILDREN_PUBLIC_ID, request);
+
+      target.clear();
+      for (int i = 0; i < response.get_children_public_id().ids().size(); i++)
+      {
+        target.push_back(response.get_children_public_id().ids(i));
+      }
+    }
+
+    
+    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      uint32_t limit) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_exported_resources()->set_since(since);
+      request.mutable_get_exported_resources()->set_limit(limit);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_EXPORTED_RESOURCES, request);
+
+      done = response.get_exported_resources().done();
+        
+      target.clear();
+      for (int i = 0; i < response.get_exported_resources().resources().size(); i++)
+      {
+        target.push_back(Convert(response.get_exported_resources().resources(i)));
+      }
+    }
+
+    
+    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_LAST_CHANGE);
+
+      target.clear();
+      if (response.get_last_change().found())
+      {
+        target.push_back(Convert(response.get_last_change().change()));
+      }
+    }
+
+    
+    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_LAST_EXPORTED_RESOURCE);
+
+      target.clear();
+      if (response.get_last_exported_resource().found())
+      {
+        target.push_back(Convert(response.get_last_exported_resource().resource()));
+      }
+    }
+
+    
+    virtual void GetMainDicomTags(DicomMap& target,
+                                  int64_t id) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_main_dicom_tags()->set_id(id);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_MAIN_DICOM_TAGS, request);
+
+      target.Clear();
+
+      for (int i = 0; i < response.get_main_dicom_tags().tags().size(); i++)
+      {
+        const DatabasePluginMessages::GetMainDicomTags_Response_Tag& tag = response.get_main_dicom_tags().tags(i);
+        if (tag.group() > 0xffffu ||
+            tag.element() > 0xffffu)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+        else
+        {
+          target.SetValue(tag.group(), tag.element(), tag.value(), false);
+        }
+      }
+    }
+
+    
+    virtual std::string GetPublicId(int64_t resourceId) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_public_id()->set_id(resourceId);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_PUBLIC_ID, request);
+      return response.get_public_id().id();
+    }
+
+    
+    virtual uint64_t GetResourcesCount(ResourceType resourceType) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_resources_count()->set_type(Convert(resourceType));
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_RESOURCES_COUNT, request);
+      return response.get_resources_count().count();
+    }
+
+    
+    virtual ResourceType GetResourceType(int64_t resourceId) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_resource_type()->set_id(resourceId);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_RESOURCE_TYPE, request);
+      return Convert(response.get_resource_type().type());
+    }
+
+    
+    virtual uint64_t GetTotalCompressedSize() ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_TOTAL_COMPRESSED_SIZE);
+      return response.get_total_compressed_size().size();
+    }
+
+    
+    virtual uint64_t GetTotalUncompressedSize() ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_TOTAL_UNCOMPRESSED_SIZE);
+      return response.get_total_uncompressed_size().size();
+    }
+
+    
+    virtual bool IsProtectedPatient(int64_t internalId) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_is_protected_patient()->set_patient_id(internalId);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_IS_PROTECTED_PATIENT, request);
+      return response.is_protected_patient().protected_patient();
+    }
+
+    
+    virtual void ListAvailableAttachments(std::set<FileContentType>& target,
+                                          int64_t id) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_list_available_attachments()->set_id(id);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LIST_AVAILABLE_ATTACHMENTS, request);
+
+      target.clear();
+      for (int i = 0; i < response.list_available_attachments().attachments().size(); i++)
+      {
+        FileContentType attachment = static_cast<FileContentType>(response.list_available_attachments().attachments(i));
+
+        if (target.find(attachment) == target.end())
+        {
+          target.insert(attachment);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+      }
+    }
+
+    
+    virtual void LogChange(ChangeType changeType,
+                           ResourceType resourceType,
+                           int64_t internalId,
+                           const std::string& /* publicId - unused */,
+                           const std::string& date) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_log_change()->set_change_type(changeType);
+      request.mutable_log_change()->set_resource_type(Convert(resourceType));
+      request.mutable_log_change()->set_resource_id(internalId);
+      request.mutable_log_change()->set_date(date);
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_LOG_CHANGE, request);
+    }
+
+    
+    virtual void LogExportedResource(const ExportedResource& resource) ORTHANC_OVERRIDE
+    {
+      // TODO: "seq" is ignored, could be simplified in "ExportedResource"
+      
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_log_exported_resource()->set_resource_type(Convert(resource.GetResourceType()));
+      request.mutable_log_exported_resource()->set_public_id(resource.GetPublicId());
+      request.mutable_log_exported_resource()->set_modality(resource.GetModality());
+      request.mutable_log_exported_resource()->set_date(resource.GetDate());
+      request.mutable_log_exported_resource()->set_patient_id(resource.GetPatientId());
+      request.mutable_log_exported_resource()->set_study_instance_uid(resource.GetStudyInstanceUid());
+      request.mutable_log_exported_resource()->set_series_instance_uid(resource.GetSeriesInstanceUid());
+      request.mutable_log_exported_resource()->set_sop_instance_uid(resource.GetSopInstanceUid());
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_LOG_EXPORTED_RESOURCE, request);
+    }
+
+    
+    virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t& revision,
+                                  int64_t id,
+                                  FileContentType contentType) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_lookup_attachment()->set_id(id);
+      request.mutable_lookup_attachment()->set_content_type(contentType);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_ATTACHMENT, request);
+
+      if (response.lookup_attachment().found())
+      {
+        attachment = Convert(response.lookup_attachment().attachment());
+        revision = response.lookup_attachment().revision();
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    
+    virtual bool LookupGlobalProperty(std::string& target,
+                                      GlobalProperty property,
+                                      bool shared) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_lookup_global_property()->set_server_id(shared ? "" : database_.GetServerIdentifier());
+      request.mutable_lookup_global_property()->set_property(property);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_GLOBAL_PROPERTY, request);
+
+      if (response.lookup_global_property().found())
+      {
+        target = response.lookup_global_property().value();
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    
+    virtual bool LookupMetadata(std::string& target,
+                                int64_t& revision,
+                                int64_t id,
+                                MetadataType type) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_lookup_metadata()->set_id(id);
+      request.mutable_lookup_metadata()->set_metadata_type(type);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_METADATA, request);
+
+      if (response.lookup_metadata().found())
+      {
+        target = response.lookup_metadata().value();
+        revision = response.lookup_metadata().revision();
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    
+    virtual bool LookupParent(int64_t& parentId,
+                              int64_t resourceId) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_lookup_parent()->set_id(resourceId);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_PARENT, request);
+
+      if (response.lookup_parent().found())
+      {
+        parentId = response.lookup_parent().parent();
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    
+    virtual bool LookupResource(int64_t& id,
+                                ResourceType& type,
+                                const std::string& publicId) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_lookup_resource()->set_public_id(publicId);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_RESOURCE, request);
+
+      if (response.lookup_resource().found())
+      {
+        id = response.lookup_resource().internal_id();
+        type = Convert(response.lookup_resource().type());
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    
+    virtual bool SelectPatientToRecycle(int64_t& internalId) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_SELECT_PATIENT_TO_RECYCLE);
+
+      if (response.select_patient_to_recycle().found())
+      {
+        internalId = response.select_patient_to_recycle().patient_id();
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    
+    virtual bool SelectPatientToRecycle(int64_t& internalId,
+                                        int64_t patientIdToAvoid) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_select_patient_to_recycle_with_avoid()->set_patient_id_to_avoid(patientIdToAvoid);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_SELECT_PATIENT_TO_RECYCLE_WITH_AVOID, request);
+
+      if (response.select_patient_to_recycle_with_avoid().found())
+      {
+        internalId = response.select_patient_to_recycle_with_avoid().patient_id();
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    
+    virtual void SetGlobalProperty(GlobalProperty property,
+                                   bool shared,
+                                   const std::string& value) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_set_global_property()->set_server_id(shared ? "" : database_.GetServerIdentifier());
+      request.mutable_set_global_property()->set_property(property);
+      request.mutable_set_global_property()->set_value(value);
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_SET_GLOBAL_PROPERTY, request);
+    }
+
+    
+    virtual void ClearMainDicomTags(int64_t id) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_clear_main_dicom_tags()->set_id(id);
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_CLEAR_MAIN_DICOM_TAGS, request);
+    }
+
+    
+    virtual void SetMetadata(int64_t id,
+                             MetadataType type,
+                             const std::string& value,
+                             int64_t revision) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_set_metadata()->set_id(id);
+      request.mutable_set_metadata()->set_metadata_type(type);
+      request.mutable_set_metadata()->set_value(value);
+      request.mutable_set_metadata()->set_revision(revision);
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_SET_METADATA, request);
+    }
+
+    
+    virtual void SetProtectedPatient(int64_t internalId, 
+                                     bool isProtected) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_set_protected_patient()->set_patient_id(internalId);
+      request.mutable_set_protected_patient()->set_protected_patient(isProtected);
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_SET_PROTECTED_PATIENT, request);
+    }
+
+
+    virtual bool IsDiskSizeAbove(uint64_t threshold) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_is_disk_size_above()->set_threshold(threshold);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_IS_DISK_SIZE_ABOVE, request);
+
+      return response.is_disk_size_above().result();
+    }
+
+    
+    virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
+                                      std::list<std::string>* instancesId, // Can be NULL if not needed
+                                      const std::vector<DatabaseConstraint>& lookup,
+                                      ResourceType queryLevel,
+                                      uint32_t limit) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_lookup_resources()->set_query_level(Convert(queryLevel));
+      request.mutable_lookup_resources()->set_limit(limit);
+      request.mutable_lookup_resources()->set_retrieve_instances_ids(instancesId != NULL);
+
+      request.mutable_lookup_resources()->mutable_lookup()->Reserve(lookup.size());
+      
+      for (size_t i = 0; i < lookup.size(); i++)
+      {
+        DatabasePluginMessages::DatabaseConstraint* constraint = request.mutable_lookup_resources()->add_lookup();
+        constraint->set_level(Convert(lookup[i].GetLevel()));
+        constraint->set_tag_group(lookup[i].GetTag().GetGroup());
+        constraint->set_tag_element(lookup[i].GetTag().GetElement());
+        constraint->set_is_identifier_tag(lookup[i].IsIdentifier());
+        constraint->set_is_case_sensitive(lookup[i].IsCaseSensitive());
+        constraint->set_is_mandatory(lookup[i].IsMandatory());
+
+        constraint->mutable_values()->Reserve(lookup[i].GetValuesCount());
+        for (size_t j = 0; j < lookup[i].GetValuesCount(); j++)
+        {
+          constraint->add_values(lookup[i].GetValue(j));
+        }
+
+        switch (lookup[i].GetConstraintType())
+        {
+          case ConstraintType_Equal:
+            constraint->set_type(DatabasePluginMessages::CONSTRAINT_EQUAL);
+            break;
+            
+          case ConstraintType_SmallerOrEqual:
+            constraint->set_type(DatabasePluginMessages::CONSTRAINT_SMALLER_OR_EQUAL);
+            break;
+            
+          case ConstraintType_GreaterOrEqual:
+            constraint->set_type(DatabasePluginMessages::CONSTRAINT_GREATER_OR_EQUAL);
+            break;
+            
+          case ConstraintType_Wildcard:
+            constraint->set_type(DatabasePluginMessages::CONSTRAINT_WILDCARD);
+            break;
+            
+          case ConstraintType_List:
+            constraint->set_type(DatabasePluginMessages::CONSTRAINT_LIST);
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+      }
+      
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_RESOURCES, request);
+
+      for (int i = 0; i < response.lookup_resources().resources_ids().size(); i++)
+      {
+        resourcesId.push_back(response.lookup_resources().resources_ids(i));
+      }
+      
+      if (instancesId != NULL)
+      {
+        if (response.lookup_resources().resources_ids().size() != response.lookup_resources().instances_ids().size())
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+        else
+        {
+          for (int i = 0; i < response.lookup_resources().instances_ids().size(); i++)
+          {
+            instancesId->push_back(response.lookup_resources().instances_ids(i));
+          }
+        }
+      }
+    }
+
+    
+    virtual bool CreateInstance(CreateInstanceResult& result, /* out */
+                                int64_t& instanceId,          /* out */
+                                const std::string& patient,
+                                const std::string& study,
+                                const std::string& series,
+                                const std::string& instance) ORTHANC_OVERRIDE
+    {
+      // TODO: "CreateInstanceResult" => constructor and getters
+      
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_create_instance()->set_patient(patient);
+      request.mutable_create_instance()->set_study(study);
+      request.mutable_create_instance()->set_series(series);
+      request.mutable_create_instance()->set_instance(instance);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_CREATE_INSTANCE, request);
+
+      instanceId = response.create_instance().instance_id();
+
+      if (response.create_instance().is_new_instance())
+      {
+        result.isNewPatient_ = response.create_instance().is_new_patient();
+        result.isNewStudy_ = response.create_instance().is_new_study();
+        result.isNewSeries_ = response.create_instance().is_new_series();
+        result.patientId_ = response.create_instance().patient_id();
+        result.studyId_ = response.create_instance().study_id();
+        result.seriesId_ = response.create_instance().series_id();
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    
+    virtual void SetResourcesContent(const ResourcesContent& content) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+
+      request.mutable_set_resources_content()->mutable_tags()->Reserve(content.GetListTags().size());
+      for (ResourcesContent::ListTags::const_iterator it = content.GetListTags().begin(); it != content.GetListTags().end(); ++it)
+      {
+        DatabasePluginMessages::SetResourcesContent_Request_Tag* tag = request.mutable_set_resources_content()->add_tags();
+        tag->set_resource_id(it->GetResourceId());
+        tag->set_is_identifier(it->IsIdentifier());
+        tag->set_group(it->GetTag().GetGroup());
+        tag->set_element(it->GetTag().GetElement());
+        tag->set_value(it->GetValue());
+      }
+      
+      request.mutable_set_resources_content()->mutable_metadata()->Reserve(content.GetListMetadata().size());
+      for (ResourcesContent::ListMetadata::const_iterator it = content.GetListMetadata().begin(); it != content.GetListMetadata().end(); ++it)
+      {
+        DatabasePluginMessages::SetResourcesContent_Request_Metadata* metadata = request.mutable_set_resources_content()->add_metadata();
+        metadata->set_resource_id(it->GetResourceId());
+        metadata->set_metadata(it->GetType());
+        metadata->set_value(it->GetValue());
+      }
+
+      ExecuteTransaction(DatabasePluginMessages::OPERATION_SET_RESOURCES_CONTENT, request);
+    }
+
+    
+    virtual void GetChildrenMetadata(std::list<std::string>& target,
+                                     int64_t resourceId,
+                                     MetadataType metadata) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_get_children_metadata()->set_id(resourceId);
+      request.mutable_get_children_metadata()->set_metadata(metadata);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_CHILDREN_METADATA, request);
+
+      for (int i = 0; i < response.get_children_metadata().values().size(); i++)
+      {
+        target.push_back(response.get_children_metadata().values(i));
+      }
+    }
+
+    
+    virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_LAST_CHANGE_INDEX);
+      return response.get_last_change_index().result();
+    }
+
+    
+    virtual bool LookupResourceAndParent(int64_t& id,
+                                         ResourceType& type,
+                                         std::string& parentPublicId,
+                                         const std::string& publicId) ORTHANC_OVERRIDE
+    {
+      DatabasePluginMessages::TransactionRequest request;
+      request.mutable_lookup_resource_and_parent()->set_public_id(publicId);
+
+      DatabasePluginMessages::TransactionResponse response;
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_RESOURCE_AND_PARENT, request);
+
+      if (response.lookup_resource_and_parent().found())
+      {
+        id = response.lookup_resource_and_parent().id();
+        type = Convert(response.lookup_resource_and_parent().type());
+
+        switch (type)
+        {
+          case ResourceType_Patient:
+            if (!response.lookup_resource_and_parent().parent_public_id().empty())
+            {
+              throw OrthancException(ErrorCode_DatabasePlugin);
+            }
+            break;
+            
+          case ResourceType_Study:
+          case ResourceType_Series:
+          case ResourceType_Instance:
+            if (response.lookup_resource_and_parent().parent_public_id().empty())
+            {
+              throw OrthancException(ErrorCode_DatabasePlugin);
+            }
+            else
+            {
+              parentPublicId = response.lookup_resource_and_parent().parent_public_id();
+            }
+            break;
+            
+          default:
+            throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+        
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+  };
+
+
+  OrthancPluginDatabaseV4::OrthancPluginDatabaseV4(SharedLibrary& library,
+                                                   PluginsErrorDictionary&  errorDictionary,
+                                                   const _OrthancPluginRegisterDatabaseBackendV4& database,
+                                                   const std::string& serverIdentifier) :
+    library_(library),
+    errorDictionary_(errorDictionary),
+    definition_(database),
+    serverIdentifier_(serverIdentifier),
+    open_(false),
+    databaseVersion_(0),
+    hasFlushToDisk_(false),
+    hasRevisionsSupport_(false)
+  {
+    CLOG(INFO, PLUGINS) << "Identifier of this Orthanc server for the global properties "
+                        << "of the custom database: \"" << serverIdentifier << "\"";
+
+    if (definition_.backend == NULL ||
+        definition_.operations == NULL ||
+        definition_.finalize == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+  }
+
+  
+  OrthancPluginDatabaseV4::~OrthancPluginDatabaseV4()
+  {
+    definition_.finalize(definition_.backend);
+  }
+
+  
+  void OrthancPluginDatabaseV4::Open()
+  {
+    if (open_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    
+    {
+      DatabasePluginMessages::DatabaseRequest request;
+      DatabasePluginMessages::DatabaseResponse response;
+      ExecuteDatabase(response, *this, DatabasePluginMessages::OPERATION_OPEN, request);
+    }
+
+    {
+      DatabasePluginMessages::DatabaseRequest request;
+      DatabasePluginMessages::DatabaseResponse response;
+      ExecuteDatabase(response, *this, DatabasePluginMessages::OPERATION_GET_SYSTEM_INFORMATION, request);
+      databaseVersion_ = response.get_system_information().database_version();
+      hasFlushToDisk_ = response.get_system_information().supports_flush_to_disk();
+      hasRevisionsSupport_ = response.get_system_information().supports_revisions();
+    }
+
+    open_ = true;    
+  }
+
+
+  void OrthancPluginDatabaseV4::Close()
+  {
+    if (!open_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      DatabasePluginMessages::DatabaseRequest request;
+      DatabasePluginMessages::DatabaseResponse response;
+      ExecuteDatabase(response, *this, DatabasePluginMessages::OPERATION_CLOSE, request);
+    }
+  }
+  
+
+  bool OrthancPluginDatabaseV4::HasFlushToDisk() const
+  {
+    if (!open_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return hasFlushToDisk_;
+    }
+  }
+
+
+  void OrthancPluginDatabaseV4::FlushToDisk()
+  {
+    if (!open_ ||
+        !hasFlushToDisk_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      DatabasePluginMessages::DatabaseRequest request;
+      DatabasePluginMessages::DatabaseResponse response;
+      ExecuteDatabase(response, *this, DatabasePluginMessages::OPERATION_FLUSH_TO_DISK, request);
+    }
+  }
+  
+
+  IDatabaseWrapper::ITransaction* OrthancPluginDatabaseV4::StartTransaction(TransactionType type,
+                                                                            IDatabaseListener& listener)
+  {
+    if (!open_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return new Transaction(*this, listener, type);
+    }
+  }
+
+  
+  unsigned int OrthancPluginDatabaseV4::GetDatabaseVersion()
+  {
+    if (!open_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return databaseVersion_;
+    }
+  }
+
+  
+  void OrthancPluginDatabaseV4::Upgrade(unsigned int targetVersion,
+                                        IStorageArea& storageArea)
+  {
+    if (!open_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      VoidDatabaseListener listener;
+      Transaction transaction(*this, listener, TransactionType_ReadWrite);
+
+      try
+      {
+        DatabasePluginMessages::DatabaseRequest request;
+        request.mutable_upgrade()->set_target_version(targetVersion);
+        request.mutable_upgrade()->set_storage_area(reinterpret_cast<intptr_t>(&storageArea));
+        request.mutable_upgrade()->set_transaction(reinterpret_cast<intptr_t>(transaction.GetTransactionObject()));
+        
+        DatabasePluginMessages::DatabaseResponse response;
+
+        ExecuteDatabase(response, *this, DatabasePluginMessages::OPERATION_UPGRADE, request);
+        transaction.Commit(0);
+      }
+      catch (OrthancException& e)
+      {
+        transaction.Rollback();
+        throw;
+      }
+    }
+  }
+
+  
+  bool OrthancPluginDatabaseV4::HasRevisionsSupport() const
+  {
+    if (!open_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return hasRevisionsSupport_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Mon Apr 03 17:00:51 2023 +0200
@@ -0,0 +1,99 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+
+#include "../../../OrthancFramework/Sources/SharedLibrary.h"
+#include "../../Sources/Database/IDatabaseWrapper.h"
+#include "../Include/orthanc/OrthancCPlugin.h"
+#include "PluginsErrorDictionary.h"
+
+namespace Orthanc
+{
+  class OrthancPluginDatabaseV4 : public IDatabaseWrapper
+  {
+  private:
+    class Transaction;
+
+    SharedLibrary&                          library_;
+    PluginsErrorDictionary&                 errorDictionary_;
+    _OrthancPluginRegisterDatabaseBackendV4 definition_;
+    std::string                             serverIdentifier_;
+    bool                                    open_;
+    unsigned int                            databaseVersion_;
+    bool                                    hasFlushToDisk_;
+    bool                                    hasRevisionsSupport_;
+
+    void CheckSuccess(OrthancPluginErrorCode code) const;
+
+  public:
+    OrthancPluginDatabaseV4(SharedLibrary& library,
+                            PluginsErrorDictionary& errorDictionary,
+                            const _OrthancPluginRegisterDatabaseBackendV4& database,
+                            const std::string& serverIdentifier);
+
+    virtual ~OrthancPluginDatabaseV4();
+
+    const _OrthancPluginRegisterDatabaseBackendV4& GetDefinition() const
+    {
+      return definition_;
+    }
+
+    PluginsErrorDictionary& GetErrorDictionary() const
+    {
+      return errorDictionary_;
+    }
+
+    const std::string& GetServerIdentifier() const
+    {
+      return serverIdentifier_;
+    }
+    
+    virtual void Open() ORTHANC_OVERRIDE;
+
+    virtual void Close() ORTHANC_OVERRIDE;
+
+    const SharedLibrary& GetSharedLibrary() const
+    {
+      return library_;
+    }
+
+    virtual void FlushToDisk() ORTHANC_OVERRIDE;
+
+    virtual bool HasFlushToDisk() const ORTHANC_OVERRIDE;
+
+    virtual IDatabaseWrapper::ITransaction* StartTransaction(TransactionType type,
+                                                             IDatabaseListener& listener)
+      ORTHANC_OVERRIDE;
+
+    virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE;
+
+    virtual void Upgrade(unsigned int targetVersion,
+                         IStorageArea& storageArea) ORTHANC_OVERRIDE;    
+
+    virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE;
+  };
+}
+
+#endif
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Fri Mar 31 16:32:02 2023 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Mon Apr 03 17:00:51 2023 +0200
@@ -63,6 +63,7 @@
 #include "../../Sources/ServerToolbox.h"
 #include "OrthancPluginDatabase.h"
 #include "OrthancPluginDatabaseV3.h"
+#include "OrthancPluginDatabaseV4.h"
 #include "PluginsEnumerations.h"
 #include "PluginsJob.h"
 
@@ -1544,8 +1545,9 @@
     Properties properties_;
     int argc_;
     char** argv_;
-    std::unique_ptr<OrthancPluginDatabase>  database_;
+    std::unique_ptr<OrthancPluginDatabase>    database_;
     std::unique_ptr<OrthancPluginDatabaseV3>  databaseV3_;  // New in Orthanc 1.9.2
+    std::unique_ptr<OrthancPluginDatabaseV4>  databaseV4_;  // New in Orthanc 1.12.0
     PluginsErrorDictionary  dictionary_;
     std::string databaseServerIdentifier_;   // New in Orthanc 1.9.2
     unsigned int maxDatabaseRetries_;   // New in Orthanc 1.9.2
@@ -5487,14 +5489,15 @@
 
       case _OrthancPluginService_RegisterDatabaseBackend:
       {
-        LOG(WARNING) << "Performance warning: Plugin has registered a custom database back-end with an old API";
+        LOG(WARNING) << "Performance warning: Plugin has registered a custom database back-end with an old API (version 1)";
         LOG(WARNING) << "The database backend has *no* support for revisions of metadata and attachments";
 
         const _OrthancPluginRegisterDatabaseBackend& p =
           *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackend*>(parameters);
 
         if (pimpl_->database_.get() == NULL &&
-            pimpl_->databaseV3_.get() == NULL)
+            pimpl_->databaseV3_.get() == NULL &&
+            pimpl_->databaseV4_.get() == NULL)
         {
           pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(), 
                                                             *p.backend, NULL, 0, p.payload));
@@ -5511,14 +5514,15 @@
 
       case _OrthancPluginService_RegisterDatabaseBackendV2:
       {
-        LOG(WARNING) << "Performance warning: Plugin has registered a custom database back-end with an old API";
+        LOG(WARNING) << "Performance warning: Plugin has registered a custom database back-end with an old API (version 2)";
         LOG(WARNING) << "The database backend has *no* support for revisions of metadata and attachments";
 
         const _OrthancPluginRegisterDatabaseBackendV2& p =
           *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV2*>(parameters);
 
         if (pimpl_->database_.get() == NULL &&
-            pimpl_->databaseV3_.get() == NULL)
+            pimpl_->databaseV3_.get() == NULL &&
+            pimpl_->databaseV4_.get() == NULL)
         {
           pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(),
                                                             *p.backend, p.extensions,
@@ -5536,13 +5540,14 @@
 
       case _OrthancPluginService_RegisterDatabaseBackendV3:
       {
-        CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end";
+        LOG(WARNING) << "Performance warning: Plugin has registered a custom database back-end with an old API (version 3)";
 
         const _OrthancPluginRegisterDatabaseBackendV3& p =
           *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV3*>(parameters);
 
         if (pimpl_->database_.get() == NULL &&
-            pimpl_->databaseV3_.get() == NULL)
+            pimpl_->databaseV3_.get() == NULL &&
+            pimpl_->databaseV4_.get() == NULL)
         {
           pimpl_->databaseV3_.reset(new OrthancPluginDatabaseV3(plugin, GetErrorDictionary(), p.backend,
                                                                 p.backendSize, p.database, pimpl_->databaseServerIdentifier_));
@@ -5556,6 +5561,28 @@
         return true;
       }
 
+      case _OrthancPluginService_RegisterDatabaseBackendV4:
+      {
+        CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end";
+
+        const _OrthancPluginRegisterDatabaseBackendV4& p =
+          *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV4*>(parameters);
+
+        if (pimpl_->database_.get() == NULL &&
+            pimpl_->databaseV3_.get() == NULL &&
+            pimpl_->databaseV4_.get() == NULL)
+        {
+          pimpl_->databaseV4_.reset(new OrthancPluginDatabaseV4(plugin, GetErrorDictionary(), p, pimpl_->databaseServerIdentifier_));
+          pimpl_->maxDatabaseRetries_ = p.maxDatabaseRetries;
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_DatabaseBackendAlreadyRegistered);
+        }
+
+        return true;
+      }
+
       case _OrthancPluginService_DatabaseAnswer:
         throw OrthancException(ErrorCode_InternalError);   // Implemented before locking (*)
 
@@ -5695,7 +5722,8 @@
   {
     boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
     return (pimpl_->database_.get() != NULL ||
-            pimpl_->databaseV3_.get() != NULL);
+            pimpl_->databaseV3_.get() != NULL ||
+            pimpl_->databaseV4_.get() != NULL);
   }
 
 
@@ -5735,6 +5763,10 @@
     {
       return *pimpl_->databaseV3_;
     }
+    else if (pimpl_->databaseV4_.get() != NULL)
+    {
+      return *pimpl_->databaseV4_;
+    }
     else
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
@@ -5752,6 +5784,10 @@
     {
       return pimpl_->databaseV3_->GetSharedLibrary();
     }
+    else if (pimpl_->databaseV4_.get() != NULL)
+    {
+      return pimpl_->databaseV4_->GetSharedLibrary();
+    }
     else
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Mar 31 16:32:02 2023 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Apr 03 17:00:51 2023 +0200
@@ -17,7 +17,7 @@
  *    - Possibly register its callback for received DICOM instances using ::OrthancPluginRegisterOnStoredInstanceCallback().
  *    - Possibly register its callback for changes to the DICOM store using ::OrthancPluginRegisterOnChangeCallback().
  *    - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea2().
- *    - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV3().
+ *    - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV4().
  *    - Possibly register a handler for C-Find SCP using OrthancPluginRegisterFindCallback().
  *    - Possibly register a handler for C-Find SCP against DICOM worklists using OrthancPluginRegisterWorklistCallback().
  *    - Possibly register a handler for C-Move SCP using OrthancPluginRegisterMoveCallback().
@@ -119,8 +119,8 @@
 #endif
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
-#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     11
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  3
+#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     12
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  0
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -535,7 +535,8 @@
     _OrthancPluginService_StorageAreaRead = 5004,
     _OrthancPluginService_StorageAreaRemove = 5005,
     _OrthancPluginService_RegisterDatabaseBackendV3 = 5006,  /* New in Orthanc 1.9.2 */
-
+    _OrthancPluginService_RegisterDatabaseBackendV4 = 5007,  /* New in Orthanc 1.12.0 */
+    
     /* Primitives for handling images */
     _OrthancPluginService_GetImagePixelFormat = 6000,
     _OrthancPluginService_GetImageWidth = 6001,
@@ -9164,6 +9165,66 @@
   }
 
 
+  /**
+   * @brief Signature of a callback function that is triggered when
+   * the Orthanc core requests an operation from the database plugin.
+   * Both request and response are encoded as protobuf buffers.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginCallDatabaseBackendV4) (
+    OrthancPluginMemoryBuffer64* response,
+    void* backend,
+    const void* request,
+    uint64_t requestSize);
+
+  /**
+   * @brief Signature of a callback function that is triggered when
+   * the database plugin must be finalized.
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginFinalizeDatabaseBackendV4) (void* backend);
+
+  typedef struct
+  {
+    void*                                   backend;
+    uint32_t                                maxDatabaseRetries;
+    OrthancPluginCallDatabaseBackendV4      operations;
+    OrthancPluginFinalizeDatabaseBackendV4  finalize;
+  } _OrthancPluginRegisterDatabaseBackendV4;
+
+  /**
+   * Register a custom database back-end.
+   *
+   * This function was added in Orthanc SDK 1.12.0. It uses Google
+   * Protocol Buffers for the communications between the Orthanc core
+   * and database plugins. Check out "OrthancDatabasePlugin.proto" for
+   * the definition of the protobuf messages.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param backend Pointer to the custom database backend.
+   * @param maxDatabaseRetries Maximum number of retries if transaction doesn't succeed.
+   * If no retry is successful, OrthancPluginErrorCode_DatabaseCannotSerialize is generated.
+   * @param operations Access to the operations of the custom database backend.
+   * @param finalize Callback to deallocate the custom database backend.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterDatabaseBackendV4(
+    OrthancPluginContext*                   context,
+    void*                                   backend,
+    uint32_t                                maxDatabaseRetries,
+    OrthancPluginCallDatabaseBackendV4      operations,
+    OrthancPluginFinalizeDatabaseBackendV4  finalize)
+  {
+    _OrthancPluginRegisterDatabaseBackendV4 params;
+    params.backend = backend;
+    params.maxDatabaseRetries = maxDatabaseRetries;
+    params.operations = operations;
+    params.finalize = finalize;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV4, &params);
+  }
+
 #ifdef  __cplusplus
 }
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Mon Apr 03 17:00:51 2023 +0200
@@ -0,0 +1,851 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+/**
+ * This Protocol Buffers prototype describes the exchanges between the
+ * Orthanc core and its database plugins. The various calls correspond
+ * to the "IDatabaseWrapper" interface in the source code of Orthanc.
+ *
+ * WARNING: *NEVER* modify or remove existing entries. It is only
+ * allowed to *add* new stuff.
+ **/
+
+syntax = "proto3";
+
+/**
+ * Turn off protobuf reflection to avoid clashes between the Orthanc
+ * core and the database plugin, otherwise both will try to register
+ * the same messages in the process-wide descriptor pool, which would
+ * result in protobuf error "File already exists in database".
+ **/
+option optimize_for = LITE_RUNTIME;
+
+package Orthanc.DatabasePluginMessages;
+
+
+/**
+ * Data structures that are common with the Orthanc core.
+ **/
+
+message FileInfo {
+  string  uuid = 1;
+  int32   content_type = 2;      // opaque "FileContentType" in Orthanc
+  uint64  uncompressed_size = 3;
+  string  uncompressed_hash = 4;
+  int32   compression_type = 5;  // opaque "CompressionType" in Orthanc
+  uint64  compressed_size = 6;
+  string  compressed_hash = 7;
+}
+
+enum ResourceType {
+  RESOURCE_PATIENT = 0;
+  RESOURCE_STUDY = 1;
+  RESOURCE_SERIES = 2;
+  RESOURCE_INSTANCE = 3;
+}
+
+enum ConstraintType {
+  CONSTRAINT_EQUAL = 0;
+  CONSTRAINT_SMALLER_OR_EQUAL = 1;
+  CONSTRAINT_GREATER_OR_EQUAL = 2;
+  CONSTRAINT_WILDCARD = 3;
+  CONSTRAINT_LIST = 4;
+}
+
+message ServerIndexChange {
+  int64         seq = 1;
+  int32         change_type = 2;   // opaque "ChangeType" in Orthanc
+  ResourceType  resource_type = 3;
+  string        public_id = 4;
+  string        date = 5;
+}
+
+message ExportedResource {
+  int64         seq = 1;
+  ResourceType  resource_type = 2;
+  string        public_id = 3;
+  string        modality = 4;
+  string        date = 5;
+  string        patient_id = 6;
+  string        study_instance_uid = 7;
+  string        series_instance_uid = 8;
+  string        sop_instance_uid = 9;
+}
+
+message DatabaseConstraint {
+  ResourceType     level = 1;
+  uint32           tag_group = 2;
+  uint32           tag_element = 3;
+  bool             is_identifier_tag = 4;
+  bool             is_case_sensitive = 5;
+  bool             is_mandatory = 6;
+  ConstraintType   type = 7;
+  repeated string  values = 8;
+}
+
+
+/**
+ * Database-level operations.
+ **/
+
+enum DatabaseOperation {
+  OPERATION_GET_SYSTEM_INFORMATION = 0;
+  OPERATION_OPEN = 1;
+  OPERATION_CLOSE = 2;
+  OPERATION_FLUSH_TO_DISK = 3;
+  OPERATION_START_TRANSACTION = 4;
+  OPERATION_UPGRADE = 5;
+  OPERATION_FINALIZE_TRANSACTION = 6;
+}
+
+enum TransactionType {
+  TRANSACTION_READ_ONLY = 0;
+  TRANSACTION_READ_WRITE = 1;
+}
+
+message GetSystemInformation {
+  message Request {
+  }
+  message Response {
+    uint32 database_version = 1;
+    bool supports_flush_to_disk = 2;
+    bool supports_revisions = 3;
+  }
+}
+
+message Open {
+  message Request {
+  }
+  message Response {
+  }
+}
+
+message Close {
+  message Request {
+  }
+  message Response {
+  }
+}
+
+message FlushToDisk {
+  message Request {
+  }
+  message Response {
+  }
+}
+
+message StartTransaction {
+  message Request {
+    TransactionType type = 1;
+  }
+  message Response {
+    sfixed64 transaction = 1;
+  }
+}
+
+message Upgrade {
+  /**
+   * It is guaranteed that a read-write transaction is created by the
+   * Orthanc core before executing this operation.
+   **/
+  message Request {
+    uint32 target_version = 1;
+    sfixed64 storage_area = 2;
+    sfixed64 transaction = 3;
+  }
+  message Response {
+  }
+}
+
+message FinalizeTransaction {
+  message Request {
+    sfixed64 transaction = 1;
+  }
+  message Response {
+  }
+}
+
+message DatabaseRequest {
+  sfixed64           database = 1;
+  DatabaseOperation  operation = 2;
+
+  GetSystemInformation.Request  get_system_information = 100;
+  Open.Request                  open = 101;
+  Close.Request                 close = 102;
+  FlushToDisk.Request           flush_to_disk = 103;
+  StartTransaction.Request      start_transaction = 104;
+  Upgrade.Request               upgrade = 105;
+  FinalizeTransaction.Request   finalize_transaction = 106;
+}
+
+message DatabaseResponse {
+  GetSystemInformation.Response  get_system_information = 100;
+  Open.Response                  open = 101;
+  Close.Response                 close = 102;
+  FlushToDisk.Response           flush_to_disk = 103;
+  StartTransaction.Response      start_transaction = 104;
+  Upgrade.Response               upgrade = 105;
+  FinalizeTransaction.Response   finalize_transaction = 106;
+}
+
+
+/**
+ * Transaction-level operations.
+ **/
+
+enum TransactionOperation {
+  OPERATION_ROLLBACK = 0;
+  OPERATION_COMMIT = 1;
+  OPERATION_ADD_ATTACHMENT = 2;
+  OPERATION_CLEAR_CHANGES = 3;
+  OPERATION_CLEAR_EXPORTED_RESOURCES = 4;
+  OPERATION_DELETE_ATTACHMENT = 5;
+  OPERATION_DELETE_METADATA = 6;
+  OPERATION_DELETE_RESOURCE = 7;
+  OPERATION_GET_ALL_METADATA = 8;
+  OPERATION_GET_ALL_PUBLIC_IDS = 9;
+  OPERATION_GET_ALL_PUBLIC_IDS_WITH_LIMITS = 10;
+  OPERATION_GET_CHANGES = 11;
+  OPERATION_GET_CHILDREN_INTERNAL_ID = 12;
+  OPERATION_GET_CHILDREN_PUBLIC_ID = 13;
+  OPERATION_GET_EXPORTED_RESOURCES = 14;
+  OPERATION_GET_LAST_CHANGE = 15;
+  OPERATION_GET_LAST_EXPORTED_RESOURCE = 16;
+  OPERATION_GET_MAIN_DICOM_TAGS = 17;
+  OPERATION_GET_PUBLIC_ID = 18;
+  OPERATION_GET_RESOURCES_COUNT = 19;
+  OPERATION_GET_RESOURCE_TYPE = 20;
+  OPERATION_GET_TOTAL_COMPRESSED_SIZE = 21;
+  OPERATION_GET_TOTAL_UNCOMPRESSED_SIZE = 22;
+  OPERATION_IS_PROTECTED_PATIENT = 23;
+  OPERATION_LIST_AVAILABLE_ATTACHMENTS = 24;
+  OPERATION_LOG_CHANGE = 25;
+  OPERATION_LOG_EXPORTED_RESOURCE = 26;
+  OPERATION_LOOKUP_ATTACHMENT = 27;
+  OPERATION_LOOKUP_GLOBAL_PROPERTY = 28;
+  OPERATION_LOOKUP_METADATA = 29;
+  OPERATION_LOOKUP_PARENT = 30;
+  OPERATION_LOOKUP_RESOURCE = 31;
+  OPERATION_SELECT_PATIENT_TO_RECYCLE = 32;
+  OPERATION_SELECT_PATIENT_TO_RECYCLE_WITH_AVOID = 33;
+  OPERATION_SET_GLOBAL_PROPERTY = 34;
+  OPERATION_CLEAR_MAIN_DICOM_TAGS = 35;
+  OPERATION_SET_METADATA = 36;
+  OPERATION_SET_PROTECTED_PATIENT = 37;
+  OPERATION_IS_DISK_SIZE_ABOVE = 38;
+  OPERATION_LOOKUP_RESOURCES = 39;
+  OPERATION_CREATE_INSTANCE = 40;
+  OPERATION_SET_RESOURCES_CONTENT = 41;
+  OPERATION_GET_CHILDREN_METADATA = 42;
+  OPERATION_GET_LAST_CHANGE_INDEX = 43;
+  OPERATION_LOOKUP_RESOURCE_AND_PARENT = 44;
+}
+
+message Rollback {
+  message Request {
+  }
+  message Response {
+  }
+}
+
+message Commit {
+  message Request {
+    int64 file_size_delta = 1;
+  }
+  message Response {
+  }
+}
+
+message AddAttachment {
+  message Request {
+    int64 id = 1;
+    FileInfo attachment = 2;
+    int64 revision = 3;
+  }
+  message Response {
+  }
+}
+
+message ClearChanges {
+  message Request {
+  }
+  message Response {
+  }
+}
+
+message ClearExportedResources {
+  message Request {
+  }
+  message Response {
+  }
+}
+
+message DeleteAttachment {
+  message Request {
+    int64 id = 1;
+    int32 type = 2;
+  }
+  message Response {
+    FileInfo deleted_attachment = 1;
+  }
+}
+
+message DeleteMetadata {
+  message Request {
+    int64 id = 1;
+    int32 type = 2;
+  }
+  message Response {
+  }
+}
+
+message DeleteResource {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    message Resource {
+      ResourceType level = 1;
+      string public_id = 2;
+    }
+    repeated FileInfo deleted_attachments = 1;
+    repeated Resource deleted_resources = 2;
+    bool is_remaining_ancestor = 3;
+    Resource remaining_ancestor = 4;
+  }
+}
+
+message GetAllMetadata {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    message Metadata {
+      int32 type = 1;
+      string value = 2;
+    }
+    repeated Metadata metadata = 1;
+  }
+}
+
+message GetAllPublicIds {
+  message Request {
+    ResourceType resource_type = 1;
+  }
+  message Response {
+    repeated string ids = 1;
+  }
+}
+
+message GetAllPublicIdsWithLimits {
+  message Request {
+    ResourceType resource_type = 1;
+    int64 since = 2;
+    uint32 limit = 3;
+  }
+  message Response {
+    repeated string ids = 1;
+  }
+}
+
+message GetChanges {
+  message Request {
+    int64 since = 1;
+    uint32 limit = 2;
+  }
+  message Response {
+    repeated ServerIndexChange changes = 1;
+    bool done = 2;
+  }
+}
+
+message GetChildrenInternalId {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    repeated int64 ids = 1;
+  }
+}
+
+message GetChildrenPublicId {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    repeated string ids = 1;
+  }
+}
+
+message GetExportedResources {
+  message Request {
+    int64 since = 1;
+    uint32 limit = 2;
+  }
+  message Response {
+    repeated ExportedResource resources = 1;
+    bool done = 2;
+  }
+}
+
+message GetLastChange {
+  message Request {
+  }
+  message Response {
+    bool found = 1;
+    ServerIndexChange change = 2;
+  }
+}
+
+message GetLastExportedResource {
+  message Request {
+  }
+  message Response {
+    bool found = 1;
+    ExportedResource resource = 2;
+  }
+}
+
+message GetMainDicomTags {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    message Tag {
+      uint32 group = 1;
+      uint32 element = 2;
+      string value = 3;
+    }
+    repeated Tag tags = 1;
+  }
+}
+
+message GetPublicId {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    string id = 1;
+  }
+}
+
+message GetResourcesCount {
+  message Request {
+    ResourceType type = 1;
+  }
+  message Response {
+    uint64 count = 1;
+  }
+}
+
+message GetResourceType {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    ResourceType type = 1;
+  }
+}
+
+message GetTotalCompressedSize {
+  message Request {
+  }
+  message Response {
+    uint64 size = 1;
+  }
+}
+
+message GetTotalUncompressedSize {
+  message Request {
+  }
+  message Response {
+    uint64 size = 1;
+  }
+}
+
+message IsProtectedPatient {
+  message Request {
+    int64 patient_id = 1;
+  }
+  message Response {
+    bool protected_patient = 1;
+  }
+}
+
+message ListAvailableAttachments {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    repeated int32 attachments = 1;
+  }
+}
+
+message LogChange {
+  message Request {
+    int32         change_type = 1;
+    ResourceType  resource_type = 2;
+    int64         resource_id = 3;
+    string        date = 4;
+  }
+  message Response {
+  }
+}
+
+message LogExportedResource {
+  message Request {
+    ResourceType  resource_type = 1;
+    string        public_id = 2;
+    string        modality = 3;
+    string        date = 4;
+    string        patient_id = 5;
+    string        study_instance_uid = 6;
+    string        series_instance_uid = 7;
+    string        sop_instance_uid = 8;
+  }
+  message Response {
+  }
+}
+
+message LookupAttachment {
+  message Request {
+    int64 id = 1;
+    int32 content_type = 2;
+  }
+  message Response {
+    bool found = 1;
+    FileInfo attachment = 2;
+    int64 revision = 3;
+  }
+}
+
+message LookupGlobalProperty {
+  message Request {
+    string server_id = 1;
+    int32 property = 2;
+  }
+  message Response {
+    bool found = 1;
+    string value = 2;
+  }
+}
+
+message LookupMetadata {
+  message Request {
+    int64 id = 1;
+    int32 metadata_type = 2;
+  }
+  message Response {
+    bool found = 1;
+    string value = 2;
+    int64 revision = 3;
+  }
+}
+
+message LookupParent {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+    bool found = 1;
+    int64 parent = 2;
+  }
+}
+
+message LookupResource {
+  message Request {
+    string public_id = 1;
+  }
+  message Response {
+    bool found = 1;
+    int64 internal_id = 2;
+    ResourceType type = 3;
+  }
+}
+
+message SelectPatientToRecycle {
+  message Request {
+  }
+  message Response {
+    bool found = 1;
+    int64 patient_id = 2;
+  }
+}
+
+message SelectPatientToRecycleWithAvoid {
+  message Request {
+    int64 patient_id_to_avoid = 1;
+  }
+  message Response {
+    bool found = 1;
+    int64 patient_id = 2;
+  }
+}
+
+message SetGlobalProperty {
+  message Request {
+    string server_id = 1;
+    int32 property = 2;
+    string value = 3;
+  }
+  message Response {
+  }
+}
+
+message ClearMainDicomTags {
+  message Request {
+    int64 id = 1;
+  }
+  message Response {
+  }
+}
+
+message SetMetadata {
+  message Request {
+    int64 id = 1;
+    int32 metadata_type = 2;
+    string value = 3;
+    int64 revision = 4;
+  }
+  message Response {
+  }
+}
+
+message SetProtectedPatient {
+  message Request {
+    int64 patient_id = 1;
+    bool protected_patient = 2;
+  }
+  message Response {
+  }
+}
+
+message IsDiskSizeAbove {
+  message Request {
+    uint64 threshold = 1;
+  }
+  message Response {
+    bool result = 1;
+  }
+}
+
+message LookupResources {
+  message Request {
+    repeated DatabaseConstraint lookup = 1;
+    ResourceType query_level = 2;
+    uint32 limit = 3;
+    bool retrieve_instances_ids = 4;
+  }
+  message Response {
+    repeated string resources_ids = 1;
+    repeated string instances_ids = 2;  // Only filled if "retrieve_instances" is true
+  }
+}
+
+message CreateInstance {
+  message Request {
+    string patient = 1;
+    string study = 2;
+    string series = 3;
+    string instance = 4;
+  }
+  message Response {
+    bool is_new_instance = 1;
+    int64 instance_id = 2;
+
+    // The fields below are only set if "is_new_instance" is true
+    bool is_new_patient = 3;
+    bool is_new_study = 4;
+    bool is_new_series = 5;
+    int64 patient_id = 6;
+    int64 study_id = 7;
+    int64 series_id = 8;
+  }
+}
+
+message SetResourcesContent {
+  message Request {
+    message Tag {
+      int64 resource_id = 1;
+      bool is_identifier = 2;
+      uint32 group = 3;
+      uint32 element = 4;
+      string value = 5;
+    }
+
+    message Metadata {
+      int64 resource_id = 1;
+      int32 metadata = 2;
+      string value = 3;
+    }
+
+    repeated Tag tags = 1;
+    repeated Metadata metadata = 2;
+  }
+  message Response {
+  }
+}
+
+message GetChildrenMetadata {
+  message Request {
+    int64 id = 1;
+    int32 metadata = 2;
+  }
+  message Response {
+    repeated string values = 1;
+  }
+}
+
+message GetLastChangeIndex {
+  message Request {
+  }
+  message Response {
+    int64 result = 1;
+  }
+}
+
+message LookupResourceAndParent {
+  message Request {
+    string public_id = 1;
+  }
+  message Response {
+    bool found = 1;
+    int64 id = 2;
+    ResourceType type = 3;
+    string parent_public_id = 4;  // Only for study, series, or instance
+  }
+}
+
+message TransactionRequest {
+  sfixed64              transaction = 1;
+  TransactionOperation  operation = 2;
+
+  Rollback.Request                        rollback = 100;
+  Commit.Request                          commit = 101;
+  AddAttachment.Request                   add_attachment = 102;
+  ClearChanges.Request                    clear_changes = 103;
+  ClearExportedResources.Request          clear_exported_resources = 104;
+  DeleteAttachment.Request                delete_attachment = 105;
+  DeleteMetadata.Request                  delete_metadata = 106;
+  DeleteResource.Request                  delete_resource = 107;
+  GetAllMetadata.Request                  get_all_metadata = 108;
+  GetAllPublicIds.Request                 get_all_public_ids = 109;
+  GetAllPublicIdsWithLimits.Request       get_all_public_ids_with_limits = 110;
+  GetChanges.Request                      get_changes = 111;
+  GetChildrenInternalId.Request           get_children_internal_id = 112;
+  GetChildrenPublicId.Request             get_children_public_id = 113;
+  GetExportedResources.Request            get_exported_resources = 114;
+  GetLastChange.Request                   get_last_change = 115;
+  GetLastExportedResource.Request         get_last_exported_resource = 116;
+  GetMainDicomTags.Request                get_main_dicom_tags = 117;
+  GetPublicId.Request                     get_public_id = 118;
+  GetResourcesCount.Request               get_resources_count = 119;
+  GetResourceType.Request                 get_resource_type = 120;
+  GetTotalCompressedSize.Request          get_total_compressed_size = 121;
+  GetTotalUncompressedSize.Request        get_total_uncompressed_size = 122;
+  IsProtectedPatient.Request              is_protected_patient = 123;
+  ListAvailableAttachments.Request        list_available_attachments = 124;
+  LogChange.Request                       log_change = 125;
+  LogExportedResource.Request             log_exported_resource = 126;
+  LookupAttachment.Request                lookup_attachment = 127;
+  LookupGlobalProperty.Request            lookup_global_property = 128;
+  LookupMetadata.Request                  lookup_metadata = 129;
+  LookupParent.Request                    lookup_parent = 130;
+  LookupResource.Request                  lookup_resource = 131;
+  SelectPatientToRecycle.Request          select_patient_to_recycle = 132;
+  SelectPatientToRecycleWithAvoid.Request select_patient_to_recycle_with_avoid = 133;
+  SetGlobalProperty.Request               set_global_property = 134;
+  ClearMainDicomTags.Request              clear_main_dicom_tags = 135;
+  SetMetadata.Request                     set_metadata = 136;
+  SetProtectedPatient.Request             set_protected_patient = 137;
+  IsDiskSizeAbove.Request                 is_disk_size_above = 138;
+  LookupResources.Request                 lookup_resources = 139;
+  CreateInstance.Request                  create_instance = 140;
+  SetResourcesContent.Request             set_resources_content = 141;
+  GetChildrenMetadata.Request             get_children_metadata = 142;
+  GetLastChangeIndex.Request              get_last_change_index = 143;
+  LookupResourceAndParent.Request         lookup_resource_and_parent = 144;
+}
+
+message TransactionResponse {
+  Rollback.Response                        rollback = 100;
+  Commit.Response                          commit = 101;
+  AddAttachment.Response                   add_attachment = 102;
+  ClearChanges.Response                    clear_changes = 103;
+  ClearExportedResources.Response          clear_exported_resources = 104;
+  DeleteAttachment.Response                delete_attachment = 105;
+  DeleteMetadata.Response                  delete_metadata = 106;
+  DeleteResource.Response                  delete_resource = 107;
+  GetAllMetadata.Response                  get_all_metadata = 108;
+  GetAllPublicIds.Response                 get_all_public_ids = 109;
+  GetAllPublicIdsWithLimits.Response       get_all_public_ids_with_limits = 110;
+  GetChanges.Response                      get_changes = 111;
+  GetChildrenInternalId.Response           get_children_internal_id = 112;
+  GetChildrenPublicId.Response             get_children_public_id = 113;
+  GetExportedResources.Response            get_exported_resources = 114;
+  GetLastChange.Response                   get_last_change = 115;
+  GetLastExportedResource.Response         get_last_exported_resource = 116;
+  GetMainDicomTags.Response                get_main_dicom_tags = 117;
+  GetPublicId.Response                     get_public_id = 118;
+  GetResourcesCount.Response               get_resources_count = 119;
+  GetResourceType.Response                 get_resource_type = 120;
+  GetTotalCompressedSize.Response          get_total_compressed_size = 121;
+  GetTotalUncompressedSize.Response        get_total_uncompressed_size = 122;
+  IsProtectedPatient.Response              is_protected_patient = 123;
+  ListAvailableAttachments.Response        list_available_attachments = 124;
+  LogChange.Response                       log_change = 125;
+  LogExportedResource.Response             log_exported_resource = 126;
+  LookupAttachment.Response                lookup_attachment = 127;
+  LookupGlobalProperty.Response            lookup_global_property = 128;
+  LookupMetadata.Response                  lookup_metadata = 129;
+  LookupParent.Response                    lookup_parent = 130;
+  LookupResource.Response                  lookup_resource = 131;
+  SelectPatientToRecycle.Response          select_patient_to_recycle = 132;
+  SelectPatientToRecycleWithAvoid.Response select_patient_to_recycle_with_avoid = 133;
+  SetGlobalProperty.Response               set_global_property = 134;
+  ClearMainDicomTags.Response              clear_main_dicom_tags = 135;
+  SetMetadata.Response                     set_metadata = 136;
+  SetProtectedPatient.Response             set_protected_patient = 137;
+  IsDiskSizeAbove.Response                 is_disk_size_above = 138;
+  LookupResources.Response                 lookup_resources = 139;
+  CreateInstance.Response                  create_instance = 140;
+  SetResourcesContent.Response             set_resources_content = 141;
+  GetChildrenMetadata.Response             get_children_metadata = 142;
+  GetLastChangeIndex.Response              get_last_change_index = 143;
+  LookupResourceAndParent.Response         lookup_resource_and_parent = 144;
+}
+
+enum RequestType {
+  REQUEST_DATABASE = 0;
+  REQUEST_TRANSACTION = 1;
+}
+
+message Request {
+  RequestType         type = 1;
+  DatabaseRequest     database_request = 2;
+  TransactionRequest  transaction_request = 3;
+}
+
+message Response {
+  DatabaseResponse     database_response = 2;
+  TransactionResponse  transaction_response = 3;
+}
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Fri Mar 31 16:32:02 2023 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Mon Apr 03 17:00:51 2023 +0200
@@ -42,7 +42,7 @@
   class IDatabaseWrapper : public boost::noncopyable
   {
   public:
-    struct CreateInstanceResult
+    struct CreateInstanceResult : public boost::noncopyable
     {
       bool     isNewPatient_;
       bool     isNewStudy_;
@@ -94,13 +94,13 @@
 
       virtual void GetAllPublicIds(std::list<std::string>& target,
                                    ResourceType resourceType,
-                                   size_t since,
-                                   size_t limit) = 0;
+                                   int64_t since,
+                                   uint32_t limit) = 0;
 
       virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
                               bool& done /*out*/,
                               int64_t since,
-                              uint32_t maxResults) = 0;
+                              uint32_t limit) = 0;
 
       virtual void GetChildrenInternalId(std::list<int64_t>& target,
                                          int64_t id) = 0;
@@ -111,7 +111,7 @@
       virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
                                         bool& done /*out*/,
                                         int64_t since,
-                                        uint32_t maxResults) = 0;
+                                        uint32_t limit) = 0;
 
       virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) = 0;
 
@@ -130,15 +130,16 @@
     
       virtual uint64_t GetTotalUncompressedSize() = 0;
 
-      virtual bool IsExistingResource(int64_t internalId) = 0;
-
       virtual bool IsProtectedPatient(int64_t internalId) = 0;
 
       virtual void ListAvailableAttachments(std::set<FileContentType>& target,
                                             int64_t id) = 0;
 
-      virtual void LogChange(int64_t internalId,
-                             const ServerIndexChange& change) = 0;
+      virtual void LogChange(ChangeType changeType,
+                             ResourceType resourceType,
+                             int64_t internalId,
+                             const std::string& publicId,  /* only for compatibility with V1 and V2 plugins */
+                             const std::string& date) = 0;
 
       virtual void LogExportedResource(const ExportedResource& resource) = 0;
     
@@ -199,7 +200,7 @@
                                         std::list<std::string>* instancesId, // Can be NULL if not needed
                                         const std::vector<DatabaseConstraint>& lookup,
                                         ResourceType queryLevel,
-                                        size_t limit) = 0;
+                                        uint32_t limit) = 0;
 
       // Returns "true" iff. the instance is new and has been inserted
       // into the database. If "false" is returned, the content of
--- a/OrthancServer/Sources/Database/ResourcesContent.cpp	Fri Mar 31 16:32:02 2023 +0200
+++ b/OrthancServer/Sources/Database/ResourcesContent.cpp	Mon Apr 03 17:00:51 2023 +0200
@@ -140,13 +140,13 @@
     for (std::list<TagValue>::const_iterator
            it = tags_.begin(); it != tags_.end(); ++it)
     {
-      if (it->isIdentifier_)
+      if (it->IsIdentifier())
       {
-        compatibility.SetIdentifierTag(it->resourceId_, it->tag_,  it->value_);
+        compatibility.SetIdentifierTag(it->GetResourceId(), it->GetTag(),  it->GetValue());
       }
       else
       {
-        compatibility.SetMainDicomTag(it->resourceId_, it->tag_,  it->value_);
+        compatibility.SetMainDicomTag(it->GetResourceId(), it->GetTag(),  it->GetValue());
       }
     }
 
@@ -154,7 +154,7 @@
            it = metadata_.begin(); it != metadata_.end(); ++it)
     {
       assert(isNewResource_);
-      compatibility.SetMetadata(it->resourceId_, it->metadata_,  it->value_, 0 /* initial revision number */);
+      compatibility.SetMetadata(it->GetResourceId(), it->GetType(),  it->GetValue(), 0 /* initial revision number */);
     }
   }
 }
--- a/OrthancServer/Sources/Database/ResourcesContent.h	Fri Mar 31 16:32:02 2023 +0200
+++ b/OrthancServer/Sources/Database/ResourcesContent.h	Mon Apr 03 17:00:51 2023 +0200
@@ -39,13 +39,15 @@
   class ResourcesContent : public boost::noncopyable
   {
   public:
-    struct TagValue
+    class TagValue
     {
+    private:
       int64_t      resourceId_;
       bool         isIdentifier_;
       DicomTag     tag_;
       std::string  value_;
 
+    public:
       TagValue(int64_t resourceId,
                bool isIdentifier,
                const DicomTag& tag,
@@ -56,22 +58,59 @@
         value_(value)
       {
       }
+
+      int64_t GetResourceId() const
+      {
+        return resourceId_;
+      }
+
+      bool IsIdentifier() const
+      {
+        return isIdentifier_;
+      }
+
+      const DicomTag& GetTag() const
+      {
+        return tag_;
+      }
+
+      const std::string& GetValue() const
+      {
+        return value_;
+      }
     };
 
-    struct Metadata
+    class Metadata
     {
+    private:
       int64_t       resourceId_;
-      MetadataType  metadata_;
+      MetadataType  type_;
       std::string   value_;
 
+    public:
       Metadata(int64_t  resourceId,
-               MetadataType metadata,
+               MetadataType type,
                const std::string& value) :
         resourceId_(resourceId),
-        metadata_(metadata),
+        type_(type),
         value_(value)
       {
       }
+
+      int64_t GetResourceId() const
+      {
+        return resourceId_;
+      }
+
+      MetadataType GetType() const
+      {
+        return type_;
+      }
+
+      const std::string& GetValue() const
+      {
+        return value_;
+      }
     };
 
     typedef std::list<TagValue>  ListTags;
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Fri Mar 31 16:32:02 2023 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Mon Apr 03 17:00:51 2023 +0200
@@ -233,11 +233,11 @@
     void GetChangesInternal(std::list<ServerIndexChange>& target,
                             bool& done,
                             SQLite::Statement& s,
-                            uint32_t maxResults)
+                            uint32_t limit)
     {
       target.clear();
 
-      while (target.size() < maxResults && s.Step())
+      while (target.size() < limit && s.Step())
       {
         int64_t seq = s.ColumnInt64(0);
         ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1));
@@ -250,18 +250,18 @@
         target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date));
       }
 
-      done = !(target.size() == maxResults && s.Step());
+      done = !(target.size() == limit && s.Step());
     }
 
 
     void GetExportedResourcesInternal(std::list<ExportedResource>& target,
                                       bool& done,
                                       SQLite::Statement& s,
-                                      uint32_t maxResults)
+                                      uint32_t limit)
     {
       target.clear();
 
-      while (target.size() < maxResults && s.Step())
+      while (target.size() < limit && s.Step())
       {
         int64_t seq = s.ColumnInt64(0);
         ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(1));
@@ -280,7 +280,7 @@
         target.push_back(resource);
       }
 
-      done = !(target.size() == maxResults && s.Step());
+      done = !(target.size() == limit && s.Step());
     }
 
 
@@ -341,7 +341,7 @@
                                       std::list<std::string>* instancesId,
                                       const std::vector<DatabaseConstraint>& lookup,
                                       ResourceType queryLevel,
-                                      size_t limit) ORTHANC_OVERRIDE
+                                      uint32_t limit) ORTHANC_OVERRIDE
     {
       LookupFormatter formatter;
 
@@ -509,8 +509,8 @@
 
     virtual void GetAllPublicIds(std::list<std::string>& target,
                                  ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit) ORTHANC_OVERRIDE
+                                 int64_t since,
+                                 uint32_t limit) ORTHANC_OVERRIDE
     {
       if (limit == 0)
       {
@@ -536,12 +536,12 @@
     virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
                             bool& done /*out*/,
                             int64_t since,
-                            uint32_t maxResults) ORTHANC_OVERRIDE
+                            uint32_t limit) ORTHANC_OVERRIDE
     {
       SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?");
       s.BindInt64(0, since);
-      s.BindInt(1, maxResults + 1);
-      GetChangesInternal(target, done, s, maxResults);
+      s.BindInt(1, limit + 1);
+      GetChangesInternal(target, done, s, limit);
     }
 
 
@@ -588,13 +588,13 @@
     virtual void GetExportedResources(std::list<ExportedResource>& target,
                                       bool& done,
                                       int64_t since,
-                                      uint32_t maxResults) ORTHANC_OVERRIDE
+                                      uint32_t limit) ORTHANC_OVERRIDE
     {
       SQLite::Statement s(db_, SQLITE_FROM_HERE, 
                           "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?");
       s.BindInt64(0, since);
-      s.BindInt(1, maxResults + 1);
-      GetExportedResourcesInternal(target, done, s, maxResults);
+      s.BindInt(1, limit + 1);
+      GetExportedResourcesInternal(target, done, s, limit);
     }
 
 
@@ -731,15 +731,6 @@
     }
 
 
-    virtual bool IsExistingResource(int64_t internalId) ORTHANC_OVERRIDE
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                          "SELECT * FROM Resources WHERE internalId=?");
-      s.BindInt64(0, internalId);
-      return s.Step();
-    }
-
-
     virtual bool IsProtectedPatient(int64_t internalId) ORTHANC_OVERRIDE
     {
       SQLite::Statement s(db_, SQLITE_FROM_HERE,
@@ -765,14 +756,17 @@
     }
 
 
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change) ORTHANC_OVERRIDE
+    virtual void LogChange(ChangeType changeType,
+                           ResourceType resourceType,
+                           int64_t internalId,
+                           const std::string& /* publicId - unused */,
+                           const std::string& date) ORTHANC_OVERRIDE
     {
       SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes (seq, changeType, internalId, resourceType, date) VALUES(NULL, ?, ?, ?, ?)");
-      s.BindInt(0, change.GetChangeType());
+      s.BindInt(0, changeType);
       s.BindInt64(1, internalId);
-      s.BindInt(2, change.GetResourceType());
-      s.BindString(3, change.GetDate());
+      s.BindInt(2, resourceType);
+      s.BindString(3, date);
       s.Run();
     }
 
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Fri Mar 31 16:32:02 2023 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Mon Apr 03 17:00:51 2023 +0200
@@ -415,7 +415,7 @@
 
     if (changeType <= ChangeType_INTERNAL_LastLogged)
     {
-      transaction_.LogChange(internalId, change);
+      transaction_.LogChange(changeType, resourceType, internalId, publicId, change.GetDate());
     }
 
     GetTransactionContext().SignalChange(change);
--- a/OrthancServer/Sources/OrthancInitialization.cpp	Fri Mar 31 16:32:02 2023 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Mon Apr 03 17:00:51 2023 +0200
@@ -51,6 +51,10 @@
 
 #include <dcmtk/dcmnet/diutil.h>  // For DCM_dcmnetLogger
 
+#if ORTHANC_ENABLE_PLUGINS == 1
+#  include <google/protobuf/any.h>
+#endif
+
 
 static const char* const STORAGE_DIRECTORY = "StorageDirectory";
 static const char* const ORTHANC_STORAGE = "OrthancStorage";
@@ -315,6 +319,10 @@
     
     OrthancConfiguration::WriterLock lock;
 
+#if ORTHANC_ENABLE_PLUGINS == 1
+    GOOGLE_PROTOBUF_VERIFY_VERSION;
+#endif
+    
     InitializeServerEnumerations();
 
     // Read the user-provided configuration
@@ -391,6 +399,10 @@
   {
     OrthancConfiguration::WriterLock lock;
     Orthanc::FinalizeFramework();
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    google::protobuf::ShutdownProtobufLibrary();
+#endif
   }