zcatsql/src/backup.zig
reugenio 5e28cbe4bf refactor: modularize root.zig into specialized modules
Split monolithic root.zig (4200 lines) into 9 focused modules:
- c.zig: centralized @cImport for SQLite
- errors.zig: Error enum and resultToError
- types.zig: OpenFlags, ColumnType, Limit, enums
- database.zig: Database struct with all methods
- statement.zig: Statement struct with bindings/columns
- functions.zig: UDFs, hooks, and C callbacks
- backup.zig: Backup and Blob I/O
- pool.zig: ConnectionPool (thread-safe)
- root.zig: re-exports + tests (~1100 lines)

Total: ~3600 lines (74% reduction in root.zig)
All 47 tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 19:54:19 +01:00

292 lines
8.3 KiB
Zig

//! SQLite Backup and Blob I/O
//!
//! Provides the Backup struct for database copying and the Blob struct
//! for incremental blob I/O operations.
const std = @import("std");
const c = @import("c.zig").c;
const errors = @import("errors.zig");
const Error = errors.Error;
const resultToError = errors.resultToError;
pub const Database = @import("database.zig").Database;
// ============================================================================
// Backup API
// ============================================================================
/// SQLite online backup handle.
///
/// Allows copying database content from one database to another while
/// both databases are in use.
pub const Backup = struct {
handle: ?*c.sqlite3_backup,
dest_db: *Database,
source_db: *Database,
const Self = @This();
/// Initializes a backup from source to destination database.
pub fn init(
dest_db: *Database,
dest_name: [:0]const u8,
source_db: *Database,
source_name: [:0]const u8,
) Error!Self {
const handle = c.sqlite3_backup_init(
dest_db.handle,
dest_name.ptr,
source_db.handle,
source_name.ptr,
);
if (handle == null) {
const err_code = c.sqlite3_errcode(dest_db.handle);
return resultToError(err_code);
}
return .{
.handle = handle,
.dest_db = dest_db,
.source_db = source_db,
};
}
/// Convenience function to backup the main database.
pub fn initMain(dest_db: *Database, source_db: *Database) Error!Self {
return init(dest_db, "main", source_db, "main");
}
/// Copies up to `n_pages` pages from source to destination.
/// Use -1 to copy all remaining pages in one call.
/// Returns true if there are more pages to copy.
pub fn step(self: *Self, n_pages: i32) Error!bool {
const result = c.sqlite3_backup_step(self.handle, n_pages);
return switch (result) {
c.SQLITE_OK => true,
c.SQLITE_DONE => false,
c.SQLITE_BUSY, c.SQLITE_LOCKED => Error.Busy,
else => resultToError(result),
};
}
/// Copies all remaining pages in one call.
pub fn stepAll(self: *Self) Error!void {
_ = try self.step(-1);
}
/// Returns the number of pages still to be copied.
pub fn remaining(self: *Self) i32 {
return c.sqlite3_backup_remaining(self.handle);
}
/// Returns the total number of pages in the source database.
pub fn pageCount(self: *Self) i32 {
return c.sqlite3_backup_pagecount(self.handle);
}
/// Returns the progress as a percentage (0-100).
pub fn progress(self: *Self) u8 {
const total = self.pageCount();
if (total == 0) return 100;
const done = total - self.remaining();
return @intCast(@divFloor(done * 100, total));
}
/// Finishes the backup operation and releases resources.
pub fn finish(self: *Self) Error!void {
if (self.handle) |h| {
const result = c.sqlite3_backup_finish(h);
self.handle = null;
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
}
/// Alias for finish() for RAII-style usage.
pub fn deinit(self: *Self) void {
self.finish() catch {};
}
};
// ============================================================================
// Blob I/O
// ============================================================================
/// Blob handle for incremental I/O operations.
///
/// Allows reading and writing large BLOBs incrementally without
/// loading the entire blob into memory.
pub const Blob = struct {
handle: ?*c.sqlite3_blob,
db: *Database,
const Self = @This();
/// Opens a blob for incremental I/O.
pub fn open(
db: *Database,
schema: [:0]const u8,
table: [:0]const u8,
column: [:0]const u8,
rowid: i64,
writable: bool,
) Error!Self {
var handle: ?*c.sqlite3_blob = null;
const flags: c_int = if (writable) 1 else 0;
const result = c.sqlite3_blob_open(
db.handle,
schema.ptr,
table.ptr,
column.ptr,
rowid,
flags,
&handle,
);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
return Self{
.handle = handle,
.db = db,
};
}
/// Opens a blob with allocator for runtime strings.
pub fn openAlloc(
db: *Database,
allocator: std.mem.Allocator,
schema: []const u8,
table: []const u8,
column: []const u8,
rowid: i64,
writable: bool,
) !Self {
const schema_z = try allocator.dupeZ(u8, schema);
defer allocator.free(schema_z);
const table_z = try allocator.dupeZ(u8, table);
defer allocator.free(table_z);
const column_z = try allocator.dupeZ(u8, column);
defer allocator.free(column_z);
return Self.open(db, schema_z, table_z, column_z, rowid, writable);
}
/// Closes the blob handle.
pub fn close(self: *Self) Error!void {
if (self.handle) |h| {
const result = c.sqlite3_blob_close(h);
self.handle = null;
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
}
/// Closes the blob handle without checking for errors.
pub fn deinit(self: *Self) void {
if (self.handle) |h| {
_ = c.sqlite3_blob_close(h);
self.handle = null;
}
}
/// Returns the size of the blob in bytes.
pub fn bytes(self: *Self) i32 {
if (self.handle) |h| {
return c.sqlite3_blob_bytes(h);
}
return 0;
}
/// Reads data from the blob.
pub fn read(self: *Self, buffer: []u8, offset: i32) Error!void {
if (self.handle == null) return error.SqliteError;
const result = c.sqlite3_blob_read(
self.handle,
buffer.ptr,
@intCast(buffer.len),
offset,
);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
/// Writes data to the blob.
pub fn write(self: *Self, data: []const u8, offset: i32) Error!void {
if (self.handle == null) return error.SqliteError;
const result = c.sqlite3_blob_write(
self.handle,
data.ptr,
@intCast(data.len),
offset,
);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
/// Moves the blob handle to a different row.
pub fn reopen(self: *Self, rowid: i64) Error!void {
if (self.handle == null) return error.SqliteError;
const result = c.sqlite3_blob_reopen(self.handle, rowid);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
/// Reads the entire blob into a newly allocated buffer.
pub fn readAll(self: *Self, allocator: std.mem.Allocator) ![]u8 {
const size = self.bytes();
if (size <= 0) return &[_]u8{};
const buffer = try allocator.alloc(u8, @intCast(size));
errdefer allocator.free(buffer);
try self.read(buffer, 0);
return buffer;
}
};
// ============================================================================
// Convenience functions
// ============================================================================
/// Copies an entire database to another database.
pub fn backupDatabase(dest_db: *Database, source_db: *Database) Error!void {
var backup = try Backup.initMain(dest_db, source_db);
defer backup.deinit();
try backup.stepAll();
}
/// Copies a database to a file.
pub fn backupToFile(source_db: *Database, path: [:0]const u8) Error!void {
var dest_db = try Database.open(path);
defer dest_db.close();
try backupDatabase(&dest_db, source_db);
}
/// Loads a database from a file into memory.
pub fn loadFromFile(path: [:0]const u8) Error!Database {
var file_db = try Database.open(path);
defer file_db.close();
var mem_db = try Database.open(":memory:");
errdefer mem_db.close();
try backupDatabase(&mem_db, &file_db);
return mem_db;
}