diff --git a/Cargo.lock b/Cargo.lock index fe01e11720..efebf50629 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3842,6 +3842,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "sqlx-example-sqlite-serialize" +version = "0.1.0" +dependencies = [ + "anyhow", + "sqlx", + "tokio", +] + [[package]] name = "sqlx-example-sqlite-todos" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index c88ab231e2..f85324a55a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ members = [ "examples/postgres/transaction", "examples/sqlite/todos", "examples/sqlite/extension", + "examples/sqlite/serialize", ] [workspace.package] diff --git a/examples/sqlite/serialize/Cargo.toml b/examples/sqlite/serialize/Cargo.toml new file mode 100644 index 0000000000..0ccadebad7 --- /dev/null +++ b/examples/sqlite/serialize/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "sqlx-example-sqlite-serialize" +version = "0.1.0" +edition = "2024" +workspace = "../../../" + +[dependencies] +anyhow = "1.0" +sqlx = { path = "../../../", features = ["sqlite", "sqlite-deserialize", "runtime-tokio"] } +tokio = { version = "1", features = ["rt", "macros"] } diff --git a/examples/sqlite/serialize/src/main.rs b/examples/sqlite/serialize/src/main.rs new file mode 100644 index 0000000000..6982cc1094 --- /dev/null +++ b/examples/sqlite/serialize/src/main.rs @@ -0,0 +1,72 @@ +/// Demonstrates serialize/deserialize by embedding a SQLite database inside a custom +/// binary container format. +/// +/// The container prepends a magic header to the raw SQLite bytes, making it impossible +/// to open directly with `SqliteConnectOptions::filename()`. This is the whole point: +/// `sqlite3_serialize` / `sqlite3_deserialize` let you treat a database as an opaque +/// byte slice that can live inside any format you control. +/// +/// Container layout: +/// [4 bytes] magic: b"SQLX" +/// [n bytes] SQLite database bytes +use sqlx::sqlite::SqliteOwnedBuf; +use sqlx::{Connection, SqliteConnection}; +use std::io::{self, Write}; +use std::path::Path; + +const MAGIC: &[u8; 4] = b"SQLX"; + +fn write_container(path: &Path, db_bytes: &[u8]) -> io::Result<()> { + let mut file = std::fs::File::create(path)?; + file.write_all(MAGIC)?; + file.write_all(db_bytes)?; + Ok(()) +} + +fn read_container(path: &Path) -> io::Result> { + let raw = std::fs::read(path)?; + assert_eq!(&raw[..4], MAGIC, "not a valid container file"); + Ok(raw[4..].to_vec()) +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + let container_path = Path::new("notes.sqlx"); + + let mut conn = SqliteConnection::connect("sqlite::memory:").await?; + + sqlx::raw_sql( + "create table notes(id integer primary key, body text not null); + insert into notes(body) values ('hello'), ('world');", + ) + .execute(&mut conn) + .await?; + + // serialize and persist inside the custom container + let snapshot: SqliteOwnedBuf = conn.serialize(None).await?; + write_container(container_path, snapshot.as_ref())?; + conn.close().await?; + + // restore into a fresh in-memory connection + let db_bytes = read_container(container_path)?; + let owned = SqliteOwnedBuf::try_from(db_bytes.as_slice())?; + let mut restored = SqliteConnection::connect("sqlite::memory:").await?; + restored.deserialize(None, owned, false).await?; + + let rows = sqlx::query_as::<_, (i64, String)>("select id, body from notes order by id") + .fetch_all(&mut restored) + .await?; + assert_eq!(rows.len(), 2); + + sqlx::query("insert into notes(body) values ('from restored connection')") + .execute(&mut restored) + .await?; + + // serialize the updated database back into the container + let updated: SqliteOwnedBuf = restored.serialize(None).await?; + write_container(container_path, updated.as_ref())?; + + std::fs::remove_file(container_path)?; + + Ok(()) +}