Orchestrates SQLite sync over a shared folder. More...
| Header: | #include <SyncEngine> |
| enum | SyncError { NoError, DatabaseError, TransportError, VersionMismatch, SchemaMismatch, ChangesetError } |
| 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() |
| 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) |
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.
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();
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 });
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()
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.
Error types emitted with the syncErrorOccurred() signal.
| Constant | Value | Description |
|---|---|---|
syncengine::SyncEngine::NoError | 0 | No error. |
syncengine::SyncEngine::DatabaseError | 1 | Failed to open or access the local database. |
syncengine::SyncEngine::TransportError | 2 | Failed to read or write changeset files on the shared folder. |
syncengine::SyncEngine::VersionMismatch | 3 | A remote changeset requires a newer schema version than this client supports. |
syncengine::SyncEngine::SchemaMismatch | 4 | A remote changeset was skipped because the local table schema is incompatible (column count, missing table, or primary key mismatch). |
syncengine::SyncEngine::ChangesetError | 5 | A remote changeset could not be applied (general apply failure). |
[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.
Returns true if the engine is currently running.
Returns the current schema version.
See also setSchemaVersion().
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().
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.
Stops the sync timer and closes the database.
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.