//! SQLite Serialize/Deserialize API //! //! Provides functions to serialize a database to a byte array and deserialize //! a byte array back into a database. This is useful for: //! //! - Transferring databases over network connections //! - Storing databases as BLOBs in other databases //! - Creating in-memory snapshots of databases //! - Implementing custom backup solutions //! //! ## Quick Start //! //! ```zig //! const sqlite = @import("zcatsql"); //! //! // Serialize a database to bytes //! var db = try sqlite.open(":memory:"); //! try db.exec("CREATE TABLE test (x)"); //! try db.exec("INSERT INTO test VALUES (42)"); //! //! const bytes = try sqlite.serialize.toBytes(&db, allocator, "main"); //! defer allocator.free(bytes); //! //! // Deserialize bytes into a new database //! var db2 = try sqlite.serialize.fromBytes(bytes, ":memory:"); //! defer db2.close(); //! //! // db2 now contains the same data as db //! ``` //! //! ## Notes //! //! - Requires SQLite 3.36.0 or later (SQLITE_ENABLE_DESERIALIZE) //! - Serialized data is in SQLite's native page format //! - Deserialize creates a resizable in-memory database const std = @import("std"); const c = @import("c.zig").c; const errors = @import("errors.zig"); const Database = @import("database.zig").Database; const Error = errors.Error; const resultToError = errors.resultToError; /// Serialization flags for sqlite3_serialize. pub const SerializeFlags = packed struct { /// Return no copy - the returned pointer points to SQLite's internal buffer. /// The caller must not modify or free this pointer, and it becomes invalid /// when the database changes. no_copy: bool = false, _padding: u31 = 0, pub fn toInt(self: SerializeFlags) c_uint { return @bitCast(self); } }; /// Deserialization flags for sqlite3_deserialize. pub const DeserializeFlags = packed struct { /// The deserialized database is read-only. read_only: bool = false, /// Free the byte array when done (SQLite takes ownership). free_on_close: bool = false, /// The database can grow by resizing the byte array. resizable: bool = false, _padding: u29 = 0, pub fn toInt(self: DeserializeFlags) c_uint { return @bitCast(self); } }; // ============================================================================ // Core Serialize/Deserialize Functions // ============================================================================ /// Serializes a database to a byte array. /// /// This function creates a copy of the database in its native SQLite format. /// The returned slice is owned by the caller and must be freed with the /// provided allocator. /// /// Parameters: /// - `db`: The database to serialize /// - `allocator`: Allocator for the returned byte array /// - `schema`: Database schema to serialize ("main", "temp", or attached db name) /// /// Returns: Slice containing the serialized database, or error /// /// Example: /// ```zig /// const bytes = try serialize.toBytes(&db, allocator, "main"); /// defer allocator.free(bytes); /// // Save bytes to file, send over network, etc. /// ``` pub fn toBytes(db: *Database, allocator: std.mem.Allocator, schema: [:0]const u8) ![]u8 { var size: i64 = 0; const ptr = c.sqlite3_serialize( db.handle, schema.ptr, &size, 0, // Copy the data ); if (ptr == null) { // sqlite3_serialize returns NULL for empty databases or on error // Check if the database is empty by querying its size if (size == 0) { // Empty database - return empty slice return &[_]u8{}; } return Error.OutOfMemory; } defer c.sqlite3_free(ptr); const len: usize = @intCast(size); const result = try allocator.alloc(u8, len); errdefer allocator.free(result); const src: [*]const u8 = @ptrCast(ptr); @memcpy(result, src[0..len]); return result; } /// Serializes a database without copying (returns pointer to internal buffer). /// /// WARNING: The returned slice points to SQLite's internal buffer and: /// - Must NOT be freed by the caller /// - Becomes invalid when the database is modified or closed /// - Is only suitable for immediate read-only operations /// /// Use `toBytes` for a safe copy that persists independently. /// /// Parameters: /// - `db`: The database to serialize /// - `schema`: Database schema to serialize /// /// Returns: Slice pointing to internal buffer (do not free!), or null pub fn toBytesNoCopy(db: *Database, schema: [:0]const u8) ?[]const u8 { var size: i64 = 0; const ptr = c.sqlite3_serialize( db.handle, schema.ptr, &size, c.SQLITE_SERIALIZE_NOCOPY, ); if (ptr == null or size <= 0) return null; const len: usize = @intCast(size); const src: [*]const u8 = @ptrCast(ptr); return src[0..len]; } /// Deserializes a byte array into a database. /// /// Creates a new database connection and populates it with the data from /// the serialized byte array. The new database is resizable and writable /// by default. /// /// Parameters: /// - `data`: Serialized database bytes (from `toBytes` or file) /// - `path`: Path for the database (use ":memory:" for in-memory) /// /// Returns: New database connection, or error /// /// Example: /// ```zig /// // Load serialized bytes from file /// const bytes = try std.fs.cwd().readFileAlloc(allocator, "backup.db", max_size); /// defer allocator.free(bytes); /// /// // Create database from bytes /// var db = try serialize.fromBytes(bytes, ":memory:"); /// defer db.close(); /// ``` pub fn fromBytes(data: []const u8, path: [:0]const u8) Error!Database { return fromBytesWithFlags(data, path, .{ .resizable = true }); } /// Deserializes a byte array into a database with custom flags. /// /// Parameters: /// - `data`: Serialized database bytes /// - `path`: Path for the database /// - `flags`: Deserialization options /// /// Returns: New database connection, or error pub fn fromBytesWithFlags(data: []const u8, path: [:0]const u8, flags: DeserializeFlags) Error!Database { // Open a new database connection var db = try Database.open(path); errdefer db.close(); // Deserialize into the connection try deserializeInto(&db, "main", data, flags); return db; } /// Deserializes a byte array into an existing database connection. /// /// This replaces the contents of the specified schema in the database /// with the deserialized data. /// /// Parameters: /// - `db`: Target database connection /// - `schema`: Schema name ("main", "temp", or attached db) /// - `data`: Serialized database bytes /// - `flags`: Deserialization options /// /// Example: /// ```zig /// var db = try sqlite.open(":memory:"); /// defer db.close(); /// /// // Load data from bytes /// try serialize.deserializeInto(&db, "main", bytes, .{ .resizable = true }); /// ``` pub fn deserializeInto( db: *Database, schema: [:0]const u8, data: []const u8, flags: DeserializeFlags, ) Error!void { if (data.len == 0) { // Nothing to deserialize return; } // SQLite needs the buffer to be allocated with sqlite3_malloc for FREEONCLOSE // We'll always make a copy to be safe const buf_ptr = c.sqlite3_malloc64(@intCast(data.len)); if (buf_ptr == null) return Error.OutOfMemory; const buf: [*]u8 = @ptrCast(buf_ptr); @memcpy(buf[0..data.len], data); // Combine flags - always free the buffer we allocated var combined_flags = flags; combined_flags.free_on_close = true; const result = c.sqlite3_deserialize( db.handle, schema.ptr, buf, @intCast(data.len), @intCast(data.len), combined_flags.toInt(), ); if (result != c.SQLITE_OK) { c.sqlite3_free(buf_ptr); return resultToError(result); } } /// Deserializes a byte array as a read-only database. /// /// The database cannot be modified after deserialization. /// This is slightly more efficient than a writable database. pub fn fromBytesReadOnly(data: []const u8, path: [:0]const u8) Error!Database { return fromBytesWithFlags(data, path, .{ .read_only = true }); } // ============================================================================ // High-Level Convenience Functions // ============================================================================ /// Saves a database to a file using serialization. /// /// This is an alternative to the Backup API that works by serializing /// the entire database to memory first. Better for small databases. /// /// Example: /// ```zig /// try serialize.saveToFile(&db, allocator, "backup.sqlite"); /// ``` pub fn saveToFile(db: *Database, allocator: std.mem.Allocator, path: []const u8) !void { const bytes = try toBytes(db, allocator, "main"); defer allocator.free(bytes); const file = try std.fs.cwd().createFile(path, .{}); defer file.close(); try file.writeAll(bytes); } /// Loads a database from a file using deserialization. /// /// Creates an in-memory database from a file's contents. /// /// Example: /// ```zig /// var db = try serialize.loadFromFile(allocator, "backup.sqlite"); /// defer db.close(); /// ``` pub fn loadFromFile(allocator: std.mem.Allocator, path: []const u8) !Database { const file = try std.fs.cwd().openFile(path, .{}); defer file.close(); const stat = try file.stat(); const bytes = try allocator.alloc(u8, stat.size); defer allocator.free(bytes); const read = try file.readAll(bytes); if (read != stat.size) return error.UnexpectedEndOfFile; return try fromBytes(bytes, ":memory:"); } /// Creates an in-memory clone of a database. /// /// This is useful for creating a snapshot that can be modified without /// affecting the original database. /// /// Example: /// ```zig /// var clone = try serialize.cloneToMemory(&db, allocator); /// defer clone.close(); /// // Modify clone without affecting original db /// ``` pub fn cloneToMemory(db: *Database, allocator: std.mem.Allocator) !Database { const bytes = try toBytes(db, allocator, "main"); defer allocator.free(bytes); return try fromBytes(bytes, ":memory:"); } /// Compares two databases for equality by comparing their serialized forms. /// /// Note: This compares the raw page data, so databases with the same /// logical content but different physical layouts will compare as different. /// Use VACUUM on both databases first for a reliable comparison. pub fn equals(db1: *Database, db2: *Database, allocator: std.mem.Allocator) !bool { const bytes1 = try toBytes(db1, allocator, "main"); defer allocator.free(bytes1); const bytes2 = try toBytes(db2, allocator, "main"); defer allocator.free(bytes2); return std.mem.eql(u8, bytes1, bytes2); } /// Returns the serialized size of a database without creating a copy. /// /// Useful for pre-allocating buffers or checking database size. pub fn serializedSize(db: *Database, schema: [:0]const u8) i64 { var size: i64 = 0; const ptr = c.sqlite3_serialize( db.handle, schema.ptr, &size, c.SQLITE_SERIALIZE_NOCOPY, ); // We don't need to free since we used NOCOPY _ = ptr; return size; }