zcatsql/src/serialize.zig
reugenio c5e6cec4a6 refactor: rename zsqlite to zcatsql
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>
2025-12-09 02:19:52 +01:00

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;
}