35098262cadd — Chris Cannam tip 8 months ago
Add importString/loadString functions; fix some memory leaks; update to avoid deprecated APIs; tidy in a few places
M CHANGELOG +5 -0
@@ 1,4 1,9 @@ 
 
+Changes in Dataquay 0.9.5 since the previous version 0.9.1:
+
+ * Add importString methods
+
+	
 Changes in Dataquay 0.9.1 since the previous version 0.9:
 
  * Update to support Qt5 and newer C++ compilers. Now requires C++11

          
M README.txt +8 -9
@@ 7,17 7,16 @@ API for an RDF data store using Qt class
 
   http://breakfastquay.com/dataquay/
 
-This is version 0.9.1 of Dataquay.  Note that this is a pre-1.0
+This is version 0.9.5 of Dataquay.  Note that this is a pre-1.0
 release and the API is still subject to change.
 
-Dataquay is simple to use and easy to integrate. It is principally
-intended for use in Qt-based applications that would like to use an
-RDF datastore as backing for in-memory project data, to avoid having
-to provide application data-specific file formats and to make it easy
-to augment the data with descriptive metadata pulled in from external
-sources. Dataquay is also intended to be useful for applications whose
-primary purpose is not related to RDF but that have ad-hoc RDF needs
-for metadata management.
+Dataquay is principally intended for use in Qt-based applications that
+would like to use an RDF datastore as backing for in-memory project
+data, to avoid having to provide application data-specific file
+formats and to make it easy to augment the data with descriptive
+metadata pulled in from external sources. Dataquay is also intended to
+be useful for applications whose primary purpose is not related to RDF
+but that have ad-hoc RDF needs for metadata management.
 
 Dataquay does not include the datastore implementation itself; instead
 it is a wrapper around either Redland (http://librdf.org) or Sord

          
M dataquay/BasicStore.h +18 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_BASIC_STORE_H_
-#define _DATAQUAY_BASIC_STORE_H_
+#ifndef DATAQUAY_BASIC_STORE_H
+#define DATAQUAY_BASIC_STORE_H
 
 #include "Store.h"
 

          
@@ 117,6 117,8 @@ public:
 
     void save(QString filename) const;
     void import(QUrl url, ImportDuplicatesMode idm, QString format = "");
+    void importString(QString encodedRdf, Uri baseUri,
+                      ImportDuplicatesMode idm, QString format = "");
 
     Features getSupportedFeatures() const;
 

          
@@ 138,6 140,20 @@ public:
      */
     static BasicStore *load(QUrl url, QString format = "");
 
+    /**
+     * Construct a new BasicStore from the RDF document encoded in the
+     * given string.  May throw RDFException.  The returned BasicStore
+     * is owned by the caller and must be deleted using delete when
+     * finished with.  The return value is never NULL; all errors
+     * result in exceptions.
+     *
+     * If format is specified, it will be taken as the RDF parse
+     * format (e.g. ntriples).  The set of supported format strings
+     * depends on the underlying RDF library configuration.  The
+     * default is to guess the format if possible.
+     */
+    static BasicStore *loadString(QString encodedRdf, Uri baseUri, QString format = "");
+
 private:
     class D;
     D *m_d;

          
M dataquay/Connection.h +4 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_CONNECTION_H_
-#define _DATAQUAY_CONNECTION_H_
+#ifndef DATAQUAY_CONNECTION_H
+#define DATAQUAY_CONNECTION_H
 
 #include "Store.h"
 

          
@@ 106,6 106,8 @@ public:
     Uri expand(QString uri) const;
     void save(QString filename) const;
     void import(QUrl url, ImportDuplicatesMode idm, QString format = "");
+    void importString(QString encodedRdf, Uri uri,
+                      ImportDuplicatesMode idm, QString format = "");
     Features getSupportedFeatures() const;
 
 public slots:

          
M dataquay/Node.h +2 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_NODE_H_
-#define _DATAQUAY_NODE_H_
+#ifndef DATAQUAY_NODE_H
+#define DATAQUAY_NODE_H
 
 namespace Dataquay {
 class Node;

          
M dataquay/PropertyObject.h +2 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_PROPERTY_OBJECT_H_
-#define _DATAQUAY_PROPERTY_OBJECT_H_
+#ifndef DATAQUAY_PROPERTY_OBJECT_H
+#define DATAQUAY_PROPERTY_OBJECT_H
 
 #include <QString>
 #include <QStringList>

          
M dataquay/RDFException.h +2 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_EXCEPTION_H_
-#define _DATAQUAY_EXCEPTION_H_
+#ifndef DATAQUAY_EXCEPTION_H
+#define DATAQUAY_EXCEPTION_H
 
 #include <QString>
 #include <exception>

          
M dataquay/Store.h +18 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_STORE_H_
-#define _DATAQUAY_STORE_H_
+#ifndef DATAQUAY_STORE_H
+#define DATAQUAY_STORE_H
 
 #include "Triple.h"
 

          
@@ 244,6 244,22 @@ public:
     virtual void import(QUrl url, ImportDuplicatesMode idm, QString format = "") = 0;
 
     /**
+     * Import the RDF document encoded in the given string into the
+     * current store (in addition to its existing contents).  Its
+     * behaviour when a triple is encountered that already exists in
+     * the store is controlled by the ImportDuplicatesMode.
+     * 
+     * May throw RDFException or RDFDuplicateImportException.
+     *
+     * If format is specified, it will be taken as the RDF parse
+     * format (e.g. ntriples).  The set of supported format strings
+     * depends on the underlying RDF library configuration.  The
+     * default is to guess the format if possible.
+     */
+    virtual void importString(QString encodedRdf, Uri baseUri,
+                              ImportDuplicatesMode idm, QString format = "") = 0;
+
+    /**
      * Feature defines the set of optional features a Store
      * implementation may support.  Code that uses Store should check
      * that the features it requires are available before trying to

          
M dataquay/Transaction.h +2 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_TRANSACTION_H_
-#define _DATAQUAY_TRANSACTION_H_
+#ifndef DATAQUAY_TRANSACTION_H
+#define DATAQUAY_TRANSACTION_H
 
 #include "Store.h"
 

          
M dataquay/TransactionalStore.h +6 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_TRANSACTIONAL_STORE_H_
-#define _DATAQUAY_TRANSACTIONAL_STORE_H_
+#ifndef DATAQUAY_TRANSACTIONAL_STORE_H
+#define DATAQUAY_TRANSACTIONAL_STORE_H
 
 #include "Transaction.h"
 

          
@@ 136,6 136,8 @@ public:
     Uri expand(QString uri) const;
     void save(QString filename) const;
     void import(QUrl url, ImportDuplicatesMode idm, QString format = "");
+    void importString(QString encodedRdf, Uri baseUri,
+                      ImportDuplicatesMode idm, QString format = "");
     Features getSupportedFeatures() const;
 
 signals:

          
@@ 180,6 182,8 @@ private:
         Uri expand(QString uri) const;
         void save(QString filename) const;
         void import(QUrl url, ImportDuplicatesMode idm, QString format = "");
+        void importString(QString encodedRdf, Uri baseUri,
+                          ImportDuplicatesMode idm, QString format = "");
         Features getSupportedFeatures() const;
 
         // Transaction interface

          
M dataquay/Triple.h +2 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_TRIPLE_H_
-#define _DATAQUAY_TRIPLE_H_
+#ifndef DATAQUAY_TRIPLE_H
+#define DATAQUAY_TRIPLE_H
 
 #include "Node.h"
 

          
M dataquay/Uri.h +2 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_URI_H_
-#define _DATAQUAY_URI_H_
+#ifndef DATAQUAY_URI_H
+#define DATAQUAY_URI_H
 
 namespace Dataquay {
 class Uri;

          
M dataquay/objectmapper/ContainerBuilder.h +2 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_CONTAINER_BUILDER_H_
-#define _DATAQUAY_CONTAINER_BUILDER_H_
+#ifndef DATAQUAY_CONTAINER_BUILDER_H
+#define DATAQUAY_CONTAINER_BUILDER_H
 
 #include <QHash>
 #include <QString>

          
M dataquay/objectmapper/ObjectBuilder.h +2 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_OBJECT_BUILDER_H_
-#define _DATAQUAY_OBJECT_BUILDER_H_
+#ifndef DATAQUAY_OBJECT_BUILDER_H
+#define DATAQUAY_OBJECT_BUILDER_H
 
 #include <QHash>
 #include <QString>

          
M dataquay/objectmapper/ObjectLoader.h +2 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_OBJECT_LOADER_H_
-#define _DATAQUAY_OBJECT_LOADER_H_
+#ifndef DATAQUAY_OBJECT_LOADER_H
+#define DATAQUAY_OBJECT_LOADER_H
 
 #include "../Node.h"
 

          
M dataquay/objectmapper/ObjectMapper.h +2 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_OBJECT_MAPPER_H_
-#define _DATAQUAY_OBJECT_MAPPER_H_
+#ifndef DATAQUAY_OBJECT_MAPPER_H
+#define DATAQUAY_OBJECT_MAPPER_H
 
 #include "../Node.h"
 #include "../Store.h"

          
M dataquay/objectmapper/ObjectMapperDefs.h +2 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_OBJECT_MAPPER_DEFS_H_
-#define _DATAQUAY_OBJECT_MAPPER_DEFS_H_
+#ifndef DATAQUAY_OBJECT_MAPPER_DEFS_H
+#define DATAQUAY_OBJECT_MAPPER_DEFS_H
 
 namespace Dataquay
 {

          
M dataquay/objectmapper/ObjectMapperExceptions.h +2 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_OBJECT_MAPPER_EXCEPTIONS_H_
-#define _DATAQUAY_OBJECT_MAPPER_EXCEPTIONS_H_
+#ifndef DATAQUAY_OBJECT_MAPPER_EXCEPTIONS_H
+#define DATAQUAY_OBJECT_MAPPER_EXCEPTIONS_H
 
 #include <exception>
 #include <QString>

          
M dataquay/objectmapper/ObjectMapperForwarder.h +2 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_OBJECT_MAPPER_FORWARDER_H_
-#define _DATAQUAY_OBJECT_MAPPER_FORWARDER_H_
+#ifndef DATAQUAY_OBJECT_MAPPER_FORWARDER_H
+#define DATAQUAY_OBJECT_MAPPER_FORWARDER_H
 
 #include <QObject>
 

          
M dataquay/objectmapper/ObjectStorer.h +2 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_OBJECT_STORER_H_
-#define _DATAQUAY_OBJECT_STORER_H_
+#ifndef DATAQUAY_OBJECT_STORER_H
+#define DATAQUAY_OBJECT_STORER_H
 
 #include "../Node.h"
 

          
M dataquay/objectmapper/TypeMapping.h +2 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_TYPE_MAPPING_H_
-#define _DATAQUAY_TYPE_MAPPING_H_
+#ifndef DATAQUAY_TYPE_MAPPING_H
+#define DATAQUAY_TYPE_MAPPING_H
 
 #include "../Uri.h"
 

          
M lib.pro +1 -1
@@ 13,7 13,7 @@ exists(config.pri) {
 	include(./config.pri)
 }
 
-VERSION=0.9.1
+VERSION=0.9.5
 OBJECTS_DIR = o
 MOC_DIR = o
 

          
M src/Connection.cpp +17 -0
@@ 60,6 60,8 @@ public:
     Uri expand(QString uri) const;
     void save(QString filename) const;
     void import(QUrl url, ImportDuplicatesMode idm, QString format);
+    void importString(QString encodedRdf, Uri baseUri,
+                      ImportDuplicatesMode idm, QString format);
     Features getSupportedFeatures() const;
     
     void commit();

          
@@ 228,6 230,14 @@ Connection::D::import(QUrl url, ImportDu
     m_tx->import(url, idm, format);
 }
 
+void
+Connection::D::importString(QString encodedRdf, Uri baseUri,
+                            ImportDuplicatesMode idm, QString format)
+{
+    start();
+    m_tx->importString(encodedRdf, baseUri, idm, format);
+}
+
 Connection::Features
 Connection::D::getSupportedFeatures() const
 {

          
@@ 339,6 349,13 @@ Connection::import(QUrl url, ImportDupli
     return m_d->import(url, idm, format);
 }
 
+void
+Connection::importString(QString encodedRdf, Uri baseUri,
+                         ImportDuplicatesMode idm, QString format)
+{
+    return m_d->importString(encodedRdf, baseUri, idm, format);
+}
+
 Connection::Features
 Connection::getSupportedFeatures() const
 {

          
M src/Debug.h +2 -2
@@ 31,8 31,8 @@ 
     authorization.
 */
 
-#ifndef _DATAQUAY_INTERNAL_DEBUG_H_
-#define _DATAQUAY_INTERNAL_DEBUG_H_
+#ifndef DATAQUAY_INTERNAL_DEBUG_H
+#define DATAQUAY_INTERNAL_DEBUG_H
 
 #include <QTextStream>
 

          
M src/TransactionalStore.cpp +43 -6
@@ 213,12 213,7 @@ public:
         Operation op(this, tx);
         m_store->save(filename);
     }
-/*!!!
-    void import(Transaction *tx, QUrl url, ImportDuplicatesMode idm, QString format) {
-        Operation op(this, tx);
-        m_store->import(url, idm, format);
-    }
-*/
+
     Features getSupportedFeatures() const {
         return m_store->getSupportedFeatures();
     }

          
@@ 569,6 564,30 @@ public:
         }
     }
 
