SQLite User Forum

Possible regression SQLite 3.48.0 sqlite3changeset_apply
Login

Possible regression SQLite 3.48.0 sqlite3changeset_apply

(1) By Bart Louwers (bartlouwers) on 2025-01-29 17:02:03 [source]

In SQLite 3.47.2, when a changeset causes a SQLITE_CHANGESET_FOREIGN_KEY conflict, and SQLITE_CHANGESET_OMIT is returned in the conflict handler, sqlite3changeset_apply will return SQLITE_OK.

However, this behavior seems to have changed in 3.48.0, where sqlite3changeset_apply returns SQLITE_CONSTRAINT in this case, even though the change was omitted.

I have prepared a reproduction which you can find below. Note: I have used an LLM to help me write this code, but as you can see it faithfully reproduces the problem. This problem was discovered when updating SQLite to 3.48.0 for the Node.js project. https://github.com/nodejs/node/pull/56654

Different versions have different outputs:

% ./main-3470200 
Conflict type is FOREIGN_KEY as expected.
% ./main-3480000
Error 19 (sqlite3changeset_apply): not an error

main.c

#include <stdio.h>
#include <stdlib.h>
#include <sqlite3.h>

/* Simple error-check helper that aborts on non-OK results. */
static void checkRC(int rc, sqlite3 *db, const char *msg) {
    if (rc != SQLITE_OK) {
        fprintf(stderr, "Error %d (%s): %s\n", rc, msg, sqlite3_errmsg(db));
        sqlite3_close(db);
        exit(1);
    }
}

/* Conflict callback for sqlite3changeset_apply(). */
static int conflictCallback(void *pCtx, int eConflict, sqlite3_changeset_iter *pIter) {
    int *pConflictType = (int*) pCtx;
    *pConflictType = eConflict;
    return SQLITE_CHANGESET_OMIT; /* Skip the conflicting change */
}

int main(void) {
    sqlite3 *db1 = NULL;
    sqlite3 *db2 = NULL;
    int rc;
    char *errMsg = NULL;

    /* Open two in-memory databases */
    rc = sqlite3_open(":memory:", &db1);
    checkRC(rc, db1, "sqlite3_open db1");
    rc = sqlite3_open(":memory:", &db2);
    checkRC(rc, db2, "sqlite3_open db2");

    /* Create schema */
    const char *createDataTableSql = "CREATE TABLE data (key INTEGER PRIMARY KEY, value TEXT)";
    const char *createOtherTableSql = "CREATE TABLE other (key INTEGER PRIMARY KEY, ref REFERENCES data(key))";

    rc = sqlite3_exec(db1, createDataTableSql, NULL, NULL, &errMsg);
    checkRC(rc, db1, "create data table in db1");
    rc = sqlite3_exec(db2, createDataTableSql, NULL, NULL, &errMsg);
    checkRC(rc, db2, "create data table in db2");

    rc = sqlite3_exec(db1, createOtherTableSql, NULL, NULL, &errMsg);
    checkRC(rc, db1, "create other table in db1");
    rc = sqlite3_exec(db2, createOtherTableSql, NULL, NULL, &errMsg);
    checkRC(rc, db2, "create other table in db2");

    /* Insert identical rows */
    rc = sqlite3_exec(db1, "INSERT INTO data (key, value) VALUES (1, 'hello')", NULL, NULL, &errMsg);
    checkRC(rc, db1, "insert into data in db1");
    rc = sqlite3_exec(db2, "INSERT INTO data (key, value) VALUES (1, 'hello')", NULL, NULL, &errMsg);
    checkRC(rc, db2, "insert into data in db2");

    rc = sqlite3_exec(db1, "INSERT INTO other (key, ref) VALUES (1, 1)", NULL, NULL, &errMsg);
    checkRC(rc, db1, "insert into other in db1");
    rc = sqlite3_exec(db2, "INSERT INTO other (key, ref) VALUES (1, 1)", NULL, NULL, &errMsg);
    checkRC(rc, db2, "insert into other in db2");

    /* Delete from 'other' in db1 */
    rc = sqlite3_exec(db1, "DELETE FROM other WHERE key = 1", NULL, NULL, &errMsg);
    checkRC(rc, db1, "delete from other in db1");

    /* Create a session */
    sqlite3_session *pSession = NULL;
    rc = sqlite3session_create(db1, "main", &pSession);
    checkRC(rc, db1, "sqlite3session_create");
    rc = sqlite3session_attach(pSession, "data");
    checkRC(rc, db1, "sqlite3session_attach");

    /* Delete from 'data' in db1 */
    rc = sqlite3_exec(db1, "DELETE FROM data WHERE key = 1", NULL, NULL, &errMsg);
    checkRC(rc, db1, "delete from data in db1");

    /* Enable foreign keys in db2 */
    rc = sqlite3_exec(db2, "PRAGMA foreign_keys = ON", NULL, NULL, &errMsg);
    checkRC(rc, db2, "pragma foreign_keys=ON in db2");

    /* Extract changeset */
    int nChangeset = 0;
    void *pChangeset = NULL;
    rc = sqlite3session_changeset(pSession, &nChangeset, &pChangeset);
    checkRC(rc, db1, "sqlite3session_changeset");
    sqlite3session_delete(pSession);

    /* Apply changeset with correct number of arguments */
    int conflictType = 0;
    rc = sqlite3changeset_apply(db2, nChangeset, pChangeset, NULL, conflictCallback, (void*)&conflictType);
    checkRC(rc, db2, "sqlite3changeset_apply");

    sqlite3_free(pChangeset);

    /* Check conflict type */
    if (conflictType == SQLITE_CHANGESET_FOREIGN_KEY) {
        printf("Conflict type is FOREIGN_KEY as expected.\n");
    } else {
        printf("Unexpected conflict type: %d (expected %d)\n", conflictType, SQLITE_CHANGESET_FOREIGN_KEY);
    }

    /* Clean up */
    sqlite3_close(db1);
    sqlite3_close(db2);
    return 0;
}

(2.1) By Bart Louwers (bartlouwers) on 2025-01-29 17:06:37 edited from 2.0 in reply to 1 [link] [source]

(3) By Dan Kennedy (dan) on 2025-01-29 19:39:18 in reply to 1 [link] [source]

Thanks for reporting this. It is a bug. Now fixed here:

https://sqlite.org/src/info/d7c07581

If you rerun the test suite with the fixed version, can you post here to let us know whether it passed or not?

Thanks,

Dan.

(4) By Bart Louwers (bartlouwers) on 2025-01-29 20:26:48 in reply to 3 [link] [source]

I can confirm that with that commit, both my reproduction example and the test I wrote for Node.js work as expected again.