changeset 183:baada606da3c

databasewrapper
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 12 Nov 2012 14:52:30 +0100
parents 93ff5babcaf8
children d4e967d401d3
files CMakeLists.txt OrthancServer/DatabaseWrapper.cpp OrthancServer/DatabaseWrapper.h OrthancServer/IServerIndexListener.h OrthancServer/PrepareDatabase2.sql OrthancServer/ServerEnumerations.h OrthancServer/ServerIndex.h Resources/CMake/BoostConfiguration.cmake UnitTests/ServerIndex.cpp
diffstat 9 files changed, 893 insertions(+), 489 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Mon Nov 12 10:36:58 2012 +0100
+++ b/CMakeLists.txt	Mon Nov 12 14:52:30 2012 +0100
@@ -139,6 +139,7 @@
   OrthancServer/OrthancRestApi.cpp
   OrthancServer/ServerIndex.cpp
   OrthancServer/ToDcmtkBridge.cpp
+  OrthancServer/DatabaseWrapper.cpp
   )
 
 # Ensure autogenerated code is built before building ServerLibrary
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DatabaseWrapper.cpp	Mon Nov 12 14:52:30 2012 +0100
@@ -0,0 +1,476 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012 Medical Physics Department, CHU of Liege,
+ * 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.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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 "DatabaseWrapper.h"
+
+#include "../Core/DicomFormat/DicomArray.h"
+#include "EmbeddedResources.h"
+
+#include <glog/logging.h>
+#include <stdio.h>
+
+namespace Orthanc
+{
+
+  namespace Internals
+  {
+    class SignalFileDeleted : public SQLite::IScalarFunction
+    {
+    private:
+      IServerIndexListener& listener_;
+
+    public:
+      SignalFileDeleted(IServerIndexListener& listener) :
+        listener_(listener)
+      {
+      }
+
+      virtual const char* GetName() const
+      {
+        return "SignalFileDeleted";
+      }
+
+      virtual unsigned int GetCardinality() const
+      {
+        return 1;
+      }
+
+      virtual void Compute(SQLite::FunctionContext& context)
+      {
+        listener_.SignalFileDeleted(context.GetStringValue(0));
+      }
+    };
+
+    class SignalRemainingAncestor : public SQLite::IScalarFunction
+    {
+    private:
+      bool hasRemainingAncestor_;
+      std::string remainingPublicId_;
+      ResourceType remainingType_;
+
+    public:
+      void Reset()
+      {
+        hasRemainingAncestor_ = false;
+      }
+
+      virtual const char* GetName() const
+      {
+        return "SignalRemainingAncestor";
+      }
+
+      virtual unsigned int GetCardinality() const
+      {
+        return 2;
+      }
+
+      virtual void Compute(SQLite::FunctionContext& context)
+      {
+        VLOG(1) << "There exists a remaining ancestor with public ID \""
+                << context.GetStringValue(0)
+                << "\" of type "
+                << context.GetIntValue(1);
+
+        if (!hasRemainingAncestor_ ||
+            remainingType_ >= context.GetIntValue(1))
+        {
+          hasRemainingAncestor_ = true;
+          remainingPublicId_ = context.GetStringValue(0);
+          remainingType_ = static_cast<ResourceType>(context.GetIntValue(1));
+        }
+      }
+
+      bool HasRemainingAncestor() const
+      {
+        return hasRemainingAncestor_;
+      }
+
+      const std::string& GetRemainingAncestorId() const
+      {
+        assert(hasRemainingAncestor_);
+        return remainingPublicId_;
+      }
+
+      ResourceType GetRemainingAncestorType() const
+      {
+        assert(hasRemainingAncestor_);
+        return remainingType_;
+      }
+    };
+  }
+
+
+  
+  void DatabaseWrapper::SetGlobalProperty(const std::string& name,
+                                          const std::string& value)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)");
+    s.BindString(0, name);
+    s.BindString(1, value);
+    s.Run();
+  }
+
+  bool DatabaseWrapper::FindGlobalProperty(std::string& target,
+                                           const std::string& name)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT value FROM GlobalProperties WHERE name=?");
+    s.BindString(0, name);
+
+    if (!s.Step())
+    {
+      return false;
+    }
+    else
+    {
+      target = s.ColumnString(0);
+      return true;
+    }
+  }
+
+  std::string DatabaseWrapper::GetGlobalProperty(const std::string& name,
+                                                 const std::string& defaultValue)
+  {
+    std::string s;
+    if (FindGlobalProperty(s, name))
+    {
+      return s;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+  int64_t DatabaseWrapper::CreateResource(const std::string& publicId,
+                                          ResourceType type)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)");
+    s.BindInt(0, type);
+    s.BindString(1, publicId);
+    s.Run();
+    return db_.GetLastInsertRowId();
+  }
+
+  bool DatabaseWrapper::FindResource(const std::string& publicId,
+                                     int64_t& id,
+                                     ResourceType& type)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT internalId, resourceType FROM Resources WHERE publicId=?");
+    s.BindString(0, publicId);
+
+    if (!s.Step())
+    {
+      return false;
+    }
+    else
+    {
+      id = s.ColumnInt(0);
+      type = static_cast<ResourceType>(s.ColumnInt(1));
+
+      // Check whether there is a single resource with this public id
+      assert(!s.Step());
+
+      return true;
+    }
+  }
+
+  void DatabaseWrapper::AttachChild(int64_t parent,
+                                    int64_t child)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?");
+    s.BindInt(0, parent);
+    s.BindInt(1, child);
+    s.Run();
+  }
+
+  void DatabaseWrapper::DeleteResource(int64_t id)
+  {
+    signalRemainingAncestor_->Reset();
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?");
+    s.BindInt(0, id);
+    s.Run();
+
+    if (signalRemainingAncestor_->HasRemainingAncestor())
+    {
+      listener_.SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorType(),
+                                        signalRemainingAncestor_->GetRemainingAncestorId());
+    }
+  }
+
+  void DatabaseWrapper::SetMetadata(int64_t id,
+                                    MetadataType type,
+                                    const std::string& value)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)");
+    s.BindInt(0, id);
+    s.BindInt(1, type);
+    s.BindString(2, value);
+    s.Run();
+  }
+
+  bool DatabaseWrapper::FindMetadata(std::string& target,
+                                     int64_t id,
+                                     MetadataType type)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT value FROM Metadata WHERE id=? AND type=?");
+    s.BindInt(0, id);
+    s.BindInt(1, type);
+
+    if (!s.Step())
+    {
+      return false;
+    }
+    else
+    {
+      target = s.ColumnString(0);
+      return true;
+    }
+  }
+
+  std::string DatabaseWrapper::GetMetadata(int64_t id,
+                                           MetadataType type,
+                                           const std::string& defaultValue)
+  {
+    std::string s;
+    if (FindMetadata(s, id, type))
+    {
+      return s;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+  void DatabaseWrapper::AttachFile(int64_t id,
+                                   const std::string& name,
+                                   const std::string& fileUuid,
+                                   size_t compressedSize,
+                                   size_t uncompressedSize,
+                                   CompressionType compressionType)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?)");
+    s.BindInt(0, id);
+    s.BindString(1, name);
+    s.BindString(2, fileUuid);
+    s.BindInt(3, compressedSize);
+    s.BindInt(4, uncompressedSize);
+    s.BindInt(5, compressionType);
+    s.Run();
+  }
+
+  bool DatabaseWrapper::FindFile(int64_t id,
+                                 const std::string& name,
+                                 std::string& fileUuid,
+                                 size_t& compressedSize,
+                                 size_t& uncompressedSize,
+                                 CompressionType& compressionType)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT uuid, compressedSize, uncompressedSize, compressionType FROM AttachedFiles WHERE id=? AND name=?");
+    s.BindInt(0, id);
+    s.BindString(1, name);
+
+    if (!s.Step())
+    {
+      return false;
+    }
+    else
+    {
+      fileUuid = s.ColumnString(0);
+      compressedSize = s.ColumnInt(1);
+      uncompressedSize = s.ColumnInt(2);
+      compressionType = static_cast<CompressionType>(s.ColumnInt(3));
+      return true;
+    }
+  }
+
+  void DatabaseWrapper::SetMainDicomTags(int64_t id,
+                                         const DicomMap& tags)
+  {
+    DicomArray flattened(tags);
+    for (size_t i = 0; i < flattened.GetSize(); i++)
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)");
+      s.BindInt(0, id);
+      s.BindInt(1, flattened.GetElement(i).GetTag().GetGroup());
+      s.BindInt(2, flattened.GetElement(i).GetTag().GetElement());
+      s.BindString(3, flattened.GetElement(i).GetValue().AsString());
+      s.Run();
+    }
+  }
+
+  void DatabaseWrapper::GetMainDicomTags(DicomMap& map,
+                                         int64_t id)
+  {
+    map.Clear();
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?");
+    s.BindInt(0, id);
+    while (s.Step())
+    {
+      map.SetValue(s.ColumnInt(1),
+                   s.ColumnInt(2),
+                   s.ColumnString(3));
+    }
+  }
+
+
+  bool DatabaseWrapper::GetParentPublicId(std::string& result,
+                                          int64_t id)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b "
+                        "WHERE a.internalId = b.parentId AND b.internalId = ?");     
+    s.BindInt(0, id);
+
+    if (s.Step())
+    {
+      result = s.ColumnString(0);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  void DatabaseWrapper::GetChildrenPublicId(std::list<std::string>& result,
+                                            int64_t id)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b  "
+                        "WHERE a.parentId = b.internalId AND b.internalId = ?");     
+    s.BindInt(0, id);
+
+    result.clear();
+
+    while (s.Step())
+    {
+      result.push_back(s.ColumnString(0));
+    }
+  }
+
+
+  void DatabaseWrapper::LogChange(ChangeType changeType,
+                                  const std::string& publicId,
+                                  ResourceType resourceType,
+                                  const boost::posix_time::ptime& date)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)");
+    s.BindInt(0, changeType);
+    s.BindString(1, publicId);
+    s.BindInt(2, resourceType);
+    s.BindString(3, boost::posix_time::to_iso_string(date));
+    s.Run();      
+  }
+
+
+  void DatabaseWrapper::LogExportedInstance(const std::string& remoteModality,
+                                            DicomInstanceHasher& hasher,
+                                            const boost::posix_time::ptime& date)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO ExportedInstances VALUES(NULL, ?, ?, ?, ?, ?, ?)");
+    s.BindString(0, remoteModality);
+    s.BindString(1, hasher.HashInstance());
+    s.BindString(2, hasher.GetPatientId());
+    s.BindString(3, hasher.GetStudyUid());
+    s.BindString(4, hasher.GetSeriesUid());
+    s.BindString(5, hasher.GetInstanceUid());
+    s.BindString(6, boost::posix_time::to_iso_string(date));
+    s.Run();      
+  }
+    
+
+  int64_t DatabaseWrapper::GetTableRecordCount(const std::string& table)
+  {
+    char buf[128];
+    sprintf(buf, "SELECT COUNT(*) FROM %s", table.c_str());
+    SQLite::Statement s(db_, buf);
+
+    assert(s.Step());
+    int64_t c = s.ColumnInt(0);
+    assert(!s.Step());
+
+    return c;
+  }
+
+    
+  uint64_t DatabaseWrapper::GetTotalCompressedSize()
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles");
+    s.Run();
+    return static_cast<uint64_t>(s.ColumnInt64(0));
+  }
+
+    
+  uint64_t DatabaseWrapper::GetTotalUncompressedSize()
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles");
+    s.Run();
+    return static_cast<uint64_t>(s.ColumnInt64(0));
+  }
+
+
+  DatabaseWrapper::DatabaseWrapper(const std::string& path,
+                                   IServerIndexListener& listener) :
+    listener_(listener)
+  {
+    db_.Open(path);
+    Open();
+  }
+
+  DatabaseWrapper::DatabaseWrapper(IServerIndexListener& listener) :
+    listener_(listener)
+  {
+    db_.OpenInMemory();
+    Open();
+  }
+
+  void DatabaseWrapper::Open()
+  {
+    if (!db_.DoesTableExist("GlobalProperties"))
+    {
+      LOG(INFO) << "Creating the database";
+      std::string query;
+      EmbeddedResources::GetFileResource(query, EmbeddedResources::PREPARE_DATABASE_2);
+      db_.Execute(query);
+    }
+
+    signalRemainingAncestor_ = new Internals::SignalRemainingAncestor;
+    db_.Register(signalRemainingAncestor_);
+    db_.Register(new Internals::SignalFileDeleted(listener_));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DatabaseWrapper.h	Mon Nov 12 14:52:30 2012 +0100
@@ -0,0 +1,151 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012 Medical Physics Department, CHU of Liege,
+ * 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.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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
+
+#include "../Core/SQLite/Connection.h"
+#include "../Core/DicomFormat/DicomInstanceHasher.h"
+#include "IServerIndexListener.h"
+
+#include <list>
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    class SignalRemainingAncestor;
+  }
+
+  /**
+   * This class manages an instance of the Orthanc SQLite database. It
+   * translates low-level requests into SQL statements. Mutual
+   * exclusion MUST be implemented at a higher level.
+   **/
+  class DatabaseWrapper
+  {
+  private:
+    IServerIndexListener& listener_;
+    SQLite::Connection db_;
+    Internals::SignalRemainingAncestor* signalRemainingAncestor_;
+
+    void Open();
+
+  public:
+    void SetGlobalProperty(const std::string& name,
+                           const std::string& value);
+
+    bool FindGlobalProperty(std::string& target,
+                            const std::string& name);
+
+    std::string GetGlobalProperty(const std::string& name,
+                                  const std::string& defaultValue = "");
+
+    int64_t CreateResource(const std::string& publicId,
+                           ResourceType type);
+
+    bool FindResource(const std::string& publicId,
+                      int64_t& id,
+                      ResourceType& type);
+
+    void AttachChild(int64_t parent,
+                     int64_t child);
+
+    void DeleteResource(int64_t id);
+
+    void SetMetadata(int64_t id,
+                     MetadataType type,
+                     const std::string& value);
+
+    bool FindMetadata(std::string& target,
+                      int64_t id,
+                      MetadataType type);
+
+    std::string GetMetadata(int64_t id,
+                            MetadataType type,
+                            const std::string& defaultValue = "");
+
+    void AttachFile(int64_t id,
+                    const std::string& name,
+                    const std::string& fileUuid,
+                    size_t compressedSize,
+                    size_t uncompressedSize,
+                    CompressionType compressionType);
+
+    void AttachFile(int64_t id,
+                    const std::string& name,
+                    const std::string& fileUuid,
+                    size_t fileSize)
+    {
+      AttachFile(id, name, fileUuid, fileSize, fileSize, CompressionType_None);
+    }
+
+    bool FindFile(int64_t id,
+                  const std::string& name,
+                  std::string& fileUuid,
+                  size_t& compressedSize,
+                  size_t& uncompressedSize,
+                  CompressionType& compressionType);
+
+    void SetMainDicomTags(int64_t id,
+                          const DicomMap& tags);
+
+    void GetMainDicomTags(DicomMap& map,
+                          int64_t id);
+
+    bool GetParentPublicId(std::string& result,
+                           int64_t id);
+
+    void GetChildrenPublicId(std::list<std::string>& result,
+                             int64_t id);
+
+    void LogChange(ChangeType changeType,
+                   const std::string& publicId,
+                   ResourceType resourceType,
+                   const boost::posix_time::ptime& date);
+
+    void LogExportedInstance(const std::string& remoteModality,
+                             DicomInstanceHasher& hasher,
+                             const boost::posix_time::ptime& date);
+    
+    int64_t GetTableRecordCount(const std::string& table);
+    
+    uint64_t GetTotalCompressedSize();
+    
+    uint64_t GetTotalUncompressedSize();
+
+    DatabaseWrapper(const std::string& path,
+                    IServerIndexListener& listener);
+
+    DatabaseWrapper(IServerIndexListener& listener);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/IServerIndexListener.h	Mon Nov 12 14:52:30 2012 +0100
@@ -0,0 +1,53 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012 Medical Physics Department, CHU of Liege,
+ * 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.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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
+
+#include <string>
+#include "ServerEnumerations.h"
+
+namespace Orthanc
+{
+  class IServerIndexListener
+  {
+  public:
+    virtual ~IServerIndexListener()
+    {
+    }
+
+    virtual void SignalRemainingAncestor(ResourceType parentType,
+                                         const std::string& publicId) = 0;
+
+    virtual void SignalFileDeleted(const std::string& fileUuid) = 0;                     
+                                 
+  };
+}
--- a/OrthancServer/PrepareDatabase2.sql	Mon Nov 12 10:36:58 2012 +0100
+++ b/OrthancServer/PrepareDatabase2.sql	Mon Nov 12 14:52:30 2012 +0100
@@ -29,14 +29,39 @@
        id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
        name TEXT,
        uuid TEXT,
+       compressedSize INTEGER,
        uncompressedSize INTEGER,
        compressionType INTEGER,
        PRIMARY KEY(id, name)
        );              
 
