Browse Source

Merge branch 'graph_txn' of scossu/lsup_rdf into master

scossu 1 year ago
parent
commit
cc0c173e5b
5 changed files with 184 additions and 72 deletions
  1. 59 18
      include/graph.h
  2. 43 46
      src/graph.c
  3. 30 8
      src/store_mdb.c
  4. 1 0
      src/term.c
  5. 51 0
      test/test_graph.c

+ 59 - 18
include/graph.h

@@ -12,16 +12,17 @@ typedef struct graph_t LSUP_Graph;
 
 /** @brief Graph iterator.
  *
- * This opaque handle is generated by #LSUP_graph_lookup and is used to iterate
- * over lookup results with #LSUP_graph_iter_next. It must be freed with
- * #LSUP_graph_iter_free when done.
+ * This opaque handle is generated by #LSUP_graph_lookup_txn and is used to
+ * iterate over lookup results with #LSUP_graph_iter_next. It must be freed
+ * with #LSUP_graph_iter_free when done.
  */
 typedef struct graph_iter_t LSUP_GraphIterator;
 
 
 /** @brief Create an empty graph.
  *
- * @param[in] uri URI of the new graph. If NULL, a UUID4 URN is generated.
+ * @param[in] uri URI of the new graph. If NULL, a UUID4 URN is generated. The
+ *  graph owns the handle.
  *
  * @param store_type[in] Type of store backing the graph. One of the values of
  *  #LSUP_StoreType.
@@ -50,12 +51,20 @@ LSUP_graph_new (
  *
  * The destination graph is not initialized here, so the copy is cumulative.
  *
+ * @param[in] txn Transaction handle. It may be NULL.
+ *
  * @param src[in] Source graph.
  *
  * @param dest[in] Destination graph.
  */
 LSUP_rc
-LSUP_graph_copy_contents (const LSUP_Graph *src, LSUP_Graph *dest);
+LSUP_graph_copy_contents_txn (
+        void *txn, const LSUP_Graph *src, LSUP_Graph *dest);
+
+
+/// Non-transactional version of #LSUP_graph_copy_contents_txn.
+#define LSUP_graph_copy_contents(...) \
+    LSUP_graph_copy_contents_txn (NULL, __VA_ARGS__)
 
 
 /** Perform a boolean operation between two graphs.
@@ -64,6 +73,8 @@ LSUP_graph_copy_contents (const LSUP_Graph *src, LSUP_Graph *dest);
  * between two other graphs. The resulting graph may be of any store type and
  * may be the result of graphs of different store types.
  *
+ * @param[in] txn Transaction handle. It may be NULL.
+ *
  * @param op[in] Operation to perform. One of #LSUP_bool_op.
  *
  * @param gr1[in] First operand.
@@ -77,9 +88,13 @@ LSUP_graph_copy_contents (const LSUP_Graph *src, LSUP_Graph *dest);
  * @return LSUP_OK on success; <0 on error.
  */
 LSUP_rc
-LSUP_graph_bool_op(
-        const LSUP_bool_op op, const LSUP_Graph *gr1, const LSUP_Graph *gr2,
-        LSUP_Graph *res);
+LSUP_graph_bool_op_txn (
+        void *txn, const LSUP_bool_op op,
+        const LSUP_Graph *gr1, const LSUP_Graph *gr2, LSUP_Graph *res);
+
+
+/// Non-transactional version of #LSUP_graph_bool_op_txn.
+#define LSUP_graph_bool_op(...) LSUP_graph_bool_op_txn (NULL, __VA_ARGS__)
 
 
 /** @brief Free a graph.
@@ -190,12 +205,14 @@ LSUP_graph_contains (const LSUP_Graph *gr, const LSUP_Triple *spo);
  *
  * @param[in] flags Unused for now, use 0. TODO
  *
+ * @param[out] txn Address to be populated with the new transaction handle.
+ *
  * @return LSUP_OK on success; LSUP_VALUE_ERR if the graph store does not
  *  support transactions; LSUP_TXN_ERR if the store has already an uncommitted
  *  transaction; <0 on other errors from the underlying store.
  */
 LSUP_rc
-LSUP_graph_begin (LSUP_Graph *gr, int flags);
+LSUP_graph_begin (LSUP_Graph *gr, int flags, void **txn);
 
 
 /** @brief Commit a transaction.
@@ -211,7 +228,7 @@ LSUP_graph_begin (LSUP_Graph *gr, int flags);
  * @return LSUP_OK if the transaction was committed successfully; LSUP_NOACTION
  *  if NULL was passed; LSUP_TXN_ERR on error.
  */