+    void importString(QString encodedRdf, Uri baseUri,
+                      ImportDuplicatesMode idm, QString format) {
+        check();
+        BasicStore *bs = 0;
+        try {
+            bs = BasicStore::loadString(encodedRdf, baseUri, format);
+            Triples ts = bs->match(Triple());
+            foreach (Triple t, ts) {
+                bool added = m_td->add(m_tx, t);
+                if (added) {
+                    m_changes.push_back(Change(AddTriple, t));
+                } else if (idm == ImportFailOnDuplicates) {
+                    throw RDFDuplicateImportException("Duplicate statement encountered on import in ImportFailOnDuplicates mode");
+                }
+            }
+            delete bs;
+            bs = 0;
+        } catch (const RDFException &) {
+            delete bs;
+            abandon();
+            throw;
+        }
+    }
+
     Features getSupportedFeatures() const {
         return m_td->getSupportedFeatures();
     }

          
@@ 637,6 656,17 @@ TransactionalStore::import(QUrl url, Imp
     return tx->import(url, idm, format);
 }
 
+void
+TransactionalStore::importString(QString encodedRdf, Uri baseUri,
+                                 ImportDuplicatesMode idm, QString format)
+{
+    if (!m_d->hasWrap()) {
+        throw RDFException("TransactionalStore::import() called without Transaction");
+    }
+    unique_ptr<Transaction> tx(startTransaction());
+    return tx->importString(encodedRdf, baseUri, idm, format);
+}
+
 TransactionalStore::Features
 TransactionalStore::getSupportedFeatures() const
 {

          
@@ 850,6 880,13 @@ TransactionalStore::TSTransaction::impor
     m_d->import(url, idm, format);
 }
 