+CREATE TABLE Changes(
+       seq INTEGER PRIMARY KEY AUTOINCREMENT,
+       changeType INTEGER,
+       publicId TEXT,
+       resourceType INTEGER,
+       date TEXT
+       );
+
+CREATE TABLE ExportedInstances(
+       seq INTEGER PRIMARY KEY AUTOINCREMENT,
+       remoteModality TEXT,
+       publicId TEXT,
+       patientId TEXT,
+       studyInstanceUid TEXT,
+       seriesInstanceUid TEXT,
+       sopInstanceUid TEXT,
+       date TEXT
+       ); 
+
 CREATE INDEX ChildrenIndex ON Resources(parentId);
 CREATE INDEX PublicIndex ON Resources(publicId);
 
+CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id);
+CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement);
+CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY);
+
+CREATE INDEX ChangesIndex ON Changes(publicId);
 
 CREATE TRIGGER AttachedFileDeleted
 AFTER DELETE ON AttachedFiles
@@ -47,13 +72,14 @@
 CREATE TRIGGER ResourceDeleted
 AFTER DELETE ON Resources
 BEGIN
-  SELECT SignalResourceDeleted(old.resourceType, old.parentId);
+  SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) 
+    FROM Resources AS parent WHERE internalId = old.parentId;
 END;
 