-LSUP_rc LSUP_graph_commit (LSUP_Graph *gr);
+LSUP_rc LSUP_graph_commit (LSUP_Graph *gr, void *txn);
 
 
 /** @brief Abort (roll back) a transaction.
@@ -223,10 +240,13 @@ LSUP_rc LSUP_graph_commit (LSUP_Graph *gr);
  *
  * @param[in] gr Graph handle.
  */
-void LSUP_graph_abort (LSUP_Graph *gr);
+void LSUP_graph_abort (LSUP_Graph *gr, void *txn);
 
 
 /** @brief Initialize an iterator to add triples.
+ *
+ * @param[in] txn Transaction handle. It may be NULL. If not NULL, its handle
+ * will be bound to the iterator handle for its whole lifa cycle.
  *
  * @param[in] gr Graph to add to. It is added to the iterator state.
  *
@@ -234,8 +254,11 @@ void LSUP_graph_abort (LSUP_Graph *gr);
  * must be freed with #LSUP_graph_add_done().
  */
 LSUP_GraphIterator *
-LSUP_graph_add_init (LSUP_Graph *gr);
+LSUP_graph_add_init_txn (void *txn, LSUP_Graph *gr);
+
 
+/// Non-transactional version of #LSUP_graph_init_txn.
+#define LSUP_graph_add_init(...) LSUP_graph_add_init_txn (NULL, __VA_ARGS__)
 
 
 /** @brief Add a single triple to the store.
@@ -259,6 +282,8 @@ LSUP_graph_add_done (LSUP_GraphIterator *it);
 
 
 /** @brief Add triples to a graph.
+ *
+ * @param[in] txn Transaction handle. It may be NULL.
  *
  * @param[in] gr Graph to add triples to.
  *
@@ -270,11 +295,17 @@ LSUP_graph_add_done (LSUP_GraphIterator *it);
  *  inserted.
  */
 LSUP_rc
-LSUP_graph_add (
-        LSUP_Graph *gr, const LSUP_Triple trp[], size_t *ct);
+LSUP_graph_add_txn (
+        void *txn, LSUP_Graph *gr, const LSUP_Triple trp[], size_t *ct);
+
+
+/// Non-transactional version of #LSUP_graph_add_txn.
+#define LSUP_graph_add(...) LSUP_graph_add_txn (NULL, __VA_ARGS__)
 
 
 /** @brief Delete triples by a matching pattern.
+ *
+ * @param[in] txn Transaction handle. It may be NULL.
  *
  * @param gr[in] Graph to delete triples from.
  *
@@ -284,12 +315,18 @@ LSUP_graph_add (
  *  deleted.
  */
 LSUP_rc
