Hi!

I'm using the SQLite session extension to create changesets, invert them
and apply them to undo previous changes in the database.  (Essentially
what I need to do is persistent savepoints.)

This works well so far, but I recently wondered about the interaction
with UNIQUE constraints.  In particular, let's say that I record a
changeset of the following modification:  I remove some rows from a
table, and *then* I insert new rows that have the same values in UNIQUE
columns as the previously removed ones.  That obviously works fine
because I delete first and *then* insert.

However, my understanding of how the session extension works (according
to its docs) is that when I invert and apply the changeset, it will
*first* insert the deleted rows, and *then* delete the inserted ones.
(Because it inverts each operation but not the order of them.)

I created a simplified example program that performs exactly this on an
in-memory database.  The code (plus the shell script to build it with
the flags I used) is below and can also be found on Github:

  https://gist.github.com/domob1812/a3b78739772a1ff0c001be6cbc600f17

(Perhaps that is easier to view than in the email.)

When I run that on my system (with SQLite 3.16.2 from Debian Stretch), I
get errors printed from ErrorLogger like this:

E0826 10:08:16.840441 21876 test.cpp:16] SQLite error (code 2067): abort
at 16 in [INSERT INTO main."mytable" VALUES(?, ?)]: UNIQUE constraint
failed: mytable.value
E0826 10:08:16.840520 21876 test.cpp:16] SQLite error (code 2067): abort
at 16 in [INSERT INTO main."mytable" VALUES(?, ?)]: UNIQUE constraint
failed: mytable.value

This matches what I expect.  However, it seems that applying the
changeset still works fine, and I get the correct "old" state restored.

Is this just "by chance", or can I safely ignore these errors in this
context and rely on the ability to apply inverted changesets even if
they (intermittently) violate UNIQUE constraints?

Thanks a lot for any insights!

Yours,
Daniel

=================================
Build script:

#!/bin/sh -e

PKGS="sqlite3 libglog"
CFLAGS="`pkg-config --cflags ${PKGS}` -std=c++14 -Wall -Werror -pedantic"
CFLAGS="${CFLAGS} -std=c++14 -Wall -Werror -pedantic"
CFLAGS="${CFLAGS} -DSQLITE_ENABLE_SESSION -DSQLITE_ENABLE_PREUPDATE_HOOK"
LIBS=`pkg-config --libs ${PKGS}`

g++ ${CFLAGS} ${LIBS} test.cpp -o test

=================================
test.cpp:

/* Test code for UNIQUE keys and inverting SQLite changesets.  */

#include <sqlite3.h>

#include <glog/logging.h>

#include <cstdlib>
#include <string>

namespace
{

void
ErrorLogger (void* arg, const int errCode, const char* msg)
{
  LOG (ERROR) << "SQLite error (code " << errCode << "): " << msg;
}

void
Execute (sqlite3* db, const std::string& sql)
{
  VLOG (1) << "Executing SQL:\n" << sql;
  char* err;
  const int rc = sqlite3_exec (db, sql.c_str (), nullptr, nullptr, &err);
  if (rc != SQLITE_OK)
    LOG (FATAL) << "SQL error: " << err;
  sqlite3_free (err);
}

void
Print (sqlite3* db)
{
  const std::string sql = R"(
    SELECT `id`, `value`
      FROM `mytable`
      ORDER BY `id` ASC
  )";

  sqlite3_stmt* stmt;
  CHECK_EQ (sqlite3_prepare_v2 (db, sql.c_str (), sql.size (), &stmt,
nullptr),
            SQLITE_OK);

  while (true)
    {
      const int rc = sqlite3_step (stmt);
      if (rc == SQLITE_DONE)
        break;
      CHECK_EQ (rc, SQLITE_ROW);

      LOG (INFO)
          << "  Row: (" << sqlite3_column_int (stmt, 0)
          << ", " << sqlite3_column_int (stmt, 1) << ")";
    }

  CHECK_EQ (sqlite3_finalize (stmt), SQLITE_OK);
}

int
AbortOnConflict (void* ctx, const int conflict, sqlite3_changeset_iter* it)
{
  LOG (ERROR) << "Changeset application has conflict of type " << conflict;
  return SQLITE_CHANGESET_ABORT;
}

} // anonymous namespace

int
main ()
{
  LOG (INFO)
      << "Using SQLite version " << SQLITE_VERSION
      << " (library version: " << sqlite3_libversion () << ")";

  CHECK_EQ (sqlite3_config (SQLITE_CONFIG_LOG, &ErrorLogger, nullptr),
            SQLITE_OK);

  sqlite3* db;
  CHECK_EQ (sqlite3_open (":memory:", &db), SQLITE_OK);
  LOG (INFO) << "Opened in-memory database";

  Execute (db, R"(
    CREATE TABLE `mytable`
      (`id` INTEGER PRIMARY KEY,
       `value` INTEGER,
       UNIQUE (`value`));
    INSERT INTO `mytable` (`id`, `value`) VALUES (1, 42), (2, 100);
  )");

  LOG (INFO) << "Initial state:";
  Print (db);

  /* Now we modify the table and record the result in a changeset.  The
     modification is valid with respect to the UNIQUE constraint, but only
     because we delete the existing entries first and insert afterwards.  */
  sqlite3_session* session;
  CHECK_EQ (sqlite3session_create (db, "main", &session), SQLITE_OK);
  CHECK_EQ (sqlite3session_attach (session, nullptr), SQLITE_OK);

  Execute (db, R"(
    DELETE FROM `mytable`;
    INSERT INTO `mytable` (`id`, `value`) VALUES (3, 42), (4, 100);
  )");

  LOG (INFO) << "Modified state:";
  Print (db);

  /* Extract the changeset, invert it and apply the inverted changeset to
     undo the previous changes.  This fails with the UNIQUE constraint
     violation because the order of delete/insert is wrong.  */
  int changeSize;
  void* changeBytes;
  CHECK_EQ (sqlite3session_changeset (session, &changeSize, &changeBytes),
            SQLITE_OK);
  sqlite3session_delete (session);
  int invertedSize;
  void* invertedBytes;
  CHECK_EQ (sqlite3changeset_invert (changeSize, changeBytes,
                                     &invertedSize, &invertedBytes),
            SQLITE_OK);
  sqlite3_free (changeBytes);
  LOG (INFO) << "Extracted inverted changeset";

  LOG (INFO) << "Applying inverted changeset...";
  CHECK_EQ (sqlite3changeset_apply (db, invertedSize, invertedBytes,
nullptr,
                                    &AbortOnConflict, nullptr),
            SQLITE_OK);
  sqlite3_free (invertedBytes);

  LOG (INFO) << "Restored state:";
  Print (db);

  CHECK_EQ (sqlite3_close (db), SQLITE_OK);

  return EXIT_SUCCESS;
}

=================================

-- 
https://www.domob.eu/
OpenPGP: 1142 850E 6DFF 65BA 63D6  88A8 B249 2AC4 A733 0737
Namecoin: id/domob -> https://nameid.org/?name=domob
--
3.6.0: Bar-Pri-Ran-Rog-Sam-Val-Wiz
To go: Arc-Cav-Hea-Kni-Mon-Tou

Attachment: signature.asc
Description: OpenPGP digital signature

_______________________________________________
sqlite-users mailing list
sqlite-users@mailinglists.sqlite.org
http://mailinglists.sqlite.org/cgi-bin/mailman/listinfo/sqlite-users

Reply via email to