-
--- -- Delete a resource when its unique child is deleted  TODO TODO
--- CREATE TRIGGER ResourceRemovedUpward
--- AFTER DELETE ON Resources
--- FOR EACH ROW
---   WHEN (SELECT COUNT(*) FROM ParentRelationship WHERE parent = old.
--- END;
+-- Delete a parent resource when its unique child is deleted 
+CREATE TRIGGER ResourceDeletedParentCleaning
+AFTER DELETE ON Resources
+FOR EACH ROW WHEN (SELECT COUNT(*) FROM Resources WHERE parentId = old.parentId) = 0
+BEGIN
+  DELETE FROM Resources WHERE internalId = old.parentId;
+END;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerEnumerations.h	Mon Nov 12 14:52:30 2012 +0100
@@ -0,0 +1,84 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012 Medical Physics Department, CHU of Liege,
+ * 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.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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
+
+namespace Orthanc
+{
+  enum SeriesStatus
+  {
+    SeriesStatus_Complete,
+    SeriesStatus_Missing,
+    SeriesStatus_Inconsistent,
+    SeriesStatus_Unknown
+  };
+
+  enum StoreStatus
+  {
+    StoreStatus_Success,
+    StoreStatus_AlreadyStored,
+    StoreStatus_Failure
+  };
+
+
+  enum ResourceType
+  {
+    ResourceType_Patient = 1,
+    ResourceType_Study = 2,
+    ResourceType_Series = 3,
+    ResourceType_Instance = 4
+  };
+
+  enum CompressionType
+  {
+    CompressionType_None = 1,
+    CompressionType_Zlib = 2
+  };
+
+  enum MetadataType
+  {
+    MetadataType_Instance_IndexInSeries = 2,
+    MetadataType_Instance_ReceptionDate = 4,
+    MetadataType_Instance_RemoteAet = 1,
+    MetadataType_Series_ExpectedNumberOfInstances = 3
+  };
+
+  enum ChangeType
+  {
+    ChangeType_CompletedSeries = 1,
+    ChangeType_NewInstance = 3,
+    ChangeType_NewPatient = 4,
+    ChangeType_NewSeries = 2,
+    ChangeType_NewStudy = 5,
+    ChangeType_InvalidSeries = 6
+  };
+}
--- a/OrthancServer/ServerIndex.h	Mon Nov 12 10:36:58 2012 +0100
+++ b/OrthancServer/ServerIndex.h	Mon Nov 12 14:52:30 2012 +0100
@@ -37,36 +37,11 @@
 #include "../Core/DicomFormat/DicomMap.h"
 #include "../Core/FileStorage.h"
 #include "../Core/DicomFormat/DicomInstanceHasher.h"
+#include "ServerEnumerations.h"
 
 
 namespace Orthanc
 {
-  enum SeriesStatus
-  {
-    SeriesStatus_Complete,
-    SeriesStatus_Missing,
-    SeriesStatus_Inconsistent,
-    SeriesStatus_Unknown
-  };
-
-
-  enum StoreStatus
-  {
-    StoreStatus_Success,
-    StoreStatus_AlreadyStored,
-    StoreStatus_Failure
-  };
-
-
-  enum ResourceType
-  {
-    ResourceType_Patient = 1,
-    ResourceType_Study = 2,
-    ResourceType_Series = 3,
-    ResourceType_Instance = 4
-  };
-
-
   namespace Internals
   {
     class SignalDeletedLevelFunction;
--- a/Resources/CMake/BoostConfiguration.cmake	Mon Nov 12 10:36:58 2012 +0100
+++ b/Resources/CMake/BoostConfiguration.cmake	Mon Nov 12 14:52:30 2012 +0100
@@ -8,7 +8,7 @@
   #set(Boost_USE_STATIC_LIBS ON)
 
   find_package(Boost
-    COMPONENTS filesystem thread system)
+    COMPONENTS filesystem thread system date_time)
 
   if (NOT Boost_FOUND)
     message(FATAL_ERROR "Unable to locate Boost on this system")
--- a/UnitTests/ServerIndex.cpp	Mon Nov 12 10:36:58 2012 +0100
+++ b/UnitTests/ServerIndex.cpp	Mon Nov 12 14:52:30 2012 +0100
@@ -1,471 +1,48 @@
 #include "gtest/gtest.h"
 
+#include "../OrthancServer/DatabaseWrapper.h"
+
 #include <ctype.h>
-
-#include "../Core/SQLite/Connection.h"
-#include "../Core/Compression/ZlibCompressor.h"
-#include "../Core/DicomFormat/DicomTag.h"
-#include "../Core/DicomFormat/DicomArray.h"
-#include "../Core/FileStorage.h"
-#include "../OrthancCppClient/HttpClient.h"
-#include "../Core/HttpServer/HttpHandler.h"
-#include "../Core/OrthancException.h"
-#include "../Core/Toolbox.h"
-#include "../Core/Uuid.h"
-#include "../OrthancServer/FromDcmtkBridge.h"
-#include "../OrthancServer/OrthancInitialization.h"
-#include "../OrthancServer/ServerIndex.h"
-#include "EmbeddedResources.h"
-
 #include <glog/logging.h>
-#include <boost/thread.hpp>
-
-
-namespace Orthanc
-{
-  enum CompressionType
-  {
-    CompressionType_None = 1,
-    CompressionType_Zlib = 2
-  };
-
-  enum MetadataType
-  {
-    MetadataType_Instance_RemoteAet = 1,
-    MetadataType_Instance_IndexInSeries = 2,
-    MetadataType_Series_ExpectedNumberOfInstances = 3
-  };
-
-  class IServerIndexListener
-  {
-  public:
-    virtual ~IServerIndexListener()
-    {
-    }
-
-    virtual void SignalResourceDeleted(ResourceType type,
-                                       const std::string& parentPublicId) = 0;
-
-    virtual void SignalFileDeleted(const std::string& fileUuid) = 0;                     
-                                 
-  };
-
-  namespace Internals
-  {
-    class SignalFileDeleted : public SQLite::IScalarFunction
-    {
-    private:
-      IServerIndexListener& listener_;
-
-    public:
-      SignalFileDeleted(IServerIndexListener& listener) :
-        listener_(listener)
-      {
-      }
-
-      virtual const char* GetName() const
-      {
-        return "SignalFileDeleted";
-      }
-
-      virtual unsigned int GetCardinality() const
-      {
-        return 1;
-      }
-
-      virtual void Compute(SQLite::FunctionContext& context)
-      {
-        listener_.SignalFileDeleted(context.GetStringValue(0));
-      }
-    };
-
-    class SignalResourceDeleted : public SQLite::IScalarFunction
-    {
-    public:
-      virtual const char* GetName() const
-      {
-        return "SignalResourceDeleted";
-      }
-
-      virtual unsigned int GetCardinality() const
-      {
-        return 2;
-      }
-
-      virtual void Compute(SQLite::FunctionContext& context)
-      {
-        LOG(INFO) << "A resource has been removed, of type "
-                  << context.GetIntValue(0)
-                  << ", with parent "
-                  << context.GetIntValue(1);
-      }
-    };
-  }
 
 
-  class ServerIndexHelper
-  {
-  private:
-    IServerIndexListener& listener_;
-    SQLite::Connection db_;
-    boost::mutex mutex_;
-
-    void Open(const std::string& path);
-
-  public:
-    void SetGlobalProperty(const std::string& name,
-                           const std::string& value)
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)");
-      s.BindString(0, name);
-      s.BindString(1, value);
-      s.Run();
-    }
-
-    bool FindGlobalProperty(std::string& target,
-                            const std::string& name)
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                          "SELECT value FROM GlobalProperties WHERE name=?");
-      s.BindString(0, name);
-
-      if (!s.Step())
-      {
-        return false;
-      }
-      else
-      {
-        target = s.ColumnString(0);
-        return true;
-      }
-    }
-
-    std::string GetGlobalProperty(const std::string& name,
-                                  const std::string& defaultValue = "")
-    {
-      std::string s;
-      if (FindGlobalProperty(s, name))
-      {
-        return s;
-      }
-      else
-      {
-        return defaultValue;
-      }
-    }
-
-    int64_t CreateResource(const std::string& publicId,
-                           ResourceType type)
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)");
-      s.BindInt(0, type);
-      s.BindString(1, publicId);
-      s.Run();
-      return db_.GetLastInsertRowId();
-    }
-
-    bool FindResource(const std::string& publicId,
-                      int64_t& id,
-                      ResourceType& type)
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                          "SELECT internalId, resourceType FROM Resources WHERE publicId=?");
-      s.BindString(0, publicId);
-
-      if (!s.Step())
-      {
-        return false;
-      }
-      else
-      {
-        id = s.ColumnInt(0);
-        type = static_cast<ResourceType>(s.ColumnInt(1));
-
-        // Check whether there is a single resource with this public id
-        assert(!s.Step());
-
-        return true;
-      }
-    }
-
-    void AttachChild(int64_t parent,
-                     int64_t child)
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?");
-      s.BindInt(0, parent);
-      s.BindInt(1, child);
-      s.Run();
-    }
-
-    void DeleteResource(int64_t id)
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?");
-      s.BindInt(0, id);
-      s.Run();      
-    }
-
-    void SetMetadata(int64_t id,
-                     MetadataType type,
-                     const std::string& value)
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)");
-      s.BindInt(0, id);
-      s.BindInt(1, type);
-      s.BindString(2, value);
-      s.Run();
-    }
-
-    bool FindMetadata(std::string& target,
-                      int64_t id,
-                      MetadataType type)
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                          "SELECT value FROM Metadata WHERE id=? AND type=?");
-      s.BindInt(0, id);
-      s.BindInt(1, type);
-
-      if (!s.Step())
-      {
-        return false;
-      }
-      else
-      {
-        target = s.ColumnString(0);
-        return true;
-      }
-    }
-
-    std::string GetMetadata(int64_t id,
-                            MetadataType type,
-                            const std::string& defaultValue = "")
-    {
-      std::string s;
-      if (FindMetadata(s, id, type))
-      {
-        return s;
-      }
-      else
-      {
-        return defaultValue;
-      }
-    }
+using namespace Orthanc;
 