-LSUP_graph_remove (
-        LSUP_Graph *gr, const LSUP_Term *s, const LSUP_Term *p,
+LSUP_graph_remove_txn (
+        void *txn, LSUP_Graph *gr, const LSUP_Term *s, const LSUP_Term *p,
         const LSUP_Term *o, size_t *ct);
 
 
+/// Non-transactional version of #LSUP_graph_remove_txn.
+#define LSUP_graph_remove(...) LSUP_graph_remove_txn (NULL, __VA_ARGS__)
+
+
 /** Look up triples by a matching pattern and yield an iterator.
+ *
+ * @param[in] txn Transaction handle. It may be NULL.
  *
  * @param gr[in] Graph to look up.
  *
@@ -300,13 +337,17 @@ LSUP_graph_remove (
  *  freed with #LSUP_graph_iter_free after use.
  */
 LSUP_GraphIterator *
-LSUP_graph_lookup (const LSUP_Graph *gr, const LSUP_Term *s,
+LSUP_graph_lookup_txn (void *txn, const LSUP_Graph *gr, const LSUP_Term *s,
         const LSUP_Term *p, const LSUP_Term *o, size_t *ct);
 
 
+/// Non-transactional version of #LSUP_graph_lookup_txn.
+#define LSUP_graph_lookup(...) LSUP_graph_lookup_txn (NULL, __VA_ARGS__)
+
+
 /** @brief Advance a cursor obtained by a lookup and return a matching triple.
  *
- * @param it[in] Iterator handle obtained through #LSUP_graph_lookup.
+ * @param it[in] Iterator handle obtained through #LSUP_graph_lookup_txn.
  *
  * @param spo[out] Triple handle pointer to be populated with the next result.
  * If not NULL, it will allocate a new triple and new terms, and should be

+ 43 - 46
src/graph.c

@@ -12,7 +12,6 @@ struct graph_t {
                                               * NOTE: This is
                                               * NULL for permanent stores.
                                               */
-    void *                  txn;            ///< Store transaction.
 };
 
 struct graph_iter_t {
@@ -87,17 +86,15 @@ LSUP_graph_new (
     if (gr->store->sif->features & LSUP_STORE_PERM) gr->nsm = NULL;
     else gr->nsm = nsm ? nsm : LSUP_default_nsm;
 
-    gr->txn = NULL;
-
     log_debug ("Graph created.");
     return gr;
 }
 
 
 LSUP_rc
-LSUP_graph_bool_op(
-        const LSUP_bool_op op, const LSUP_Graph *gr1, const LSUP_Graph *gr2,
-        LSUP_Graph *res)
+LSUP_graph_bool_op_txn (
+        void *txn, const LSUP_bool_op op,
+        const LSUP_Graph *gr1, const LSUP_Graph *gr2, LSUP_Graph *res)
 {
     LSUP_rc rc = LSUP_NOACTION;
     if (UNLIKELY (
@@ -127,16 +124,16 @@ LSUP_graph_bool_op(
     LSUP_BufferTriple *sspo = BTRP_DUMMY;
     size_t ct;
 
-    add_it = res->store->sif->add_init_fn (res->store->data, res_sc, gr1->txn);
+    add_it = res->store->sif->add_init_fn (res->store->data, res_sc, txn);
 
     if (op == LSUP_BOOL_XOR) {
         // Add triples from gr2 if not found in gr1.
         lu2_it = gr2->store->sif->lookup_fn (
-                gr2->store->data, NULL, NULL, NULL, gr2_sc, NULL, gr1->txn);
+                gr2->store->data, NULL, NULL, NULL, gr2_sc, NULL, txn);
         while (gr2->store->sif->lu_next_fn (lu2_it, sspo, NULL) == LSUP_OK) {
             lu1_it = gr1->store->sif->lookup_fn (
                     gr1->store->data, sspo->s, sspo->p, sspo->o, gr1_sc,
-                    gr1->txn, &ct);
+                    txn, &ct);
             if (ct > 0)
                 res->store->sif->add_iter_fn (add_it, sspo);
             gr1->store->sif->lu_free_fn (lu1_it);
@@ -145,11 +142,11 @@ LSUP_graph_bool_op(
     }
 
     lu1_it = gr1->store->sif->lookup_fn (
-            gr1->store->data, NULL, NULL, NULL, gr1_sc, gr1->txn, NULL);
+            gr1->store->data, NULL, NULL, NULL, gr1_sc, txn, NULL);
     while (gr1->store->sif->lu_next_fn (lu1_it, sspo, NULL) == LSUP_OK) {
         lu2_it = gr2->store->sif->lookup_fn (
                 gr2->store->data, sspo->s, sspo->p, sspo->o, gr2_sc,
-                gr1->txn, &ct);
+                txn, &ct);
         // For XOR and subtraction, add if not found.
         // For intersection, add if found.
         if ((ct == 0) ^ (op == LSUP_BOOL_INTERSECTION))
@@ -246,14 +243,14 @@ LSUP_graph_equals (const LSUP_Graph *gr1, const LSUP_Graph *gr2)
 
 
 LSUP_GraphIterator *
-LSUP_graph_add_init (LSUP_Graph *gr)
+LSUP_graph_add_init_txn (void *txn, LSUP_Graph *gr)
 {
     LSUP_GraphIterator *it;
     CALLOC_GUARD (it, NULL);
 
     LSUP_Buffer *sc = LSUP_term_serialize (gr->uri);
 
-    it->data = gr->store->sif->add_init_fn (gr->store->data, sc, gr->txn);
+    it->data = gr->store->sif->add_init_fn (gr->store->data, sc, txn);
     LSUP_buffer_free (sc);
 
     it->graph = gr;
@@ -272,19 +269,19 @@ LSUP_graph_add_iter (LSUP_GraphIterator *it, const LSUP_Triple *spo)
     if (UNLIKELY (!sspo)) return LSUP_MEM_ERR;
     const LSUP_StoreInt *sif = it->graph->store->sif;
 
-    LSUP_rc rc = sif->add_iter_fn (it->data, sspo);
-    PCHECK (rc, finally);
+    LSUP_rc rc;
+
+    PCHECK (rc = sif->add_iter_fn (it->data, sspo), finally);
 
-    // Store datatype term permanently if the store supports it.
+    // Store datatype term permanently.
     if (rc == LSUP_OK && sif->add_term_fn) {
-        void *txn;
         for (int i = 0; i < 3; i++) {
             LSUP_Term *term = LSUP_triple_pos (spo, i);
             if (term->type == LSUP_TERM_LITERAL) {
                 LSUP_Buffer *ser_dtype = LSUP_term_serialize (term->datatype);
-                // Run add_term in the iterator's txn.
-                txn = sif->iter_txn_fn ? sif->iter_txn_fn (it->data) : NULL;
-                sif->add_term_fn ( it->graph->store->data, ser_dtype, txn);
+                LSUP_rc term_rc = sif->add_term_fn (
+                        it->graph->store->data, ser_dtype, it->data);
+                PCHECK (term_rc, finally);
                 LSUP_buffer_free (ser_dtype);
             }
         }
@@ -307,12 +304,13 @@ LSUP_graph_add_done (LSUP_GraphIterator *it)
 
 
 LSUP_rc
-LSUP_graph_add (LSUP_Graph *gr, const LSUP_Triple trp[], size_t *ct)
+LSUP_graph_add_txn (
+        void *txn, LSUP_Graph *gr, const LSUP_Triple trp[], size_t *ct)
 {
     LSUP_rc rc = LSUP_NOACTION;
 
     // Initialize iterator.
-    LSUP_GraphIterator *it = LSUP_graph_add_init (gr);
+    LSUP_GraphIterator *it = LSUP_graph_add_init_txn (txn, gr);
 
     if (ct) *ct = 0;
     // Serialize and insert RDF triples.
@@ -331,6 +329,7 @@ LSUP_graph_add (LSUP_Graph *gr, const LSUP_Triple trp[], size_t *ct)
             rc = db_rc;
             goto finally;
         }
+        log_trace ("Graph size at end of add iter: %lu", LSUP_graph_size (gr));
     }
 
 finally:
@@ -341,9 +340,10 @@ finally:
 
 
 LSUP_rc
-LSUP_graph_remove (
-        LSUP_Graph *gr, const LSUP_Term *s, const LSUP_Term *p,
-        const LSUP_Term *o, size_t *ct)
+LSUP_graph_remove_txn (
+        void *txn, LSUP_Graph *gr,
+        const LSUP_Term *s, const LSUP_Term *p, const LSUP_Term *o,
+        size_t *ct)
 {
     LSUP_rc rc;
 
@@ -354,7 +354,7 @@ LSUP_graph_remove (
         *sc = LSUP_term_serialize (gr->uri);
 
     rc = gr->store->sif->remove_fn (
-            gr->store->data, ss, sp, so, sc, gr->txn, ct);
+            gr->store->data, ss, sp, so, sc, txn, ct);
 
     LSUP_buffer_free (ss);
     LSUP_buffer_free (sp);
@@ -371,14 +371,16 @@ LSUP_graph_remove (
  * The destination graph is not initialized here, so the copy is cumulative.
  */
 LSUP_rc
-LSUP_graph_copy_contents (const LSUP_Graph *src, LSUP_Graph *dest)
+LSUP_graph_copy_contents_txn (
+        void *txn, const LSUP_Graph *src, LSUP_Graph *dest)
 {
     LSUP_rc rc = LSUP_NOACTION;
 
-    LSUP_GraphIterator *it = LSUP_graph_lookup (src, NULL, NULL, NULL, NULL);
+    LSUP_GraphIterator *it = LSUP_graph_lookup_txn (
+            txn, src, NULL, NULL, NULL, NULL);
 
     LSUP_Triple *spo = NULL;
-    LSUP_GraphIterator *add_it = LSUP_graph_add_init (dest);
+    LSUP_GraphIterator *add_it = LSUP_graph_add_init_txn (txn, dest);
     while (LSUP_graph_iter_next (it, &spo) != LSUP_END) {
         LSUP_rc add_rc = LSUP_graph_add_iter (add_it, spo);
         LSUP_triple_free (spo);
@@ -397,9 +399,10 @@ LSUP_graph_copy_contents (const LSUP_Graph *src, LSUP_Graph *dest)
 
 
 LSUP_GraphIterator *
-LSUP_graph_lookup (
-        const LSUP_Graph *gr, const LSUP_Term *s, const LSUP_Term *p,
-        const LSUP_Term *o, size_t *ct)
+LSUP_graph_lookup_txn (
+        void *txn, const LSUP_Graph *gr,
+        const LSUP_Term *s, const LSUP_Term *p, const LSUP_Term *o,
+        size_t *ct)
 {
     LSUP_GraphIterator *it;
     MALLOC_GUARD (it, NULL);
@@ -411,7 +414,7 @@ LSUP_graph_lookup (
         *sc = LSUP_term_serialize (gr->uri);
 
     it->data = gr->store->sif->lookup_fn (
-            gr->store->data, ss, sp, so, sc, gr->txn, ct);
+            gr->store->data, ss, sp, so, sc, txn, ct);
 
     LSUP_buffer_free (ss);
     LSUP_buffer_free (sp);
@@ -489,30 +492,25 @@ LSUP_graph_contains (const LSUP_Graph *gr, const LSUP_Triple *spo)
 
 
 LSUP_rc
-LSUP_graph_begin (LSUP_Graph *gr, int flags) {
+LSUP_graph_begin (LSUP_Graph *gr, int flags, void **txn) {
     if (!(gr->store->sif->features & LSUP_STORE_TXN)) return LSUP_VALUE_ERR;
 
-    return gr->store->sif->txn_begin_fn(gr->store->data, flags, &gr->txn);
+    return gr->store->sif->txn_begin_fn (gr->store->data, flags, txn);
 }
 
 
 LSUP_rc
-LSUP_graph_commit (LSUP_Graph *gr)
+LSUP_graph_commit (LSUP_Graph *gr, void *txn)
 {
-    LSUP_rc rc = gr->store->sif->txn_commit_fn (gr->txn);
-
-    if (rc == LSUP_OK) gr->txn = NULL;
+    if (!(gr->store->sif->features & LSUP_STORE_TXN)) return LSUP_VALUE_ERR;
 
-    return rc;
+    return gr->store->sif->txn_commit_fn (txn);
 }
 
 
 void
-LSUP_graph_abort (LSUP_Graph *gr)
-{
-    gr->store->sif->txn_abort_fn (gr->txn);
-    gr->txn = NULL;
-}
+LSUP_graph_abort (LSUP_Graph *gr, void *txn)
+{ return gr->store->sif->txn_abort_fn (txn); }
 
 
 LSUP_LinkMap *
@@ -733,4 +731,3 @@ graph_iter_alloc_buffers (LSUP_GraphIterator *it)
  */
 
 size_t LSUP_graph_size (const LSUP_Graph *gr);
-

+ 30 - 8
src/store_mdb.c

@@ -435,7 +435,12 @@ mdbstore_new (const char *id, size_t _unused)
         mdbstore_nsm_put (store, LSUP_default_nsm, txn);
 
         // Index default context.
-        mdbstore_add_term (store, LSUP_default_ctx_buf, txn);
+        // Create a dummy iterator just to use the current txn.
+        MDBIterator *it;
+        CALLOC_GUARD (it, NULL);
+        it->txn = txn;
+        mdbstore_add_term (store, LSUP_default_ctx_buf, it);
+        free (it);
     }
 
     store->flags |= LSSTORE_OPEN;
@@ -544,8 +549,8 @@ mdbiter_txn (void *h)
  * @sa #store_add_init_fn_t
  *
  * @param[in] th Previously opened MDB_txn handle, if the add loop shall be
- *  run within a broader transaction. The transaction must be read-write. The
- *  operation will always open a new transaction that is closed with
+ *  run within an enclosing transaction. The transaction must be read-write.
+ *  The operation will always open a new transaction that is closed with
  *  #mdbstore_add_done() or #mdbstore_add_abort(). If this parameter is not
  *  NULL, the loop transaction will have the passed txn set as its parent.
  */
@@ -1100,16 +1105,32 @@ mdbstore_tkey_exists (MDBStore *store, LSUP_Key tkey)
 #endif
 
 
+/** @brief Add a term to the store.
+ *
+ * @param[in] h #MDBStore handle.
+ *
+ * @param[in] sterm Serialized term to store.
+ *
+ * @param[in] ith #MDBIterator handle. Only the transaction handle inside this
+ * is used. It may be NULL, in which case a new transaction is opened and
+ * closed for the operation.
+ *
+ * @return LSUP_OK on success; <0 on error.
+ */
 static LSUP_rc
-mdbstore_add_term (void *h, const LSUP_Buffer *sterm, void *th)
+mdbstore_add_term (void *h, const LSUP_Buffer *sterm, void *ith)
 {
+    //log_trace ("Adding term to MDB store: %s", sterm->addr);
     MDBStore *store = h;
     int db_rc;
     MDB_val key, data;
 
+    MDBIterator *it = ith;
     MDB_txn *txn;
-    // If store->txn exists, open a child txn, otherwise reuse the same txn.
-    if (th) txn = th;
+    // If a transaction is active in the iterator, use it, otherwise open and
+    // close a new one.
+    bool borrowed_txn = (it && it->txn);
+    if (borrowed_txn) txn = it->txn;
     else RCCK (mdb_txn_begin (store->env, NULL, 0, &txn));
 
     MDB_cursor *cur;
@@ -1125,12 +1146,13 @@ mdbstore_add_term (void *h, const LSUP_Buffer *sterm, void *th)
     db_rc = mdb_cursor_put (cur, &key, &data, MDB_NOOVERWRITE);
     if (db_rc != MDB_KEYEXIST) CHECK (db_rc, fail);
 
-    if (!th) CHECK (db_rc = mdb_txn_commit (txn), fail);
+    if (!borrowed_txn) CHECK (db_rc = mdb_txn_commit (txn), fail);
 
     return LSUP_OK;
 
 fail:
-    if (!th) mdb_txn_abort (txn);
+    if (!borrowed_txn) mdb_txn_abort (txn);
+    log_trace ("Aborted txn for adding term.");
     return LSUP_DB_ERR;
 }
 

+ 1 - 0
src/term.c

@@ -330,6 +330,7 @@ LSUP_term_serialize (const LSUP_Term *term)
     LSUP_Buffer *sterm;
     MALLOC_GUARD (sterm, NULL);
 
+    //log_trace ("Effective term being serialized: %s", tmp_term->data);
     int rc = tpl_jot (
             TPL_MEM, &sterm->addr, &sterm->size, TERM_PACK_FMT,
             &tmp_term->type, &tmp_term->data, &metadata);

+ 51 - 0
test/test_graph.c

@@ -213,6 +213,42 @@ _graph_remove (LSUP_StoreType type)
 }
 
 
+static int
+_graph_txn (LSUP_StoreType type)
+{
+    const LSUP_StoreInt *sif = LSUP_store_int (type);
+    if (!(sif->features & LSUP_STORE_TXN)) return 0;
+
+    if (sif->setup_fn) sif->setup_fn (NULL, true);
+
+    LSUP_Triple *trp = create_triples();
+
+    LSUP_Graph *gr = LSUP_graph_new (
+            LSUP_iriref_new (NULL, NULL), type, NULL, NULL, 0);
+
+    void *txn;
+    size_t ct;
+
+    EXPECT_PASS (LSUP_graph_begin (gr, 0, &txn));
+    EXPECT_PASS (LSUP_graph_add_txn (txn, gr, trp, &ct));
+    LSUP_graph_abort (gr, txn);
+
+    // NOTE that ct reports the count before the txn was aborted. This is
+    // intentional.
+    EXPECT_INT_EQ (ct, 8);
+    EXPECT_INT_EQ (LSUP_graph_size (gr), 0);
+
+    EXPECT_PASS (LSUP_graph_begin (gr, 0, &txn));
+    EXPECT_PASS (LSUP_graph_add_txn (txn, gr, trp, &ct));
+    LSUP_graph_commit (gr, txn);
+
+    EXPECT_INT_EQ (ct, 8);
+    EXPECT_INT_EQ (LSUP_graph_size (gr), 8);
+
+    return 0;
+}
+
+
 static int
 test_environment()
 {
@@ -276,6 +312,20 @@ BACKEND_TBL
 }
 
 
+static int test_graph_txn()
+{
+    /*
+     * Test transactions only if the backend supports them.
+     */
+#define ENTRY(a, b) \
+    if (_graph_txn (LSUP_STORE_##a) != 0) return -1;
+BACKEND_TBL
+#undef ENTRY
+
+    return 0;
+}
+
+
 static int test_graph_copy()
 {
     LSUP_Triple *trp = create_triples();
@@ -329,6 +379,7 @@ int graph_tests()
     RUN (test_graph_lookup);
     RUN (test_graph_remove);
     RUN (test_graph_copy);
+    RUN (test_graph_txn);
 
     return 0;
 }