+void
+TransactionalStore::TSTransaction::importString(QString encodedRdf, Uri baseUri,
+                                                ImportDuplicatesMode idm, QString format)
+{
+    m_d->importString(encodedRdf, baseUri, idm, format);
+}
+
 TransactionalStore::TSTransaction::Features
 TransactionalStore::TSTransaction::getSupportedFeatures() const
 {

          
M src/backend/BasicStoreRedland.cpp +216 -80
@@ 347,7 347,9 @@ public:
         QMutexLocker locker(&m_librdfLock);
         librdf_node *node = librdf_new_node_from_blank_identifier(m_w.getWorld(), 0);
         if (!node) throw RDFInternalError("Failed to create new blank node");
-        return lrdfNodeToNode(node);
+        Node n = lrdfNodeToNode(node);
+        librdf_free_node(node);
+        return n;
     }
 
     void save(QString filename) const {

          
@@ 357,6 359,14 @@ public:
 
         DQ_DEBUG << "BasicStore::save(" << filename << ")" << endl;
 
+        QFile f(filename);
+        if (!f.exists()) {
+            if (!f.open(QFile::WriteOnly)) {
+                throw RDFException("Failed to open file for writing", filename);
+            }
+            f.close();
+        }
+
         librdf_uri *base_uri = uriToLrdfUri(m_baseUri);
         librdf_serializer *s = librdf_new_serializer(m_w.getWorld(), "turtle", 0, 0);
         if (!s) throw RDFInternalError("Failed to construct RDF serializer");

          
@@ 366,15 376,7 @@ public:
             QByteArray b = i.key().toUtf8();
             librdf_serializer_set_namespace(s, uriToLrdfUri(i.value()), b.data());
         }