-    void AttachFile(int64_t id,
-                    const std::string& name,
-                    const std::string& fileUuid,
-                    size_t uncompressedSize,
-                    CompressionType compressionType)
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?)");
-      s.BindInt(0, id);
-      s.BindString(1, name);
-      s.BindString(2, fileUuid);
-      s.BindInt(3, uncompressedSize);
-      s.BindInt(4, compressionType);
-      s.Run();
-    }
-
-    bool FindFile(int64_t id,
-                  const std::string& name,
-                  std::string& fileUuid,
-                  size_t& uncompressedSize,
-                  CompressionType& compressionType)
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                          "SELECT uuid, uncompressedSize, compressionType FROM AttachedFiles WHERE id=? AND name=?");
-      s.BindInt(0, id);
-      s.BindString(1, name);
-
-      if (!s.Step())
-      {
-        return false;
-      }
-      else
-      {
-        fileUuid = s.ColumnString(0);
-        uncompressedSize = s.ColumnInt(1);
-        compressionType = static_cast<CompressionType>(s.ColumnInt(2));
-        return true;
-      }
-    }
-
-    void SetMainDicomTags(int64_t id,
-                          const DicomMap& tags)
-    {
-      DicomArray flattened(tags);
-      for (size_t i = 0; i < flattened.GetSize(); i++)
-      {
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)");
-        s.BindInt(0, id);
-        s.BindInt(1, flattened.GetElement(i).GetTag().GetGroup());
-        s.BindInt(2, flattened.GetElement(i).GetTag().GetElement());
-        s.BindString(3, flattened.GetElement(i).GetValue().AsString());
-        s.Run();
-      }
-    }
-
-    void GetMainDicomTags(DicomMap& map,
-                          int64_t id)
-    {
-      map.Clear();
-
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?");
-      s.BindInt(0, id);
-      while (s.Step())
-      {
-        map.SetValue(s.ColumnInt(1),
-                     s.ColumnInt(2),
-                     s.ColumnString(3));
-      }
-    }
-
-
-    bool GetParentPublicId(std::string& result,
-                           int64_t id)
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b "
-                          "WHERE a.internalId = b.parentId AND b.internalId = ?");     
-      s.BindInt(0, id);
-
-      if (s.Step())
-      {
-        result = s.ColumnString(0);
-        return true;
-      }
-      else
-      {
-        return false;
-      }
-    }
-
-
-    void GetChildrenPublicId(std::list<std::string>& result,
-                             int64_t id)
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b  "
-                          "WHERE a.parentId = b.internalId AND b.internalId = ?");     
-      s.BindInt(0, id);
-
-      result.clear();
-
-      while (s.Step())
-      {
-        result.push_back(s.ColumnString(0));
-      }
-    }
-    
-
-    int64_t GetTableRecordCount(const std::string& table)
-    {
-      char buf[128];
-      sprintf(buf, "SELECT COUNT(*) FROM %s", table.c_str());
-      SQLite::Statement s(db_, buf);
-
-      assert(s.Step());
-      int64_t c = s.ColumnInt(0);
-      assert(!s.Step());
-
-      return c;
-    }
-
-    ServerIndexHelper(const std::string& path,
-                      IServerIndexListener& listener) :
-      listener_(listener)
-    {
-      Open(path);
-    }
-
-    ServerIndexHelper(IServerIndexListener& listener) :
-      listener_(listener)
-    {
-      Open("");
-    }
-  };
-
-
-
-  void ServerIndexHelper::Open(const std::string& path)
-  {
-    if (path == "")
-    {
-      db_.OpenInMemory();
-    }
-    else
-    {
-      db_.Open(path);
-    }
-
-    if (!db_.DoesTableExist("GlobalProperties"))
-    {
-      LOG(INFO) << "Creating the database";
-      std::string query;
-      EmbeddedResources::GetFileResource(query, EmbeddedResources::PREPARE_DATABASE_2);
-      db_.Execute(query);
-    }
-
-    db_.Register(new Internals::SignalFileDeleted(listener_));
-    db_.Register(new Internals::SignalResourceDeleted);
-  }
-
-
+namespace
+{
   class ServerIndexListener : public IServerIndexListener
   {
   public:
-    virtual void SignalResourceDeleted(ResourceType type,
-                                       const std::string& parentPublicId) 
+    std::set<std::string> deletedFiles_;
+    std::string ancestorId_;
+    ResourceType ancestorType_;
+
+    void Reset()
     {
+      ancestorId_ = "";
+      deletedFiles_.clear();
+    }
+
+    virtual void SignalRemainingAncestor(ResourceType type,
+                                         const std::string& publicId) 
+    {
+      ancestorId_ = publicId;
+      ancestorType_ = type;
     }
 
     virtual void SignalFileDeleted(const std::string& fileUuid)
     {
+      deletedFiles_.insert(fileUuid);
       LOG(INFO) << "A file must be removed: " << fileUuid;
     }                                
   };
-
-  /*
-  class ServerIndex2
-  {
-  private:
-    ServerIndexListener listener_;
-    ServerIndexHelper helper_;
-
-    void Open(const std::string& storagePath)
-    {
-      boost::filesystem::path p = storagePath;
-
-      try
-      {
-        boost::filesystem::create_directories(storagePath);
-      }
-      catch (boost::filesystem::filesystem_error)
-      {
-      }
-
-      p /= "index";
-    }
-
-  public:
-    ServerIndexHelper(const std::string& storagePath) :
-      helper_(storagePath)
-    {
-      Open(storagePath);
-    }
-  };
-  */
 }
 
 
