• QSQLiteSyncEngine
  • SyncEngine
  • syncengine::SyncEngine Class

    class syncengine::SyncEngine

    Orchestrates SQLite sync over a shared folder. More...

    Header: #include <SyncEngine>

    Public Types

    enum SyncError { NoError, DatabaseError, TransportError, VersionMismatch, SchemaMismatch, ChangesetError }

    Public Functions

    SyncEngine(const int &database, const QString &sharedFolderPath, const QString &clientId, QObject *parent = nullptr)
    bool isRunning() const
    int schemaVersion() const
    void setSchemaVersion(int version)
    bool start(int syncIntervalMs = 1000)
    void stop()
    int sync()

    Signals

    void changesetApplied(const QString &filename)
    void conflictResolved(const QString &tableName, int conflictType)
    void syncCompleted(int appliedCount)
    void syncErrorOccurred(syncengine::SyncEngine::SyncError error, const QString &message)

    Detailed Description

    SyncEngine is the main entry point for the sync library. Clients write to their own local SQLite database. When changes are made, the engine captures a binary changeset using the SQLite Session Extension, annotates it with a hybrid logical clock (HLC) timestamp and the client's schema version, and writes it to a shared folder. Other clients watch the folder and apply incoming changesets with automatic conflict resolution.

    The library ships a QSQLITE_SYNC driver plugin which works almost exactly like Qt's built-in QSQLITE driver. The difference? SQLite Session Extension support.

    Once set up, use QSqlDatabase and QSqlQuery as normal and the database changes will push any local changes to the shared folder automatically.

    To pull and apply changes from remote clients, users can either configure it to happen automatically via the start() method, or call the sync() method manually.

    // Open with the QSQLITE_SYNC driver.
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE_SYNC");
    db.setDatabaseName("myapp.db");
    db.open();
    
    // Create the sync engine with a shared folder and client ID.
    SyncEngine engine(db, "/shared/folder", "client-id");
    engine.setSchemaVersion(1);
    engine.start();
    
    // Use QSqlQuery as you normally would for database operations.
    // Writes are pushed to the shared folder automatically.
    QSqlQuery q(db);
    q.exec("CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, name TEXT)");
    q.exec("INSERT INTO items (name) VALUES ('hello')");
    q.prepare("INSERT INTO items (name) VALUES (?)");
    q.bindValue(0, "world");
    q.exec();
    
    // Call sync() to pull and apply changes from other clients.
    engine.sync();

    Transactions, QSqlTableModel, and all other Qt SQL classes work as expected.

    Fallback Usage (without QSQLITE_SYNC driver)

    If the driver plugin is not available (e.g., Qt source tree not installed), the engine falls back to a parallel connection and requires beginWrite() / endWrite() calls to track changes:

    SyncEngine engine("/path/to/local.db", "/shared/folder", "client-id");
    engine.start();
    engine.beginWrite();
    engine.database()->exec("INSERT INTO items (name) VALUES ('hello')");
    engine.endWrite();

    Pushing and Pulling

    Sync has two directions – pushing local changes out and pulling remote changes in – and they work differently:

    Pushing (automatic). When users write to the database via QSqlQuery, the engine's commit hook captures the changes and writes a changeset file to the shared folder immediately.

    Pulling (manual or timed). Remote changesets sitting in the shared folder are NOT applied automatically in the background. Users control when they're applied by calling sync(), either manually or on a timer:

    // Option 1: auto-pull on a timer.
    // start() accepts an interval in milliseconds (default: 1000ms).
    engine.start(5000);  // pull every 5 seconds
    
    // Option 2: pull manually whenever you want.
    engine.start(0);     // disable the timer
    engine.sync();       // pull now

    Users should generally prefer explicit pulling. This is because applying remote changes modifies the local database, which could affect in-progress queries or UI state. By controlling the timing, users can pull at safe points, for example before or after a UI update.

    Listen for the syncCompleted signal to be notified of potential changes:

    connect(&engine, &SyncEngine::syncCompleted, [&](int count) {
        if (count > 0)
            model->select();  // refresh QSqlTableModel
    });

    Schema Versioning

    The engine embeds a schema version in each changeset filename. Clients reject changesets from a newer schema version and emit syncErrorOccurred() with VersionMismatch and an actionable upgrade message. Rejected changesets remain in the shared folder and are automatically retried after the client upgrades.

    engine.setSchemaVersion(2);  // Ideally set before start()

    Conflict Resolution

    When two clients modify the same row, conflicts are resolved using hybrid logical clock (HLC) timestamps: the change with the higher HLC wins. Equal HLCs are broken by client ID for deterministic convergence.

    In practice, this means the last client to write after syncing will win. Syncing advances a client's HLC to be at least as high as all received changesets, so any subsequent local write is guaranteed to have a higher HLC than anything previously synced. The net effect is that the most recent write from a client that is up-to-date takes priority.

    Member Type Documentation

    enum SyncEngine::SyncError

    Error types emitted with the syncErrorOccurred() signal.

    ConstantValueDescription
    syncengine::SyncEngine::NoError0No error.
    syncengine::SyncEngine::DatabaseError1Failed to open or access the local database.
    syncengine::SyncEngine::TransportError2Failed to read or write changeset files on the shared folder.
    syncengine::SyncEngine::VersionMismatch3A remote changeset requires a newer schema version than this client supports.
    syncengine::SyncEngine::SchemaMismatch4A remote changeset was skipped because the local table schema is incompatible (column count, missing table, or primary key mismatch).
    syncengine::SyncEngine::ChangesetError5A remote changeset could not be applied (general apply failure).

    Member Function Documentation

    [explicit] SyncEngine::SyncEngine(const int &database, const QString &sharedFolderPath, const QString &clientId, QObject *parent = nullptr)

    Constructs a SyncEngine from a QSqlDatabase opened with the QSQLITE_SYNC driver. This is the recommended constructor.

    All writes made through QSqlQuery on database are automatically captured as changesets and synced to sharedFolderPath. No manual beginWrite() / endWrite() calls are needed.

    If database uses the plain QSQLITE driver instead, the engine falls back to a parallel connection and auto-capture is disabled. In that case, use beginWrite() / endWrite() to track changes.

    [signal] void SyncEngine::changesetApplied(const QString &filename)

    Emitted when a remote changeset has been successfully applied.

    [signal] void SyncEngine::conflictResolved(const QString &tableName, int conflictType)

    Emitted when a conflict was resolved during changeset application.

    bool SyncEngine::isRunning() const

    Returns true if the engine is currently running.

    int SyncEngine::schemaVersion() const

    Returns the current schema version.

    See also setSchemaVersion().

    void SyncEngine::setSchemaVersion(int version)

    Sets the schema version for this client. Produced changesets are stamped with this version. Incoming changesets from a newer version are rejected with a syncError(). The application is responsible for managing the version number.

    See also schemaVersion().

    bool SyncEngine::start(int syncIntervalMs = 1000)

    Opens the database and begins syncing. If syncIntervalMs is greater than zero, a periodic sync timer is started. Pass 0 to disable automatic syncing. Returns true on success.

    void SyncEngine::stop()

    Stops the sync timer and closes the database.

    int SyncEngine::sync()

    Manually triggers a sync cycle: pulls remote changesets from the shared folder and applies them. Returns the number of changesets applied.

    [signal] void SyncEngine::syncCompleted(int appliedCount)

    Emitted after a sync cycle completes. appliedCount is the number of changesets that were successfully applied.

    [signal] void SyncEngine::syncErrorOccurred(syncengine::SyncEngine::SyncError error, const QString &message)

    Emitted when a sync error occurs. The error type identifies the category of problem, and message contains a human-readable description. Use error for programmatic handling (e.g., prompting the user to upgrade) and message for display or logging.