Consistent naming with zcat ecosystem (zcatui, zcatgui, zcatsql). All lowercase per Zig naming conventions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
367 lines
11 KiB
Zig
367 lines
11 KiB
Zig
//! 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;
|
|
}
|