-
-using namespace Orthanc;
-
-TEST(ServerIndexHelper, Simple)
+TEST(DatabaseWrapper, Simple)
 {
   ServerIndexListener listener;
-  /*Toolbox::RemoveFile("toto");
-    ServerIndexHelper index("toto", listener);*/
-  ServerIndexHelper index(listener);
-
-  LOG(WARNING) << "ok";
+  DatabaseWrapper index(listener);
 
   int64_t a[] = {
     index.CreateResource("a", ResourceType_Patient),   // 0
@@ -514,10 +91,13 @@
     ASSERT_EQ("e", l.front());
   }
 
+  index.AttachFile(a[4], "_json", "my json file", 21, 42, CompressionType_Zlib);
+  index.AttachFile(a[4], "_dicom", "my dicom file", 42);
+  index.AttachFile(a[6], "_hello", "world", 44);
+  index.SetMetadata(a[4], MetadataType_Instance_RemoteAet, "PINNACLE");
 
-  index.AttachFile(a[4], "_json", "my json file", 42, CompressionType_Zlib);
-  index.AttachFile(a[4], "_dicom", "my dicom file", 42, CompressionType_None);
-  index.SetMetadata(a[4], MetadataType_Instance_RemoteAet, "PINNACLE");
+  ASSERT_EQ(21 + 42 + 44, index.GetTotalCompressedSize());
+  ASSERT_EQ(42 + 42 + 44, index.GetTotalUncompressedSize());
 
   DicomMap m;
   m.SetValue(0x0010, 0x0010, "PatientName");
@@ -541,23 +121,81 @@
   ASSERT_EQ("World", index.GetGlobalProperty("Hello"));
   ASSERT_EQ("None", index.GetGlobalProperty("Hello2", "None"));
 
-  size_t us;
+  size_t us, cs;
   CompressionType ct;
-  ASSERT_TRUE(index.FindFile(a[4], "_json", s, us, ct));
+  ASSERT_TRUE(index.FindFile(a[4], "_json", s, cs, us, ct));
   ASSERT_EQ("my json file", s);
+  ASSERT_EQ(21, cs);
   ASSERT_EQ(42, us);
   ASSERT_EQ(CompressionType_Zlib, ct);
 
+  ASSERT_EQ(0, listener.deletedFiles_.size());
   ASSERT_EQ(7, index.GetTableRecordCount("Resources"));
-  ASSERT_EQ(2, index.GetTableRecordCount("AttachedFiles"));
+  ASSERT_EQ(3, index.GetTableRecordCount("AttachedFiles"));
   ASSERT_EQ(1, index.GetTableRecordCount("Metadata"));
   ASSERT_EQ(1, index.GetTableRecordCount("MainDicomTags"));
   index.DeleteResource(a[0]);
+
+  ASSERT_EQ(2, listener.deletedFiles_.size());
+  ASSERT_NE(listener.deletedFiles_.end(), listener.deletedFiles_.find("my json file"));
+  ASSERT_NE(listener.deletedFiles_.end(), listener.deletedFiles_.find("my dicom file"));
+
   ASSERT_EQ(2, index.GetTableRecordCount("Resources"));
   ASSERT_EQ(0, index.GetTableRecordCount("Metadata"));
+  ASSERT_EQ(1, index.GetTableRecordCount("AttachedFiles"));
+  ASSERT_EQ(0, index.GetTableRecordCount("MainDicomTags"));
+  index.DeleteResource(a[5]);
+  ASSERT_EQ(0, index.GetTableRecordCount("Resources"));
   ASSERT_EQ(0, index.GetTableRecordCount("AttachedFiles"));
-  ASSERT_EQ(0, index.GetTableRecordCount("MainDicomTags"));
+  ASSERT_EQ(1, index.GetTableRecordCount("GlobalProperties"));
+
+  ASSERT_EQ(3, listener.deletedFiles_.size());
+  ASSERT_NE(listener.deletedFiles_.end(), listener.deletedFiles_.find("world"));
+}
+
+
+
+
+TEST(DatabaseWrapper, Upward)
+{
+  ServerIndexListener listener;
+  DatabaseWrapper index(listener);
+
+  int64_t a[] = {
+    index.CreateResource("a", ResourceType_Patient),   // 0
+    index.CreateResource("b", ResourceType_Study),     // 1
+    index.CreateResource("c", ResourceType_Series),    // 2
+    index.CreateResource("d", ResourceType_Instance),  // 3
+    index.CreateResource("e", ResourceType_Instance),  // 4
+    index.CreateResource("f", ResourceType_Study),     // 5
+    index.CreateResource("g", ResourceType_Series),    // 6
+    index.CreateResource("h", ResourceType_Series)     // 7
+  };
+
+  index.AttachChild(a[0], a[1]);
+  index.AttachChild(a[1], a[2]);
+  index.AttachChild(a[2], a[3]);
+  index.AttachChild(a[2], a[4]);
+  index.AttachChild(a[1], a[6]);
+  index.AttachChild(a[0], a[5]);
+  index.AttachChild(a[5], a[7]);
+
+  listener.Reset();
+  index.DeleteResource(a[3]);
+  ASSERT_EQ("c", listener.ancestorId_);
+  ASSERT_EQ(ResourceType_Series, listener.ancestorType_);
+
+  listener.Reset();
+  index.DeleteResource(a[4]);
+  ASSERT_EQ("b", listener.ancestorId_);
+  ASSERT_EQ(ResourceType_Study, listener.ancestorType_);
+
+  listener.Reset();
+  index.DeleteResource(a[7]);
+  ASSERT_EQ("a", listener.ancestorId_);
+  ASSERT_EQ(ResourceType_Patient, listener.ancestorType_);
+
+  listener.Reset();
   index.DeleteResource(a[6]);
-  ASSERT_EQ(0, index.GetTableRecordCount("Resources"));
-  ASSERT_EQ(1, index.GetTableRecordCount("GlobalProperties"));
+  ASSERT_EQ("", listener.ancestorId_);  // No more ancestor
 }