-        librdf_serializer_set_namespace(s, uriToLrdfUri(m_baseUri), "");
-
-        QFile f(filename);
-        if (!f.exists()) {
-            if (!f.open(QFile::WriteOnly)) {
-                throw RDFException("Failed to open file for writing", filename);
-            }
-            f.close();
-        }
+        librdf_serializer_set_namespace(s, base_uri, "");
 
         QString tmpFilename = QString("%1.part").arg(filename);
         QByteArray b = QFile::encodeName(tmpFilename);

          
@@ 382,12 384,14 @@ public:
         
         if (librdf_serializer_serialize_model_to_file(s, lname, base_uri, m_model)) {
             librdf_free_serializer(s);
+            librdf_free_uri(base_uri);
             QFile::remove(tmpFilename);
             throw RDFException("Failed to export RDF model to temporary file",
                                tmpFilename);
         }
 
         librdf_free_serializer(s);
+        librdf_free_uri(base_uri);
 
         // New file is now completed; the following is scruffy, but
         // that shouldn't really matter now

          
@@ 440,6 444,8 @@ public:
                 librdf_free_parser(parser);
                 DQ_DEBUG << "librdf_parser_parse_into_model failed" << endl;
                 DQ_DEBUG << "luri = " << (const char *)librdf_uri_as_string(luri) << ", base_uri = " << (const char *)librdf_uri_as_string(base_uri) << endl;
+                librdf_free_uri(luri);
+                librdf_free_uri(base_uri);
                 throw RDFException("Failed to import model from URL",
                                    url.toString());
             }

          
@@ 464,60 470,97 @@ public:
                 throw RDFInternalError("Failed to create import RDF data model");
             }
 
