Cómo funciona?
Tu aplicación usará una versión modificada de la librería de SQLite la cual contiene el código de LiteSync para acceder a tu base de datos.
Las modificaciones en la librería de SQLite son internas y la interfaz es la misma.
Las librerías LiteSync se comunicarán entre ellas, intercambiándose transacciones de datos.

Replicaciones
La primera vez que la aplicación este abierta conectará con el/los nodo(s) y descargará una copia reciente de la base de datos.
En una centralizada topología el nodo primario enviará la copia de la base de datos a los nodos secundarios.
Una vez esté descargado el nodo comienza la sincronización.
Sincronización
Una vez los nodos tengan la misma base db intercambiarán transacciones que fueron ejecutadas cuando estuvieron fuera de línea.
Luego de esto entrarán en modo en línea y una vez una transacción ha sido ejecutada en un nodo es transferida para ser ejecutada en los nodos conectados.
Si el nodo está fuera de línea entonces la transacción es guardada en un registro local para ser intercambiada luego.
NECESITO CAMBIAR MI APLICACIÓN DE CÓDIGO?
Hay unos cuántos pasos pero básicamente debemos cambiar la cadena URI en la base de datos abriéndola desde aquí:
"file:/path/to/app.db"
para algo así:
"file:/path/to/app.db?node=secondary&connect=tcp://server.ip:1234"
Las buenas noticias son que LiteSync usa la interfaz nativa de SQLite3. significa que no necesitamos usar otra API.
Conección
Cada nodo tiene 2 opciones:
enlazar a una dirección
conectar a la dirección de pares
Entonces puedes elegir cual lado conectará al otro. Esto es indispensable cuando un lado está detrás de un router o firewall.
Topologías Soportadas
Topología Estrella, Centralizada

