1 /**
2  * Orthanc - A Lightweight, RESTful DICOM Store
3  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4  * Department, University Hospital of Liege, Belgium
5  * Copyright (C) 2017-2020 Osimis S.A., Belgium
6  *
7  * This program is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * In addition, as a special exception, the copyright holders of this
13  * program give permission to link the code of its release with the
14  * OpenSSL project's "OpenSSL" library (or with modified versions of it
15  * that use the same license as the "OpenSSL" library), and distribute
16  * the linked executables. You must obey the GNU General Public License
17  * in all respects for all of the code used other than "OpenSSL". If you
18  * modify file(s) with this exception, you may extend this exception to
19  * your version of the file(s), but you are not obligated to do so. If
20  * you do not wish to do so, delete this exception statement from your
21  * version. If you delete this exception statement from all source files
22  * in the program, then also delete it here.
23  *
24  * This program is distributed in the hope that it will be useful, but
25  * WITHOUT ANY WARRANTY; without even the implied warranty of
26  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
27  * General Public License for more details.
28  *
29  * You should have received a copy of the GNU General Public License
30  * along with this program. If not, see <http://www.gnu.org/licenses/>.
31  **/
32 
33 
34 #include "../PrecompiledHeadersServer.h"
35 #include "SQLiteDatabaseWrapper.h"
36 
37 #include "../../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
38 #include "../../../OrthancFramework/Sources/Logging.h"
39 #include "../../../OrthancFramework/Sources/SQLite/Transaction.h"
40 #include "../Search/ISqlLookupFormatter.h"
41 #include "../ServerToolbox.h"
42 
43 #include <OrthancServerResources.h>
44 
45 #include <stdio.h>
46 #include <boost/lexical_cast.hpp>
47 
48 namespace Orthanc
49 {
50   namespace Internals
51   {
52     class SignalFileDeleted : public SQLite::IScalarFunction
53     {
54     private:
55       IDatabaseListener& listener_;
56 
57     public:
SignalFileDeleted(IDatabaseListener & listener)58       SignalFileDeleted(IDatabaseListener& listener) :
59         listener_(listener)
60       {
61       }
62 
GetName() const63       virtual const char* GetName() const
64       {
65         return "SignalFileDeleted";
66       }
67 
GetCardinality() const68       virtual unsigned int GetCardinality() const
69       {
70         return 7;
71       }
72 
Compute(SQLite::FunctionContext & context)73       virtual void Compute(SQLite::FunctionContext& context)
74       {
75         std::string uncompressedMD5, compressedMD5;
76 
77         if (!context.IsNullValue(5))
78         {
79           uncompressedMD5 = context.GetStringValue(5);
80         }
81 
82         if (!context.IsNullValue(6))
83         {
84           compressedMD5 = context.GetStringValue(6);
85         }
86 
87         FileInfo info(context.GetStringValue(0),
88                       static_cast<FileContentType>(context.GetIntValue(1)),
89                       static_cast<uint64_t>(context.GetInt64Value(2)),
90                       uncompressedMD5,
91                       static_cast<CompressionType>(context.GetIntValue(3)),
92                       static_cast<uint64_t>(context.GetInt64Value(4)),
93                       compressedMD5);
94 
95         listener_.SignalFileDeleted(info);
96       }
97     };
98 
99     class SignalResourceDeleted : public SQLite::IScalarFunction
100     {
101     private:
102       IDatabaseListener& listener_;
103 
104     public:
SignalResourceDeleted(IDatabaseListener & listener)105       SignalResourceDeleted(IDatabaseListener& listener) :
106         listener_(listener)
107       {
108       }
109 
GetName() const110       virtual const char* GetName() const
111       {
112         return "SignalResourceDeleted";
113       }
114 
GetCardinality() const115       virtual unsigned int GetCardinality() const
116       {
117         return 2;
118       }
119 
Compute(SQLite::FunctionContext & context)120       virtual void Compute(SQLite::FunctionContext& context)
121       {
122         ResourceType type = static_cast<ResourceType>(context.GetIntValue(1));
123         ServerIndexChange change(ChangeType_Deleted, type, context.GetStringValue(0));
124         listener_.SignalChange(change);
125       }
126     };
127 
128     class SignalRemainingAncestor : public SQLite::IScalarFunction
129     {
130     private:
131       bool hasRemainingAncestor_;
132       std::string remainingPublicId_;
133       ResourceType remainingType_;
134 
135     public:
SignalRemainingAncestor()136       SignalRemainingAncestor() :
137         hasRemainingAncestor_(false)
138       {
139       }
140 
Reset()141       void Reset()
142       {
143         hasRemainingAncestor_ = false;
144       }
145 
GetName() const146       virtual const char* GetName() const
147       {
148         return "SignalRemainingAncestor";
149       }
150 
GetCardinality() const151       virtual unsigned int GetCardinality() const
152       {
153         return 2;
154       }
155 
Compute(SQLite::FunctionContext & context)156       virtual void Compute(SQLite::FunctionContext& context)
157       {
158         CLOG(TRACE, SQLITE) << "There exists a remaining ancestor with public ID \""
159                             << context.GetStringValue(0) << "\" of type "
160                             << context.GetIntValue(1);
161 
162         if (!hasRemainingAncestor_ ||
163             remainingType_ >= context.GetIntValue(1))
164         {
165           hasRemainingAncestor_ = true;
166           remainingPublicId_ = context.GetStringValue(0);
167           remainingType_ = static_cast<ResourceType>(context.GetIntValue(1));
168         }
169       }
170 
HasRemainingAncestor() const171       bool HasRemainingAncestor() const
172       {
173         return hasRemainingAncestor_;
174       }
175 
GetRemainingAncestorId() const176       const std::string& GetRemainingAncestorId() const
177       {
178         assert(hasRemainingAncestor_);
179         return remainingPublicId_;
180       }
181 
GetRemainingAncestorType() const182       ResourceType GetRemainingAncestorType() const
183       {
184         assert(hasRemainingAncestor_);
185         return remainingType_;
186       }
187     };
188   }
189 
190 
GetChangesInternal(std::list<ServerIndexChange> & target,bool & done,SQLite::Statement & s,uint32_t maxResults)191   void SQLiteDatabaseWrapper::GetChangesInternal(std::list<ServerIndexChange>& target,
192                                                  bool& done,
193                                                  SQLite::Statement& s,
194                                                  uint32_t maxResults)
195   {
196     target.clear();
197 
198     while (target.size() < maxResults && s.Step())
199     {
200       int64_t seq = s.ColumnInt64(0);
201       ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1));
202       ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(3));
203       const std::string& date = s.ColumnString(4);
204 
205       int64_t internalId = s.ColumnInt64(2);
206       std::string publicId = GetPublicId(internalId);
207 
208       target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date));
209     }
210 
211     done = !(target.size() == maxResults && s.Step());
212   }
213 
214 
GetExportedResourcesInternal(std::list<ExportedResource> & target,bool & done,SQLite::Statement & s,uint32_t maxResults)215   void SQLiteDatabaseWrapper::GetExportedResourcesInternal(std::list<ExportedResource>& target,
216                                                            bool& done,
217                                                            SQLite::Statement& s,
218                                                            uint32_t maxResults)
219   {
220     target.clear();
221 
222     while (target.size() < maxResults && s.Step())
223     {
224       int64_t seq = s.ColumnInt64(0);
225       ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(1));
226       std::string publicId = s.ColumnString(2);
227 
228       ExportedResource resource(seq,
229                                 resourceType,
230                                 publicId,
231                                 s.ColumnString(3),  // modality
232                                 s.ColumnString(8),  // date
233                                 s.ColumnString(4),  // patient ID
234                                 s.ColumnString(5),  // study instance UID
235                                 s.ColumnString(6),  // series instance UID
236                                 s.ColumnString(7)); // sop instance UID
237 
238       target.push_back(resource);
239     }
240 
241     done = !(target.size() == maxResults && s.Step());
242   }
243 
244 
GetChildren(std::list<std::string> & childrenPublicIds,int64_t id)245   void SQLiteDatabaseWrapper::GetChildren(std::list<std::string>& childrenPublicIds,
246                                           int64_t id)
247   {
248     SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE parentId=?");
249     s.BindInt64(0, id);
250 
251     childrenPublicIds.clear();
252     while (s.Step())
253     {
254       childrenPublicIds.push_back(s.ColumnString(0));
255     }
256   }
257 
258 
DeleteResource(int64_t id)259   void SQLiteDatabaseWrapper::DeleteResource(int64_t id)
260   {
261     signalRemainingAncestor_->Reset();
262 
263     SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?");
264     s.BindInt64(0, id);
265     s.Run();
266 
267     if (signalRemainingAncestor_->HasRemainingAncestor() &&
268         listener_ != NULL)
269     {
270       listener_->SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorType(),
271                                          signalRemainingAncestor_->GetRemainingAncestorId());
272     }
273   }
274 
275 
GetParentPublicId(std::string & target,int64_t id)276   bool SQLiteDatabaseWrapper::GetParentPublicId(std::string& target,
277                                                 int64_t id)
278   {
279     SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b "
280                         "WHERE a.internalId = b.parentId AND b.internalId = ?");
281     s.BindInt64(0, id);
282 
283     if (s.Step())
284     {
285       target = s.ColumnString(0);
286       return true;
287     }
288     else
289     {
290       return false;
291     }
292   }
293 
294 
GetTableRecordCount(const std::string & table)295   int64_t SQLiteDatabaseWrapper::GetTableRecordCount(const std::string& table)
296   {
297     /**
298      * "Generally one cannot use SQL parameters/placeholders for
299      * database identifiers (tables, columns, views, schemas, etc.) or
300      * database functions (e.g., CURRENT_DATE), but instead only for
301      * binding literal values." => To avoid any SQL injection, we
302      * check that the "table" parameter has only alphabetic
303      * characters.
304      * https://stackoverflow.com/a/1274764/881731
305      **/
306     for (size_t i = 0; i < table.size(); i++)
307     {
308       if (!isalpha(table[i]))
309       {
310         throw OrthancException(ErrorCode_ParameterOutOfRange);
311       }
312     }
313 
314     // Don't use "SQLITE_FROM_HERE", otherwise "table" would be cached
315     SQLite::Statement s(db_, "SELECT COUNT(*) FROM " + table);
316 
317     if (s.Step())
318     {
319       int64_t c = s.ColumnInt(0);
320       assert(!s.Step());
321       return c;
322     }
323     else
324     {
325       throw OrthancException(ErrorCode_InternalError);
326     }
327   }
328 
329 
SQLiteDatabaseWrapper(const std::string & path)330   SQLiteDatabaseWrapper::SQLiteDatabaseWrapper(const std::string& path) :
331     listener_(NULL),
332     signalRemainingAncestor_(NULL),
333     version_(0)
334   {
335     db_.Open(path);
336   }
337 
338 
SQLiteDatabaseWrapper()339   SQLiteDatabaseWrapper::SQLiteDatabaseWrapper() :
340     listener_(NULL),
341     signalRemainingAncestor_(NULL),
342     version_(0)
343   {
344     db_.OpenInMemory();
345   }
346 
347 
GetGlobalIntegerProperty(GlobalProperty property,int defaultValue)348   int SQLiteDatabaseWrapper::GetGlobalIntegerProperty(GlobalProperty property,
349                                                       int defaultValue)
350   {
351     std::string tmp;
352 
353     if (!LookupGlobalProperty(tmp, GlobalProperty_DatabasePatchLevel))
354     {
355       return defaultValue;
356     }
357     else
358     {
359       try
360       {
361         return boost::lexical_cast<int>(tmp);
362       }
363       catch (boost::bad_lexical_cast&)
364       {
365         throw OrthancException(ErrorCode_ParameterOutOfRange,
366                                "Global property " + boost::lexical_cast<std::string>(property) +
367                                " should be an integer, but found: " + tmp);
368       }
369     }
370   }
371 
372 
Open()373   void SQLiteDatabaseWrapper::Open()
374   {
375     db_.Execute("PRAGMA ENCODING=\"UTF-8\";");
376 
377     // Performance tuning of SQLite with PRAGMAs
378     // http://www.sqlite.org/pragma.html
379     db_.Execute("PRAGMA SYNCHRONOUS=NORMAL;");
380     db_.Execute("PRAGMA JOURNAL_MODE=WAL;");
381     db_.Execute("PRAGMA LOCKING_MODE=EXCLUSIVE;");
382     db_.Execute("PRAGMA WAL_AUTOCHECKPOINT=1000;");
383     //db_.Execute("PRAGMA TEMP_STORE=memory");
384 
385     // Make "LIKE" case-sensitive in SQLite
386     db_.Execute("PRAGMA case_sensitive_like = true;");
387 
388     {
389       SQLite::Transaction t(db_);
390       t.Begin();
391 
392       if (!db_.DoesTableExist("GlobalProperties"))
393       {
394         LOG(INFO) << "Creating the database";
395         std::string query;
396         ServerResources::GetFileResource(query, ServerResources::PREPARE_DATABASE);
397         db_.Execute(query);
398       }
399 
400       // Check the version of the database
401       std::string tmp;
402       if (!LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion))
403       {
404         tmp = "Unknown";
405       }
406 
407       bool ok = false;
408       try
409       {
410         LOG(INFO) << "Version of the Orthanc database: " << tmp;
411         version_ = boost::lexical_cast<unsigned int>(tmp);
412         ok = true;
413       }
414       catch (boost::bad_lexical_cast&)
415       {
416       }
417 
418       if (!ok)
419       {
420         throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
421                                "Incompatible version of the Orthanc database: " + tmp);
422       }
423 
424       // New in Orthanc 1.5.1
425       if (version_ == 6)
426       {
427         if (!LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast) ||
428             tmp != "1")
429         {
430           LOG(INFO) << "Installing the SQLite triggers to track the size of the attachments";
431           std::string query;
432           ServerResources::GetFileResource(query, ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE);
433           db_.Execute(query);
434         }
435       }
436 
437       t.Commit();
438     }
439 
440     signalRemainingAncestor_ = new Internals::SignalRemainingAncestor;
441     db_.Register(signalRemainingAncestor_);
442   }
443 
444 
ExecuteUpgradeScript(SQLite::Connection & db,ServerResources::FileResourceId script)445   static void ExecuteUpgradeScript(SQLite::Connection& db,
446                                    ServerResources::FileResourceId script)
447   {
448     std::string upgrade;
449     ServerResources::GetFileResource(upgrade, script);
450     db.BeginTransaction();
451     db.Execute(upgrade);
452     db.CommitTransaction();
453   }
454 
455 
Upgrade(unsigned int targetVersion,IStorageArea & storageArea)456   void SQLiteDatabaseWrapper::Upgrade(unsigned int targetVersion,
457                                       IStorageArea& storageArea)
458   {
459     if (targetVersion != 6)
460     {
461       throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
462     }
463 
464     // This version of Orthanc is only compatible with versions 3, 4,
465     // 5 and 6 of the DB schema
466     if (version_ != 3 &&
467         version_ != 4 &&
468         version_ != 5 &&
469         version_ != 6)
470     {
471       throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
472     }
473 
474     if (version_ == 3)
475     {
476       LOG(WARNING) << "Upgrading database version from 3 to 4";
477       ExecuteUpgradeScript(db_, ServerResources::UPGRADE_DATABASE_3_TO_4);
478       version_ = 4;
479     }
480 
481     if (version_ == 4)
482     {
483       LOG(WARNING) << "Upgrading database version from 4 to 5";
484       ExecuteUpgradeScript(db_, ServerResources::UPGRADE_DATABASE_4_TO_5);
485       version_ = 5;
486     }
487 
488     if (version_ == 5)
489     {
490       LOG(WARNING) << "Upgrading database version from 5 to 6";
491       // No change in the DB schema, the step from version 5 to 6 only
492       // consists in reconstructing the main DICOM tags information
493       // (as more tags got included).
494       db_.BeginTransaction();
495       ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Patient);
496       ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Study);
497       ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Series);
498       ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Instance);
499       db_.Execute("UPDATE GlobalProperties SET value=\"6\" WHERE property=" +
500                   boost::lexical_cast<std::string>(GlobalProperty_DatabaseSchemaVersion) + ";");
501       db_.CommitTransaction();
502       version_ = 6;
503     }
504   }
505 
506 
SetListener(IDatabaseListener & listener)507   void SQLiteDatabaseWrapper::SetListener(IDatabaseListener& listener)
508   {
509     listener_ = &listener;
510     db_.Register(new Internals::SignalFileDeleted(listener));
511     db_.Register(new Internals::SignalResourceDeleted(listener));
512   }
513 
514 
ClearTable(const std::string & tableName)515   void SQLiteDatabaseWrapper::ClearTable(const std::string& tableName)
516   {
517     db_.Execute("DELETE FROM " + tableName);
518   }
519 
520 
LookupParent(int64_t & parentId,int64_t resourceId)521   bool SQLiteDatabaseWrapper::LookupParent(int64_t& parentId,
522                                            int64_t resourceId)
523   {
524     SQLite::Statement s(db_, SQLITE_FROM_HERE,
525                         "SELECT parentId FROM Resources WHERE internalId=?");
526     s.BindInt64(0, resourceId);
527 
528     if (!s.Step())
529     {
530       throw OrthancException(ErrorCode_UnknownResource);
531     }
532 
533     if (s.ColumnIsNull(0))
534     {
535       return false;
536     }
537     else
538     {
539       parentId = s.ColumnInt(0);
540       return true;
541     }
542   }
543 
544 
GetResourceType(int64_t resourceId)545   ResourceType SQLiteDatabaseWrapper::GetResourceType(int64_t resourceId)
546   {
547     SQLite::Statement s(db_, SQLITE_FROM_HERE,
548                         "SELECT resourceType FROM Resources WHERE internalId=?");
549     s.BindInt64(0, resourceId);
550 
551     if (s.Step())
552     {
553       return static_cast<ResourceType>(s.ColumnInt(0));
554     }
555     else
556     {
557       throw OrthancException(ErrorCode_UnknownResource);
558     }
559   }
560 
561 
GetPublicId(int64_t resourceId)562   std::string SQLiteDatabaseWrapper::GetPublicId(int64_t resourceId)
563   {
564     SQLite::Statement s(db_, SQLITE_FROM_HERE,
565                         "SELECT publicId FROM Resources WHERE internalId=?");
566     s.BindInt64(0, resourceId);
567 
568     if (s.Step())
569     {
570       return s.ColumnString(0);
571     }
572     else
573     {
574       throw OrthancException(ErrorCode_UnknownResource);
575     }
576   }
577 
578 
GetChanges(std::list<ServerIndexChange> & target,bool & done,int64_t since,uint32_t maxResults)579   void SQLiteDatabaseWrapper::GetChanges(std::list<ServerIndexChange>& target /*out*/,
580                                          bool& done /*out*/,
581                                          int64_t since,
582                                          uint32_t maxResults)
583   {
584     SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?");
585     s.BindInt64(0, since);
586     s.BindInt(1, maxResults + 1);
587     GetChangesInternal(target, done, s, maxResults);
588   }
589 
590 
GetLastChange(std::list<ServerIndexChange> & target)591   void SQLiteDatabaseWrapper::GetLastChange(std::list<ServerIndexChange>& target /*out*/)
592   {
593     bool done;  // Ignored
594     SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1");
595     GetChangesInternal(target, done, s, 1);
596   }
597 
598 
599   class SQLiteDatabaseWrapper::Transaction : public IDatabaseWrapper::ITransaction
600   {
601   private:
602     SQLiteDatabaseWrapper&                that_;
603     std::unique_ptr<SQLite::Transaction>  transaction_;
604     int64_t                               initialDiskSize_;
605 
606   public:
Transaction(SQLiteDatabaseWrapper & that)607     Transaction(SQLiteDatabaseWrapper& that) :
608       that_(that),
609       transaction_(new SQLite::Transaction(that_.db_))
610     {
611 #if defined(NDEBUG)
612       // Release mode
613       initialDiskSize_ = 0;
614 #else
615       // Debug mode
616       initialDiskSize_ = static_cast<int64_t>(that_.GetTotalCompressedSize());
617 #endif
618     }
619 
Begin()620     virtual void Begin()
621     {
622       transaction_->Begin();
623     }
624 
Rollback()625     virtual void Rollback()
626     {
627       transaction_->Rollback();
628     }
629 
Commit(int64_t fileSizeDelta)630     virtual void Commit(int64_t fileSizeDelta /* only used in debug */)
631     {
632       transaction_->Commit();
633 
634       assert(initialDiskSize_ + fileSizeDelta >= 0 &&
635              initialDiskSize_ + fileSizeDelta == static_cast<int64_t>(that_.GetTotalCompressedSize()));
636     }
637   };
638 
639 
StartTransaction()640   IDatabaseWrapper::ITransaction* SQLiteDatabaseWrapper::StartTransaction()
641   {
642     return new Transaction(*this);
643   }
644 
645 
GetAllMetadata(std::map<MetadataType,std::string> & target,int64_t id)646   void SQLiteDatabaseWrapper::GetAllMetadata(std::map<MetadataType, std::string>& target,
647                                              int64_t id)
648   {
649     target.clear();
650 
651     SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type, value FROM Metadata WHERE id=?");
652     s.BindInt64(0, id);
653 
654     while (s.Step())
655     {
656       MetadataType key = static_cast<MetadataType>(s.ColumnInt(0));
657       target[key] = s.ColumnString(1);
658     }
659   }
660 
661 
SetGlobalProperty(GlobalProperty property,const std::string & value)662   void SQLiteDatabaseWrapper::SetGlobalProperty(GlobalProperty property,
663                                                 const std::string& value)
664   {
665     SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)");
666     s.BindInt(0, property);
667     s.BindString(1, value);
668     s.Run();
669   }
670 
671 
LookupGlobalProperty(std::string & target,GlobalProperty property)672   bool SQLiteDatabaseWrapper::LookupGlobalProperty(std::string& target,
673                                                    GlobalProperty property)
674   {
675     SQLite::Statement s(db_, SQLITE_FROM_HERE,
676                         "SELECT value FROM GlobalProperties WHERE property=?");
677     s.BindInt(0, property);
678 
679     if (!s.Step())
680     {
681       return false;
682     }
683     else
684     {
685       target = s.ColumnString(0);
686       return true;
687     }
688   }
689 
690 
CreateResource(const std::string & publicId,ResourceType type)691   int64_t SQLiteDatabaseWrapper::CreateResource(const std::string& publicId,
692                                                 ResourceType type)
693   {
694     SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)");
695     s.BindInt(0, type);
696     s.BindString(1, publicId);
697     s.Run();
698     return db_.GetLastInsertRowId();
699   }
700 
701 
LookupResource(int64_t & id,ResourceType & type,const std::string & publicId)702   bool SQLiteDatabaseWrapper::LookupResource(int64_t& id,
703                                              ResourceType& type,
704                                              const std::string& publicId)
705   {
706     SQLite::Statement s(db_, SQLITE_FROM_HERE,
707                         "SELECT internalId, resourceType FROM Resources WHERE publicId=?");
708     s.BindString(0, publicId);
709 
710     if (!s.Step())
711     {
712       return false;
713     }
714     else
715     {
716       id = s.ColumnInt(0);
717       type = static_cast<ResourceType>(s.ColumnInt(1));
718 
719       // Check whether there is a single resource with this public id
720       assert(!s.Step());
721 
722       return true;
723     }
724   }
725 
726 
AttachChild(int64_t parent,int64_t child)727   void SQLiteDatabaseWrapper::AttachChild(int64_t parent,
728                                           int64_t child)
729   {
730     SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?");
731     s.BindInt64(0, parent);
732     s.BindInt64(1, child);
733     s.Run();
734   }
735 
736 
SetMetadata(int64_t id,MetadataType type,const std::string & value)737   void SQLiteDatabaseWrapper::SetMetadata(int64_t id,
738                                           MetadataType type,
739                                           const std::string& value)
740   {
741     SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)");
742     s.BindInt64(0, id);
743     s.BindInt(1, type);
744     s.BindString(2, value);
745     s.Run();
746   }
747 
748 
DeleteMetadata(int64_t id,MetadataType type)749   void SQLiteDatabaseWrapper::DeleteMetadata(int64_t id,
750                                              MetadataType type)
751   {
752     SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Metadata WHERE id=? and type=?");
753     s.BindInt64(0, id);
754     s.BindInt(1, type);
755     s.Run();
756   }
757 
758 
LookupMetadata(std::string & target,int64_t id,MetadataType type)759   bool SQLiteDatabaseWrapper::LookupMetadata(std::string& target,
760                                              int64_t id,
761                                              MetadataType type)
762   {
763     SQLite::Statement s(db_, SQLITE_FROM_HERE,
764                         "SELECT value FROM Metadata WHERE id=? AND type=?");
765     s.BindInt64(0, id);
766     s.BindInt(1, type);
767 
768     if (!s.Step())
769     {
770       return false;
771     }
772     else
773     {
774       target = s.ColumnString(0);
775       return true;
776     }
777   }
778 
779 
AddAttachment(int64_t id,const FileInfo & attachment)780   void SQLiteDatabaseWrapper::AddAttachment(int64_t id,
781                                             const FileInfo& attachment)
782   {
783     SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
784     s.BindInt64(0, id);
785     s.BindInt(1, attachment.GetContentType());
786     s.BindString(2, attachment.GetUuid());
787     s.BindInt64(3, attachment.GetCompressedSize());
788     s.BindInt64(4, attachment.GetUncompressedSize());
789     s.BindInt(5, attachment.GetCompressionType());
790     s.BindString(6, attachment.GetUncompressedMD5());
791     s.BindString(7, attachment.GetCompressedMD5());
792     s.Run();
793   }
794 
795 
DeleteAttachment(int64_t id,FileContentType attachment)796   void SQLiteDatabaseWrapper::DeleteAttachment(int64_t id,
797                                                FileContentType attachment)
798   {
799     SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM AttachedFiles WHERE id=? AND fileType=?");
800     s.BindInt64(0, id);
801     s.BindInt(1, attachment);
802     s.Run();
803   }
804 
805 
ListAvailableAttachments(std::list<FileContentType> & target,int64_t id)806   void SQLiteDatabaseWrapper::ListAvailableAttachments(std::list<FileContentType>& target,
807                                                        int64_t id)
808   {
809     target.clear();
810 
811     SQLite::Statement s(db_, SQLITE_FROM_HERE,
812                         "SELECT fileType FROM AttachedFiles WHERE id=?");
813     s.BindInt64(0, id);
814 
815     while (s.Step())
816     {
817       target.push_back(static_cast<FileContentType>(s.ColumnInt(0)));
818     }
819   }
820 
LookupAttachment(FileInfo & attachment,int64_t id,FileContentType contentType)821   bool SQLiteDatabaseWrapper::LookupAttachment(FileInfo& attachment,
822                                                int64_t id,
823                                                FileContentType contentType)
824   {
825     SQLite::Statement s(db_, SQLITE_FROM_HERE,
826                         "SELECT uuid, uncompressedSize, compressionType, compressedSize, "
827                         "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?");
828     s.BindInt64(0, id);
829     s.BindInt(1, contentType);
830 
831     if (!s.Step())
832     {
833       return false;
834     }
835     else
836     {
837       attachment = FileInfo(s.ColumnString(0),
838                             contentType,
839                             s.ColumnInt64(1),
840                             s.ColumnString(4),
841                             static_cast<CompressionType>(s.ColumnInt(2)),
842                             s.ColumnInt64(3),
843                             s.ColumnString(5));
844       return true;
845     }
846   }
847 
848 
ClearMainDicomTags(int64_t id)849   void SQLiteDatabaseWrapper::ClearMainDicomTags(int64_t id)
850   {
851     {
852       SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DicomIdentifiers WHERE id=?");
853       s.BindInt64(0, id);
854       s.Run();
855     }
856 
857     {
858       SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM MainDicomTags WHERE id=?");
859       s.BindInt64(0, id);
860       s.Run();
861     }
862   }
863 
864 
SetMainDicomTag(int64_t id,const DicomTag & tag,const std::string & value)865   void SQLiteDatabaseWrapper::SetMainDicomTag(int64_t id,
866                                               const DicomTag& tag,
867                                               const std::string& value)
868   {
869     SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)");
870     s.BindInt64(0, id);
871     s.BindInt(1, tag.GetGroup());
872     s.BindInt(2, tag.GetElement());
873     s.BindString(3, value);
874     s.Run();
875   }
876 
877 
SetIdentifierTag(int64_t id,const DicomTag & tag,const std::string & value)878   void SQLiteDatabaseWrapper::SetIdentifierTag(int64_t id,
879                                                const DicomTag& tag,
880                                                const std::string& value)
881   {
882     SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)");
883     s.BindInt64(0, id);
884     s.BindInt(1, tag.GetGroup());
885     s.BindInt(2, tag.GetElement());
886     s.BindString(3, value);
887     s.Run();
888   }
889 
890 
GetMainDicomTags(DicomMap & map,int64_t id)891   void SQLiteDatabaseWrapper::GetMainDicomTags(DicomMap& map,
892                                                int64_t id)
893   {
894     map.Clear();
895 
896     SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?");
897     s.BindInt64(0, id);
898     while (s.Step())
899     {
900       map.SetValue(s.ColumnInt(1),
901                    s.ColumnInt(2),
902                    s.ColumnString(3), false);
903     }
904   }
905 
906 
GetChildrenPublicId(std::list<std::string> & target,int64_t id)907   void SQLiteDatabaseWrapper::GetChildrenPublicId(std::list<std::string>& target,
908                                                   int64_t id)
909   {
910     SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b  "
911                         "WHERE a.parentId = b.internalId AND b.internalId = ?");
912     s.BindInt64(0, id);
913 
914     target.clear();
915 
916     while (s.Step())
917     {
918       target.push_back(s.ColumnString(0));
919     }
920   }
921 
922 
GetChildrenInternalId(std::list<int64_t> & target,int64_t id)923   void SQLiteDatabaseWrapper::GetChildrenInternalId(std::list<int64_t>& target,
924                                                     int64_t id)
925   {
926     SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b  "
927                         "WHERE a.parentId = b.internalId AND b.internalId = ?");
928     s.BindInt64(0, id);
929 
930     target.clear();
931 
932     while (s.Step())
933     {
934       target.push_back(s.ColumnInt64(0));
935     }
936   }
937 
938 
LogChange(int64_t internalId,const ServerIndexChange & change)939   void SQLiteDatabaseWrapper::LogChange(int64_t internalId,
940                                         const ServerIndexChange& change)
941   {
942     SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)");
943     s.BindInt(0, change.GetChangeType());
944     s.BindInt64(1, internalId);
945     s.BindInt(2, change.GetResourceType());
946     s.BindString(3, change.GetDate());
947     s.Run();
948   }
949 
950 
LogExportedResource(const ExportedResource & resource)951   void SQLiteDatabaseWrapper::LogExportedResource(const ExportedResource& resource)
952   {
953     SQLite::Statement s(db_, SQLITE_FROM_HERE,
954                         "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)");
955 
956     s.BindInt(0, resource.GetResourceType());
957     s.BindString(1, resource.GetPublicId());
958     s.BindString(2, resource.GetModality());
959     s.BindString(3, resource.GetPatientId());
960     s.BindString(4, resource.GetStudyInstanceUid());
961     s.BindString(5, resource.GetSeriesInstanceUid());
962     s.BindString(6, resource.GetSopInstanceUid());
963     s.BindString(7, resource.GetDate());
964     s.Run();
965   }
966 
967 
GetExportedResources(std::list<ExportedResource> & target,bool & done,int64_t since,uint32_t maxResults)968   void SQLiteDatabaseWrapper::GetExportedResources(std::list<ExportedResource>& target,
969                                                    bool& done,
970                                                    int64_t since,
971                                                    uint32_t maxResults)
972   {
973     SQLite::Statement s(db_, SQLITE_FROM_HERE,
974                         "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?");
975     s.BindInt64(0, since);
976     s.BindInt(1, maxResults + 1);
977     GetExportedResourcesInternal(target, done, s, maxResults);
978   }
979 
980 
GetLastExportedResource(std::list<ExportedResource> & target)981   void SQLiteDatabaseWrapper::GetLastExportedResource(std::list<ExportedResource>& target)
982   {
983     bool done;  // Ignored
984     SQLite::Statement s(db_, SQLITE_FROM_HERE,
985                         "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1");
986     GetExportedResourcesInternal(target, done, s, 1);
987   }
988 
989 
GetTotalCompressedSize()990   uint64_t SQLiteDatabaseWrapper::GetTotalCompressedSize()
991   {
992     // Old SQL query that was used in Orthanc <= 1.5.0:
993     // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles");
994 
995     SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0");
996     s.Run();
997     return static_cast<uint64_t>(s.ColumnInt64(0));
998   }
999 
1000 
GetTotalUncompressedSize()1001   uint64_t SQLiteDatabaseWrapper::GetTotalUncompressedSize()
1002   {
1003     // Old SQL query that was used in Orthanc <= 1.5.0:
1004     // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles");
1005 
1006     SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1");
1007     s.Run();
1008     return static_cast<uint64_t>(s.ColumnInt64(0));
1009   }
1010 
1011 
GetResourceCount(ResourceType resourceType)1012   uint64_t SQLiteDatabaseWrapper::GetResourceCount(ResourceType resourceType)
1013   {
1014     SQLite::Statement s(db_, SQLITE_FROM_HERE,
1015                         "SELECT COUNT(*) FROM Resources WHERE resourceType=?");
1016     s.BindInt(0, resourceType);
1017 
1018     if (!s.Step())
1019     {
1020       return 0;
1021     }
1022     else
1023     {
1024       int64_t c = s.ColumnInt(0);
1025       assert(!s.Step());
1026       return c;
1027     }
1028   }
1029 
1030 
GetAllPublicIds(std::list<std::string> & target,ResourceType resourceType)1031   void SQLiteDatabaseWrapper::GetAllPublicIds(std::list<std::string>& target,
1032                                               ResourceType resourceType)
1033   {
1034     SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?");
1035     s.BindInt(0, resourceType);
1036 
1037     target.clear();
1038     while (s.Step())
1039     {
1040       target.push_back(s.ColumnString(0));
1041     }
1042   }
1043 
1044 
GetAllPublicIds(std::list<std::string> & target,ResourceType resourceType,size_t since,size_t limit)1045   void SQLiteDatabaseWrapper::GetAllPublicIds(std::list<std::string>& target,
1046                                               ResourceType resourceType,
1047                                               size_t since,
1048                                               size_t limit)
1049   {
1050     if (limit == 0)
1051     {
1052       target.clear();
1053       return;
1054     }
1055 
1056     SQLite::Statement s(db_, SQLITE_FROM_HERE,
1057                         "SELECT publicId FROM Resources WHERE "
1058                         "resourceType=? LIMIT ? OFFSET ?");
1059     s.BindInt(0, resourceType);
1060     s.BindInt64(1, limit);
1061     s.BindInt64(2, since);
1062 
1063     target.clear();
1064     while (s.Step())
1065     {
1066       target.push_back(s.ColumnString(0));
1067     }
1068   }
1069 
1070 
SelectPatientToRecycle(int64_t & internalId)1071   bool SQLiteDatabaseWrapper::SelectPatientToRecycle(int64_t& internalId)
1072   {
1073     SQLite::Statement s(db_, SQLITE_FROM_HERE,
1074                         "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1");
1075 
1076     if (!s.Step())
1077     {
1078       // No patient remaining or all the patients are protected
1079       return false;
1080     }
1081     else
1082     {
1083       internalId = s.ColumnInt(0);
1084       return true;
1085     }
1086   }
1087 
1088 
SelectPatientToRecycle(int64_t & internalId,int64_t patientIdToAvoid)1089   bool SQLiteDatabaseWrapper::SelectPatientToRecycle(int64_t& internalId,
1090                                                      int64_t patientIdToAvoid)
1091   {
1092     SQLite::Statement s(db_, SQLITE_FROM_HERE,
1093                         "SELECT patientId FROM PatientRecyclingOrder "
1094                         "WHERE patientId != ? ORDER BY seq ASC LIMIT 1");
1095     s.BindInt64(0, patientIdToAvoid);
1096 
1097     if (!s.Step())
1098     {
1099       // No patient remaining or all the patients are protected
1100       return false;
1101     }
1102     else
1103     {
1104       internalId = s.ColumnInt(0);
1105       return true;
1106     }
1107   }
1108 
1109 
IsProtectedPatient(int64_t internalId)1110   bool SQLiteDatabaseWrapper::IsProtectedPatient(int64_t internalId)
1111   {
1112     SQLite::Statement s(db_, SQLITE_FROM_HERE,
1113                         "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?");
1114     s.BindInt64(0, internalId);
1115     return !s.Step();
1116   }
1117 
1118 
SetProtectedPatient(int64_t internalId,bool isProtected)1119   void SQLiteDatabaseWrapper::SetProtectedPatient(int64_t internalId,
1120                                                   bool isProtected)
1121   {
1122     if (isProtected)
1123     {
1124       SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?");
1125       s.BindInt64(0, internalId);
1126       s.Run();
1127     }
1128     else if (IsProtectedPatient(internalId))
1129     {
1130       SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
1131       s.BindInt64(0, internalId);
1132       s.Run();
1133     }
1134     else
1135     {
1136       // Nothing to do: The patient is already unprotected
1137     }
1138   }
1139 
1140 
IsExistingResource(int64_t internalId)1141   bool SQLiteDatabaseWrapper::IsExistingResource(int64_t internalId)
1142   {
1143     SQLite::Statement s(db_, SQLITE_FROM_HERE,
1144                         "SELECT * FROM Resources WHERE internalId=?");
1145     s.BindInt64(0, internalId);
1146     return s.Step();
1147   }
1148 
1149 
IsDiskSizeAbove(uint64_t threshold)1150   bool SQLiteDatabaseWrapper::IsDiskSizeAbove(uint64_t threshold)
1151   {
1152     return GetTotalCompressedSize() > threshold;
1153   }
1154 
1155 
1156 
1157   class SQLiteDatabaseWrapper::LookupFormatter : public ISqlLookupFormatter
1158   {
1159   private:
1160     std::list<std::string>  values_;
1161 
1162   public:
GenerateParameter(const std::string & value)1163     virtual std::string GenerateParameter(const std::string& value)
1164     {
1165       values_.push_back(value);
1166       return "?";
1167     }
1168 
FormatResourceType(ResourceType level)1169     virtual std::string FormatResourceType(ResourceType level)
1170     {
1171       return boost::lexical_cast<std::string>(level);
1172     }
1173 
FormatWildcardEscape()1174     virtual std::string FormatWildcardEscape()
1175     {
1176       return "ESCAPE '\\'";
1177     }
1178 
Bind(SQLite::Statement & statement) const1179     void Bind(SQLite::Statement& statement) const
1180     {
1181       size_t pos = 0;
1182 
1183       for (std::list<std::string>::const_iterator
1184              it = values_.begin(); it != values_.end(); ++it, pos++)
1185       {
1186         statement.BindString(pos, *it);
1187       }
1188     }
1189   };
1190 
1191 
AnswerLookup(std::list<std::string> & resourcesId,std::list<std::string> & instancesId,SQLite::Connection & db,ResourceType level)1192   static void AnswerLookup(std::list<std::string>& resourcesId,
1193                            std::list<std::string>& instancesId,
1194                            SQLite::Connection& db,
1195                            ResourceType level)
1196   {
1197     resourcesId.clear();
1198     instancesId.clear();
1199 
1200     std::unique_ptr<SQLite::Statement> statement;
1201 
1202     switch (level)
1203     {
1204       case ResourceType_Patient:
1205       {
1206         statement.reset(
1207           new SQLite::Statement(
1208             db, SQLITE_FROM_HERE,
1209             "SELECT patients.publicId, instances.publicID FROM Lookup AS patients "
1210             "INNER JOIN Resources studies ON patients.internalId=studies.parentId "
1211             "INNER JOIN Resources series ON studies.internalId=series.parentId "
1212             "INNER JOIN Resources instances ON series.internalId=instances.parentId "
1213             "GROUP BY patients.publicId"));
1214 
1215         break;
1216       }
1217 
1218       case ResourceType_Study:
1219       {
1220         statement.reset(
1221           new SQLite::Statement(
1222             db, SQLITE_FROM_HERE,
1223             "SELECT studies.publicId, instances.publicID FROM Lookup AS studies "
1224             "INNER JOIN Resources series ON studies.internalId=series.parentId "
1225             "INNER JOIN Resources instances ON series.internalId=instances.parentId "
1226             "GROUP BY studies.publicId"));
1227 
1228         break;
1229       }
1230 
1231       case ResourceType_Series:
1232       {
1233         statement.reset(
1234           new SQLite::Statement(
1235             db, SQLITE_FROM_HERE,
1236             "SELECT series.publicId, instances.publicID FROM Lookup AS series "
1237             "INNER JOIN Resources instances ON series.internalId=instances.parentId "
1238             "GROUP BY series.publicId"));
1239 
1240         break;
1241       }
1242 
1243       case ResourceType_Instance:
1244       {
1245         statement.reset(
1246           new SQLite::Statement(
1247             db, SQLITE_FROM_HERE, "SELECT publicId, publicId FROM Lookup"));
1248 
1249         break;
1250       }
1251 
1252       default:
1253         throw OrthancException(ErrorCode_InternalError);
1254     }
1255 
1256     assert(statement.get() != NULL);
1257 
1258     while (statement->Step())
1259     {
1260       resourcesId.push_back(statement->ColumnString(0));
1261       instancesId.push_back(statement->ColumnString(1));
1262     }
1263   }
1264 
1265 
ApplyLookupResources(std::list<std::string> & resourcesId,std::list<std::string> * instancesId,const std::vector<DatabaseConstraint> & lookup,ResourceType queryLevel,size_t limit)1266   void SQLiteDatabaseWrapper::ApplyLookupResources(std::list<std::string>& resourcesId,
1267                                                    std::list<std::string>* instancesId,
1268                                                    const std::vector<DatabaseConstraint>& lookup,
1269                                                    ResourceType queryLevel,
1270                                                    size_t limit)
1271   {
1272     LookupFormatter formatter;
1273 
1274     std::string sql;
1275     LookupFormatter::Apply(sql, formatter, lookup, queryLevel, limit);
1276 
1277     sql = "CREATE TEMPORARY TABLE Lookup AS " + sql;
1278 
1279     {
1280       SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS Lookup");
1281       s.Run();
1282     }
1283 
1284     {
1285       SQLite::Statement statement(db_, sql);
1286       formatter.Bind(statement);
1287       statement.Run();
1288     }
1289 
1290     if (instancesId != NULL)
1291     {
1292       AnswerLookup(resourcesId, *instancesId, db_, queryLevel);
1293     }
1294     else
1295     {
1296       resourcesId.clear();
1297 
1298       SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Lookup");
1299 
1300       while (s.Step())
1301       {
1302         resourcesId.push_back(s.ColumnString(0));
1303       }
1304     }
1305   }
1306 
1307 
GetLastChangeIndex()1308   int64_t SQLiteDatabaseWrapper::GetLastChangeIndex()
1309   {
1310     SQLite::Statement s(db_, SQLITE_FROM_HERE,
1311                         "SELECT seq FROM sqlite_sequence WHERE name='Changes'");
1312 
1313     if (s.Step())
1314     {
1315       int64_t c = s.ColumnInt(0);
1316       assert(!s.Step());
1317       return c;
1318     }
1319     else
1320     {
1321       // No change has been recorded so far in the database
1322       return 0;
1323     }
1324   }
1325 
1326 
TagMostRecentPatient(int64_t patient)1327   void SQLiteDatabaseWrapper::TagMostRecentPatient(int64_t patient)
1328   {
1329     {
1330       SQLite::Statement s(db_, SQLITE_FROM_HERE,
1331                           "DELETE FROM PatientRecyclingOrder WHERE patientId=?");
1332       s.BindInt64(0, patient);
1333       s.Run();
1334 
1335       assert(db_.GetLastChangeCount() == 0 ||
1336              db_.GetLastChangeCount() == 1);
1337 
1338       if (db_.GetLastChangeCount() == 0)
1339       {
1340         // The patient was protected, there was nothing to delete from the recycling order
1341         return;
1342       }
1343     }
1344 
1345     {
1346       SQLite::Statement s(db_, SQLITE_FROM_HERE,
1347                           "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
1348       s.BindInt64(0, patient);
1349       s.Run();
1350     }
1351   }
1352 }
1353