├── .gitignore ├── src ├── c.zig ├── errors.zig ├── test.zig └── sqlite.zig ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | -------------------------------------------------------------------------------- /src/c.zig: -------------------------------------------------------------------------------- 1 | pub const c = @cImport(@cInclude("sqlite3.h")); 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 nDimensional Studios 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/errors.zig: -------------------------------------------------------------------------------- 1 | const c = @import("c.zig").c; 2 | 3 | pub const Error = error{ 4 | // Generic error 5 | SQLITE_ERROR, 6 | // Internal logic error in SQLite 7 | SQLITE_INTERNAL, 8 | // Access permission denied 9 | SQLITE_PERM, 10 | // Callback routine requested an abort 11 | SQLITE_ABORT, 12 | // The database file is locked 13 | SQLITE_BUSY, 14 | // A table in the database is locked 15 | SQLITE_LOCKED, 16 | // A malloc() failed 17 | SQLITE_NOMEM, 18 | // Attempt to write a readonly database 19 | SQLITE_READONLY, 20 | // Operation terminated by sqlite3_interrupt() 21 | SQLITE_INTERRUPT, 22 | // Some kind of disk I/O error occurred 23 | SQLITE_IOERR, 24 | // The database disk image is malformed 25 | SQLITE_CORRUPT, 26 | // Unknown opcode in sqlite3_file_control() 27 | SQLITE_NOTFOUND, 28 | // Insertion failed because database is full 29 | SQLITE_FULL, 30 | // Unable to open the database file 31 | SQLITE_CANTOPEN, 32 | // Database lock protocol error 33 | SQLITE_PROTOCOL, 34 | // Internal use only 35 | SQLITE_EMPTY, 36 | // The database schema changed 37 | SQLITE_SCHEMA, 38 | // String or BLOB exceeds size limit 39 | SQLITE_TOOBIG, 40 | // Abort due to constraint violation 41 | SQLITE_CONSTRAINT, 42 | // Data type mismatch 43 | SQLITE_MISMATCH, 44 | // Library used incorrectly 45 | SQLITE_MISUSE, 46 | // Uses OS features not supported on host 47 | SQLITE_NOLFS, 48 | // Authorization denied 49 | SQLITE_AUTH, 50 | // Not used 51 | SQLITE_FORMAT, 52 | // 2nd parameter to sqlite3_bind out of range 53 | SQLITE_RANGE, 54 | // File opened that is not a database file 55 | SQLITE_NOTADB, 56 | // Notifications from sqlite3_log() 57 | SQLITE_NOTICE, 58 | // Warnings from sqlite3_log() 59 | SQLITE_WARNING, 60 | // sqlite3_step() has another row ready 61 | SQLITE_ROW, 62 | // sqlite3_step() has finished executing 63 | SQLITE_DONE, 64 | }; 65 | 66 | pub fn getError(code: c_int) Error { 67 | return switch (code & 0xFF) { 68 | c.SQLITE_ERROR => Error.SQLITE_ERROR, 69 | c.SQLITE_INTERNAL => Error.SQLITE_INTERNAL, 70 | c.SQLITE_PERM => Error.SQLITE_PERM, 71 | c.SQLITE_ABORT => Error.SQLITE_ABORT, 72 | c.SQLITE_BUSY => Error.SQLITE_BUSY, 73 | c.SQLITE_LOCKED => Error.SQLITE_LOCKED, 74 | c.SQLITE_NOMEM => Error.SQLITE_NOMEM, 75 | c.SQLITE_READONLY => Error.SQLITE_READONLY, 76 | c.SQLITE_INTERRUPT => Error.SQLITE_INTERRUPT, 77 | c.SQLITE_IOERR => Error.SQLITE_IOERR, 78 | c.SQLITE_CORRUPT => Error.SQLITE_CORRUPT, 79 | c.SQLITE_NOTFOUND => Error.SQLITE_NOTFOUND, 80 | c.SQLITE_FULL => Error.SQLITE_FULL, 81 | c.SQLITE_CANTOPEN => Error.SQLITE_CANTOPEN, 82 | c.SQLITE_PROTOCOL => Error.SQLITE_PROTOCOL, 83 | c.SQLITE_EMPTY => Error.SQLITE_EMPTY, 84 | c.SQLITE_SCHEMA => Error.SQLITE_SCHEMA, 85 | c.SQLITE_TOOBIG => Error.SQLITE_TOOBIG, 86 | c.SQLITE_CONSTRAINT => Error.SQLITE_CONSTRAINT, 87 | c.SQLITE_MISMATCH => Error.SQLITE_MISMATCH, 88 | c.SQLITE_MISUSE => Error.SQLITE_MISUSE, 89 | c.SQLITE_NOLFS => Error.SQLITE_NOLFS, 90 | c.SQLITE_AUTH => Error.SQLITE_AUTH, 91 | c.SQLITE_FORMAT => Error.SQLITE_FORMAT, 92 | c.SQLITE_RANGE => Error.SQLITE_RANGE, 93 | c.SQLITE_NOTADB => Error.SQLITE_NOTADB, 94 | c.SQLITE_NOTICE => Error.SQLITE_NOTICE, 95 | c.SQLITE_WARNING => Error.SQLITE_WARNING, 96 | c.SQLITE_ROW => Error.SQLITE_ROW, 97 | c.SQLITE_DONE => Error.SQLITE_DONE, 98 | else => @panic("invalid error code"), 99 | }; 100 | } 101 | 102 | pub fn throw(code: c_int) Error!void { 103 | return switch (code & 0xFF) { 104 | c.SQLITE_OK => {}, 105 | else => getError(code), 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-sqlite 2 | 3 | Simple, low-level, explicitly-typed SQLite bindings for Zig. 4 | 5 | ## Table of Contents 6 | 7 | - [Installation](#installation) 8 | - [Usage](#usage) 9 | - [Methods](#methods) 10 | - [Queries](#queries) 11 | - [Errors](#errors) 12 | - [Notes](#notes) 13 | - [Build options](#build-options) 14 | - [License](#license) 15 | 16 | ## Installation 17 | 18 | This library uses and requires Zig version `0.15.1` or later. 19 | 20 | ``` 21 | zig fetch --save=sqlite \ 22 | https://github.com/nDimensional/zig-sqlite/archive/refs/tags/v0.3.2-3500400.tar.gz 23 | ``` 24 | 25 | Then add `sqlite` as an import to your root modules in `build.zig`: 26 | 27 | ```zig 28 | fn build(b: *std.Build) void { 29 | const app = b.addExecutable(.{ ... }); 30 | // ... 31 | 32 | const sqlite = b.dependency("sqlite", .{}); 33 | app.root_module.addImport("sqlite", sqlite.module("sqlite")); 34 | } 35 | ``` 36 | 37 | ## Usage 38 | 39 | Open databases using `Database.open` and close them with `db.close()`: 40 | 41 | ```zig 42 | const sqlite = @import("sqlite"); 43 | 44 | { 45 | // in-memory database 46 | const db = try sqlite.Database.open(.{}); 47 | defer db.close(); 48 | } 49 | 50 | { 51 | // persistent database 52 | const db = try sqlite.Database.open(.{ .path = "path/to/db.sqlite" }); 53 | defer db.close(); 54 | } 55 | ``` 56 | 57 | Execute one-off statements using `Database.exec`: 58 | 59 | ```zig 60 | try db.exec("CREATE TABLE users (id TEXT PRIMARY KEY, age FLOAT)", .{}); 61 | ``` 62 | 63 | Prepare statements using `Database.prepare`, and finalize them with `stmt.finalize()`. Statements must be given explicit comptime params and result types, and are typed as `sqlite.Statement(Params, Result)`. 64 | 65 | - The comptime `Params` type must be a struct whose fields are (possibly optional) float, integer, `sqlite.Blob`, or `sqlite.Text` types. 66 | - The comptime `Result` type must either be `void`, indicating a method that returns no data, or a struct of the same kind as param types, indicating a query that returns rows. 67 | 68 | `sqlite.Blob` and `sqlite.Text` are wrapper structs with a single field `data: []const u8`. 69 | 70 | ### Methods 71 | 72 | If the `Result` type is `void`, use the `exec(params: Params): !void` method to execute the statement several times with different params. 73 | 74 | ```zig 75 | const User = struct { id: sqlite.Text, age: ?f32 }; 76 | const insert = try db.prepare(User, void, "INSERT INTO users VALUES (:id, :age)"); 77 | defer insert.finalize(); 78 | 79 | try insert.exec(.{ .id = sqlite.text("a"), .age = 21 }); 80 | try insert.exec(.{ .id = sqlite.text("b"), .age = null }); 81 | ``` 82 | 83 | ### Queries 84 | 85 | If the `Result` type is a struct, use `stmt.bind(params)` in conjunction with `defer stmt.reset()`, then `stmt.step()` over the results. 86 | 87 | > ℹ️ Every `bind` should be paired with a `reset`, just like every `prepare` is paired with a `finalize`. 88 | 89 | ```zig 90 | const User = struct { id: sqlite.Text, age: ?f32 }; 91 | const select = try db.prepare( 92 | struct { min: f32 }, 93 | User, 94 | "SELECT * FROM users WHERE age >= :min", 95 | ); 96 | 97 | defer select.finalize(); 98 | 99 | // Get a single row 100 | { 101 | try select.bind(.{ .min = 0 }); 102 | defer select.reset(); 103 | 104 | if (try select.step()) |user| { 105 | // user.id: sqlite.Text 106 | // user.age: ?f32 107 | std.log.info("id: {s}, age: {d}", .{ user.id.data, user.age orelse 0 }); 108 | } 109 | } 110 | 111 | // Iterate over all rows 112 | { 113 | try select.bind(.{ .min = 0 }); 114 | defer select.reset(); 115 | 116 | while (try select.step()) |user| { 117 | std.log.info("id: {s}, age: {d}", .{ user.id.data, user.age orelse 0 }); 118 | } 119 | } 120 | 121 | // Iterate again, with different params 122 | { 123 | try select.bind(.{ .min = 21 }); 124 | defer select.reset(); 125 | 126 | while (try select.step()) |user| { 127 | std.log.info("id: {s}, age: {d}", .{ user.id.data, user.age orelse 0 }); 128 | } 129 | } 130 | ``` 131 | 132 | Text and blob values must not be retained across steps. **You are responsible for copying them.** 133 | 134 | ### Errors 135 | 136 | The basic sqlite errors (`SQLITE_ERROR`, `SQLITE_BUSY`, `SQLITE_CANTOPEN`, etc) are exported as a `sqlite.Error` error union. If an error has been thrown, you can access a detailed error message with `db.errmsg(): ?[*:0]const u8`. 137 | 138 | ## Notes 139 | 140 | Crafting sensible Zig bindings for SQLite involves making tradeoffs between following the Zig philosophy ("deallocation must succeed") and matching the SQLite API, in which closing databases or finalizing statements may return error codes. 141 | 142 | This library takes the following approach: 143 | 144 | - `Database.close` calls `sqlite3_close_v2` and panics if it returns an error code. 145 | - `Statement.finalize` calls `sqlite3_finalize` and panics if it returns an error code. 146 | - `Statement.step` automatically calls `sqlite3_reset` if `sqlite3_step` returns an error code. 147 | - In SQLite, `sqlite3_reset` returns the error code from the most recent call to `sqlite3_step`. This is handled gracefully. 148 | - `Statement.reset` calls both `sqlite3_reset` and `sqlite3_clear_bindings`, and panics if either return an error code. 149 | 150 | These should only result in panic through gross misuse or in extremely unusual situations, e.g. `sqlite3_reset` failing internally. All "normal" errors are faithfully surfaced as Zig errors. 151 | 152 | ## Build options 153 | 154 | ```zig 155 | struct { 156 | SQLITE_ENABLE_COLUMN_METADATA: bool = false, 157 | SQLITE_ENABLE_DBSTAT_VTAB: bool = false, 158 | SQLITE_ENABLE_FTS3: bool = false, 159 | SQLITE_ENABLE_FTS4: bool = false, 160 | SQLITE_ENABLE_FTS5: bool = false, 161 | SQLITE_ENABLE_GEOPOLY: bool = false, 162 | SQLITE_ENABLE_ICU: bool = false, 163 | SQLITE_ENABLE_MATH_FUNCTIONS: bool = false, 164 | SQLITE_ENABLE_RBU: bool = false, 165 | SQLITE_ENABLE_RTREE: bool = false, 166 | SQLITE_ENABLE_STAT4: bool = false, 167 | SQLITE_OMIT_DECLTYPE: bool = false, 168 | SQLITE_OMIT_JSON: bool = false, 169 | SQLITE_USE_URI: bool = false, 170 | } 171 | ``` 172 | 173 | Set these by passing e.g. `-DSQLITE_ENABLE_RTREE` in the CLI, or by setting `.SQLITE_ENABLE_RTREE = true` in the `args` parameter to `std.Build.dependency`. For example: 174 | 175 | ```zig 176 | pub fn build(b: *std.Build) !void { 177 | // ... 178 | 179 | const sqlite = b.dependency("sqlite", .{ .SQLITE_ENABLE_RTREE = true }); 180 | } 181 | ``` 182 | 183 | ## License 184 | 185 | MIT © nDimensional Studios 186 | -------------------------------------------------------------------------------- /src/test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const c = @cImport(@cInclude("sqlite3.h")); 4 | const errors = @import("errors.zig"); 5 | const sqlite = @import("sqlite.zig"); 6 | 7 | test "open and close an in-memory database" { 8 | const db = try sqlite.Database.open(.{}); 9 | defer db.close(); 10 | } 11 | 12 | test "insert" { 13 | const db = try sqlite.Database.open(.{}); 14 | defer db.close(); 15 | 16 | try db.exec("CREATE TABLE users(id TEXT PRIMARY KEY, age FLOAT)", .{}); 17 | const User = struct { id: sqlite.Text, age: ?f32 }; 18 | 19 | { 20 | const insert = try db.prepare(User, void, "INSERT INTO users VALUES (:id, :age)"); 21 | defer insert.finalize(); 22 | 23 | try insert.exec(.{ .id = sqlite.text("a"), .age = 5 }); 24 | try insert.exec(.{ .id = sqlite.text("b"), .age = 7 }); 25 | try insert.exec(.{ .id = sqlite.text("c"), .age = null }); 26 | } 27 | 28 | { 29 | const select = try db.prepare(struct {}, User, "SELECT id, age FROM users"); 30 | defer select.finalize(); 31 | 32 | try select.bind(.{}); 33 | defer select.reset(); 34 | 35 | if (try select.step()) |user| { 36 | try std.testing.expectEqualSlices(u8, "a", user.id.data); 37 | try std.testing.expectEqual(@as(?f32, 5), user.age); 38 | } else try std.testing.expect(false); 39 | 40 | if (try select.step()) |user| { 41 | try std.testing.expectEqualSlices(u8, "b", user.id.data); 42 | try std.testing.expectEqual(@as(?f32, 7), user.age); 43 | } else try std.testing.expect(false); 44 | 45 | if (try select.step()) |user| { 46 | try std.testing.expectEqualSlices(u8, "c", user.id.data); 47 | try std.testing.expectEqual(@as(?f32, null), user.age); 48 | } else try std.testing.expect(false); 49 | 50 | try std.testing.expectEqual(@as(?User, null), try select.step()); 51 | } 52 | } 53 | 54 | test "count" { 55 | const db = try sqlite.Database.open(.{}); 56 | defer db.close(); 57 | 58 | try db.exec("CREATE TABLE users(id TEXT PRIMARY KEY, age FLOAT)", .{}); 59 | try db.exec("INSERT INTO users VALUES(\"a\", 21)", .{}); 60 | try db.exec("INSERT INTO users VALUES(\"b\", 23)", .{}); 61 | try db.exec("INSERT INTO users VALUES(\"c\", NULL)", .{}); 62 | 63 | { 64 | const Result = struct { age: f32 }; 65 | const select = try db.prepare(struct {}, Result, "SELECT age FROM users"); 66 | defer select.finalize(); 67 | 68 | try select.bind(.{}); 69 | defer select.reset(); 70 | 71 | try std.testing.expectEqual(@as(?Result, .{ .age = 21 }), try select.step()); 72 | } 73 | 74 | { 75 | const Result = struct { count: usize }; 76 | const select = try db.prepare(struct {}, Result, "SELECT count(*) as count FROM users"); 77 | defer select.finalize(); 78 | 79 | try select.bind(.{}); 80 | defer select.reset(); 81 | 82 | try std.testing.expectEqual(@as(?Result, .{ .count = 3 }), try select.step()); 83 | } 84 | } 85 | 86 | test "example" { 87 | const db = try sqlite.Database.open(.{}); 88 | defer db.close(); 89 | 90 | try db.exec("CREATE TABLE users (id TEXT PRIMARY KEY, age FLOAT)", .{}); 91 | 92 | const User = struct { id: sqlite.Text, age: ?f32 }; 93 | const insert = try db.prepare( 94 | User, 95 | void, 96 | "INSERT INTO users VALUES (:id, :age)", 97 | ); 98 | defer insert.finalize(); 99 | 100 | try insert.exec(.{ .id = sqlite.text("a"), .age = 21 }); 101 | try insert.exec(.{ .id = sqlite.text("b"), .age = null }); 102 | 103 | const select = try db.prepare( 104 | struct { min: f32 }, 105 | User, 106 | "SELECT * FROM users WHERE age >= :min", 107 | ); 108 | 109 | defer select.finalize(); 110 | 111 | // Get a single row 112 | { 113 | try select.bind(.{ .min = 0 }); 114 | defer select.reset(); 115 | 116 | if (try select.step()) |user| { 117 | // user.id: sqlite.Text 118 | // user.age: ?f32 119 | std.log.info("{s} age: {d}", .{ user.id.data, user.age orelse 0 }); 120 | } 121 | } 122 | 123 | // Iterate over all rows 124 | { 125 | try select.bind(.{ .min = 0 }); 126 | defer select.reset(); 127 | 128 | while (try select.step()) |user| { 129 | std.log.info("{s} age: {d}", .{ user.id.data, user.age orelse 0 }); 130 | } 131 | } 132 | 133 | // Iterate again, with different params 134 | { 135 | try select.bind(.{ .min = 21 }); 136 | defer select.reset(); 137 | 138 | while (try select.step()) |user| { 139 | std.log.info("{s} age: {d}", .{ user.id.data, user.age orelse 0 }); 140 | } 141 | } 142 | } 143 | 144 | test "deserialize" { 145 | const allocator = std.heap.c_allocator; 146 | 147 | var tmp = std.testing.tmpDir(.{}); 148 | defer tmp.cleanup(); 149 | 150 | const db1 = try open(allocator, tmp.dir, "db.sqlite"); 151 | defer db1.close(); 152 | 153 | try db1.exec("CREATE TABLE users (id INTEGER PRIMARY KEY)", .{}); 154 | try db1.exec("INSERT INTO users VALUES (:id)", .{ .id = @as(usize, 0) }); 155 | try db1.exec("INSERT INTO users VALUES (:id)", .{ .id = @as(usize, 1) }); 156 | 157 | const file = try tmp.dir.openFile("db.sqlite", .{}); 158 | defer file.close(); 159 | 160 | const data = try file.readToEndAlloc(allocator, 4096 * 8); 161 | defer allocator.free(data); 162 | 163 | const db2 = try sqlite.Database.import(data); 164 | defer db2.close(); 165 | 166 | const User = struct { id: usize }; 167 | var rows = std.ArrayList(User){}; 168 | defer rows.deinit(allocator); 169 | 170 | const stmt = try db2.prepare(struct {}, User, "SELECT id FROM users"); 171 | defer stmt.finalize(); 172 | 173 | try stmt.bind(.{}); 174 | defer stmt.reset(); 175 | while (try stmt.step()) |row| { 176 | try rows.append(allocator, row); 177 | } 178 | 179 | try std.testing.expectEqualSlices(User, &.{ .{ .id = 0 }, .{ .id = 1 } }, rows.items); 180 | } 181 | 182 | test "default result fields" { 183 | const db = try sqlite.Database.open(.{}); 184 | defer db.close(); 185 | 186 | const Params = struct {}; 187 | const Result = struct { id: u32, foo: u32 = 8 }; 188 | 189 | try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT)", .{}); 190 | try db.exec("INSERT INTO users(id) VALUES (:id)", .{ .id = @as(u32, 9) }); 191 | 192 | const select_users = try db.prepare(Params, Result, "SELECT id FROM users"); 193 | try select_users.bind(.{}); 194 | defer select_users.reset(); 195 | 196 | const user = try select_users.step() orelse return error.NotFound; 197 | try std.testing.expectEqual(9, user.id); 198 | try std.testing.expectEqual(8, user.foo); 199 | } 200 | 201 | test "errmsg" { 202 | const db = try sqlite.Database.open(.{}); 203 | defer db.close(); 204 | 205 | const Params = struct {}; 206 | const Result = struct { id: u32, foo: u32 = 8 }; 207 | 208 | try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT)", .{}); 209 | 210 | try std.testing.expectError(sqlite.Error.SQLITE_ERROR, db.prepare(Params, Result, "FJDKLSFJDKSL")); 211 | const errmsg = db.errmsg() orelse return error.NOERR; 212 | try std.testing.expectEqualSlices(u8, 213 | \\near "FJDKLSFJDKSL": syntax error 214 | , std.mem.span(errmsg)); 215 | } 216 | 217 | fn open(allocator: std.mem.Allocator, dir: std.fs.Dir, name: []const u8) !sqlite.Database { 218 | const path_dir = try dir.realpathAlloc(allocator, "."); 219 | defer allocator.free(path_dir); 220 | 221 | const path_file = try std.fs.path.joinZ(allocator, &.{ path_dir, name }); 222 | defer allocator.free(path_file); 223 | 224 | return try sqlite.Database.open(.{ .path = path_file }); 225 | } 226 | -------------------------------------------------------------------------------- /src/sqlite.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const errors = @import("errors.zig"); 3 | 4 | pub const c = @import("c.zig").c; 5 | 6 | pub const Error = errors.Error; 7 | 8 | /// A wrapper struct that binds SQLite's blob type 9 | pub const Blob = struct { data: []const u8 }; 10 | 11 | /// A wrapper struct that binds SQLite's text type 12 | pub const Text = struct { data: []const u8 }; 13 | 14 | /// Creates a Blob value from a byte slice 15 | pub fn blob(data: []const u8) Blob { 16 | return .{ .data = data }; 17 | } 18 | 19 | /// Creates a Text value from a string 20 | pub fn text(data: []const u8) Text { 21 | return .{ .data = data }; 22 | } 23 | 24 | /// Represents a connection to a SQLite database. 25 | /// 26 | /// Example: 27 | /// ``` 28 | /// const db = try Database.open(.{ 29 | /// .path = "mydb.sqlite", 30 | /// .mode = .ReadWrite, 31 | /// .create = true, 32 | /// }); 33 | /// defer db.close(); 34 | /// ``` 35 | pub const Database = struct { 36 | pub const Mode = enum { ReadWrite, ReadOnly }; 37 | 38 | pub const Options = struct { 39 | /// Path to the database file. If null, creates an in-memory database 40 | path: ?[*:0]const u8 = null, 41 | /// Access mode for the database 42 | mode: Mode = .ReadWrite, 43 | /// Create the database if it doesn't exist 44 | create: bool = true, 45 | }; 46 | 47 | ptr: ?*c.sqlite3, 48 | 49 | pub fn open(options: Options) !Database { 50 | var ptr: ?*c.sqlite3 = null; 51 | 52 | var flags: c_int = 0; 53 | switch (options.mode) { 54 | .ReadOnly => { 55 | flags |= c.SQLITE_OPEN_READONLY; 56 | }, 57 | .ReadWrite => { 58 | flags |= c.SQLITE_OPEN_READWRITE; 59 | if (options.create and options.path != null) { 60 | flags |= c.SQLITE_OPEN_CREATE; 61 | } 62 | }, 63 | } 64 | 65 | try errors.throw(c.sqlite3_open_v2(options.path, &ptr, flags, null)); 66 | 67 | return .{ .ptr = ptr }; 68 | } 69 | 70 | /// Must not be in WAL mode. Returns a read-only in-memory database. 71 | pub fn import(data: []const u8) !Database { 72 | const db = try Database.open(.{ .mode = .ReadOnly }); 73 | const ptr: [*]u8 = @constCast(data.ptr); 74 | const len: c_longlong = @intCast(data.len); 75 | const flags = c.SQLITE_DESERIALIZE_READONLY; 76 | try errors.throw(c.sqlite3_deserialize(db.ptr, "main", ptr, len, len, flags)); 77 | return db; 78 | } 79 | 80 | pub fn close(db: Database) void { 81 | errors.throw(c.sqlite3_close_v2(db.ptr)) catch |err| { 82 | const msg = c.sqlite3_errmsg(db.ptr); 83 | std.debug.panic("sqlite3_close_v2: {s} {s}", .{ @errorName(err), msg }); 84 | }; 85 | } 86 | 87 | pub inline fn prepare(db: Database, comptime Params: type, comptime Result: type, sql: []const u8) !Statement(Params, Result) { 88 | return try Statement(Params, Result).prepare(db, sql); 89 | } 90 | 91 | pub fn exec(db: Database, sql: []const u8, params: anytype) !void { 92 | const stmt = try Statement(@TypeOf(params), void).prepare(db, sql); 93 | defer stmt.finalize(); 94 | 95 | try stmt.exec(params); 96 | } 97 | 98 | pub inline fn errmsg(db: Database) ?[*:0]const u8 { 99 | return c.sqlite3_errmsg(db.ptr); 100 | } 101 | }; 102 | 103 | pub fn Statement(comptime Params: type, comptime Result: type) type { 104 | const param_bindings = switch (@typeInfo(Params)) { 105 | .@"struct" => |info| Binding.parseStruct(info), 106 | else => @compileError("Params type must be a struct"), 107 | }; 108 | 109 | const column_bindings = switch (@typeInfo(Result)) { 110 | .void => .{}, 111 | .@"struct" => |info| Binding.parseStruct(info), 112 | else => @compileError("Result type must be a struct or void"), 113 | }; 114 | 115 | const param_count = param_bindings.len; 116 | const column_count = column_bindings.len; 117 | const placeholder: c_int = -1; 118 | 119 | return struct { 120 | const Self = @This(); 121 | 122 | ptr: ?*c.sqlite3_stmt = null, 123 | param_index_map: [param_count]c_int = .{placeholder} ** param_count, 124 | column_index_map: [column_count]c_int = .{placeholder} ** column_count, 125 | 126 | pub fn prepare(db: Database, sql: []const u8) !Self { 127 | var stmt = Self{}; 128 | 129 | try errors.throw(c.sqlite3_prepare_v2(db.ptr, sql.ptr, @intCast(sql.len), &stmt.ptr, null)); 130 | errdefer errors.throw(c.sqlite3_finalize(stmt.ptr)) catch |err| { 131 | const msg = c.sqlite3_errmsg(db.ptr); 132 | std.debug.panic("sqlite3_finalize: {s} {s}", .{ @errorName(err), msg }); 133 | }; 134 | 135 | // Populate stmt.param_index_map 136 | { 137 | const count = c.sqlite3_bind_parameter_count(stmt.ptr); 138 | 139 | var idx: c_int = 1; 140 | params: while (idx <= count) : (idx += 1) { 141 | const parameter_name = c.sqlite3_bind_parameter_name(stmt.ptr, idx); 142 | if (parameter_name == null) { 143 | return error.InvalidParameter; 144 | } 145 | 146 | const name = std.mem.span(parameter_name); 147 | if (name.len == 0) { 148 | return error.InvalidParameter; 149 | } else switch (name[0]) { 150 | ':', '$', '@' => {}, 151 | else => return error.InvalidParameter, 152 | } 153 | 154 | inline for (param_bindings, 0..) |binding, i| { 155 | if (std.mem.eql(u8, binding.field.name, name[1..])) { 156 | if (stmt.param_index_map[i] == placeholder) { 157 | stmt.param_index_map[i] = idx; 158 | continue :params; 159 | } else { 160 | return error.DuplicateParameter; 161 | } 162 | } 163 | } 164 | 165 | return error.MissingParameter; 166 | } 167 | } 168 | 169 | // Populate stmt.column_index_map 170 | { 171 | const count = c.sqlite3_column_count(stmt.ptr); 172 | 173 | var n: c_int = 0; 174 | columns: while (n < count) : (n += 1) { 175 | const column_name = c.sqlite3_column_name(stmt.ptr, n); 176 | if (column_name == null) { 177 | return error.OutOfMemory; 178 | } 179 | 180 | const name = std.mem.span(column_name); 181 | 182 | inline for (column_bindings, 0..) |binding, i| { 183 | if (std.mem.eql(u8, binding.field.name, name)) { 184 | if (stmt.column_index_map[i] == placeholder) { 185 | stmt.column_index_map[i] = n; 186 | continue :columns; 187 | } else { 188 | return error.DuplicateColumn; 189 | } 190 | } 191 | } 192 | } 193 | 194 | inline for (column_bindings, 0..) |binding, i| { 195 | if (stmt.column_index_map[i] == placeholder and binding.default_value_ptr == null) { 196 | return error.MissingColumn; 197 | } 198 | } 199 | 200 | // for (stmt.column_index_map) |i| { 201 | // if (i == placeholder) { 202 | // return error.MissingColumn; 203 | // } 204 | // } 205 | } 206 | 207 | return stmt; 208 | } 209 | 210 | pub fn finalize(stmt: Self) void { 211 | errors.throw(c.sqlite3_finalize(stmt.ptr)) catch |err| { 212 | const db = c.sqlite3_db_handle(stmt.ptr); 213 | const msg = c.sqlite3_errmsg(db); 214 | std.debug.panic("sqlite3_finalize: {s} {s}", .{ @errorName(err), msg }); 215 | }; 216 | } 217 | 218 | pub fn reset(stmt: Self) void { 219 | errors.throw(c.sqlite3_reset(stmt.ptr)) catch |err| { 220 | const msg = c.sqlite3_errmsg(c.sqlite3_db_handle(stmt.ptr)); 221 | std.debug.panic("sqlite3_reset: {s} {s}", .{ @errorName(err), msg }); 222 | }; 223 | 224 | errors.throw(c.sqlite3_clear_bindings(stmt.ptr)) catch |err| { 225 | const msg = c.sqlite3_errmsg(c.sqlite3_db_handle(stmt.ptr)); 226 | std.debug.panic("sqlite3_clear_bindings: {s} {s}", .{ @errorName(err), msg }); 227 | }; 228 | } 229 | 230 | pub fn exec(stmt: Self, params: Params) !void { 231 | switch (@typeInfo(Result)) { 232 | .void => {}, 233 | else => @compileError("only void Result types can call .exec"), 234 | } 235 | 236 | try stmt.bind(params); 237 | defer stmt.reset(); 238 | try stmt.step() orelse {}; 239 | } 240 | 241 | pub fn step(stmt: Self) !?Result { 242 | switch (c.sqlite3_step(stmt.ptr)) { 243 | c.SQLITE_ROW => return try stmt.parseRow(), 244 | c.SQLITE_DONE => return null, 245 | else => |code| { 246 | // sqlite3_reset returns the same code we already have 247 | const rc = c.sqlite3_reset(stmt.ptr); 248 | if (rc == code) { 249 | return errors.getError(code); 250 | } else { 251 | const err = errors.getError(rc); 252 | const msg = c.sqlite3_errmsg(c.sqlite3_db_handle(stmt.ptr)); 253 | std.debug.panic("sqlite3_reset: {s} {s}", .{ @errorName(err), msg }); 254 | } 255 | }, 256 | } 257 | } 258 | 259 | pub fn bind(stmt: Self, params: Params) !void { 260 | inline for (param_bindings, 0..) |binding, i| { 261 | const idx = stmt.param_index_map[i]; 262 | if (binding.nullable) { 263 | if (@field(params, binding.field.name)) |value| { 264 | switch (binding.type) { 265 | .int32 => try stmt.bindInt32(idx, @intCast(value)), 266 | .int64 => try stmt.bindInt64(idx, @intCast(value)), 267 | .float64 => try stmt.bindFloat64(idx, @floatCast(value)), 268 | .blob => try stmt.bindBlob(idx, value), 269 | .text => try stmt.bindText(idx, value), 270 | } 271 | } else { 272 | try stmt.bindNull(idx); 273 | } 274 | } else { 275 | const value = @field(params, binding.field.name); 276 | switch (binding.type) { 277 | .int32 => try stmt.bindInt32(idx, @intCast(value)), 278 | .int64 => try stmt.bindInt64(idx, @intCast(value)), 279 | .float64 => try stmt.bindFloat64(idx, @floatCast(value)), 280 | .blob => try stmt.bindBlob(idx, value), 281 | .text => try stmt.bindText(idx, value), 282 | } 283 | } 284 | } 285 | } 286 | 287 | fn bindNull(stmt: Self, idx: c_int) !void { 288 | try errors.throw(c.sqlite3_bind_null(stmt.ptr, idx)); 289 | } 290 | 291 | fn bindInt32(stmt: Self, idx: c_int, value: i32) !void { 292 | try errors.throw(c.sqlite3_bind_int(stmt.ptr, idx, value)); 293 | } 294 | 295 | fn bindInt64(stmt: Self, idx: c_int, value: i64) !void { 296 | try errors.throw(c.sqlite3_bind_int64(stmt.ptr, idx, value)); 297 | } 298 | 299 | fn bindFloat64(stmt: Self, idx: c_int, value: f64) !void { 300 | try errors.throw(c.sqlite3_bind_double(stmt.ptr, idx, value)); 301 | } 302 | 303 | fn bindBlob(stmt: Self, idx: c_int, value: Blob) !void { 304 | const ptr = value.data.ptr; 305 | const len = value.data.len; 306 | try errors.throw(c.sqlite3_bind_blob64(stmt.ptr, idx, ptr, @intCast(len), c.SQLITE_STATIC)); 307 | } 308 | 309 | fn bindText(stmt: Self, idx: c_int, value: Text) !void { 310 | const ptr = value.data.ptr; 311 | const len = value.data.len; 312 | try errors.throw(c.sqlite3_bind_text64(stmt.ptr, idx, ptr, @intCast(len), c.SQLITE_STATIC, c.SQLITE_UTF8)); 313 | } 314 | 315 | fn parseRow(stmt: Self) !Result { 316 | var result: Result = undefined; 317 | 318 | inline for (column_bindings, 0..) |binding, i| { 319 | const n = stmt.column_index_map[i]; 320 | 321 | if (n == placeholder) { 322 | // default value 323 | if (binding.default_value_ptr) |ptr| { 324 | const typed_ptr: *const binding.field.type = @ptrCast(@alignCast(ptr)); 325 | @field(result, binding.field.name) = typed_ptr.*; 326 | } else { 327 | return error.MissingColumn; 328 | } 329 | } else { 330 | switch (c.sqlite3_column_type(stmt.ptr, n)) { 331 | c.SQLITE_NULL => if (binding.nullable) { 332 | @field(result, binding.field.name) = null; 333 | } else { 334 | return error.InvalidColumnType; 335 | }, 336 | 337 | c.SQLITE_INTEGER => switch (binding.type) { 338 | .int32 => |info| { 339 | const value = stmt.columnInt32(n); 340 | switch (info.signedness) { 341 | .signed => {}, 342 | .unsigned => { 343 | if (value < 0) { 344 | return error.IntegerOutOfRange; 345 | } 346 | }, 347 | } 348 | 349 | @field(result, binding.field.name) = @intCast(value); 350 | }, 351 | .int64 => |info| { 352 | const value = stmt.columnInt64(n); 353 | switch (info.signedness) { 354 | .signed => {}, 355 | .unsigned => { 356 | if (value < 0) { 357 | return error.IntegerOutOfRange; 358 | } 359 | }, 360 | } 361 | 362 | @field(result, binding.field.name) = @intCast(value); 363 | }, 364 | else => return error.InvalidColumnType, 365 | }, 366 | 367 | c.SQLITE_FLOAT => switch (binding.type) { 368 | .float64 => @field(result, binding.field.name) = @floatCast(stmt.columnFloat64(n)), 369 | else => return error.InvalidColumnType, 370 | }, 371 | 372 | c.SQLITE_BLOB => switch (binding.type) { 373 | .blob => @field(result, binding.field.name) = stmt.columnBlob(n), 374 | else => return error.InvalidColumnType, 375 | }, 376 | 377 | c.SQLITE_TEXT => switch (binding.type) { 378 | .text => @field(result, binding.field.name) = stmt.columnText(n), 379 | else => return error.InvalidColumnType, 380 | }, 381 | 382 | else => @panic("internal SQLite error"), 383 | } 384 | } 385 | } 386 | 387 | return result; 388 | } 389 | 390 | fn columnInt32(stmt: Self, n: c_int) i32 { 391 | return c.sqlite3_column_int(stmt.ptr, n); 392 | } 393 | 394 | fn columnInt64(stmt: Self, n: c_int) i64 { 395 | return c.sqlite3_column_int64(stmt.ptr, n); 396 | } 397 | 398 | fn columnFloat64(stmt: Self, n: c_int) f64 { 399 | return c.sqlite3_column_double(stmt.ptr, n); 400 | } 401 | 402 | fn columnBlob(stmt: Self, n: c_int) Blob { 403 | const ptr: [*]const u8 = @ptrCast(c.sqlite3_column_blob(stmt.ptr, n)); 404 | const len = c.sqlite3_column_bytes(stmt.ptr, n); 405 | if (len < 0) { 406 | std.debug.panic("sqlite3_column_bytes: len < 0", .{}); 407 | } 408 | 409 | return blob(ptr[0..@intCast(len)]); 410 | } 411 | 412 | fn columnText(stmt: Self, n: c_int) Text { 413 | const ptr: [*]const u8 = @ptrCast(c.sqlite3_column_text(stmt.ptr, n)); 414 | const len = c.sqlite3_column_bytes(stmt.ptr, n); 415 | if (len < 0) { 416 | std.debug.panic("sqlite3_column_bytes: len < 0", .{}); 417 | } 418 | 419 | return text(ptr[0..@intCast(len)]); 420 | } 421 | }; 422 | } 423 | 424 | const Binding = struct { 425 | pub const TypeTag = enum { 426 | int32, 427 | int64, 428 | float64, 429 | blob, 430 | text, 431 | }; 432 | 433 | pub const Type = union(TypeTag) { 434 | int32: std.builtin.Type.Int, 435 | int64: std.builtin.Type.Int, 436 | float64: std.builtin.Type.Float, 437 | blob: void, 438 | text: void, 439 | 440 | pub fn parse(comptime T: type) Type { 441 | return switch (T) { 442 | Blob => .{ .blob = {} }, 443 | Text => .{ .text = {} }, 444 | else => switch (@typeInfo(T)) { 445 | .int => |info| switch (info.signedness) { 446 | .signed => if (info.bits <= 32) .{ .int32 = info } else .{ .int64 = info }, 447 | .unsigned => if (info.bits <= 31) .{ .int32 = info } else .{ .int64 = info }, 448 | }, 449 | .float => |info| .{ .float64 = info }, 450 | else => @compileError("invalid binding type"), 451 | }, 452 | }; 453 | } 454 | }; 455 | 456 | field: std.builtin.Type.StructField, 457 | type: Type, 458 | nullable: bool, 459 | default_value_ptr: ?*const anyopaque, 460 | 461 | pub fn parseStruct(comptime info: std.builtin.Type.Struct) [info.fields.len]Binding { 462 | var bindings: [info.fields.len]Binding = undefined; 463 | inline for (info.fields, 0..) |field, i| { 464 | bindings[i] = parseField(field); 465 | } 466 | 467 | return bindings; 468 | } 469 | 470 | pub fn parseField(comptime field: std.builtin.Type.StructField) Binding { 471 | return switch (@typeInfo(field.type)) { 472 | .optional => |field_type| Binding{ 473 | .field = field, 474 | .type = Type.parse(field_type.child), 475 | .nullable = true, 476 | .default_value_ptr = field.default_value_ptr, 477 | }, 478 | else => Binding{ 479 | .field = field, 480 | .type = Type.parse(field.type), 481 | .nullable = false, 482 | .default_value_ptr = field.default_value_ptr, 483 | }, 484 | }; 485 | } 486 | }; 487 | --------------------------------------------------------------------------------