Files
envr/zig-vendor/zig-sqlite/sqlite.zig
2026-04-26 17:29:37 -04:00

4121 lines
141 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const build_options = @import("build_options");
const debug = std.debug;
const heap = std.heap;
const io = std.io;
const mem = std.mem;
const testing = std.testing;
pub const c = @import("c.zig").c;
const versionGreaterThanOrEqualTo = @import("c.zig").versionGreaterThanOrEqualTo;
pub const ParsedQuery = @import("query.zig").ParsedQuery;
const errors = @import("errors.zig");
pub const errorFromResultCode = errors.errorFromResultCode;
pub const Error = errors.Error;
pub const DetailedError = errors.DetailedError;
const getLastDetailedErrorFromDb = errors.getLastDetailedErrorFromDb;
const getDetailedErrorFromResultCode = errors.getDetailedErrorFromResultCode;
const getTestDb = @import("test.zig").getTestDb;
pub const vtab = @import("vtab.zig");
const helpers = @import("helpers.zig");
test {
_ = @import("vtab.zig");
}
const logger = std.log.scoped(.sqlite);
// Returns true if the passed type is a struct.
fn isStruct(comptime T: type) bool {
const type_info = @typeInfo(T);
return type_info == .@"struct";
}
// Returns true if the passed type will coerce to []const u8.
//
// NOTE(vincent): this is straight from the Zig stdlib before it was removed.
fn isZigString(comptime T: type) bool {
return comptime blk: {
// Only pointer types can be strings, no optionals
const info = @typeInfo(T);
if (info != .pointer) break :blk false;
const ptr = &info.pointer;
// Check for CV qualifiers that would prevent coerction to []const u8
if (ptr.is_volatile or ptr.is_allowzero) break :blk false;
// If it's already a slice, simple check.
if (ptr.size == .slice) {
break :blk ptr.child == u8;
}
// Otherwise check if it's an array type that coerces to slice.
if (ptr.size == .one) {
const child = @typeInfo(ptr.child);
if (child == .array) {
const arr = &child.array;
break :blk arr.child == u8;
}
}
break :blk false;
};
}
/// Text is used to represent a SQLite TEXT value when binding a parameter or reading a column.
pub const Text = struct { data: []const u8 };
/// ZeroBlob is a blob with a fixed length containing only zeroes.
///
/// A ZeroBlob is intended to serve as a placeholder; content can later be written with incremental i/o.
///
/// Here is an example allowing you to write up to 1024 bytes to a blob with incremental i/o.
///
/// try db.exec("INSERT INTO user VALUES(1, ?)", .{}, .{sqlite.ZeroBlob{ .length = 1024 }});
/// const row_id = db.getLastInsertRowID();
///
/// var blob = try db.openBlob(.main, "user", "data", row_id, .{ .write = true });
///
/// var blob_writer = blob.writer();
/// try blob_writer.writeAll("foobar");
///
/// try blob.close();
///
/// Search for "zeroblob" on https://sqlite.org/c3ref/blob_open.html for more details.
pub const ZeroBlob = struct {
length: usize,
};
/// Blob is a wrapper for a sqlite BLOB.
///
/// This type is useful when reading or binding data and for doing incremental i/o.
pub const Blob = struct {
const Self = @This();
pub const OpenFlags = struct {
read: bool = true,
write: bool = false,
};
pub const DatabaseName = union(enum) {
main,
temp,
attached: [:0]const u8,
fn toString(self: @This()) [:0]const u8 {
return switch (self) {
.main => "main",
.temp => "temp",
.attached => |name| name,
};
}
};
// Used when reading or binding data.
data: []const u8,
// Used for incremental i/o.
handle: *c.sqlite3_blob = undefined,
offset: c_int = 0,
size: c_int = 0,
/// close closes the blob.
pub fn close(self: *Self) Error!void {
const result = c.sqlite3_blob_close(self.handle);
if (result != c.SQLITE_OK) {
return errors.errorFromResultCode(result);
}
}
pub const Reader = io.GenericReader(*Self, errors.Error, read);
/// reader returns a io.Reader.
pub fn reader(self: *Self) Reader {
return .{ .context = self };
}
fn read(self: *Self, buffer: []u8) Error!usize {
if (self.offset >= self.size) {
return 0;
}
const tmp_buffer = blk: {
const remaining: usize = @as(usize, @intCast(self.size)) - @as(usize, @intCast(self.offset));
break :blk if (buffer.len > remaining) buffer[0..remaining] else buffer;
};
const result = c.sqlite3_blob_read(
self.handle,
tmp_buffer.ptr,
@intCast(tmp_buffer.len),
self.offset,
);
if (result != c.SQLITE_OK) {
return errors.errorFromResultCode(result);
}
self.offset += @intCast(tmp_buffer.len);
return tmp_buffer.len;
}
pub const Writer = io.GenericWriter(*Self, Error, write);
/// writer returns a io.Writer.
pub fn writer(self: *Self) Writer {
return .{ .context = self };
}
fn write(self: *Self, data: []const u8) Error!usize {
const result = c.sqlite3_blob_write(
self.handle,
data.ptr,
@intCast(data.len),
self.offset,
);
if (result != c.SQLITE_OK) {
return errors.errorFromResultCode(result);
}
self.offset += @intCast(data.len);
return data.len;
}
/// Reset the offset used for reading and writing.
pub fn reset(self: *Self) void {
self.offset = 0;
}
pub const ReopenError = error{
CannotReopenBlob,
};
/// reopen moves this blob to another row of the same table.
///
/// See https://sqlite.org/c3ref/blob_reopen.html.
pub fn reopen(self: *Self, row: i64) ReopenError!void {
const result = c.sqlite3_blob_reopen(self.handle, row);
if (result != c.SQLITE_OK) {
return error.CannotReopenBlob;
}
self.size = c.sqlite3_blob_bytes(self.handle);
self.offset = 0;
}
pub const OpenError = error{
CannotOpenBlob,
};
/// open opens a blob for incremental i/o.
fn open(db: *c.sqlite3, db_name: DatabaseName, table: [:0]const u8, column: [:0]const u8, row: i64, comptime flags: OpenFlags) OpenError!Blob {
comptime if (!flags.read and !flags.write) {
@compileError("must open a blob for either read, write or both");
};
const open_flags: c_int = if (flags.write) 1 else 0;
var blob: Blob = undefined;
const result = c.sqlite3_blob_open(
db,
db_name.toString(),
table,
column,
row,
open_flags,
@ptrCast(&blob.handle),
);
if (result == c.SQLITE_MISUSE) debug.panic("sqlite misuse while opening a blob", .{});
if (result != c.SQLITE_OK) {
return error.CannotOpenBlob;
}
blob.size = c.sqlite3_blob_bytes(blob.handle);
blob.offset = 0;
return blob;
}
};
/// ThreadingMode controls the threading mode used by SQLite.
///
/// See https://sqlite.org/threadsafe.html
pub const ThreadingMode = enum {
/// SingleThread makes SQLite unsafe to use with more than a single thread at once.
SingleThread,
/// MultiThread makes SQLite safe to use with multiple threads at once provided that
/// a single database connection is not by more than a single thread at once.
MultiThread,
/// Serialized makes SQLite safe to use with multiple threads at once with no restriction.
Serialized,
};
/// Diagnostics can be used by the library to give more information in case of failures.
pub const Diagnostics = struct {
message: []const u8 = "",
err: ?DetailedError = null,
pub fn format(self: @This(), writer: anytype) !void {
if (self.err) |err| {
if (self.message.len > 0) {
_ = try writer.print("{{message: {s}, detailed error: {f}}}", .{ self.message, err });
return;
}
_ = try err.format(writer);
return;
}
if (self.message.len > 0) {
_ = try writer.write(self.message);
return;
}
_ = try writer.write("none");
}
};
pub const InitOptions = struct {
/// mode controls how the database is opened.
///
/// Defaults to a in-memory database.
mode: Db.Mode = .Memory,
/// open_flags controls the flags used when opening a database.
///
/// Defaults to a read only database.
open_flags: Db.OpenFlags = .{},
/// threading_mode controls the threading mode used by SQLite.
///
/// Defaults to Serialized.
threading_mode: ThreadingMode = .Serialized,
/// shared_cache controls whether or not concurrent SQLite
/// connections share the same cache.
///
/// Defaults to false.
shared_cache: bool = false,
/// if provided, diags will be populated in case of failures.
diags: ?*Diagnostics = null,
};
fn isThreadSafe() bool {
return c.sqlite3_threadsafe() > 0;
}
/// Db is a wrapper around a SQLite database, providing high-level functions for executing queries.
/// A Db can be opened with a file database or a in-memory database:
///
/// // File database
/// var db = try sqlite.Db.init(.{ .mode = .{ .File = "/tmp/data.db" } });
///
/// // In memory database
/// var db = try sqlite.Db.init(.{ .mode = .{ .Memory = {} } });
///
pub const Db = struct {
const Self = @This();
db: *c.sqlite3,
/// Mode determines how the database will be opened.
///
/// * File means opening the database at this path with sqlite3_open_v2.
/// * Memory means opening the database in memory.
/// This works by opening the :memory: path with sqlite3_open_v2 with the flag SQLITE_OPEN_MEMORY.
pub const Mode = union(enum) {
File: [:0]const u8,
Memory,
};
/// OpenFlags contains various flags used when opening a SQLite databse.
///
/// These flags partially map to the flags defined in https://sqlite.org/c3ref/open.html
/// * write=false and create=false means SQLITE_OPEN_READONLY
/// * write=true and create=false means SQLITE_OPEN_READWRITE
/// * write=true and create=true means SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE
pub const OpenFlags = struct {
write: bool = false,
create: bool = false,
};
pub const InitError = error{
SQLiteBuildNotThreadSafe,
} || Error;
/// init creates a database with the provided options.
pub fn init(options: InitOptions) InitError!Self {
var dummy_diags = Diagnostics{};
var diags = options.diags orelse &dummy_diags;
// Validate the threading mode
if (options.threading_mode != .SingleThread and !isThreadSafe()) {
return error.SQLiteBuildNotThreadSafe;
}
// Compute the flags
var flags: c_int = c.SQLITE_OPEN_URI;
flags |= @as(c_int, if (options.open_flags.write) c.SQLITE_OPEN_READWRITE else c.SQLITE_OPEN_READONLY);
if (options.open_flags.create) {
flags |= c.SQLITE_OPEN_CREATE;
}
if (options.shared_cache) {
flags |= c.SQLITE_OPEN_SHAREDCACHE;
}
switch (options.threading_mode) {
.MultiThread => flags |= c.SQLITE_OPEN_NOMUTEX,
.Serialized => flags |= c.SQLITE_OPEN_FULLMUTEX,
else => {},
}
switch (options.mode) {
.File => |path| {
var db: ?*c.sqlite3 = undefined;
const result = c.sqlite3_open_v2(path.ptr, &db, flags, null);
if (result != c.SQLITE_OK or db == null) {
if (db) |v| {
diags.err = getLastDetailedErrorFromDb(v);
} else {
diags.err = getDetailedErrorFromResultCode(result);
}
return errors.errorFromResultCode(result);
}
return Self{ .db = db.? };
},
.Memory => {
flags |= c.SQLITE_OPEN_MEMORY;
var db: ?*c.sqlite3 = undefined;
const result = c.sqlite3_open_v2(":memory:", &db, flags, null);
if (result != c.SQLITE_OK or db == null) {
if (db) |v| {
diags.err = getLastDetailedErrorFromDb(v);
} else {
diags.err = getDetailedErrorFromResultCode(result);
}
return errors.errorFromResultCode(result);
}
return Self{ .db = db.? };
},
}
}
/// deinit closes the database.
pub fn deinit(self: *Self) void {
_ = c.sqlite3_close(self.db);
}
// getDetailedError returns the detailed error for the last API call if it failed.
pub fn getDetailedError(self: *Self) DetailedError {
return getLastDetailedErrorFromDb(self.db);
}
fn getPragmaQuery(comptime name: []const u8, comptime arg: ?[]const u8) []const u8 {
if (arg) |a| {
return std.fmt.comptimePrint("PRAGMA {s} = {s}", .{ name, a });
}
return std.fmt.comptimePrint("PRAGMA {s}", .{name});
}
/// getLastInsertRowID returns the last inserted rowid.
pub fn getLastInsertRowID(self: *Self) i64 {
const rowid = c.sqlite3_last_insert_rowid(self.db);
return rowid;
}
/// pragmaAlloc is like `pragma` but can allocate memory.
///
/// Useful when the pragma command returns text, for example:
///
/// const journal_mode = try db.pragma([]const u8, allocator, .{}, "journal_mode", null);
///
pub fn pragmaAlloc(self: *Self, comptime Type: type, allocator: mem.Allocator, options: QueryOptions, comptime name: []const u8, comptime arg: ?[]const u8) !?Type {
const query = comptime getPragmaQuery(name, arg);
var stmt = try self.prepare(query);
defer stmt.deinit();
return try stmt.oneAlloc(Type, allocator, options, .{});
}
/// pragma is a convenience function to use the PRAGMA statement.
///
/// Here is how to set a pragma value:
///
/// _ = try db.pragma(void, .{}, "foreign_keys", "1");
///
/// Here is how to query a pragma value:
///
/// const journal_mode = try db.pragma([128:0]const u8, .{}, "journal_mode", null);
///
/// The pragma name must be known at comptime.
///
/// This cannot allocate memory. If your pragma command returns text you must use an array or call `pragmaAlloc`.
pub fn pragma(self: *Self, comptime Type: type, options: QueryOptions, comptime name: []const u8, comptime arg: ?[]const u8) !?Type {
const query = comptime getPragmaQuery(name, arg);
var stmt = try self.prepareWithDiags(query, options);
defer stmt.deinit();
return try stmt.one(Type, options, .{});
}
/// exec is a convenience function which prepares a statement and executes it directly.
pub fn exec(self: *Self, comptime query: []const u8, options: QueryOptions, values: anytype) !void {
var stmt = try self.prepareWithDiags(query, options);
defer stmt.deinit();
try stmt.exec(options, values);
}
/// execDynamic is a convenience function which prepares a statement and executes it directly.
pub fn execDynamic(self: *Self, query: []const u8, options: QueryOptions, values: anytype) !void {
var stmt = try self.prepareDynamicWithDiags(query, options);
defer stmt.deinit();
try stmt.exec(options, values);
}
/// execAlloc is like `exec` but can allocate memory.
pub fn execAlloc(self: *Self, allocator: mem.Allocator, comptime query: []const u8, options: QueryOptions, values: anytype) !void {
var stmt = try self.prepareWithDiags(query, options);
defer stmt.deinit();
try stmt.execAlloc(allocator, options, values);
}
/// one is a convenience function which prepares a statement and reads a single row from the result set.
pub fn one(self: *Self, comptime Type: type, comptime query: []const u8, options: QueryOptions, values: anytype) !?Type {
var stmt = try self.prepareWithDiags(query, options);
defer stmt.deinit();
return try stmt.one(Type, options, values);
}
/// oneDynamic is a convenience function which prepares a statement and reads a single row from the result set.
pub fn oneDynamic(self: *Self, comptime Type: type, query: []const u8, options: QueryOptions, values: anytype) !?Type {
var stmt = try self.prepareDynamicWithDiags(query, options);
defer stmt.deinit();
return try stmt.one(Type, options, values);
}
/// oneAlloc is like `one` but can allocate memory.
pub fn oneAlloc(self: *Self, comptime Type: type, allocator: mem.Allocator, comptime query: []const u8, options: QueryOptions, values: anytype) !?Type {
var stmt = try self.prepareWithDiags(query, options);
defer stmt.deinit();
return try stmt.oneAlloc(Type, allocator, options, values);
}
/// oneDynamicAlloc is like `oneDynamic` but can allocate memory.
pub fn oneDynamicAlloc(self: *Self, comptime Type: type, allocator: mem.Allocator, query: []const u8, options: QueryOptions, values: anytype) !?Type {
var stmt = try self.prepareDynamicWithDiags(query, options);
defer stmt.deinit();
return try stmt.oneAlloc(Type, allocator, options, values);
}
/// prepareWithDiags is like `prepare` but takes an additional options argument.
pub fn prepareWithDiags(self: *Self, comptime query: []const u8, options: QueryOptions) DynamicStatement.PrepareError!blk: {
@setEvalBranchQuota(100000);
break :blk StatementType(.{}, query);
} {
@setEvalBranchQuota(100000);
return StatementType(.{}, query).prepare(self, options, 0);
}
/// prepareDynamicWithDiags is like `prepareDynamic` but takes an additional options argument.
pub fn prepareDynamicWithDiags(self: *Self, query: []const u8, options: QueryOptions) DynamicStatement.PrepareError!DynamicStatement {
return try DynamicStatement.prepare(self, query, options, 0);
}
/// prepare prepares a statement for the `query` provided.
///
/// The query is analysed at comptime to search for bind markers.
/// prepare enforces having as much fields in the `values` tuple as there are bind markers.
///
/// Example usage:
///
/// var stmt = try db.prepare("INSERT INTO foo(id, name) VALUES(?, ?)");
/// defer stmt.deinit();
///
/// The statement returned is only compatible with the number of bind markers in the input query.
/// This is done because we type check the bind parameters when executing the statement later.
///
/// If you want additional error information in case of failures, use `prepareWithDiags`.
pub fn prepare(self: *Self, comptime query: []const u8) DynamicStatement.PrepareError!blk: {
@setEvalBranchQuota(100000);
break :blk StatementType(.{}, query);
} {
@setEvalBranchQuota(100000);
return StatementType(.{}, query).prepare(self, .{}, 0);
}
/// prepareDynamic prepares a dynamic statement for the `query` provided.
///
/// The query will be directly sent to create statement without analysing.
/// That means such statements do not support comptime type-checking.
///
/// Dynamic statement supports host parameter names. See `DynamicStatement`.
pub fn prepareDynamic(self: *Self, query: []const u8) DynamicStatement.PrepareError!DynamicStatement {
return try self.prepareDynamicWithDiags(query, .{});
}
/// rowsAffected returns the number of rows affected by the last statement executed.
pub fn rowsAffected(self: *Self) usize {
return @intCast(c.sqlite3_changes(self.db));
}
/// openBlob opens a blob for incremental i/o.
///
/// Incremental i/o enables writing and reading data using a std.io.Writer and std.io.Reader:
/// * the writer type wraps sqlite3_blob_write, see https://sqlite.org/c3ref/blob_write.html
/// * the reader type wraps sqlite3_blob_read, see https://sqlite.org/c3ref/blob_read.html
///
/// Note that:
/// * the blob must exist before writing; you must use INSERT to create one first (either with data or using a placeholder with ZeroBlob).
/// * the blob is not extensible, if you want to change the blob size you must use an UPDATE statement.
///
/// You can get a std.io.Writer to write data to the blob:
///
/// var blob = try db.openBlob(.main, "mytable", "mycolumn", 1, .{ .write = true });
/// var blob_writer = blob.writer();
///
/// try blob_writer.writeAll(my_data);
///
/// You can get a std.io.Reader to read the blob data:
///
/// var blob = try db.openBlob(.main, "mytable", "mycolumn", 1, .{});
/// var blob_reader = blob.reader();
///
/// const data = try blob_reader.readAlloc(allocator);
///
/// See https://sqlite.org/c3ref/blob_open.html for more details on incremental i/o.
///
pub fn openBlob(self: *Self, db_name: Blob.DatabaseName, table: [:0]const u8, column: [:0]const u8, row: i64, comptime flags: Blob.OpenFlags) Blob.OpenError!Blob {
return Blob.open(self.db, db_name, table, column, row, flags);
}
/// savepoint starts a new named transaction.
///
/// The returned type is a helper useful for managing commits and rollbacks, for example:
///
/// var savepoint = try db.savepoint("foobar");
/// defer savepoint.rollback();
///
/// try db.exec("INSERT INTO foo(id, name) VALUES(?, ?)", .{ 1, "foo" });
///
/// savepoint.commit();
///
pub fn savepoint(self: *Self, name: []const u8) Savepoint.InitError!Savepoint {
return Savepoint.init(self, name);
}
/// CreateFunctionFlag controls the flags used when creating a custom SQL function.
/// See https://sqlite.org/c3ref/c_deterministic.html.
///
/// The flags SQLITE_UTF16LE, SQLITE_UTF16BE are not supported yet. SQLITE_UTF8 is the default and always on.
///
/// SQLITE_DIRECTONLY is only available on SQLite >= 3.30.0 so we create a different type based on the SQLite version.
///
/// TODO(vincent): allow these flags when we know how to handle UTF16 data.
/// TODO(vincent): can we refactor this somehow to share the common stuff ?
pub const CreateFunctionFlag = if (c.SQLITE_VERSION_NUMBER >= 3030000) struct {
/// Equivalent to SQLITE_DETERMINISTIC
deterministic: bool = true,
/// Equivalent to SQLITE_DIRECTONLY
direct_only: bool = true,
fn toCFlags(self: *const @This()) c_int {
var flags: c_int = c.SQLITE_UTF8;
if (self.deterministic) {
flags |= c.SQLITE_DETERMINISTIC;
}
if (self.direct_only) {
flags |= c.SQLITE_DIRECTONLY;
}
return flags;
}
} else struct {
/// Equivalent to SQLITE_DETERMINISTIC
deterministic: bool = true,
fn toCFlags(self: *const @This()) c_int {
var flags: c_int = c.SQLITE_UTF8;
if (self.deterministic) {
flags |= c.SQLITE_DETERMINISTIC;
}
return flags;
}
};
/// Creates an aggregate SQLite function with the given name.
///
/// `step_func` and `finalize_func` must be two functions. The first argument of both functions _must_ be of the type FunctionContext.
///
/// When the SQLite function is called in a statement, `step_func` will be called for each row with the input arguments.
/// Each SQLite argument is converted to a Zig value according to the following rules:
/// * TEXT values can be either sqlite.Text or []const u8
/// * BLOB values can be either sqlite.Blob or []const u8
/// * INTEGER values can be any Zig integer
/// * REAL values can be any Zig float
///
/// The final result of the SQL function call will be what `finalize_func` returns.
pub fn createAggregateFunction(self: *Self, comptime name: [:0]const u8, user_ctx: anytype, comptime step_func: anytype, comptime finalize_func: anytype, comptime create_flags: CreateFunctionFlag) Error!void {
// Validate the functions
const step_fn_info = switch (@typeInfo(@TypeOf(step_func))) {
.@"fn" => |fn_info| fn_info,
else => @compileError("cannot use func, expecting a function"),
};
if (step_fn_info.is_generic) @compileError("step function can't be generic");
if (step_fn_info.is_var_args) @compileError("step function can't be variadic");
const finalize_fn_info = switch (@typeInfo(@TypeOf(finalize_func))) {
.@"fn" => |fn_info| fn_info,
else => @compileError("cannot use func, expecting a function"),
};
if (finalize_fn_info.params.len != 1) @compileError("finalize function must take exactly one argument");
if (finalize_fn_info.is_generic) @compileError("finalize function can't be generic");
if (finalize_fn_info.is_var_args) @compileError("finalize function can't be variadic");
if (step_fn_info.params[0].type.? != finalize_fn_info.params[0].type.?) {
@compileError("both step and finalize functions must have the same first argument and it must be a FunctionContext");
}
if (step_fn_info.params[0].type.? != FunctionContext) {
@compileError("both step and finalize functions must have a first argument of type FunctionContext");
}
// subtract the context argument
const real_args_len = step_fn_info.params.len - 1;
//
const flags = create_flags.toCFlags();
const result = c.sqlite3_create_function_v2(
self.db,
name,
real_args_len,
flags,
user_ctx,
null, // xFunc
struct {
fn xStep(ctx: ?*c.sqlite3_context, argc: c_int, argv: [*c]?*c.sqlite3_value) callconv(.c) void {
debug.assert(argc == real_args_len);
const sqlite_args = argv[0..real_args_len];
var args: std.meta.ArgsTuple(@TypeOf(step_func)) = undefined;
// Pass the function context
args[0] = FunctionContext{ .ctx = ctx };
comptime var i: usize = 0;
inline while (i < real_args_len) : (i += 1) {
// Remember the firt argument is always the function context
const arg = step_fn_info.params[i + 1];
const arg_ptr = &args[i + 1];
const ArgType = arg.type.?;
helpers.setTypeFromValue(ArgType, arg_ptr, sqlite_args[i].?);
}
@call(.auto, step_func, args);
}
}.xStep,
struct {
fn xFinal(ctx: ?*c.sqlite3_context) callconv(.c) void {
var args: std.meta.ArgsTuple(@TypeOf(finalize_func)) = undefined;
// Pass the function context
args[0] = FunctionContext{ .ctx = ctx };
const result = @call(.auto, finalize_func, args);
helpers.setResult(ctx, result);
}
}.xFinal,
null,
);
if (result != c.SQLITE_OK) {
return errors.errorFromResultCode(result);
}
}
/// Creates a scalar SQLite function with the given name.
///
/// When the SQLite function is called in a statement, `func` will be called with the input arguments.
/// Each SQLite argument is converted to a Zig value according to the following rules:
/// * TEXT values can be either sqlite.Text or []const u8
/// * BLOB values can be either sqlite.Blob or []const u8
/// * INTEGER values can be any Zig integer
/// * REAL values can be any Zig float
///
/// The return type of the function is converted to a SQLite value according to the same rules but reversed.
///
pub fn createScalarFunction(self: *Self, func_name: [:0]const u8, comptime func: anytype, comptime create_flags: CreateFunctionFlag) Error!void {
const Type = @TypeOf(func);
const fn_info = switch (@typeInfo(Type)) {
.@"fn" => |fn_info| fn_info,
else => @compileError("expecting a function"),
};
if (fn_info.is_generic) @compileError("function can't be generic");
if (fn_info.is_var_args) @compileError("function can't be variadic");
const ArgTuple = std.meta.ArgsTuple(Type);
//
const flags = create_flags.toCFlags();
const result = c.sqlite3_create_function_v2(
self.db,
func_name,
fn_info.params.len,
flags,
null,
struct {
fn xFunc(ctx: ?*c.sqlite3_context, argc: c_int, argv: [*c]?*c.sqlite3_value) callconv(.c) void {
debug.assert(argc == fn_info.params.len);
const sqlite_args = argv[0..fn_info.params.len];
var fn_args: ArgTuple = undefined;
inline for (fn_info.params, 0..) |arg, i| {
const ArgType = arg.type.?;
helpers.setTypeFromValue(ArgType, &fn_args[i], sqlite_args[i].?);
}
const result = @call(.auto, func, fn_args);
helpers.setResult(ctx, result);
}
}.xFunc,
null,
null,
null,
);
if (result != c.SQLITE_OK) {
return errors.errorFromResultCode(result);
}
}
/// This is a convenience function to run statements that do not need
/// bindings to values, but have multiple commands inside.
///
/// Exmaple: 'create table a(); create table b();'
pub fn execMulti(self: *Self, query: []const u8, options: QueryOptions) !void {
var sql_tail: [*c]const u8 = query.ptr;
while (true) {
const new_query = std.mem.span(sql_tail);
if (new_query.len == 0) return;
var stmt = try DynamicStatement.prepareWithTail(self, new_query, options, 0, &sql_tail);
defer stmt.deinit();
try stmt.exec(options, .{});
}
}
pub fn createVirtualTable(
self: *Self,
comptime name: [:0]const u8,
module_context: *vtab.ModuleContext,
comptime Table: type,
) !void {
const VirtualTableType = vtab.VirtualTable(name, Table);
const result = c.sqlite3_create_module_v2(
self.db,
name,
&VirtualTableType.module,
module_context,
null,
);
if (result != c.SQLITE_OK) {
return errors.errorFromResultCode(result);
}
}
};
/// FunctionContext is the context passed as first parameter in the `step` and `finalize` functions used with `createAggregateFunction`.
/// It provides two functions:
/// * userContext to retrieve the user provided context
/// * aggregateContext to create or retrieve the aggregate context
///
/// Both functions take a type as parameter and take care of casting so the caller doesn't have to do it.
pub const FunctionContext = struct {
ctx: ?*c.sqlite3_context,
pub fn userContext(self: FunctionContext, comptime Type: type) ?Type {
const Types = splitPtrTypes(Type);
_ = Types;
if (c.sqlite3_user_data(self.ctx)) |value| {
return @ptrCast(@alignCast(value));
}
return null;
}
pub fn aggregateContext(self: FunctionContext, comptime Type: type) ?Type {
const Types = splitPtrTypes(Type);
if (c.sqlite3_aggregate_context(self.ctx, @sizeOf(Types.ValueType))) |value| {
return @ptrCast(@alignCast(value));
}
return null;
}
const SplitPtrTypes = struct {
ValueType: type,
PointerType: type,
};
fn splitPtrTypes(comptime Type: type) SplitPtrTypes {
switch (@typeInfo(Type)) {
.pointer => |ptr_info| switch (ptr_info.size) {
.one => return SplitPtrTypes{
.ValueType = ptr_info.child,
.PointerType = Type,
},
else => @compileError("cannot use type " ++ @typeName(Type) ++ ", must be a single-item pointer"),
},
.void => return SplitPtrTypes{
.ValueType = void,
.PointerType = undefined,
},
else => @compileError("cannot use type " ++ @typeName(Type) ++ ", must be a single-item pointer"),
}
}
};
/// Savepoint is a helper type for managing savepoints.
///
/// A savepoint creates a transaction like BEGIN/COMMIT but they're named and can be nested.
/// See https://sqlite.org/lang_savepoint.html.
///
/// You can create a savepoint like this:
///
/// var savepoint = try db.savepoint("foobar");
/// defer savepoint.rollback();
///
/// ...
///
/// Savepoint.commit();
///
/// This is equivalent to BEGIN/COMMIT/ROLLBACK.
///
/// Savepoints are more useful for _nesting_ transactions, for example:
///
/// var savepoint = try db.savepoint("outer");
/// defer savepoint.rollback();
///
/// try db.exec("INSERT INTO foo(id, name) VALUES(?, ?)", .{ 1, "foo" });
///
/// {
/// var savepoint2 = try db.savepoint("inner");
/// defer savepoint2.rollback();
///
/// var i: usize = 0;
/// while (i < 30) : (i += 1) {
/// try db.exec("INSERT INTO foo(id, name) VALUES(?, ?)", .{ 2, "bar" });
/// }
///
/// savepoint2.commit();
/// }
///
/// try db.exec("UPDATE bar SET processed = ? WHERE id = ?", .{ true, 20 });
///
/// savepoint.commit();
///
/// In this example if any query in the inner transaction fail, all previously executed queries are discarded but the outer transaction is untouched.
///
pub const Savepoint = struct {
const Self = @This();
db: *Db,
committed: bool,
commit_stmt: DynamicStatement,
rollback_stmt: DynamicStatement,
pub const InitError = error{
SavepointNameTooShort,
SavepointNameTooLong,
SavepointNameInvalid,
// From execDynamic
ExecReturnedData,
// From DynamiStatement
EmptyQuery,
} || std.mem.Allocator.Error || Error;
fn init(db: *Db, name: []const u8) InitError!Self {
if (name.len < 1) return error.SavepointNameTooShort;
if (name.len > 20) return error.SavepointNameTooLong;
if (!std.ascii.isAlphabetic(name[0])) return error.SavepointNameInvalid;
for (name) |b| {
if (b != '_' and !std.ascii.isAlphanumeric(b)) {
return error.SavepointNameInvalid;
}
}
var buffer: [256]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
const commit_query = try std.fmt.allocPrint(allocator, "RELEASE SAVEPOINT {s}", .{name});
const rollback_query = try std.fmt.allocPrint(allocator, "ROLLBACK TRANSACTION TO SAVEPOINT {s}", .{name});
var res = Self{
.db = db,
.committed = false,
.commit_stmt = try db.prepareDynamic(commit_query),
.rollback_stmt = try db.prepareDynamic(rollback_query),
};
try res.db.execDynamic(
try std.fmt.allocPrint(allocator, "SAVEPOINT {s}", .{name}),
.{},
.{},
);
return res;
}
pub fn commit(self: *Self) void {
self.commit_stmt.exec(.{}, .{}) catch |err| {
const detailed_error = self.db.getDetailedError();
logger.err("unable to release savepoint, error: {}, message: {f}", .{ err, detailed_error });
};
self.committed = true;
}
pub fn rollback(self: *Self) void {
defer {
self.commit_stmt.deinit();
self.rollback_stmt.deinit();
}
if (self.committed) return;
self.rollback_stmt.exec(.{}, .{}) catch |err| {
const detailed_error = self.db.getDetailedError();
std.debug.panic("unable to rollback transaction, error: {}, message: {f}\n", .{ err, detailed_error });
};
}
};
pub const QueryOptions = struct {
/// if provided, diags will be populated in case of failures.
diags: ?*Diagnostics = null,
};
/// Iterator allows iterating over a result set.
///
/// Each call to `next` returns the next row of the result set, or null if the result set is exhausted.
/// Each row will have the type `Type` so the columns returned in the result set must be compatible with this type.
///
/// Here is an example of how to use the iterator:
///
/// const User = struct {
/// name: Text,
/// age: u16,
/// };
///
/// var stmt = try db.prepare("SELECT name, age FROM user");
/// defer stmt.deinit();
///
/// var iter = try stmt.iterator(User, .{});
/// while (try iter.next(.{})) |row| {
/// ...
/// }
///
/// The iterator _must not_ outlive the statement.
pub fn Iterator(comptime Type: type) type {
return struct {
const Self = @This();
const TypeInfo = @typeInfo(Type);
db: *c.sqlite3,
stmt: *c.sqlite3_stmt,
// next scans the next row using the prepared statement.
// If it returns null iterating is done.
//
// This cannot allocate memory. If you need to read TEXT or BLOB columns you need to use arrays or alternatively call nextAlloc.
pub fn next(self: *Self, options: QueryOptions) !?Type {
var dummy_diags = Diagnostics{};
var diags = options.diags orelse &dummy_diags;
const result = c.sqlite3_step(self.stmt);
if (result == c.SQLITE_DONE) {
return null;
}
if (result != c.SQLITE_ROW) {
diags.err = getLastDetailedErrorFromDb(self.db);
return errors.errorFromResultCode(result);
}
const columns = c.sqlite3_column_count(self.stmt);
switch (TypeInfo) {
.int => {
debug.assert(columns == 1);
return self.readInt(Type, 0);
},
.float => {
debug.assert(columns == 1);
return self.readFloat(Type, 0);
},
.bool => {
debug.assert(columns == 1);
return self.readBool(0);
},
.void => {
debug.assert(columns == 1);
},
.array => {
debug.assert(columns == 1);
return try self.readArray(Type, 0);
},
.@"enum" => |TI| {
debug.assert(columns == 1);
if (comptime isZigString(Type.BaseType)) {
@compileError("cannot read into type " ++ @typeName(Type) ++ " ; BaseType " ++ @typeName(Type.BaseType) ++ " requires allocation, use nextAlloc or oneAlloc");
}
if (@typeInfo(Type.BaseType) == .int) {
const inner_value = try self.readField(Type.BaseType, options, 0);
return @enumFromInt(@as(TI.tag_type, @intCast(inner_value)));
}
@compileError("enum column " ++ @typeName(Type) ++ " must have a BaseType of either string or int");
},
.@"struct" => {
std.debug.assert(columns == TypeInfo.@"struct".fields.len);
return try self.readStruct(options);
},
else => @compileError("cannot read into type " ++ @typeName(Type) ++ " ; if dynamic memory allocation is required use nextAlloc or oneAlloc"),
}
}
// nextAlloc is like `next` but can allocate memory.
pub fn nextAlloc(self: *Self, allocator: mem.Allocator, options: QueryOptions) !?Type {
var dummy_diags = Diagnostics{};
var diags = options.diags orelse &dummy_diags;
const result = c.sqlite3_step(self.stmt);
if (result == c.SQLITE_DONE) {
return null;
}
if (result != c.SQLITE_ROW) {
diags.err = getLastDetailedErrorFromDb(self.db);
return errors.errorFromResultCode(result);
}
const columns = c.sqlite3_column_count(self.stmt);
switch (Type) {
[]const u8, []u8 => {
debug.assert(columns == 1);
return try self.readBytes(Type, allocator, 0, .Text);
},
Blob => {
debug.assert(columns == 1);
return try self.readBytes(Blob, allocator, 0, .Blob);
},
Text => {
debug.assert(columns == 1);
return try self.readBytes(Text, allocator, 0, .Text);
},
else => {},
}
switch (TypeInfo) {
.int => {
debug.assert(columns == 1);
return self.readInt(Type, 0);
},
.float => {
debug.assert(columns == 1);
return self.readFloat(Type, 0);
},
.bool => {
debug.assert(columns == 1);
return self.readBool(0);
},
.void => {
debug.assert(columns == 1);
},
.array => {
debug.assert(columns == 1);
return try self.readArray(Type, 0);
},
.pointer => {
debug.assert(columns == 1);
return try self.readPointer(Type, .{
.allocator = allocator,
}, 0);
},
.@"enum" => |TI| {
debug.assert(columns == 1);
const inner_value = try self.readField(Type.BaseType, .{ .allocator = allocator }, 0);
if (comptime isZigString(Type.BaseType)) {
// The inner value is never returned to the user, we must free it ourselves.
defer allocator.free(inner_value);
// TODO(vincent): don't use unreachable
return std.meta.stringToEnum(Type, inner_value) orelse unreachable;
}
if (@typeInfo(Type.BaseType) == .int) {
return @enumFromInt(@as(TI.tag_type, @intCast(inner_value)));
}
@compileError("enum column " ++ @typeName(Type) ++ " must have a BaseType of either string or int");
},
.@"struct" => {
std.debug.assert(columns == TypeInfo.@"struct".fields.len);
return try self.readStruct(.{
.allocator = allocator,
});
},
else => @compileError("cannot read into type " ++ @typeName(Type)),
}
}
// readArray reads a sqlite BLOB or TEXT column into an array of u8.
//
// We also require the array to be the exact size of the data, or have a sentinel;
// otherwise we have no way of communicating the end of the data to the caller.
//
// If the array is too small for the data an error will be returned.
fn readArray(self: *Self, comptime ArrayType: type, _i: usize) !ArrayType {
const i: c_int = @intCast(_i);
const type_info = @typeInfo(ArrayType);
var ret: ArrayType = undefined;
switch (type_info) {
.array => |arr| {
switch (arr.child) {
u8 => {
const size: usize = @intCast(c.sqlite3_column_bytes(self.stmt, i));
if (arr.sentinel()) |sentinel| {
// An array with a sentinel need to be as big as the data, + 1 byte for the sentinel.
if (size >= @as(usize, arr.len)) {
return error.ArrayTooSmall;
}
// Set the sentinel in the result at the correct position.
ret[size] = sentinel;
} else if (size != arr.len) {
// An array without a sentinel must have the exact same size as the data because we can't
// communicate the real size to the caller.
return error.ArraySizeMismatch;
}
const data = c.sqlite3_column_blob(self.stmt, i);
if (data != null) {
const ptr = @as([*c]const u8, @ptrCast(data))[0..size];
mem.copyForwards(u8, ret[0..], ptr);
}
},
else => @compileError("cannot read into array of " ++ @typeName(arr.child)),
}
},
else => @compileError("cannot read into type " ++ @typeName(ret)),
}
return ret;
}
// readInt reads a sqlite INTEGER column into an integer.
fn readInt(self: *Self, comptime IntType: type, i: usize) IntType {
const n = c.sqlite3_column_int64(self.stmt, @intCast(i));
return @intCast(n);
}
// readFloat reads a sqlite REAL column into a float.
fn readFloat(self: *Self, comptime FloatType: type, i: usize) FloatType {
const d = c.sqlite3_column_double(self.stmt, @intCast(i));
return @floatCast(d);
}
// readFloat reads a sqlite INTEGER column into a bool (true is anything > 0, false is anything <= 0).
fn readBool(self: *Self, i: usize) bool {
const d = c.sqlite3_column_int64(self.stmt, @intCast(i));
return d > 0;
}
const ReadBytesMode = enum {
Blob,
Text,
};
// dupeWithSentinel is like dupe/dupeZ but allows for any sentinel value.
fn dupeWithSentinel(comptime SliceType: type, allocator: mem.Allocator, data: []const u8) !SliceType {
switch (@typeInfo(SliceType)) {
.pointer => |ptr_info| {
if (ptr_info.sentinel()) |sentinel| {
const slice = try allocator.alloc(u8, data.len + 1);
mem.copyForwards(u8, slice, data);
slice[data.len] = sentinel;
return slice[0..data.len :sentinel];
} else {
return try allocator.dupe(u8, data);
}
},
else => @compileError("cannot dupe type " ++ @typeName(SliceType)),
}
}
// readBytes reads a sqlite BLOB or TEXT column.
//
// The mode controls which sqlite function is used to retrieve the data:
// * .Blob uses sqlite3_column_blob
// * .Text uses sqlite3_column_text
//
// When using .Blob you can only read into either []const u8, []u8 or Blob.
// When using .Text you can only read into either []const u8, []u8 or Text.
//
// The options must contain an `allocator` field which will be used to create a copy of the data.
fn readBytes(self: *Self, comptime BytesType: type, allocator: mem.Allocator, _i: usize, comptime mode: ReadBytesMode) !BytesType {
const i: c_int = @intCast(_i);
switch (mode) {
.Blob => {
const data = c.sqlite3_column_blob(self.stmt, i);
if (data == null) {
return switch (BytesType) {
Text, Blob => .{ .data = try allocator.dupe(u8, "") },
else => try dupeWithSentinel(BytesType, allocator, ""),
};
}
const size: usize = @intCast(c.sqlite3_column_bytes(self.stmt, i));
const ptr = @as([*c]const u8, @ptrCast(data))[0..size];
if (BytesType == Blob) {
return Blob{ .data = try allocator.dupe(u8, ptr) };
}
return try dupeWithSentinel(BytesType, allocator, ptr);
},
.Text => {
const data = c.sqlite3_column_text(self.stmt, i);
if (data == null) {
return switch (BytesType) {
Text, Blob => .{ .data = try allocator.dupe(u8, "") },
else => try dupeWithSentinel(BytesType, allocator, ""),
};
}
const size: usize = @intCast(c.sqlite3_column_bytes(self.stmt, i));
const ptr = @as([*c]const u8, @ptrCast(data))[0..size];
if (BytesType == Text) {
return Text{ .data = try allocator.dupe(u8, ptr) };
}
return try dupeWithSentinel(BytesType, allocator, ptr);
},
}
}
fn readPointer(self: *Self, comptime PointerType: type, options: anytype, i: usize) !PointerType {
if (!comptime isStruct(@TypeOf(options))) {
@compileError("options passed to readPointer must be a struct");
}
if (!@hasField(@TypeOf(options), "allocator")) {
@compileError("options passed to readPointer must have an allocator field");
}
var ret: PointerType = undefined;
switch (@typeInfo(PointerType)) {
.pointer => |ptr| {
switch (ptr.size) {
.one => {
ret = try options.allocator.create(ptr.child);
errdefer options.allocator.destroy(ret);
ret.* = try self.readField(ptr.child, options, i);
},
.slice => switch (ptr.child) {
u8 => ret = try self.readBytes(PointerType, options.allocator, i, .Text),
else => @compileError("cannot read pointer of type " ++ @typeName(PointerType)),
},
else => @compileError("cannot read pointer of type " ++ @typeName(PointerType)),
}
},
else => @compileError("cannot read pointer of type " ++ @typeName(PointerType)),
}
return ret;
}
fn readOptional(self: *Self, comptime OptionalType: type, options: anytype, _i: usize) !OptionalType {
if (!comptime isStruct(@TypeOf(options))) {
@compileError("options passed to readOptional must be a struct");
}
var ret: OptionalType = undefined;
switch (@typeInfo(OptionalType)) {
.optional => |opt| {
// Easy way to know if the column represents a null value.
const value = c.sqlite3_column_value(self.stmt, @intCast(_i));
const datatype = c.sqlite3_value_type(value);
if (datatype == c.SQLITE_NULL) {
return null;
} else {
const val = try self.readField(opt.child, options, _i);
ret = val;
return ret;
}
},
else => @compileError("cannot read optional of type " ++ @typeName(OptionalType)),
}
}
// readStruct reads an entire sqlite row into a struct.
//
// Each field correspond to a column; its position in the struct determines the column used for it.
// For example, given the following query:
//
// SELECT id, name, age FROM user
//
// The struct must have the following fields:
//
// struct {
// id: usize,
// name: []const u8,
// age: u16,
// }
//
// The field `id` will be associated with the column `id` and so on.
//
// This function relies on the fact that there are the same number of fields than columns and
// that the order is correct.
//
// TODO(vincent): add comptime checks for the fields/columns.
fn readStruct(self: *Self, options: anytype) !Type {
if (!comptime isStruct(@TypeOf(options))) {
@compileError("options passed to readStruct must be a struct");
}
var value: Type = undefined;
inline for (@typeInfo(Type).@"struct".fields, 0..) |field, _i| {
const i = @as(usize, _i);
const ret = try self.readField(field.type, options, i);
@field(value, field.name) = ret;
}
return value;
}
fn readField(self: *Self, comptime FieldType: type, options: anytype, i: usize) !FieldType {
if (!comptime isStruct(@TypeOf(options))) {
@compileError("options passed to readField must be a struct");
}
const field_type_info = @typeInfo(FieldType);
return switch (FieldType) {
Blob => blk: {
if (!@hasField(@TypeOf(options), "allocator")) {
@compileError("options passed to readPointer must have an allocator field when reading a Blob");
}
break :blk try self.readBytes(Blob, options.allocator, i, .Blob);
},
Text => blk: {
if (!@hasField(@TypeOf(options), "allocator")) {
@compileError("options passed to readField must have an allocator field when reading a Text");
}
break :blk try self.readBytes(Text, options.allocator, i, .Text);
},
else => switch (field_type_info) {
.int => self.readInt(FieldType, i),
.float => self.readFloat(FieldType, i),
.bool => self.readBool(i),
.void => {},
.array => try self.readArray(FieldType, i),
.pointer => try self.readPointer(FieldType, options, i),
.optional => try self.readOptional(FieldType, options, i),
.@"enum" => |TI| {
const inner_value = try self.readField(FieldType.BaseType, options, i);
if (comptime isZigString(FieldType.BaseType)) {
// The inner value is never returned to the user, we must free it ourselves.
defer options.allocator.free(inner_value);
return std.meta.stringToEnum(FieldType, inner_value) orelse FieldType.default;
}
if (@typeInfo(FieldType.BaseType) == .int) {
return @enumFromInt(@as(TI.tag_type, @intCast(inner_value)));
}
@compileError("enum column " ++ @typeName(FieldType) ++ " must have a BaseType of either string or int");
},
inline .@"struct", .@"union" => |TI| {
if (TI.layout == .@"packed" and !@hasField(FieldType, "readField")) {
const Backing = @Int(.unsigned, @bitSizeOf(FieldType));
return @bitCast(self.readInt(Backing, i));
}
const inner_value = try self.readField(FieldType.BaseType, options, i);
return try FieldType.readField(options.allocator, inner_value);
},
else => @compileError("cannot populate field of type " ++ @typeName(FieldType)),
},
};
}
};
}
/// StatementType returns the type of a statement you would get by calling Db.prepare and derivatives.
///
/// Useful if you want to store a statement in a struct, for example:
///
/// const MyStatements = struct {
/// insert_stmt: sqlite.StatementType(.{}, insert_query),
/// delete_stmt: sqlite.StatementType(.{}, delete_query),
/// };
///
pub fn StatementType(comptime opts: StatementOptions, comptime query: []const u8) type {
comptime {
@setEvalBranchQuota(100000);
const parsed_query = ParsedQuery(query);
return Statement(opts, parsed_query);
}
}
pub const StatementOptions = struct {};
/// DynamicStatement is a wrapper around a SQLite statement, providing high-level functions to execute
/// a statement and retrieve rows for SELECT queries.
///
/// The difference to `Statement` is that this type isn't bound to a single parsed query and can execute any query.
///
/// `DynamicStatement` supports "host parameter names", which can be used in a query to identify a bind marker:
///
/// SELECT email FROM users WHERE name = @name AND password = $password;
///
/// You can read more about these parameters in the sqlite documentation: https://sqlite.org/c3ref/bind_blob.html
///
/// To use these names use an anonymous struct with corresponding names like this:
///
/// const stmt = "SELECT * FROM users WHERE name = @name AND password = @pasdword";
/// const row = try stmt.one(Row, .{
/// .name = "Tankman",
/// .password = "Passw0rd",
/// });
///
/// This works regardless of the prefix you used in the query.
/// While using the same name with a different prefix is supported by sqlite, `DynamicStatement` doesn't support
/// it because we can't have multiple fields in a struct with the same name.
///
/// You can also use unnamed markers with a tuple:
///
/// const stmt = "SELECT email FROM users WHERE name = ? AND password = ?";
/// const row = try stmt.one(Row, .{"Tankman", "Passw0rd"});
///
/// You can only mix named and unnamed bind markers if:
/// * the bind values data is a tuple (without field names)
/// * the bind values data is a struct with the same field orders as in the query
/// This is because with a unnamed bind markers we use the field index in the struct as bind column; if the fields
/// are in the wrong order the query will not work correctly.
///
pub const DynamicStatement = struct {
db: *c.sqlite3,
stmt: *c.sqlite3_stmt,
const Self = @This();
pub const PrepareError = error{EmptyQuery} || Error;
fn prepare(db: *Db, query: []const u8, options: QueryOptions, flags: c_uint) PrepareError!Self {
return prepareWithTail(db, query, options, flags, null);
}
fn prepareWithTail(db: *Db, query: []const u8, options: QueryOptions, flags: c_uint, tail: ?*[*c]const u8) PrepareError!Self {
var dummy_diags = Diagnostics{};
var diags = options.diags orelse &dummy_diags;
const stmt = blk: {
var tmp: ?*c.sqlite3_stmt = undefined;
const result = c.sqlite3_prepare_v3(
db.db,
query.ptr,
@intCast(query.len),
flags,
&tmp,
tail,
);
if (result != c.SQLITE_OK) {
diags.err = getLastDetailedErrorFromDb(db.db);
return errors.errorFromResultCode(result);
}
if (tmp == null) {
diags.err = .{
.code = 0,
.near = -1,
.message = "the input query is not valid SQL (empty string or a comment)",
};
return error.EmptyQuery;
}
break :blk tmp.?;
};
return Self{
.db = db.db,
.stmt = stmt,
};
}
/// deinit releases the prepared statement.
///
/// After a call to `deinit` the statement must not be used.
pub fn deinit(self: *Self) void {
const result = c.sqlite3_finalize(self.stmt);
if (result != c.SQLITE_OK) {
const detailed_error = getLastDetailedErrorFromDb(self.db);
logger.err("unable to finalize prepared statement, result: {}, detailed error: {f}", .{ result, detailed_error });
}
}
/// reset resets the prepared statement to make it reusable.
pub fn reset(self: *Self) void {
const result = c.sqlite3_clear_bindings(self.stmt);
if (result != c.SQLITE_OK) {
const detailed_error = getLastDetailedErrorFromDb(self.db);
logger.err("unable to clear prepared statement bindings, result: {}, detailed error: {f}", .{ result, detailed_error });
}
const result2 = c.sqlite3_reset(self.stmt);
if (result2 != c.SQLITE_OK) {
const detailed_error = getLastDetailedErrorFromDb(self.db);
logger.err("unable to reset prepared statement, result: {}, detailed error: {f}", .{ result2, detailed_error });
}
}
fn convertResultToError(result: c_int) !void {
if (result != c.SQLITE_OK) {
return errors.errorFromResultCode(result);
}
}
fn bindField(self: *Self, comptime FieldType: type, options: anytype, comptime field_name: []const u8, i: c_int, field: FieldType) !void {
const field_type_info = @typeInfo(FieldType);
const column = i + 1;
switch (FieldType) {
Text => {
const result = c.sqlite3_bind_text(self.stmt, column, field.data.ptr, @intCast(field.data.len), null);
return convertResultToError(result);
},
Blob => {
const result = c.sqlite3_bind_blob(self.stmt, column, field.data.ptr, @intCast(field.data.len), null);
return convertResultToError(result);
},
ZeroBlob => {
const result = c.sqlite3_bind_zeroblob64(self.stmt, column, field.length);
return convertResultToError(result);
},
else => switch (field_type_info) {
.int, .comptime_int => {
const result = c.sqlite3_bind_int64(self.stmt, column, @intCast(field));
return convertResultToError(result);
},
.float, .comptime_float => {
const result = c.sqlite3_bind_double(self.stmt, column, field);
return convertResultToError(result);
},
.bool => {
const result = c.sqlite3_bind_int64(self.stmt, column, @intFromBool(field));
return convertResultToError(result);
},
.pointer => |ptr| switch (ptr.size) {
.one => {
try self.bindField(ptr.child, options, field_name, i, field.*);
},
.slice => switch (ptr.child) {
u8 => {
// NOTE(vincent): The slice must live until after the prepared statement is finaliuzed, therefore we use SQLITE_STATIC to avoid a copy
const result = c.sqlite3_bind_text(self.stmt, column, field.ptr, @intCast(field.len), c.SQLITE_STATIC);
return convertResultToError(result);
},
else => @compileError("cannot bind field " ++ field_name ++ " of type " ++ @typeName(FieldType)),
},
else => @compileError("cannot bind field " ++ field_name ++ " of type " ++ @typeName(FieldType)),
},
.array => |arr| switch (arr.child) {
u8 => {
const data: []const u8 = field[0..field.len];
// NOTE(vincent): The array is temporary and must be copied, therefore we use SQLITE_TRANSIENT
const result = c.sqlite3_bind_text(self.stmt, column, data.ptr, @intCast(data.len), c.sqliteTransientAsDestructor());
return convertResultToError(result);
},
else => @compileError("cannot bind field " ++ field_name ++ " of type array of " ++ @typeName(arr.child)),
},
.optional => |opt| if (field) |non_null_field| {
try self.bindField(opt.child, options, field_name, i, non_null_field);
} else {
const result = c.sqlite3_bind_null(self.stmt, column);
return convertResultToError(result);
},
.null => {
const result = c.sqlite3_bind_null(self.stmt, column);
return convertResultToError(result);
},
.@"enum" => {
if (comptime isZigString(FieldType.BaseType)) {
try self.bindField(FieldType.BaseType, options, field_name, i, @tagName(field));
} else if (@typeInfo(FieldType.BaseType) == .int) {
try self.bindField(FieldType.BaseType, options, field_name, i, @intFromEnum(field));
} else {
@compileError("enum column " ++ @typeName(FieldType) ++ " must have a BaseType of either string or int to bind");
}
},
.@"struct" => |info| {
if (info.layout == .@"packed") {
try self.bindField(info.backing_integer.?, options, field_name, i, @as(info.backing_integer.?, @bitCast(field)));
return;
}
if (!comptime helpers.hasFn(FieldType, "bindField")) {
@compileError("cannot bind field " ++ field_name ++ " of type " ++ @typeName(FieldType) ++ ", consider implementing the bindField() method");
}
const field_value = try field.bindField(options.allocator);
try self.bindField(FieldType.BaseType, options, field_name, i, field_value);
},
.@"union" => |info| {
if (info.layout == .@"packed") {
const Backing = @Int(.signed, @bitSizeOf(FieldType));
try self.bindField(Backing, options, field_name, i, @as(Backing, @bitCast(field)));
return;
}
if (info.tag_type) |UnionTagType| {
inline for (info.fields) |u_field| {
// This wasn't entirely obvious when I saw code like this elsewhere, it works because of type coercion.
// See https://ziglang.org/documentation/master/#Type-Coercion-unions-and-enums
const field_tag: std.meta.Tag(FieldType) = field;
const this_tag: std.meta.Tag(FieldType) = @field(UnionTagType, u_field.name);
if (field_tag == this_tag) {
const field_value = @field(field, u_field.name);
try self.bindField(u_field.type, options, u_field.name, i, field_value);
}
}
} else {
@compileError("cannot bind field " ++ field_name ++ " of type " ++ @typeName(FieldType));
}
},
else => @compileError("cannot bind field " ++ field_name ++ " of type " ++ @typeName(FieldType)),
},
}
}
// bind iterates over the fields in `values` and binds them using `bindField`.
//
// Depending on the query and the type of `values` the binding behaviour differs in regards to the column used.
//
// If `values` is a tuple (and therefore doesn't have field names) then the field _index_ is used.
// This means that if you have a query like this:
//
// SELECT id FROM user WHERE age = ? AND name = ?
//
// You must provide a tuple with the fields in the same order like this:
//
// var iter = try stmt.iterator(.{30, "Vincent"});
//
//
// If `values` is a struct (and therefore has field names) then we check sqlite to see if each field name might be a name bind marker.
// This uses sqlite3_bind_parameter_index and supports bind markers prefixed with ":", "@" and "$".
// For example if you have a query like this:
//
// SELECT id FROM user WHERE age = :age AND name = @name
//
// Then we can provide a struct with fields in any order, like this:
//
// var iter = try stmt.iterator(.{ .age = 30, .name = "Vincent" });
//
// Or
//
// var iter = try stmt.iterator(.{ .name = "Vincent", .age = 30 });
//
// Both will bind correctly.
//
// If however there are no name bind markers then the behaviour will revert to using the field index in the struct, and the fields order must be correct.
fn bind(self: *Self, options: anytype, values: anytype) !void {
const Type = @TypeOf(values);
switch (@typeInfo(Type)) {
.@"struct" => |StructTypeInfo| {
inline for (StructTypeInfo.fields, 0..) |struct_field, struct_field_i| {
const field_value = @field(values, struct_field.name);
const i = sqlite3BindParameterIndex(self.stmt, struct_field.name);
if (i >= 0) {
try self.bindField(struct_field.type, options, struct_field.name, i, field_value);
} else {
try self.bindField(struct_field.type, options, struct_field.name, struct_field_i, field_value);
}
}
},
.pointer => |PointerTypeInfo| {
switch (PointerTypeInfo.size) {
.slice => {
for (values, 0..) |value_to_bind, index| {
try self.bindField(PointerTypeInfo.child, options, "unknown", @intCast(index), value_to_bind);
}
},
else => @compileError("TODO support pointer size " ++ @tagName(PointerTypeInfo.size)),
}
},
.array => |ArrayTypeInfo| {
for (values, 0..) |value_to_bind, index| {
try self.bindField(ArrayTypeInfo.child, options, "unknown", @intCast(index), value_to_bind);
}
},
else => @compileError("Unsupported type for values: " ++ @typeName(Type)),
}
}
fn sqlite3BindParameterIndex(stmt: *c.sqlite3_stmt, comptime name: []const u8) c_int {
if (name.len == 0) return -1;
inline for (.{ ":", "@", "$" }) |prefix| {
const id = prefix ++ name;
const i = c.sqlite3_bind_parameter_index(stmt, id);
if (i > 0) return i - 1; // .bindField uses 0-based while sqlite3 uses 1-based index.
}
return -1;
}
/// exec executes a statement which does not return data.
///
/// The `options` tuple is used to provide additional state in some cases.
///
/// The `values` variable is used for the bind parameters. It must have as many fields as there are bind markers
/// in the input query string.
/// The values that will be bound depend on the numeric name when it's a tuple, or the
/// string name when it's a normal structure.
///
/// Possible errors:
/// - SQLiteError.SQLiteNotFound if some fields not found
pub fn exec(self: *Self, options: QueryOptions, values: anytype) !void {
try self.bind(.{}, values);
var dummy_diags = Diagnostics{};
var diags = options.diags orelse &dummy_diags;
const result = c.sqlite3_step(self.stmt);
switch (result) {
c.SQLITE_DONE => {},
c.SQLITE_ROW => return error.ExecReturnedData,
else => {
diags.err = getLastDetailedErrorFromDb(self.db);
return errors.errorFromResultCode(result);
},
}
}
/// iterator returns an iterator to read data from the result set, one row at a time.
///
/// The data in the row is used to populate a value of the type `Type`.
/// This means that `Type` must have as many fields as is returned in the query
/// executed by this statement.
/// This also means that the type of each field must be compatible with the SQLite type.
///
/// Here is an example of how to use the iterator:
///
/// var iter = try stmt.iterator(usize, .{});
/// while (try iter.next(.{})) |row| {
/// ...
/// }
///
/// The `values` tuple is used for the bind parameters. It must have as many fields as there are bind markers
/// in the input query string.
/// The values that will be bound depend on the numberic name when it's a tuple, or the
/// string name when it's a normal structure.
///
/// The iterator _must not_ outlive the statement.
///
/// Possible errors:
/// - SQLiteError.SQLiteNotFound if some fields not found
pub fn iterator(self: *Self, comptime Type: type, values: anytype) !Iterator(Type) {
try self.bind(.{}, values);
var res: Iterator(Type) = undefined;
res.db = self.db;
res.stmt = self.stmt;
return res;
}
/// iterator returns an iterator to read data from the result set, one row at a time.
///
/// The data in the row is used to populate a value of the type `Type`.
/// This means that `Type` must have as many fields as is returned in the query
/// executed by this statement.
/// This also means that the type of each field must be compatible with the SQLite type.
///
/// Here is an example of how to use the iterator:
///
/// var iter = try stmt.iterator(usize, .{});
/// while (try iter.next(.{})) |row| {
/// ...
/// }
///
/// The `values` tuple is used for the bind parameters. It must have as many fields as there are bind markers
/// in the input query string.
/// The values will be bound depend on the numberic name when it's a tuple, or the
/// string name when it's a normal structure.
///
/// The iterator _must not_ outlive the statement.
///
/// Possible errors:
/// - SQLiteError.SQLiteNotFound if some fields not found
pub fn iteratorAlloc(self: *Self, comptime Type: type, allocator: mem.Allocator, values: anytype) !Iterator(Type) {
try self.bind(.{ .allocator = allocator }, values);
var res: Iterator(Type) = undefined;
res.db = self.db;
res.stmt = self.stmt;
return res;
}
/// one reads a single row from the result set of this statement.
///
/// The data in the row is used to populate a value of the type `Type`.
/// This means that `Type` must have as many fields as is returned in the query
/// executed by this statement.
/// This also means that the type of each field must be compatible with the SQLite type.
///
/// Here is an example of how to use an anonymous struct type:
///
/// const row = try stmt.one(
/// struct {
/// id: usize,
/// name: [400]u8,
/// age: usize,
/// },
/// .{},
/// .{ .foo = "bar", .age = 500 },
/// );
///
/// The `options` tuple is used to provide additional state in some cases.
///
/// The `values` tuple is used for the bind parameters. It must have as many fields as there are bind markers
/// in the input query string.
///
/// This cannot allocate memory. If you need to read TEXT or BLOB columns you need to use arrays or alternatively call `oneAlloc`.
pub fn one(self: *Self, comptime Type: type, options: QueryOptions, values: anytype) !?Type {
var iter = try self.iterator(Type, values);
const row = (try iter.next(options)) orelse return null;
return row;
}
/// oneAlloc is like `one` but can allocate memory.
pub fn oneAlloc(self: *Self, comptime Type: type, allocator: mem.Allocator, options: QueryOptions, values: anytype) !?Type {
var iter = try self.iteratorAlloc(Type, allocator, values);
const row = (try iter.nextAlloc(allocator, options)) orelse return null;
return row;
}
/// all reads all rows from the result set of this statement.
///
/// The data in each row is used to populate a value of the type `Type`.
/// This means that `Type` must have as many fields as is returned in the query
/// executed by this statement.
/// This also means that the type of each field must be compatible with the SQLite type.
///
/// Here is an example of how to use an anonymous struct type:
///
/// const rows = try stmt.all(
/// struct {
/// id: usize,
/// name: []const u8,
/// age: usize,
/// },
/// allocator,
/// .{},
/// .{ .foo = "bar", .age = 500 },
/// );
///
/// The `options` tuple is used to provide additional state in some cases.
///
/// The `values` tuple is used for the bind parameters. It must have as many fields as there are bind markers
/// in the input query string.
///
/// Note that this allocates all rows into a single slice: if you read a lot of data this can use a lot of memory.
pub fn all(self: *Self, comptime Type: type, allocator: mem.Allocator, options: QueryOptions, values: anytype) ![]Type {
var iter = try self.iteratorAlloc(Type, allocator, values);
var rows: std.ArrayList(Type) = .{};
while (try iter.nextAlloc(allocator, options)) |row| {
try rows.append(allocator, row);
}
return rows.toOwnedSlice(allocator);
}
};
/// Statement is a wrapper around a SQLite statement, providing high-level functions to execute
/// a statement and retrieve rows for SELECT queries.
///
/// The exec function can be used to execute a query which does not return rows:
///
/// var stmt = try db.prepare("UPDATE foo SET id = ? WHERE name = ?");
/// defer stmt.deinit();
///
/// try stmt.exec(.{
/// .id = 200,
/// .name = "José",
/// });
///
/// The one function can be used to select a single row:
///
/// var stmt = try db.prepare("SELECT name FROM foo WHERE id = ?");
/// defer stmt.deinit();
///
/// const name = try stmt.one([]const u8, .{}, .{ .id = 200 });
///
/// The all function can be used to select all rows:
///
/// var stmt = try db.prepare("SELECT id, name FROM foo");
/// defer stmt.deinit();
///
/// const Row = struct {
/// id: usize,
/// name: []const u8,
/// };
/// const rows = try stmt.all(Row, .{ .allocator = allocator }, .{});
///
/// Look at each function for more complete documentation.
///
pub fn Statement(comptime opts: StatementOptions, comptime query: anytype) type {
_ = opts;
return struct {
const Self = @This();
dynamic_stmt: DynamicStatement,
fn prepare(db: *Db, options: QueryOptions, flags: c_uint) DynamicStatement.PrepareError!Self {
return Self{
.dynamic_stmt = try DynamicStatement.prepare(db, query.getQuery(), options, flags),
};
}
pub fn dynamic(self: *Self) *DynamicStatement {
return &self.dynamic_stmt;
}
/// deinit releases the prepared statement.
///
/// After a call to `deinit` the statement must not be used.
pub fn deinit(self: *Self) void {
self.dynamic().deinit();
}
/// reset resets the prepared statement to make it reusable.
pub fn reset(self: *Self) void {
self.dynamic().reset();
}
/// bind binds values to every bind marker in the prepared statement.
///
/// The `values` variable must be a struct where each field has the type of the corresponding bind marker.
/// For example this query:
/// SELECT 1 FROM user WHERE name = ?{text} AND age < ?{u32}
///
/// Has two bind markers, so `values` must have at least the following fields:
/// struct {
/// name: Text,
/// age: u32
/// }
///
/// The types are checked at comptime.
fn bind(self: *Self, options: anytype, values: anytype) !void {
const StructType = @TypeOf(values);
if (!comptime isStruct(StructType)) {
@compileError("options passed to Statement.bind must be a struct (DynamicStatement supports runtime slices)");
}
const StructTypeInfo = @typeInfo(StructType).@"struct";
comptime marker_len_check: {
if (query.bind_markers.len != StructTypeInfo.fields.len) {
if (query.bind_markers.len > StructTypeInfo.fields.len) {
var found_markers = 0;
for (query.bind_markers) |bind_marker| {
if (bind_marker.name) |name| {
if (@hasField(StructType, name)) {
found_markers += 1;
}
}
}
if (found_markers == query.bind_markers.len) {
break :marker_len_check;
}
}
@compileError(std.fmt.comptimePrint("expected {d} bind parameters but got {d}", .{
query.bind_markers.len,
StructTypeInfo.fields.len,
}));
}
}
inline for (StructTypeInfo.fields, 0..) |struct_field, _i| {
const bind_marker = query.bind_markers[_i];
if (bind_marker.typed) |typ| {
const FieldTypeInfo = @typeInfo(struct_field.type);
switch (FieldTypeInfo) {
.@"struct", .@"enum", .@"union" => comptime assertMarkerType(
if (@hasDecl(struct_field.type, "BaseType")) struct_field.type.BaseType else struct_field.type,
typ,
),
else => comptime assertMarkerType(struct_field.type, typ),
}
}
}
return self.dynamic().bind(options, values) catch |e| switch (e) {
errors.Error.SQLiteNotFound => unreachable, // impossible to have non-existent field
else => e,
};
}
fn assertMarkerType(comptime Actual: type, comptime Expected: type) void {
if (Actual != Expected) {
@compileError("value type " ++ @typeName(Actual) ++ " is not the bind marker type " ++ @typeName(Expected));
}
}
/// execAlloc is like `exec` but can allocate memory.
pub fn execAlloc(self: *Self, allocator: mem.Allocator, options: QueryOptions, values: anytype) !void {
try self.bind(.{ .allocator = allocator }, values);
var dummy_diags = Diagnostics{};
var diags = options.diags orelse &dummy_diags;
const result = c.sqlite3_step(self.dynamic().stmt);
switch (result) {
c.SQLITE_DONE => {},
else => {
diags.err = getLastDetailedErrorFromDb(self.dynamic().db);
return errors.errorFromResultCode(result);
},
}
}
/// exec executes a statement which does not return data.
///
/// The `options` tuple is used to provide additional state in some cases.
///
/// The `values` variable is used for the bind parameters. It must have as many fields as there are bind markers
/// in the input query string.
///
pub fn exec(self: *Self, options: QueryOptions, values: anytype) !void {
try self.bind(.{}, values);
var dummy_diags = Diagnostics{};
var diags = options.diags orelse &dummy_diags;
const result = c.sqlite3_step(self.dynamic().stmt);
switch (result) {
c.SQLITE_DONE => {},
else => {
diags.err = getLastDetailedErrorFromDb(self.dynamic().db);
return errors.errorFromResultCode(result);
},
}
}
/// iterator returns an iterator to read data from the result set, one row at a time.
///
/// The data in the row is used to populate a value of the type `Type`.
/// This means that `Type` must have as many fields as is returned in the query
/// executed by this statement.
/// This also means that the type of each field must be compatible with the SQLite type.
///
/// Here is an example of how to use the iterator:
///
/// var iter = try stmt.iterator(usize, .{});
/// while (try iter.next(.{})) |row| {
/// ...
/// }
///
/// The `values` tuple is used for the bind parameters. It must have as many fields as there are bind markers
/// in the input query string.
///
/// The iterator _must not_ outlive the statement.
pub fn iterator(self: *Self, comptime Type: type, values: anytype) !Iterator(Type) {
try self.bind(.{}, values);
var res: Iterator(Type) = undefined;
res.db = self.dynamic().db;
res.stmt = self.dynamic().stmt;
return res;
}
/// iteratorAlloc is like `iterator` but can allocate memory.
pub fn iteratorAlloc(self: *Self, comptime Type: type, allocator: mem.Allocator, values: anytype) !Iterator(Type) {
try self.bind(.{ .allocator = allocator }, values);
var res: Iterator(Type) = undefined;
res.db = self.dynamic().db;
res.stmt = self.dynamic().stmt;
return res;
}
/// one reads a single row from the result set of this statement.
///
/// The data in the row is used to populate a value of the type `Type`.
/// This means that `Type` must have as many fields as is returned in the query
/// executed by this statement.
/// This also means that the type of each field must be compatible with the SQLite type.
///
/// Here is an example of how to use an anonymous struct type:
///
/// const row = try stmt.one(
/// struct {
/// id: usize,
/// name: [400]u8,
/// age: usize,
/// },
/// .{},
/// .{ .foo = "bar", .age = 500 },
/// );
///
/// The `options` tuple is used to provide additional state in some cases.
///
/// The `values` tuple is used for the bind parameters. It must have as many fields as there are bind markers
/// in the input query string.
///
/// This cannot allocate memory. If you need to read TEXT or BLOB columns you need to use arrays or alternatively call `oneAlloc`.
pub fn one(self: *Self, comptime Type: type, options: QueryOptions, values: anytype) !?Type {
var iter = try self.iterator(Type, values);
const row = (try iter.next(options)) orelse return null;
return row;
}
/// oneAlloc is like `one` but can allocate memory.
pub fn oneAlloc(self: *Self, comptime Type: type, allocator: mem.Allocator, options: QueryOptions, values: anytype) !?Type {
var iter = try self.iteratorAlloc(Type, allocator, values);
const row = (try iter.nextAlloc(allocator, options)) orelse return null;
return row;
}
/// all reads all rows from the result set of this statement.
///
/// The data in each row is used to populate a value of the type `Type`.
/// This means that `Type` must have as many fields as is returned in the query
/// executed by this statement.
/// This also means that the type of each field must be compatible with the SQLite type.
///
/// Here is an example of how to use an anonymous struct type:
///
/// const rows = try stmt.all(
/// struct {
/// id: usize,
/// name: []const u8,
/// age: usize,
/// },
/// allocator,
/// .{},
/// .{ .foo = "bar", .age = 500 },
/// );
///
/// The `options` tuple is used to provide additional state in some cases.
///
/// The `values` tuple is used for the bind parameters. It must have as many fields as there are bind markers
/// in the input query string.
///
/// Note that this allocates all rows into a single slice: if you read a lot of data this can use a lot of memory.
pub fn all(self: *Self, comptime Type: type, allocator: mem.Allocator, options: QueryOptions, values: anytype) ![]Type {
var iter = try self.iteratorAlloc(Type, allocator, values);
var rows: std.ArrayList(Type) = .{};
while (try iter.nextAlloc(allocator, options)) |row| {
try rows.append(allocator, row);
}
return rows.toOwnedSlice(allocator);
}
};
}
const TestUser = struct {
name: []const u8,
id: usize,
age: usize,
weight: f32,
favorite_color: Color,
pub const Color = enum {
red,
magenta,
violet,
indigo,
blue,
cyan,
green,
lime,
yellow,
//
orange,
//
pub const BaseType = []const u8;
pub const default = .red;
};
};
const test_users = &[_]TestUser{
.{ .name = "Vincent", .id = 20, .age = 33, .weight = 85.4, .favorite_color = .violet },
.{ .name = "Julien", .id = 40, .age = 35, .weight = 100.3, .favorite_color = .green },
.{ .name = "José", .id = 60, .age = 40, .weight = 240.2, .favorite_color = .indigo },
};
fn createTestTables(db: *Db) !void {
const AllDDL = &[_][]const u8{
"DROP TABLE IF EXISTS user",
"DROP TABLE IF EXISTS article",
"DROP TABLE IF EXISTS test_blob",
\\CREATE TABLE user(
\\ name text,
\\ id integer PRIMARY KEY,
\\ age integer,
\\ weight real,
\\ favorite_color text
\\)
,
\\CREATE TABLE article(
\\ id integer PRIMARY KEY,
\\ author_id integer,
\\ data text,
\\ is_published integer,
\\ FOREIGN KEY(author_id) REFERENCES user(id)
\\)
};
// Create the tables
inline for (AllDDL) |ddl| {
try db.exec(ddl, .{}, .{});
}
}
fn addTestData(db: *Db) !void {
try createTestTables(db);
for (test_users) |user| {
try db.exec("INSERT INTO user(name, id, age, weight, favorite_color) VALUES(?{[]const u8}, ?{usize}, ?{usize}, ?{f32}, ?{[]const u8})", .{}, user);
const rows_inserted = db.rowsAffected();
try testing.expectEqual(@as(usize, 1), rows_inserted);
}
}
test "sqlite: db init" {
var db = try getTestDb();
defer db.deinit();
}
test "sqlite: exec multi" {
var db = try getTestDb();
defer db.deinit();
try db.execMulti("DROP TABLE IF EXISTS a;\nDROP TABLE IF EXISTS b;", .{});
try db.execMulti("CREATE TABLE a(b int);\n\n--test comment\nCREATE TABLE b(c int);", .{});
const val = try db.one(i32, "SELECT max(c) FROM b", .{}, .{});
try testing.expectEqual(@as(?i32, 0), val);
}
test "sqlite: exec multi with single statement" {
var db = try getTestDb();
defer db.deinit();
try db.exec("DROP TABLE IF EXISTS a", .{}, .{});
try db.execMulti("CREATE TABLE a(b int);", .{});
const val = try db.one(i32, "SELECT max(b) FROM a", .{}, .{});
try testing.expectEqual(@as(?i32, 0), val);
}
test "sqlite: db pragma" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
const foreign_keys = try db.pragma(usize, .{}, "foreign_keys", null);
try testing.expect(foreign_keys != null);
try testing.expectEqual(@as(usize, 0), foreign_keys.?);
if (build_options.in_memory) {
{
const journal_mode = try db.pragma([128:0]u8, .{}, "journal_mode", "wal");
try testing.expect(journal_mode != null);
try testing.expectEqualStrings("memory", mem.sliceTo(&journal_mode.?, 0));
}
{
const journal_mode = try db.pragmaAlloc([]const u8, allocator, .{}, "journal_mode", "wal");
try testing.expect(journal_mode != null);
try testing.expectEqualStrings("memory", journal_mode.?);
}
} else {
{
const journal_mode = try db.pragma([128:0]u8, .{}, "journal_mode", "wal");
try testing.expect(journal_mode != null);
try testing.expectEqualStrings("wal", mem.sliceTo(&journal_mode.?, 0));
}
{
const journal_mode = try db.pragmaAlloc([]const u8, allocator, .{}, "journal_mode", "wal");
try testing.expect(journal_mode != null);
try testing.expectEqualStrings("wal", journal_mode.?);
}
}
}
test "sqlite: last insert row id" {
var db = try getTestDb();
defer db.deinit();
try createTestTables(&db);
try db.exec("INSERT INTO user(name, age) VALUES(?, ?{u32})", .{}, .{
.name = "test-user",
.age = @as(u32, 400),
});
const id = db.getLastInsertRowID();
try testing.expectEqual(@as(i64, 1), id);
}
test "sqlite: statement exec" {
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
// Test with a Blob struct
{
try db.exec("INSERT INTO user(id, name, age) VALUES(?{usize}, ?{blob}, ?{u32})", .{}, .{
.id = @as(usize, 200),
.name = Blob{ .data = "hello" },
.age = @as(u32, 20),
});
}
// Test with a Text struct
{
try db.exec("INSERT INTO user(id, name, age) VALUES(?{usize}, ?{text}, ?{u32})", .{}, .{
.id = @as(usize, 201),
.name = Text{ .data = "hello" },
.age = @as(u32, 20),
});
}
}
test "sqlite: statement execDynamic" {
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
// Test with a Blob struct
{
try db.execDynamic("INSERT INTO user(id, name, age) VALUES(@id, @name, @age)", .{}, .{
.id = @as(usize, 200),
.name = Blob{ .data = "hello" },
.age = @as(u32, 20),
});
}
// Test with a Text struct
{
try db.execDynamic("INSERT INTO user(id, name, age) VALUES(@id, @name, @age)", .{}, .{
.id = @as(usize, 201),
.name = Text{ .data = "hello" },
.age = @as(u32, 20),
});
}
}
test "sqlite: db execAlloc" {
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
try db.execAlloc(testing.allocator, "INSERT INTO user(id, name, age) VALUES(@id, @name, @age)", .{}, .{
.id = @as(usize, 502),
.name = Blob{ .data = "hello" },
.age = @as(u32, 20),
});
}
test "sqlite: read a single user into a struct" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
var stmt = try db.prepare("SELECT * FROM user WHERE id = ?{usize}");
defer stmt.deinit();
const rows = try stmt.all(TestUser, allocator, .{}, .{
.id = @as(usize, 20),
});
for (rows) |row| {
try testing.expectEqual(test_users[0].id, row.id);
try testing.expectEqualStrings(test_users[0].name, row.name);
try testing.expectEqual(test_users[0].age, row.age);
}
// Read a row with db.one()
{
var row = try db.one(
struct {
name: [128:0]u8,
id: usize,
age: usize,
},
"SELECT name, id, age FROM user WHERE id = ?{usize}",
.{},
.{@as(usize, 20)},
);
try testing.expect(row != null);
const exp = test_users[0];
try testing.expectEqual(exp.id, row.?.id);
try testing.expectEqualStrings(exp.name, mem.sliceTo(&row.?.name, 0));
try testing.expectEqual(exp.age, row.?.age);
}
// Read a row with db.oneAlloc()
{
const row = try db.oneAlloc(
struct {
name: Text,
id: usize,
age: usize,
},
allocator,
"SELECT name, id, age FROM user WHERE id = ?{usize}",
.{},
.{@as(usize, 20)},
);
try testing.expect(row != null);
const exp = test_users[0];
try testing.expectEqual(exp.id, row.?.id);
try testing.expectEqualStrings(exp.name, row.?.name.data);
try testing.expectEqual(exp.age, row.?.age);
}
}
test "sqlite: read all users into a struct" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
var stmt = try db.prepare("SELECT * FROM user");
defer stmt.deinit();
const rows = try stmt.all(TestUser, allocator, .{}, .{});
try testing.expectEqual(@as(usize, 3), rows.len);
for (rows, 0..) |row, i| {
const exp = test_users[i];
try testing.expectEqual(exp.id, row.id);
try testing.expectEqualStrings(exp.name, row.name);
try testing.expectEqual(exp.age, row.age);
try testing.expectEqual(exp.weight, row.weight);
}
}
test "sqlite: read in an anonymous struct" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
var stmt = try db.prepare("SELECT name, id, name, age, id, weight FROM user WHERE id = ?{usize}");
defer stmt.deinit();
var row = try stmt.oneAlloc(
struct {
name: []const u8,
id: usize,
name_2: [200:0xAD]u8,
age: usize,
is_id: bool,
weight: f64,
},
allocator,
.{},
.{ .id = @as(usize, 20) },
);
try testing.expect(row != null);
const exp = test_users[0];
try testing.expectEqual(exp.id, row.?.id);
try testing.expectEqualStrings(exp.name, row.?.name);
try testing.expectEqualStrings(exp.name, mem.sliceTo(&row.?.name_2, 0xAD));
try testing.expectEqual(exp.age, row.?.age);
try testing.expect(row.?.is_id);
try testing.expectEqual(exp.weight, @as(f32, @floatCast(row.?.weight)));
}
test "sqlite: read in a Text struct" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
var stmt = try db.prepare("SELECT name, id, age FROM user WHERE id = ?{usize}");
defer stmt.deinit();
const row = try stmt.oneAlloc(
struct {
name: Text,
id: usize,
age: usize,
},
allocator,
.{},
.{@as(usize, 20)},
);
try testing.expect(row != null);
const exp = test_users[0];
try testing.expectEqual(exp.id, row.?.id);
try testing.expectEqualStrings(exp.name, row.?.name.data);
try testing.expectEqual(exp.age, row.?.age);
}
test "sqlite: read a single text value" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
const types = &[_]type{
// Slices
[]const u8,
[]u8,
[:0]const u8,
[:0]u8,
[:0xAD]const u8,
[:0xAD]u8,
// Array
[8:0]u8,
[8:0xAD]u8,
[7]u8,
// Specific text or blob
Text,
Blob,
};
inline for (types) |typ| {
const query = "SELECT name FROM user WHERE id = ?{usize}";
var stmt: StatementType(.{}, query) = try db.prepare(query);
defer stmt.deinit();
const name = try stmt.oneAlloc(typ, allocator, .{}, .{
.id = @as(usize, 20),
});
try testing.expect(name != null);
switch (typ) {
Text, Blob => {
try testing.expectEqualStrings("Vincent", name.?.data);
},
else => {
const type_info = @typeInfo(typ);
switch (type_info) {
.pointer => {
try testing.expectEqualStrings("Vincent", name.?);
},
.array => |arr| if (arr.sentinel()) |sentinel| {
const res = mem.sliceTo(&name.?, sentinel);
try testing.expectEqualStrings("Vincent", res);
} else {
const res: []const u8 = &name.?;
try testing.expectEqualStrings("Vincent", res);
},
else => @compileError("invalid type " ++ @typeName(typ)),
}
},
}
}
}
test "sqlite: read a single integer value" {
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
const types = &[_]type{
u8,
u16,
u32,
u64,
u128,
usize,
f16,
f32,
f64,
f128,
};
inline for (types) |typ| {
const query = "SELECT age FROM user WHERE id = ?{usize}";
var stmt: StatementType(.{}, query) = try db.prepare(query);
defer stmt.deinit();
const age = try stmt.one(typ, .{}, .{
.id = @as(usize, 20),
});
try testing.expect(age != null);
try testing.expectEqual(@as(typ, 33), age.?);
}
}
test "sqlite: read a single value into an enum backed by an integer" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
try createTestTables(&db);
try db.exec("INSERT INTO user(id, age) VALUES(?{usize}, ?{usize})", .{}, .{
.id = @as(usize, 10),
.age = @as(usize, 0),
});
const query = "SELECT age FROM user WHERE id = ?{usize}";
const IntColor = enum {
violet,
pub const BaseType = u1;
};
// Use one
{
var stmt: StatementType(.{}, query) = try db.prepare(query);
defer stmt.deinit();
const b = try stmt.one(IntColor, .{}, .{
.id = @as(usize, 10),
});
try testing.expect(b != null);
try testing.expectEqual(IntColor.violet, b.?);
}
// Use oneAlloc
{
var stmt: StatementType(.{}, query) = try db.prepare(query);
defer stmt.deinit();
const b = try stmt.oneAlloc(IntColor, allocator, .{}, .{
.id = @as(usize, 10),
});
try testing.expect(b != null);
try testing.expectEqual(IntColor.violet, b.?);
}
}
test "sqlite: read a single value into an enum backed by a string" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
try createTestTables(&db);
try db.exec("INSERT INTO user(id, favorite_color) VALUES(?{usize}, ?{[]const u8})", .{}, .{
.id = @as(usize, 10),
.age = @as([]const u8, "violet"),
});
const query = "SELECT favorite_color FROM user WHERE id = ?{usize}";
var stmt: StatementType(.{}, query) = try db.prepare(query);
defer stmt.deinit();
const b = try stmt.oneAlloc(TestUser.Color, allocator, .{}, .{
.id = @as(usize, 10),
});
try testing.expect(b != null);
try testing.expectEqual(TestUser.Color.violet, b.?);
}
test "sqlite: read a single value into void" {
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
const query = "SELECT age FROM user WHERE id = ?{usize}";
var stmt: StatementType(.{}, query) = try db.prepare(query);
defer stmt.deinit();
_ = try stmt.one(void, .{}, .{
.id = @as(usize, 20),
});
}
test "sqlite: read a single value into bool" {
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
const query = "SELECT id FROM user WHERE id = ?{usize}";
var stmt: StatementType(.{}, query) = try db.prepare(query);
defer stmt.deinit();
const b = try stmt.one(bool, .{}, .{
.id = @as(usize, 20),
});
try testing.expect(b != null);
try testing.expect(b.?);
}
test "sqlite: insert bool and bind bool" {
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
try db.exec("INSERT INTO article(id, author_id, is_published) VALUES(?{usize}, ?{usize}, ?{bool})", .{}, .{
.id = @as(usize, 1),
.author_id = @as(usize, 20),
.is_published = true,
});
const query = "SELECT id FROM article WHERE is_published = ?{bool}";
var stmt: StatementType(.{}, query) = try db.prepare(query);
defer stmt.deinit();
const b = try stmt.one(bool, .{}, .{
.is_published = true,
});
try testing.expect(b != null);
try testing.expect(b.?);
}
test "sqlite: bind string literal" {
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
try db.exec("INSERT INTO article(id, data) VALUES(?, ?)", .{}, .{
@as(usize, 10),
"foobar",
});
const query = "SELECT id FROM article WHERE data = ?";
var stmt = try db.prepare(query);
defer stmt.deinit();
const b = try stmt.one(usize, .{}, .{"foobar"});
try testing.expect(b != null);
try testing.expectEqual(@as(usize, 10), b.?);
}
test "sqlite: bind pointer" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
const query = "SELECT name FROM user WHERE id = ?";
var stmt = try db.prepare(query);
defer stmt.deinit();
for (test_users, 0..) |test_user, i| {
stmt.reset();
const name = try stmt.oneAlloc([]const u8, allocator, .{}, .{&test_user.id});
try testing.expect(name != null);
try testing.expectEqualStrings(test_users[i].name, name.?);
}
}
test "sqlite: read pointers" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
const query = "SELECT id, name, age, weight FROM user";
var stmt = try db.prepare(query);
defer stmt.deinit();
const rows = try stmt.all(
struct {
id: *usize,
name: *[]const u8,
age: *u32,
weight: *f32,
},
allocator,
.{},
.{},
);
try testing.expectEqual(@as(usize, 3), rows.len);
for (rows, 0..) |row, i| {
const exp = test_users[i];
try testing.expectEqual(exp.id, row.id.*);
try testing.expectEqualStrings(exp.name, row.name.*);
try testing.expectEqual(exp.age, row.age.*);
try testing.expectEqual(exp.weight, row.weight.*);
}
}
test "sqlite: optional" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
const published: ?bool = true;
{
try db.exec("INSERT INTO article(author_id, data, is_published) VALUES(?, ?, ?)", .{}, .{ 1, null, published });
var stmt = try db.prepare("SELECT data, is_published FROM article");
defer stmt.deinit();
const row = try stmt.one(
struct {
data: ?[128:0]u8,
is_published: ?bool,
},
.{},
.{},
);
try testing.expect(row != null);
try testing.expect(row.?.data == null);
try testing.expectEqual(true, row.?.is_published.?);
}
{
const data: ?[]const u8 = "hello";
try db.exec("INSERT INTO article(author_id, data) VALUES(?, :data{?[]const u8})", .{}, .{
.author_id = 20,
.dhe = data,
});
const row = try db.oneAlloc(
[]const u8,
arena.allocator(),
"SELECT data FROM article WHERE author_id = ?",
.{},
.{ .author_id = 20 },
);
try testing.expect(row != null);
try testing.expectEqualStrings(data.?, row.?);
}
}
test "sqlite: statement reset" {
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
// Add data
var stmt = try db.prepare("INSERT INTO user(name, id, age, weight, favorite_color) VALUES(?{[]const u8}, ?{usize}, ?{usize}, ?{f32}, ?{[]const u8})");
defer stmt.deinit();
const users = &[_]TestUser{
.{ .id = 200, .name = "Vincent", .age = 33, .weight = 10.0, .favorite_color = .violet },
.{ .id = 400, .name = "Julien", .age = 35, .weight = 12.0, .favorite_color = .green },
.{ .id = 600, .name = "José", .age = 40, .weight = 14.0, .favorite_color = .indigo },
};
for (users) |user| {
stmt.reset();
try stmt.exec(.{}, user);
const rows_inserted = db.rowsAffected();
try testing.expectEqual(@as(usize, 1), rows_inserted);
}
}
test "sqlite: statement iterator" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
// Cleanup first
try db.exec("DELETE FROM user", .{}, .{});
// Add data
var stmt = try db.prepare("INSERT INTO user(name, id, age, weight, favorite_color) VALUES(?{[]const u8}, ?{usize}, ?{usize}, ?{f32}, ?{[]const u8})");
defer stmt.deinit();
var expected_rows: std.ArrayList(TestUser) = .{};
var i: usize = 0;
while (i < 20) : (i += 1) {
const name = try std.fmt.allocPrint(allocator, "Vincent {d}", .{i});
const user = TestUser{ .id = i, .name = name, .age = i + 200, .weight = @floatFromInt(i + 200), .favorite_color = .indigo };
try expected_rows.append(allocator, user);
stmt.reset();
try stmt.exec(.{}, user);
const rows_inserted = db.rowsAffected();
try testing.expectEqual(@as(usize, 1), rows_inserted);
}
// Get data with a non-allocating iterator.
{
var stmt2 = try db.prepare("SELECT name, age FROM user");
defer stmt2.deinit();
const RowType = struct {
name: [128:0]u8,
age: usize,
};
var iter = try stmt2.iterator(RowType, .{});
var rows: std.ArrayList(RowType) = .{};
while (try iter.next(.{})) |row| {
try rows.append(allocator, row);
}
// Check the data
try testing.expectEqual(expected_rows.items.len, rows.items.len);
for (rows.items, 0..) |row, j| {
const exp_row = expected_rows.items[j];
try testing.expectEqualStrings(exp_row.name, mem.sliceTo(&row.name, 0));
try testing.expectEqual(exp_row.age, row.age);
}
}
// Get data with an iterator
{
var stmt2 = try db.prepare("SELECT name, age FROM user");
defer stmt2.deinit();
const RowType = struct {
name: Text,
age: usize,
};
var iter = try stmt2.iterator(RowType, .{});
var rows: std.ArrayList(RowType) = .{};
while (try iter.nextAlloc(allocator, .{})) |row| {
try rows.append(allocator, row);
}
// Check the data
try testing.expectEqual(expected_rows.items.len, rows.items.len);
for (rows.items, 0..) |row, j| {
const exp_row = expected_rows.items[j];
try testing.expectEqualStrings(exp_row.name, row.name.data);
try testing.expectEqual(exp_row.age, row.age);
}
}
}
test "sqlite: blob open, reopen" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
const blob_data1 = "\xDE\xAD\xBE\xEFabcdefghijklmnopqrstuvwxyz0123456789";
const blob_data2 = "\xCA\xFE\xBA\xBEfoobar";
// Insert two blobs with a set length
try db.exec("DROP TABLE IF EXISTS test_blob", .{}, .{});
try db.exec("CREATE TABLE test_blob(id integer primary key, data blob)", .{}, .{});
try db.exec("INSERT INTO test_blob(data) VALUES(?)", .{}, .{
.data = ZeroBlob{ .length = blob_data1.len * 2 },
});
const rowid1 = db.getLastInsertRowID();
try db.exec("INSERT INTO test_blob(data) VALUES(?)", .{}, .{
.data = ZeroBlob{ .length = blob_data2.len * 2 },
});
const rowid2 = db.getLastInsertRowID();
// Open the blob in the first row
var blob = try db.openBlob(.main, "test_blob", "data", rowid1, .{ .write = true });
{
// Write the first blob data
var blob_writer = blob.writer();
try blob_writer.writeAll(blob_data1);
try blob_writer.writeAll(blob_data1);
blob.reset();
var blob_reader = blob.reader();
const data = try blob_reader.readAllAlloc(allocator, 8192);
try testing.expectEqualSlices(u8, blob_data1 ** 2, data);
}
// Reopen the blob in the second row
try blob.reopen(rowid2);
{
// Write the second blob data
var blob_writer = blob.writer();
try blob_writer.writeAll(blob_data2);
try blob_writer.writeAll(blob_data2);
blob.reset();
var blob_reader = blob.reader();
const data = try blob_reader.readAllAlloc(allocator, 8192);
try testing.expectEqualSlices(u8, blob_data2 ** 2, data);
}
try blob.close();
}
test "sqlite: failing open" {
var diags: Diagnostics = undefined;
const res = Db.init(.{
.diags = &diags,
.open_flags = .{},
.mode = .{ .File = "/tmp/not_existing.db" },
});
try testing.expectError(error.SQLiteCantOpen, res);
try testing.expectEqual(@as(usize, 14), diags.err.?.code);
try testing.expectEqualStrings("unable to open database file", diags.err.?.message);
}
test "sqlite: failing prepare statement" {
var db = try getTestDb();
defer db.deinit();
var diags: Diagnostics = undefined;
try db.exec("DROP TABLE IF EXISTS foobar", .{}, .{});
const result = db.prepareWithDiags("SELECT id FROM foobar", .{ .diags = &diags });
try testing.expectError(error.SQLiteError, result);
const detailed_err = db.getDetailedError();
try testing.expectEqual(@as(usize, 1), detailed_err.code);
try testing.expectEqualStrings("no such table: foobar", detailed_err.message);
}
test "sqlite: diagnostics format" {
const TestCase = struct {
input: Diagnostics,
exp: []const u8,
};
const testCases = &[_]TestCase{
.{
.input = .{},
.exp = "my diagnostics: none",
},
.{
.input = .{
.message = "foobar",
},
.exp = "my diagnostics: foobar",
},
.{
.input = .{
.err = .{
.code = 20,
.near = -1,
.message = "barbaz",
},
},
.exp = "my diagnostics: {code: 20, near: -1, message: barbaz}",
},
.{
.input = .{
.message = "foobar",
.err = .{
.code = 20,
.near = 10,
.message = "barbaz",
},
},
.exp = "my diagnostics: {message: foobar, detailed error: {code: 20, near: 10, message: barbaz}}",
},
};
inline for (testCases) |tc| {
var buf: [1024]u8 = undefined;
const str = try std.fmt.bufPrint(&buf, "my diagnostics: {f}", .{tc.input});
try testing.expectEqualStrings(tc.exp, str);
}
}
test "sqlite: exec with diags, failing statement" {
var db = try getTestDb();
defer db.deinit();
var diags = Diagnostics{};
const result = blk: {
var stmt = try db.prepareWithDiags("ROLLBACK", .{ .diags = &diags });
break :blk stmt.exec(.{ .diags = &diags }, .{});
};
try testing.expectError(error.SQLiteError, result);
try testing.expect(diags.err != null);
try testing.expectEqualStrings("cannot rollback - no transaction is active", diags.err.?.message);
const detailed_err = db.getDetailedError();
try testing.expectEqual(@as(usize, 1), detailed_err.code);
try testing.expectEqualStrings("cannot rollback - no transaction is active", detailed_err.message);
}
test "sqlite: savepoint with no failures" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
{
var savepoint = try db.savepoint("outer1");
defer savepoint.rollback();
try db.exec("INSERT INTO article(author_id, data, is_published) VALUES(?, ?, ?)", .{}, .{ 1, null, true });
{
var savepoint2 = try db.savepoint("inner1");
defer savepoint2.rollback();
try db.exec("INSERT INTO article(author_id, data, is_published) VALUES(?, ?, ?)", .{}, .{ 2, "foobar", true });
savepoint2.commit();
}
savepoint.commit();
}
// No failures, expect to have two rows.
var stmt = try db.prepare("SELECT data, author_id FROM article ORDER BY id ASC");
defer stmt.deinit();
const rows = try stmt.all(
struct {
data: []const u8,
author_id: usize,
},
allocator,
.{},
.{},
);
try testing.expectEqual(@as(usize, 2), rows.len);
try testing.expectEqual(@as(usize, 1), rows[0].author_id);
try testing.expectEqualStrings("", rows[0].data);
try testing.expectEqual(@as(usize, 2), rows[1].author_id);
try testing.expectEqualStrings("foobar", rows[1].data);
}
test "sqlite: two nested savepoints with inner failure" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
{
var savepoint = try db.savepoint("outer2");
defer savepoint.rollback();
try db.exec("INSERT INTO article(author_id, data, is_published) VALUES(?, ?, ?)", .{}, .{ 10, "barbaz", true });
inner: {
var savepoint2 = try db.savepoint("inner2");
defer savepoint2.rollback();
try db.exec("INSERT INTO article(author_id, data, is_published) VALUES(?, ?, ?)", .{}, .{ 20, null, true });
// Explicitly fail
db.exec("INSERT INTO article(author_id, data, is_published) VALUES(?, ?)", .{}, .{ 22, null }) catch {
break :inner;
};
savepoint2.commit();
}
savepoint.commit();
}
// The inner transaction failed, expect to have only one row.
var stmt = try db.prepare("SELECT data, author_id FROM article");
defer stmt.deinit();
const rows = try stmt.all(
struct {
data: []const u8,
author_id: usize,
},
allocator,
.{},
.{},
);
try testing.expectEqual(@as(usize, 1), rows.len);
try testing.expectEqual(@as(usize, 10), rows[0].author_id);
try testing.expectEqualStrings("barbaz", rows[0].data);
}
test "sqlite: two nested savepoints with outer failure" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
blk: {
var savepoint = try db.savepoint("outer3");
defer savepoint.rollback();
var i: usize = 100;
while (i < 120) : (i += 1) {
try db.exec("INSERT INTO article(author_id, data, is_published) VALUES(?, ?, ?)", .{}, .{ i, null, true });
}
// Explicitly fail
db.exec("INSERT INTO article(author_id, data, is_published) VALUES(?, ?)", .{}, .{ 2, null }) catch {
break :blk;
};
savepoint.commit();
}
// The outer transaction failed, expect to have no rows.
var stmt = try db.prepare("SELECT 1 FROM article");
defer stmt.deinit();
const rows = try stmt.all(usize, allocator, .{}, .{});
try testing.expectEqual(@as(usize, 0), rows.len);
}
const MyData = struct {
data: [16]u8,
const BaseType = []const u8;
pub fn bindField(self: MyData, allocator: mem.Allocator) !BaseType {
const num = std.mem.bytesAsValue(u128, &self.data);
return try std.fmt.allocPrint(allocator, "{s}", .{std.fmt.hex(num.*)});
}
pub fn readField(alloc: mem.Allocator, value: BaseType) !MyData {
_ = alloc;
var result = [_]u8{0} ** 16;
var i: usize = 0;
while (i < result.len) : (i += 1) {
const j = i * 2;
result[i] = try std.fmt.parseUnsigned(u8, value[j..][0..2], 16);
}
return MyData{ .data = result };
}
};
test "sqlite: bind custom type" {
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
{
var i: usize = 0;
while (i < 20) : (i += 1) {
var my_data: MyData = undefined;
@memset(&my_data.data, @as(u8, @intCast(i)));
var arena = heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
// insertion
var stmt = try db.prepare("INSERT INTO article(data) VALUES(?)");
try stmt.execAlloc(arena.allocator(), .{}, .{my_data});
}
}
{
// reading back
var stmt = try db.prepare("SELECT * FROM article");
defer stmt.deinit();
const Article = struct {
id: u32,
author_id: u32,
data: MyData,
is_published: bool,
};
var arena = heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const rows = try stmt.all(Article, arena.allocator(), .{}, .{});
try testing.expectEqual(@as(usize, 20), rows.len);
for (rows, 0..) |row, i| {
var exp_data: MyData = undefined;
@memset(&exp_data.data, @as(u8, @intCast(i)));
try testing.expectEqualSlices(u8, &exp_data.data, &row.data.data);
}
}
}
test "sqlite: bind runtime slice" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
// creating array list on heap so that it's deemed runtime size
var list: std.ArrayList([]const u8) = .{};
defer list.deinit(allocator);
try list.append(allocator, "this is some data");
const args = try list.toOwnedSlice(allocator);
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
{
// insertion
var stmt = try db.prepareDynamic("INSERT INTO article(data) VALUES(?)");
defer stmt.deinit();
try stmt.exec(.{}, args);
}
}
test "sqlite: prepareDynamic" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
var diags = Diagnostics{};
var stmt = try db.prepareDynamicWithDiags("SELECT id FROM user WHERE age = ?", .{ .diags = &diags });
defer stmt.deinit();
{
var iter = try stmt.iterator(usize, .{ .age = 33 });
const id = try iter.next(.{});
try testing.expect(id != null);
try testing.expectEqual(@as(usize, 20), id.?);
}
stmt.reset();
{
var iter = try stmt.iteratorAlloc(usize, allocator, .{ .age = 33 });
const id = try iter.next(.{});
try testing.expect(id != null);
try testing.expectEqual(@as(usize, 20), id.?);
}
stmt.reset();
{
var iter = try stmt.iteratorAlloc(usize, allocator, .{33});
const id = try iter.next(.{});
try testing.expect(id != null);
try testing.expectEqual(@as(usize, 20), id.?);
}
}
test "sqlite: oneDynamic" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
var diags = Diagnostics{};
{
const id = try db.oneDynamic(
usize,
"SELECT id FROM user WHERE age = ?",
.{ .diags = &diags },
.{ .age = 33 },
);
try testing.expect(id != null);
try testing.expectEqual(@as(usize, 20), id.?);
}
{
// Mix bind marker prefix for good measure
const id = try db.oneDynamic(
usize,
"SELECT id FROM user WHERE age = $age AND weight < :weight and id < @id",
.{ .diags = &diags },
.{ .id = 400, .age = 33, .weight = 200 },
);
try testing.expect(id != null);
try testing.expectEqual(@as(usize, 20), id.?);
}
{
const id = try db.oneDynamicAlloc(
usize,
allocator,
"SELECT id FROM user WHERE age = ?",
.{ .diags = &diags },
.{ .age = 33 },
);
try testing.expect(id != null);
try testing.expectEqual(@as(usize, 20), id.?);
}
{
const id = try db.oneDynamicAlloc(
usize,
allocator,
"SELECT id FROM user WHERE age = ?",
.{ .diags = &diags },
.{33},
);
try testing.expect(id != null);
try testing.expectEqual(@as(usize, 20), id.?);
}
}
test "sqlite: one with all named parameters" {
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
var diags = Diagnostics{};
// Mix bind marker prefix for good measure
const id = try db.one(
usize,
"SELECT id FROM user WHERE age = $age AND weight < :weight and id < @my_id",
.{ .diags = &diags },
.{ .my_id = 400, .age = 33, .weight = 200 },
);
try testing.expect(id != null);
try testing.expectEqual(@as(usize, 20), id.?);
}
test "sqlite: create scalar function" {
var db = try getTestDb();
defer db.deinit();
{
try db.createScalarFunction(
"myInteger",
struct {
fn run(input: u16) u16 {
return input * 2;
}
}.run,
.{},
);
const result = try db.one(usize, "SELECT myInteger(20)", .{}, .{});
try testing.expect(result != null);
try testing.expectEqual(@as(usize, 40), result.?);
}
{
try db.createScalarFunction(
"myInteger64",
struct {
fn run(input: i64) i64 {
return @as(i64, @intCast(input)) * 2;
}
}.run,
.{},
);
const result = try db.one(usize, "SELECT myInteger64(20)", .{}, .{});
try testing.expect(result != null);
try testing.expectEqual(@as(usize, 40), result.?);
}
{
try db.createScalarFunction(
"myMax",
struct {
fn run(a: f64, b: f64) f64 {
return @max(a, b);
}
}.run,
.{},
);
const result = try db.one(f64, "SELECT myMax(2.0, 23.4)", .{}, .{});
try testing.expect(result != null);
try testing.expectEqual(@as(f64, 23.4), result.?);
}
{
try db.createScalarFunction(
"myBool",
struct {
fn run() bool {
return true;
}
}.run,
.{},
);
const result = try db.one(bool, "SELECT myBool()", .{}, .{});
try testing.expect(result != null);
try testing.expectEqual(true, result.?);
}
{
try db.createScalarFunction(
"mySlice",
struct {
fn run() []const u8 {
return "foobar";
}
}.run,
.{},
);
const result = try db.oneAlloc([]const u8, testing.allocator, "SELECT mySlice()", .{}, .{});
try testing.expect(result != null);
try testing.expectEqualStrings("foobar", result.?);
testing.allocator.free(result.?);
}
{
const Blake3 = std.crypto.hash.Blake3;
var expected_hash: [Blake3.digest_length]u8 = undefined;
Blake3.hash("hello", &expected_hash, .{});
try db.createScalarFunction(
"blake3",
struct {
fn run(input: []const u8) [std.crypto.hash.Blake3.digest_length]u8 {
var hash: [Blake3.digest_length]u8 = undefined;
Blake3.hash(input, &hash, .{});
return hash;
}
}.run,
.{},
);
const hash = try db.one([Blake3.digest_length]u8, "SELECT blake3('hello')", .{}, .{});
try testing.expect(hash != null);
try testing.expectEqual(expected_hash, hash.?);
}
{
try db.createScalarFunction(
"myText",
struct {
fn run() Text {
return Text{ .data = "foobar" };
}
}.run,
.{},
);
const result = try db.oneAlloc(Text, testing.allocator, "SELECT myText()", .{}, .{});
try testing.expect(result != null);
try testing.expectEqualStrings("foobar", result.?.data);
testing.allocator.free(result.?.data);
}
{
try db.createScalarFunction(
"myBlob",
struct {
fn run() Blob {
return Blob{ .data = "barbaz" };
}
}.run,
.{},
);
const result = try db.oneAlloc(Blob, testing.allocator, "SELECT myBlob()", .{}, .{});
try testing.expect(result != null);
try testing.expectEqualStrings("barbaz", result.?.data);
testing.allocator.free(result.?.data);
}
}
test "sqlite: create aggregate function with no aggregate context" {
var db = try getTestDb();
defer db.deinit();
var rand = std.Random.DefaultPrng.init(@intCast(std.time.milliTimestamp()));
// Create an aggregate function working with a MyContext
const MyContext = struct {
sum: u32,
};
var my_ctx = MyContext{ .sum = 0 };
try db.createAggregateFunction(
"mySum",
&my_ctx,
struct {
fn step(fctx: FunctionContext, input: u32) void {
var ctx = fctx.userContext(*MyContext) orelse return;
ctx.sum += input;
}
}.step,
struct {
fn finalize(fctx: FunctionContext) u32 {
const ctx = fctx.userContext(*MyContext) orelse return 0;
return ctx.sum;
}
}.finalize,
.{},
);
// Initialize some data
try db.exec("DROP TABLE IF EXISTS view", .{}, .{});
try db.exec("CREATE TABLE view(id integer PRIMARY KEY, nb integer)", .{}, .{});
var i: usize = 0;
var exp: usize = 0;
while (i < 20) : (i += 1) {
const val = rand.random().intRangeAtMost(u32, 0, 5205905);
exp += val;
try db.exec("INSERT INTO view(nb) VALUES(?{u32})", .{}, .{val});
}
// Get the sum and check the result
var diags = Diagnostics{};
const result = db.one(
usize,
"SELECT mySum(nb) FROM view",
.{ .diags = &diags },
.{},
) catch |err| {
debug.print("err: {f}\n", .{diags});
return err;
};
try testing.expect(result != null);
try testing.expectEqual(@as(usize, exp), result.?);
}
test "sqlite: create aggregate function with an aggregate context" {
var db = try getTestDb();
defer db.deinit();
var rand = std.Random.DefaultPrng.init(@intCast(std.time.milliTimestamp()));
try db.createAggregateFunction(
"mySum",
null,
struct {
fn step(fctx: FunctionContext, input: u32) void {
const ctx = fctx.aggregateContext(*u32) orelse return;
ctx.* += input;
}
}.step,
struct {
fn finalize(fctx: FunctionContext) u32 {
const ctx = fctx.aggregateContext(*u32) orelse return 0;
return ctx.*;
}
}.finalize,
.{},
);
// Initialize some data
try db.exec("DROP TABLE IF EXISTS view", .{}, .{});
try db.exec("CREATE TABLE view(id integer PRIMARY KEY, a integer, b integer)", .{}, .{});
var i: usize = 0;
var exp_a: usize = 0;
var exp_b: usize = 0;
while (i < 20) : (i += 1) {
const val1 = rand.random().intRangeAtMost(u32, 0, 5205905);
exp_a += val1;
const val2 = rand.random().intRangeAtMost(u32, 0, 310455);
exp_b += val2;
try db.exec("INSERT INTO view(a, b) VALUES(?{u32}, ?{u32})", .{}, .{ val1, val2 });
}
// Get the sum and check the result
var diags = Diagnostics{};
const result = db.one(
struct {
a_sum: usize,
b_sum: usize,
},
"SELECT mySum(a), mySum(b) FROM view",
.{ .diags = &diags },
.{},
) catch |err| {
debug.print("err: {f}\n", .{diags});
return err;
};
try testing.expect(result != null);
try testing.expectEqual(@as(usize, exp_a), result.?.a_sum);
try testing.expectEqual(@as(usize, exp_b), result.?.b_sum);
}
test "sqlite: empty slice" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
var list: std.ArrayList(u8) = .{};
const ptr = try list.toOwnedSlice(allocator);
try db.exec("INSERT INTO article(author_id, data) VALUES(?, ?)", .{}, .{ 1, ptr });
// Read into an array
{
var stmt = try db.prepare("SELECT data FROM article");
defer stmt.deinit();
const row = try stmt.one(
struct {
data: [128:0]u8,
},
.{},
.{},
);
try testing.expect(row != null);
try testing.expectEqualSlices(u8, "", mem.sliceTo(&row.?.data, 0));
}
// Read into an allocated slice
{
var stmt = try db.prepare("SELECT data FROM article");
defer stmt.deinit();
const row = try stmt.oneAlloc(
struct {
data: []const u8,
},
arena.allocator(),
.{},
.{},
);
try testing.expect(row != null);
try testing.expectEqualSlices(u8, "", row.?.data);
}
// Read into a Text
{
var stmt = try db.prepare("SELECT data FROM article");
defer stmt.deinit();
const row = try stmt.oneAlloc(
struct {
data: Text,
},
arena.allocator(),
.{},
.{},
);
try testing.expect(row != null);
try testing.expectEqualSlices(u8, "", row.?.data.data);
}
}
test "sqlite: fuzzer found crashes" {
const test_cases = &[_]struct {
input: []const u8,
exp_error: anyerror,
}{
.{
.input = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00CREATE TABLE \x80\x00\x00\x00ar(Wb)\x01",
.exp_error = error.EmptyQuery,
},
.{
.input = "SELECT?",
.exp_error = error.ExecReturnedData,
},
};
inline for (test_cases) |tc| {
var db = try getTestDb();
defer db.deinit();
try testing.expectError(tc.exp_error, db.execDynamic(tc.input, .{}, .{}));
}
}
test "tagged union" {
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
const Foobar = union(enum) {
name: []const u8,
age: usize,
};
try db.exec("DROP TABLE IF EXISTS foobar", .{}, .{});
try db.exec("CREATE TABLE foobar(key TEXT, value ANY)", .{}, .{});
var foobar = Foobar{ .name = "hello" };
{
try db.exec("INSERT INTO foobar(key, value) VALUES($key, $value)", .{}, .{
.key = @tagName(std.meta.activeTag(foobar)),
.value = foobar,
});
var arena = heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const result = try db.oneAlloc(
struct {
key: []const u8,
value: []const u8,
},
arena.allocator(),
"SELECT key, value FROM foobar WHERE key = $key",
.{},
.{
@tagName(std.meta.activeTag(foobar)),
},
);
try testing.expect(result != null);
try testing.expectEqualStrings("name", result.?.key);
try testing.expectEqualStrings(foobar.name, result.?.value);
}
{
foobar = Foobar{ .age = 204 };
try db.exec("INSERT INTO foobar(key, value) VALUES($key, $value)", .{}, .{
.key = @tagName(std.meta.activeTag(foobar)),
.value = foobar,
});
var arena = heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const result = try db.oneAlloc(
struct {
key: []const u8,
value: usize,
},
arena.allocator(),
"SELECT key, value FROM foobar WHERE key = $key",
.{},
.{
@tagName(std.meta.activeTag(foobar)),
},
);
try testing.expect(result != null);
try testing.expectEqualStrings("age", result.?.key);
try testing.expectEqual(foobar.age, result.?.value);
}
}
test "reuse same field twice in query string" {
var db = try getTestDb();
defer db.deinit();
try addTestData(&db);
const update_name = "NewUpdatedName";
const update_name_query = "UPDATE user SET id = $id, name = $name WHERE id = $id";
try db.exec(update_name_query, .{}, .{ .id = 20, .name = update_name });
const fetch_name_query = "SELECT name FROM user WHERE id = $id";
const name = try db.oneAlloc(
[]const u8,
testing.allocator,
fetch_name_query,
.{},
.{ .id = 20 },
);
try testing.expect(name != null);
defer testing.allocator.free(name.?);
try testing.expectEqualStrings(name.?, update_name);
}
test "fuzzing" {
const Context = struct {
fn testOne(_: @This(), input: []const u8) anyerror!void {
var db = try Db.init(.{
.mode = .Memory,
.open_flags = .{
.write = true,
.create = true,
},
});
defer db.deinit();
try db.exec("CREATE TABLE test(id integer primary key, name text, data blob)", .{}, .{});
db.execDynamic(input, .{}, .{}) catch |err| switch (err) {
error.SQLiteError => return,
error.ExecReturnedData => return,
error.EmptyQuery => return,
else => return err,
};
db.execDynamic(
"INSERT INTO test(name, data) VALUES($name, $data)",
.{},
.{
.name = "foo",
.data = input,
},
) catch |err| switch (err) {
error.SQLiteError => return,
else => return err,
};
var stmt = db.prepareDynamic("SELECT name, data FROM test") catch |err| switch (err) {
error.SQLiteError => return,
else => return err,
};
defer stmt.deinit();
var rows_memory: [4096]u8 = undefined;
var rows_fba = std.heap.FixedBufferAllocator.init(&rows_memory);
const row_opt = stmt.oneAlloc(
struct {
name: Text,
data: Blob,
},
rows_fba.allocator(),
.{},
.{},
) catch |err| switch (err) {
error.SQLiteError => return,
else => return err,
};
if (row_opt) |row| {
if (!std.mem.eql(u8, row.name.data, "foo")) return error.InvalidNameField;
if (!std.mem.eql(u8, row.data.data, input)) return error.InvalidDataField;
} else {
return error.NoRowsFound;
}
}
};
try testing.fuzz(Context{}, Context.testOne, .{});
}