-            librdf_stream *stream = 0;
-            librdf_statement *all = 0;
-
-            try { // so as to free parser and im on exception
-
-                //!!! This appears to be returning success even on a
-                //!!! syntax error -- can this be correct?
+            try {
                 if (librdf_parser_parse_into_model(parser, luri, base_uri, im)) {
                     DQ_DEBUG << "librdf_parser_parse_into_model failed" << endl;
                     DQ_DEBUG << "luri = " << (const char *)librdf_uri_as_string(luri) << ", base_uri = " << (const char *)librdf_uri_as_string(base_uri) << endl;
                     throw RDFException("Failed to import model from URL",
                                        url.toString());
                 }
-                all = tripleToStatement(Triple());
+
+                importFromTemporaryModel(im, idm);
+
+            } catch (...) {
+                librdf_free_parser(parser);
+                librdf_free_model(im);
+                librdf_free_storage(is);
+                librdf_free_uri(luri);
+                librdf_free_uri(base_uri);
+                throw;
+            }
+
+            librdf_free_model(im);
+            librdf_free_storage(is);
+        }
+
+        importNamespacesFromParser(parser);
+        librdf_free_parser(parser);
+        librdf_free_uri(luri);
+        librdf_free_uri(base_uri);
+    }
+
+    void importString(QString encodedRdf, Uri baseUri,
+                      ImportDuplicatesMode idm, QString format) {
+
+        QMutexLocker wlocker(&m_librdfLock);
+        QMutexLocker plocker(&m_prefixLock);
+
+        QString base = baseUri.toString();
+        librdf_uri *base_uri = uriToLrdfUri(Uri(base));
+
+        if (format == "") format = "guess";
+
+        librdf_parser *parser = librdf_new_parser
+            (m_w.getWorld(), format.toLocal8Bit().data(), NULL, NULL);
+        if (!parser) {
+            throw RDFInternalError("Failed to construct RDF parser");
+        }
 
-                if (idm == ImportFailOnDuplicates) {
-                    // Need to query twice, first time to check for dupes
-                    stream = librdf_model_find_statements(im, all);
-                    if (!stream) {
-                        throw RDFInternalError("Failed to list imported RDF model in duplicates check");
-                    }
-                    while (!librdf_stream_end(stream)) {
-                        librdf_statement *current = librdf_stream_get_object(stream);
-                        if (!current) continue;
-                        if (librdf_model_contains_statement(m_model, current)) {
-                            throw RDFDuplicateImportException("Duplicate statement encountered on import in ImportFailOnDuplicates mode");
-                        }
-                        librdf_stream_next(stream);
-                    }
-                    librdf_free_stream(stream);
-                    stream = 0;
+        QByteArray rdfUtf8 = encodedRdf.toUtf8();
+
+        if (idm == ImportPermitDuplicates) {
+            // The normal Redland behaviour, so the easy case.
+            if (librdf_parser_parse_string_into_model
+                (parser, (const unsigned char *)rdfUtf8.data(),
+                 base_uri, m_model)) {
+                librdf_free_parser(parser);
+                DQ_DEBUG << "librdf_parser_parse_into_model failed" << endl;
+                DQ_DEBUG << "base_uri = " << (const char *)librdf_uri_as_string(base_uri) << endl;
+                throw RDFException("Failed to import model from string");
+            }
+        } else { // ImportFailOnDuplicates and ImportIgnoreDuplicates modes
+
+            // This is complicated by our desire to avoid storing any
+            // duplicate triples on import, and optionally to be able
+            // to fail if any are found.  So we import into a separate
+            // model and then transfer over.  Not very efficient, but
+            // scalability is not generally the primary concern for us
+
+            librdf_storage *is = librdf_new_storage(m_w.getWorld(), "trees", 0, 0);
+            if (!is) is = librdf_new_storage(m_w.getWorld(), 0, 0, 0);
+            if (!is) {
+                librdf_free_parser(parser);
+                throw RDFInternalError("Failed to create import RDF data storage");
+            }
+            librdf_model *im = librdf_new_model(m_w.getWorld(), is, 0);
+            if (!im) {
+                librdf_free_storage(is);
+                librdf_free_parser(parser);
+                throw RDFInternalError("Failed to create import RDF data model");
+            }
+
+            try {
+                if (librdf_parser_parse_string_into_model
+                    (parser, (const unsigned char *)rdfUtf8.data(),
+                     base_uri, im)) {
+                    DQ_DEBUG << "librdf_parser_parse_into_model failed" << endl;
+                    DQ_DEBUG << "base_uri = " << (const char *)librdf_uri_as_string(base_uri) << endl;
+                    throw RDFException("Failed to import model from URL");
                 }
 
-                // Now import.  Have to do this "manually" because librdf
-                // may allow duplicates and we want to avoid them
-                stream = librdf_model_find_statements(im, all);
-                if (!stream) {
-                    throw RDFInternalError("Failed to list imported RDF model");
-                }
-                while (!librdf_stream_end(stream)) {
-                    librdf_statement *current = librdf_stream_get_object(stream);
-                    if (!current) continue;
-                    if (idm == ImportFailOnDuplicates || // (already tested if so)
-                        !librdf_model_contains_statement(m_model, current)) {
-                        librdf_model_add_statement(m_model, current);
-                    }
-                    librdf_stream_next(stream);
-                }
-                librdf_free_stream(stream);
-                stream = 0;
+                importFromTemporaryModel(im, idm);
 
             } catch (...) {
-                if (stream) librdf_free_stream(stream);
-                if (all) librdf_free_statement(all);
                 librdf_free_parser(parser);
                 librdf_free_model(im);
                 librdf_free_storage(is);

          
@@ 528,28 571,7 @@ public:
             librdf_free_storage(is);
         }
 
-        int namespaces = librdf_parser_get_namespaces_seen_count(parser);
-        DQ_DEBUG << "Parser found " << namespaces << " namespaces" << endl;
-        for (int i = 0; i < namespaces; ++i) {
-            const char *pfx = librdf_parser_get_namespaces_seen_prefix(parser, i);
-            librdf_uri *uri = librdf_parser_get_namespaces_seen_uri(parser, i);
-            QString qpfx = QString::fromUtf8(pfx);
-            Uri quri;
-            try {
-                quri = lrdfUriToUri(uri);
-            } catch (const RDFIncompleteURI &) {
-                continue;
-            }
-            DQ_DEBUG << "namespace " << i << ": " << qpfx << " -> " << quri << endl;
-            // don't call addPrefix; it tries to lock the mutex,
-            // and anyway we want to add the prefix only if it
-            // isn't already there (to avoid surprisingly changing
-            // a prefix in unusual cases, or changing the base URI)
-            if (m_prefixes.find(qpfx) == m_prefixes.end()) {
-                m_prefixes[qpfx] = quri;
-            }
-        }
-
+        importNamespacesFromParser(parser);
         librdf_free_parser(parser);
     }
 

          
@@ 594,6 616,84 @@ private:
 
     mutable int m_counter;
 
+    void importFromTemporaryModel(librdf_model *im, ImportDuplicatesMode idm) {
+        
+        librdf_stream *stream = 0;
+        librdf_statement *all = 0;
+
+        try {
+        
+            all = tripleToStatement(Triple());
+
+            if (idm == ImportFailOnDuplicates) {
+                // Need to query twice, first time to check for dupes
+                stream = librdf_model_find_statements(im, all);
+                if (!stream) {
+                    throw RDFInternalError("Failed to list imported RDF model in duplicates check");
+                }
+                while (!librdf_stream_end(stream)) {
+                    librdf_statement *current = librdf_stream_get_object(stream);
+                    if (!current) continue;
+                    if (librdf_model_contains_statement(m_model, current)) {
+                        throw RDFDuplicateImportException("Duplicate statement encountered on import in ImportFailOnDuplicates mode");
+                    }
+                    librdf_stream_next(stream);
+                }
+                librdf_free_stream(stream);
+                stream = 0;
+            }
+        
+            // Now import.  Have to do this "manually" because librdf
+            // may allow duplicates and we want to avoid them
+            stream = librdf_model_find_statements(im, all);
+            if (!stream) {
+                throw RDFInternalError("Failed to list imported RDF model");
+            }
+            while (!librdf_stream_end(stream)) {
+                librdf_statement *current = librdf_stream_get_object(stream);
+                if (!current) continue;
+                if (idm == ImportFailOnDuplicates || // (already tested if so)
+                    !librdf_model_contains_statement(m_model, current)) {
+                    librdf_model_add_statement(m_model, current);
+                }
+                librdf_stream_next(stream);
+            }
+ 
+        } catch (...) {
+            if (stream) librdf_free_stream(stream);
+            if (all) librdf_free_statement(all);
+            throw;
+        }
+
+        if (stream) librdf_free_stream(stream);
+        if (all) librdf_free_statement(all);
+    }
+
+    void importNamespacesFromParser(librdf_parser *parser) {
+        
+        int namespaces = librdf_parser_get_namespaces_seen_count(parser);
+        DQ_DEBUG << "Parser found " << namespaces << " namespaces" << endl;
+        for (int i = 0; i < namespaces; ++i) {
+            const char *pfx = librdf_parser_get_namespaces_seen_prefix(parser, i);
+            librdf_uri *uri = librdf_parser_get_namespaces_seen_uri(parser, i);
+            QString qpfx = QString::fromUtf8(pfx);
+            Uri quri;
+            try {
+                quri = lrdfUriToUri(uri);
+            } catch (const RDFIncompleteURI &) {
+                continue;
+            }
+            DQ_DEBUG << "namespace " << i << ": " << qpfx << " -> " << quri << endl;
+            // don't call addPrefix; it tries to lock the mutex,
+            // and anyway we want to add the prefix only if it
+            // isn't already there (to avoid surprisingly changing
+            // a prefix in unusual cases, or changing the base URI)
+            if (m_prefixes.find(qpfx) == m_prefixes.end()) {
+                m_prefixes[qpfx] = quri;
+            }
+        }
+    }
+    
     bool doAdd(Triple t) {
         librdf_statement *statement = tripleToStatement(t);
         if (!checkComplete(statement)) {

          
@@ 736,11 836,24 @@ private:
                       lrdfNodeToNode(object));
         return triple;
     }
-
+    
     bool checkComplete(librdf_statement *statement) const {
         if (librdf_statement_is_complete(statement)) return true;
         else {
-            unsigned char *text = librdf_statement_to_string(statement);
+            unsigned char *text = nullptr;
+            raptor_iostream *iostr = raptor_new_iostream_to_string
+                (librdf_world_get_raptor(m_w.getWorld()),
+                 (void **)&text, nullptr, malloc);
+            int rc = 1;
+            if (iostr) {
+                rc = librdf_statement_write(statement, iostr);
+                raptor_free_iostream(iostr);
+            }
+            if (rc) {
+                if (text) free(text);
+                std::cerr << "BasicStore::checkComplete: WARNING: RDF statement is incomplete, and writing it to a diagnostic string failed" << std::endl;
+                return false;
+            }
             QString str = QString::fromUtf8((char *)text);
             std::cerr << "BasicStore::checkComplete: WARNING: RDF statement is incomplete: " << str.toStdString() << std::endl;
             free(text);

          
@@ 754,8 867,10 @@ private:
         Triples results;
         librdf_statement *templ = tripleToStatement(t);
         librdf_stream *stream = librdf_model_find_statements(m_model, templ);
-        librdf_free_statement(templ);
-        if (!stream) throw RDFInternalError("Failed to match RDF triples");
+        if (!stream) {
+            librdf_free_statement(templ);
+            throw RDFInternalError("Failed to match RDF triples");
+        }
         while (!librdf_stream_end(stream)) {
             librdf_statement *current = librdf_stream_get_object(stream);
             if (current) results.push_back(statementToTriple(current));

          
@@ 763,6 878,7 @@ private:
             librdf_stream_next(stream);
         }
         librdf_free_stream(stream);
+        librdf_free_statement(templ);
         return results;
     }
 

          
@@ 808,7 924,10 @@ private:
                 librdf_node *node =
                     librdf_query_results_get_binding_value(results, i);
 
-                dict[key] = lrdfNodeToNode(node);
+                if (node) {
+                    dict[key] = lrdfNodeToNode(node);
+                    librdf_free_node(node);
+                }
             }
 
             returned.push_back(dict);

          
@@ 960,6 1079,13 @@ BasicStore::import(QUrl url, ImportDupli
     m_d->import(url, idm, format);
 }
 
+void
+BasicStore::importString(QString encodedRdf, Uri baseUri,
+                         ImportDuplicatesMode idm, QString format)
+{
+    m_d->importString(encodedRdf, baseUri, idm, format);
+}
+
 BasicStore *
 BasicStore::load(QUrl url, QString format)
 {

          
@@ 970,6 1096,16 @@ BasicStore::load(QUrl url, QString forma
     return s;
 }
 
+BasicStore *
+BasicStore::loadString(QString encodedRdf, Uri baseUri, QString format)
+{
+    BasicStore *s = new BasicStore();
+    s->setBaseUri(baseUri);
+    // store is empty, ImportIgnoreDuplicates is faster
+    s->importString(encodedRdf, baseUri, ImportIgnoreDuplicates, format);
+    return s;
+}
+
 BasicStore::Features
 BasicStore::getSupportedFeatures() const
 {

          
M src/backend/BasicStoreSord.cpp +156 -32
@@ 504,7 504,7 @@ public:
             }
 
             SerdStatus rv = serd_reader_read_file
-                (reader, (const uint8_t *)fileUri.toLocal8Bit().data());
+                (reader, (const uint8_t *)fileUri.toUtf8().data());
 
             if (rv != SERD_SUCCESS) {
                 serd_reader_free(reader);

          
@@ 535,7 535,7 @@ public:
             }
 
             SerdStatus rv = serd_reader_read_file
-                (reader, (const uint8_t *)fileUri.toLocal8Bit().data());
+                (reader, (const uint8_t *)fileUri.toUtf8().data());
             
             if (rv != SERD_SUCCESS) {
                 serd_reader_free(reader);

          
@@ 549,40 549,111 @@ public:
 
             serd_reader_free(reader);
 
-            SordQuad templ;
-            tripleToStatement(Triple(), templ);
+            try {
+                importFromTemporaryModel(im, idm);
+            } catch (...) {
+                sord_free(im);
+                serd_env_free(env);
+                throw;
+            }
+
+            sord_free(im);
+        }
+
+	serd_env_foreach(env, addPrefixSink, this);
+        serd_env_free(env);
+    }
+
+    void importString(QString encodedRdf, Uri baseUri,
+                      ImportDuplicatesMode idm, QString /* format */) {
+
+        DQ_DEBUG << "BasicStoreSord::importString" << endl;
 
-            if (idm == ImportFailOnDuplicates) {
+        QMutexLocker wlocker(&m_backendLock);
+        QMutexLocker plocker(&m_prefixLock);
+
+        //!!! todo: format?
+
+        QString base = baseUri.toString();
+        QByteArray bb = base.toUtf8();
+        SerdURI bu;
+
+        if (serd_uri_parse((uint8_t *)bb.data(), &bu) != SERD_SUCCESS) {
+            throw RDFInternalError("Failed to parse base URI", base);
+        }
+
+        SerdNode bn = serd_node_from_string(SERD_URI, (uint8_t *)bb.data());
+        SerdEnv *env = serd_env_new(&bn);
+
+        QByteArray rdfUtf8 = encodedRdf.toUtf8();
+
+        if (idm == ImportPermitDuplicates) {
+
+            // No special handling for duplicates, do whatever the
+            // underlying engine does
+
+            SerdReader *reader = sord_new_reader(m_model, env, SERD_TURTLE, NULL);
 
-                SordIter *itr = sord_find(im, templ);
-                while (!sord_iter_end(itr)) {
-                    SordQuad q;
-                    sord_iter_get(itr, q);
-                    if (sord_contains(m_model, q)) {
-                        Triple culprit = statementToTriple(q);
-                        sord_iter_free(itr);
-                        freeStatement(templ);
-                        sord_free(im);
-                        serd_env_free(env);
-                        throw RDFDuplicateImportException("Duplicate statement encountered on import in ImportFailOnDuplicates mode", culprit);
-                    }
-                    sord_iter_next(itr);
-                }
-                sord_iter_free(itr);
+            // if we have data in the store already, then we must add
+            // a prefix for the new blank nodes we're importing to
+            // disambiguate them
+            if (!doMatch(Triple(), true).empty()) {
+                serd_reader_add_blank_prefix
+                    (reader, (uint8_t *)(getNewString().toUtf8().data()));
+            }
+
+            SerdStatus rv = serd_reader_read_string
+                (reader, (const uint8_t *)rdfUtf8.data());
+
+            if (rv != SERD_SUCCESS) {
+                serd_reader_free(reader);
+                serd_env_free(env);
+                throw RDFException
+                    (QString("Failed to import model from string: %1")
+                     .arg(serdStatusToString(rv)));
+            }
+
+            serd_reader_free(reader);
+
+        } else {
+
+            // ImportFailOnDuplicates and ImportIgnoreDuplicates:
+            // import into a separate model and transfer across
+
+            SordModel *im = sord_new(m_w.getWorld(), 0, false); // no index
+            
+            SerdReader *reader = sord_new_reader(im, env, SERD_TURTLE, NULL);
+
+            // if we have data in the store already, then we must add
+            // a prefix for the new blank nodes we're importing to
+            // disambiguate them
+            if (!doMatch(Triple(), true).empty()) {
+                serd_reader_add_blank_prefix
+                    (reader, (uint8_t *)(getNewString().toUtf8().data()));
+            }
+
+            SerdStatus rv = serd_reader_read_string
+                (reader, (const uint8_t *)rdfUtf8.data());
+            
+            if (rv != SERD_SUCCESS) {
+                serd_reader_free(reader);
+                sord_free(im);
+                serd_env_free(env);
+                throw RDFException
+                    (QString("Failed to import model from string: %1")
+                     .arg(serdStatusToString(rv)));
+            }
+
+            serd_reader_free(reader);
+
+            try {
+                importFromTemporaryModel(im, idm);
+            } catch (...) {
+                sord_free(im);
+                serd_env_free(env);
+                throw;
             }
             
-            SordIter *itr = sord_find(im, templ);
-            while (!sord_iter_end(itr)) {
-                SordQuad q;
-                sord_iter_get(itr, q);
-                if (idm == ImportFailOnDuplicates || // (already tested if so)
-                    !sord_contains(m_model, q)) {
-                    sord_add(m_model, q);
-                }
-                sord_iter_next(itr);
-            }
-            sord_iter_free(itr);
-            freeStatement(templ);
             sord_free(im);
         }
 

          
@@ 630,6 701,42 @@ private:
     PrefixMap m_prefixes;
     mutable QMutex m_prefixLock; // also protects m_baseUri
 
+    void importFromTemporaryModel(SordModel *im, ImportDuplicatesMode idm) {
+
+        SordQuad templ;
+        tripleToStatement(Triple(), templ);
+
+        if (idm == ImportFailOnDuplicates) {
+
+            SordIter *itr = sord_find(im, templ);
+            while (!sord_iter_end(itr)) {
+                SordQuad q;
+                sord_iter_get(itr, q);
+                if (sord_contains(m_model, q)) {
+                    Triple culprit = statementToTriple(q);
+                    sord_iter_free(itr);
+                    freeStatement(templ);
+                    throw RDFDuplicateImportException("Duplicate statement encountered on import in ImportFailOnDuplicates mode", culprit);
+                }
+                sord_iter_next(itr);
+            }
+            sord_iter_free(itr);
+        }
+            
+        SordIter *itr = sord_find(im, templ);
+        while (!sord_iter_end(itr)) {
+            SordQuad q;
+            sord_iter_get(itr, q);
+            if (idm == ImportFailOnDuplicates || // (already tested if so)
+                !sord_contains(m_model, q)) {
+                sord_add(m_model, q);
+            }
+            sord_iter_next(itr);
+        }
+        sord_iter_free(itr);
+        freeStatement(templ);
+    }
+    
     bool doAdd(Triple t) {
         SordQuad statement;
         tripleToStatement(t, statement);

          
@@ 965,6 1072,13 @@ BasicStore::import(QUrl url, ImportDupli
     m_d->import(url, idm, format);
 }
 
+void
+BasicStore::importString(QString encodedRdf, Uri baseUri,
+                         ImportDuplicatesMode idm, QString format)
+{
+    m_d->importString(encodedRdf, baseUri, idm, format);
+}
+
 BasicStore *
 BasicStore::load(QUrl url, QString format)
 {

          
@@ 977,6 1091,16 @@ BasicStore::load(QUrl url, QString forma
     return s;
 }
 
+BasicStore *
+BasicStore::loadString(QString encodedRdf, Uri baseUri, QString format)
+{
+    BasicStore *s = new BasicStore();
+    s->setBaseUri(baseUri);
+    // store is empty, ImportIgnoreDuplicates is faster
+    s->importString(encodedRdf, baseUri, ImportIgnoreDuplicates, format);
+    return s;
+}
+
 BasicStore::Features
 BasicStore::getSupportedFeatures() const
 {

          
M tests/TestBasicStore.h +4 -0
@@ 562,6 562,7 @@ private slots:
         t = s2->matchOnce(Triple(Node(), Uri("a"), Node()));
         QCOMPARE(t.a, Node(Uri("file://test3.ttl#thing")));
         QCOMPARE(t.c, Node(Uri("file://test3.ttl#wotsit")));
+        delete s2;
     }
 
     void loadMultiBase() {

          
@@ 623,6 624,9 @@ private slots:
                 (Triple(Node(Uri("http://breakfastquay.com/rdf/dataquay/tests#fred")),
                         store.expand(":age"),
                         Node("42", store.expand("xsd:integer")))));
+
+        delete target;
+        delete otherStore;
     }
 
     void loadCompetingBlanks() {

          
M tests/TestTransactionalStore.h +6 -0
@@ 55,6 55,10 @@ private slots:
 	ts = new TransactionalStore(&store);
     }
 
+    void cleanupTestCase() {
+        delete ts;
+    }
+    
     void init() {
 	store.clear();
     }

          
@@ 275,6 279,8 @@ private slots:
 	cchanges = t->getCommittedChanges();
 	QCOMPARE(cchanges, changes);
 
+        delete t;
+
 	t = ts->startTransaction();
 	t->revert(changes);
 	t->commit();