En esta topología tenemos un nodo en el cual todos los demás nodos estarán conectados, entonces debe estar en línea para que la sincronización entre en juego.
Aquí hay algunas configuraciones de ejemplo:
El nodo primario puede enlazar a una dirección y los nodos secundarios conectarse a ahí.
Nodo primario:
"file:/home/user/app.db?node=primary&bind=tcp://0.0.0.0:1234"
Nodo secundario: (en otro dispositivo)
"file:/home/user/app.db?node=secondary&connect=tcp://server:1234"
El nodo primario también puede conectarse a nodos secundarios.
Nodo primario:
"file:/home/user/app.db?node=primary&connect=tcp://address1:port1,tcp://address2:port2"
Nodos secundarios: (cada uno en un dispositivo separado)
"file:/home/user/app.db?node=secondary&bind=tcp://0.0.0.0:1234"
Incluso podemos usar una mezcla de estas 2 opciones.
Nodo primario:
"file:/home/user/app.db?node=primary&bind=tcp://0.0.0.0:1234&connect=tcp://address1:port1"
Nodo Secundario 1:
"file:/home/user/app.db?node=secondary&connect=tcp://server:1234"
Nodo Secundario 2:
"file:/home/user/app.db?node=secondary&bind=tcp://0.0.0.0:1234"
Estado de Sincronización
Podemos revisar el estado de sincronización usando este comando:
PRAGMA sync_status
Devuelve una cadena JSON.
Revisando si el db está totalmente listo
Si es la primera vez que la aplicación está siendo abierta en un dispositivo puede que descargue una nueva copia de la base de datos desde otro nodo. Hasta que esté hecho no podemos acceder al db.
Podemos recuperar el estado de sincronización y revisar la variable db_is_ready
Revisa los ejemplos de aplicaciones básicas abajo.
Cómo usarlo en mi aplicación?
Hay 3 pasos:
1 Reemplaza la librería de SQLite con el que contiene LiteSync
2 Cambia la cadena de conección URI
3 Revisa el estado listo de db
Cuando se compila aplicaciones C y C++ debes vincular tu aplicación a la librería de LiteSync.
Para otros idiomas debes tener el apropiado envoltorio instalado.
Ejemplo de nodo primario
El nodo primario puede ser una aplicación normal, exactamente la misma aplicación como los nodos secundarios pero usando un URI diferente.
O podemos usar una aplicación dedicada para ser el nodo primario.
Una aplicación independientemente básica usada sólamente para el propósito de mantener un nodo db centralizado que se vería así:
Elige un idioma -->
#include <sqlite3.h> char *uri = "file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234"; int main() { sqlite3 *db; sqlite3_open(uri, &db); /* open the database */ while(1) sleep(1); /* keep the app open */ }
#include <sqlite_modern_cpp.h> #include <thread> #include <chrono> #include <iostream> using namespace sqlite; int main() { try { // open the database database db("file:app.db?node=primary&bind=tcp://0.0.0.0:1234"); // keep the app open while(1) { std::this_thread::sleep_for(std::chrono::seconds(1)); } } catch (std::exception& e) { std::cerr << e.what() << std::endl; } }
import litesync as sqlite3 conn = sqlite3.connect('file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234') # keep the app open import time while True: time.sleep(60) # in seconds
const uri = 'file:app.db?node=primary&bind=tcp://0.0.0.0:1234'; const options = { verbose: console.log }; const db = require('better-sqlite3-litesync')(uri, options); // keep the app open setInterval(function(){}, 5000);
import java.sql.Connection; import java.sql.DriverManager; public class Sample { public static void main(String[] args) { String uri = "file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234"; Connection connection = DriverManager.getConnection("jdbc:sqlite:" + uri); // keep the app open while (true) { Thread.sleep(5000); } } }
using SQLite; public class Program { public static void Main() { // open the database var uri = "file:app.db?node=primary&bind=tcp://0.0.0.0:1234"; var db = new SQLiteConnection(uri); // keep the app open while(true) { System.Threading.Thread.Sleep(5000); } } }
Imports SQLite Public Class Program Public Shared Sub Main() ' open the database Dim db As New SQLiteConnection("file:app.db?node=primary&bind=tcp://0.0.0.0:1234") ' keep the app open Do System.Threading.Thread.Sleep(5000) Loop End Sub End Class
Option Explicit Declare Sub Sleep Lib "kernel32.dll" (ByVal dwMilliseconds As Long) Public Sub Main() Dim URI As String Dim Conn As New ADODB.Connection ' Open the connection URI = "file:C:\app\mydb.db?node=primary&bind=tcp://0.0.0.0:1234" Conn.Open "DRIVER=SQLite3 ODBC Driver;Database=" & URI ' Keep the app open Do: Sleep(5000): Loop End Sub
<?php // with sqlite3: $db = new SQLite3("file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234"); // with pdo_sqlite: $pdo = new PDO("sqlite:file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234"); // keep the app open - it should not be used with apache while(1) sleep(5); ?>
use DBI; my $dbh = DBI->connect("dbi:SQLite:uri=file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234"); // keep the app open - it should not be used with apache sleep;
require 'sqlite3' db = SQLite3::Database.new "file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234" # keep the app open loop do sleep(1) end
local sqlite3 = require("lsqlite3") local db = sqlite3.open('file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234') -- keep the app open local lsocket = require("lsocket") while true do lsocket.select(5000) end
package main import ( "database/sql" _ "github.com/litesync/go-sqlite3" "time" ) func main() { db, err := sql.Open("sqlite3", "file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234") // keep the app open for { time.Sleep(1000 * time.Millisecond) } }
Ejemplo de aplicación básica
Una aplicación básica que escribe a la base de datos local se vería así:
Elige un idioma -->
#include <sqlite3.h> char *uri = "file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"; int main() { sqlite3 *db; /* open the database */ sqlite3_open(&db, uri); /* check if the db is ready */ while(1){ char *json_str = sqlite3_query_value_str(db, "PRAGMA sync_status", NULL); bool db_is_ready = strstr(json_str, "\"db_is_ready\": true") > 0; sqlite3_free(json_str); if (db_is_ready) break; sleep_ms(250); } /* access the database */ start_access(db); } char * sqlite3_query_value_str(sqlite3 *db, char *sql, char **ppErrMsg) { char *ptr = NULL; sqlite3_stmt *stmt; int rc; if (ppErrMsg) *ppErrMsg = NULL; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { if (ppErrMsg) *ppErrMsg = sqlite3_strdup(sqlite3_errmsg(db)); return NULL; } if (sqlite3_step(stmt) == SQLITE_ROW) { char *text = (char *)sqlite3_column_text(stmt, 0); if (text) { ptr = sqlite3_strdup(text); } } sqlite3_finalize(stmt); return ptr; }
#include <iostream> #include <sqlite_modern_cpp.h> #include <unistd.h> using namespace sqlite; using namespace std; int main() { try { // open the database database db("file:app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"); // wait until the database is ready while(1) { string status; db << "pragma sync_status" >> status; cout << "status : " << status << endl; if (status.find("\"db_is_ready\": true") != string::npos) break; sleep(1); } // now the application can access the database // check examples here: // https://github.com/SqliteModernCpp/sqlite_modern_cpp ... } catch (exception& e) { cout << e.what() << endl; } }
import litesync as sqlite3 import json import time conn = sqlite3.connect('file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234') # check if the db is ready while not conn.is_ready(): time.sleep(0.250) start_access(conn)
const uri = 'file:test.db?node=secondary&connect=tcp://127.0.0.1:1234'; const options = { verbose: console.log }; const db = require('better-sqlite3-litesync')(uri, options); db.on('ready', function() { // the database is ready to be accessed db.exec('CREATE TABLE IF NOT EXISTS users (name, email)'); ... });
import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; import org.json.*; public class Sample { public static void main(String[] args) { String uri = "file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"; Connection connection = DriverManager.getConnection("jdbc:sqlite:" + uri); Statement statement = connection.createStatement(); // check if the db is ready while (true) { ResultSet rs = statement.executeQuery("PRAGMA sync_status"); rs.next(); JSONObject obj = new JSONObject(rs.getString(1)); if (obj.getBoolean("db_is_ready")) break; Thread.sleep(250); } // now we can access the db start_access(connection); } }
using SQLite; public class Program { public static void Main() { // open the database var uri = "file:app.db?node=secondary&connect=tcp://server:port"; var db = new SQLiteConnection(uri); // wait until the db is ready while (!db.IsReady()) { System.Threading.Thread.Sleep(250); } // now we can use the database db.CreateTable<TodoItem>(CreateFlags.AutoIncPK); ... } }
Imports SQLite Public Class Program Public Shared Sub Main() ' open the database Dim db As New SQLiteConnection("file:app.db?node=secondary&connect=tcp://server:port") ' wait until the db is ready While Not db.IsReady() System.Threading.Thread.Sleep(250) End While ' now we can use the database db.CreateTable(Of TodoItem)(CreateFlags.AutoIncPK) ' ... End Sub End Class
Option Explicit Declare Sub Sleep Lib "kernel32.dll" (ByVal dwMilliseconds As Long) Public Sub Main() Dim Conn As New ADODB.Connection Dim Rst As ADODB.Recordset Dim URI As String URI = "file:C:\app\mydb.db?node=secondary&connect=tcp://myserver.ddns.net:1234" Conn.Open "DRIVER=SQLite3 ODBC Driver;Database=" & URI ' Check if the database is ready Do Set Rst = New ADODB.Recordset Rst.Open "PRAGMA sync_status", Conn, , , adCmdText If InStr(Rst!sync_status, """db_is_ready"": true") > 0 Then Exit Do Sleep 200 Loop ' Now we can access the db StartDbAccess(Conn) End Sub
<?php // with sqlite3: $db = new SQLite3("file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"); // with pdo_sqlite: $pdo = new PDO("sqlite:file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"); // check if the db is ready while(1) { $results = $db->query('PRAGMA sync_status'); $row = $results->fetchArray(); $status = json_decode($row[0], true); if ($status['db_is_ready'] == true) break; sleep(0.25); } // now we can access the db start_access($db); ?>
use DBI; use JSON qw( decode_json ); my $dbh = DBI->connect("dbi:SQLite:uri=file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"); // check if the db is ready - it should not be used with apache while (1) { my ($result) = $dbh->selectrow_array("PRAGMA sync_status"); my $status = decode_json($result); if ($status->{'db_is_ready'}) last; sleep; } // now we can access the db ...
require 'sqlite3' require 'json' db = SQLite3::Database.new "file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234" # check if the db is ready loop do result = db.get_first_value "PRAGMA sync_status" status = JSON.parse(result) break if status["db_is_ready"] == true sleep 0.25 end # now we can access the db start_access(db)
local sqlite3 = require "lsqlite3" local json = require "json" local db = sqlite3.open('file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234') -- check if the db is ready local lsocket = require("lsocket") while true do local result = db:rows("PRAGMA sync_status") local status = json:decode(result[0]) if status["db_is_ready"] == true then break end lsocket.select(250) end -- now we can access the db start_access(db)
package main import ( "database/sql" _ "github.com/litesync/go-sqlite3" "time" ) func main() { db, err := sql.Open("sqlite3", "file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234") // wait until the db is ready for !db.IsReady() { time.Sleep(1000 * time.Millisecond) } // now we can access the db start_access(db) }
SEGURIDAD
LiteSync utiliza el método de "secreto compartido" para controlar qué nodos pueden ser parte de la red, mediante el cifrado con una clave secreta
Es posible (y recomendado) habilitar el cifrado en la base de datos y en la comunicación entre los nodos
Consulte las instrucciones sobre Cifrado
LIMITACIONES ACTUALES
1 Las funciones no deterministas (que devuelven valores diferentes cada vez que se llaman) están bloqueadas, como random() y date('now'). Use valores explícitos generados en su aplicación
2 La palabra clave AUTOINCREMENT no es compatible, ¡pero no la necesita! (consulte el video para más detalles)
3 Solo una única aplicación puede acceder a la base de datos al mismo tiempo. Cada instancia debe usar su propia base de datos, y luego serán replicadas y sincronizadas usando LiteSync