summaryrefslogtreecommitdiff
path: root/src/main.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.zig')
-rw-r--r--src/main.zig509
1 files changed, 509 insertions, 0 deletions
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..d57e73a
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,509 @@
1const std = @import("std");
2const build_options = @import("build_options");
3const debug = std.debug;
4const mem = std.mem;
5const testing = std.testing;
6
7const c = @cImport({
8 @cInclude("sqlite3.h");
9});
10
11const logger = std.log.scoped(.sqlite);
12
13/// Db is a wrapper around a SQLite database, providing high-level functions to executing queries.
14/// A Db can be opened with a file database or a in-memory database:
15///
16/// // File database
17/// var db: sqlite.Db = undefined;
18/// try db.init(allocator, .{ .File = "/tmp/data.db" });
19///
20/// // In memory database
21/// var db: sqlite.Db = undefined;
22/// try db.init(allocator, .{ .Memory={} });
23///
24pub const Db = struct {
25 const Self = @This();
26
27 allocator: *mem.Allocator,
28 db: *c.sqlite3,
29
30 /// Mode determines how the database will be opened.
31 pub const Mode = union(enum) {
32 File: []const u8,
33 Memory,
34 };
35
36 /// init creates a database with the provided `mode`.
37 pub fn init(self: *Self, allocator: *mem.Allocator, mode: Mode) !void {
38 self.allocator = allocator;
39
40 switch (mode) {
41 .File => |path| {
42 logger.info("opening {}", .{path});
43
44 // Need a null-terminated string here.
45 const pathZ = try allocator.dupeZ(u8, path);
46 defer allocator.free(pathZ);
47
48 var db: ?*c.sqlite3 = undefined;
49 const result = c.sqlite3_open_v2(
50 pathZ,
51 &db,
52 c.SQLITE_OPEN_READWRITE | c.SQLITE_OPEN_CREATE,
53 null,
54 );
55 if (result != c.SQLITE_OK or db == null) {
56 logger.warn("unable to open database, result: {}", .{result});
57 return error.CannotOpenDatabase;
58 }
59
60 self.db = db.?;
61 },
62 .Memory => {
63 logger.info("opening in memory", .{});
64
65 var db: ?*c.sqlite3 = undefined;
66 const result = c.sqlite3_open_v2(
67 ":memory:",
68 &db,
69 c.SQLITE_OPEN_READWRITE | c.SQLITE_OPEN_MEMORY,
70 null,
71 );
72 if (result != c.SQLITE_OK or db == null) {
73 logger.warn("unable to open database, result: {}", .{result});
74 return error.CannotOpenDatabase;
75 }
76
77 self.db = db.?;
78 },
79 }
80 }
81
82 /// deinit closes the database.
83 pub fn deinit(self: *Self) void {
84 _ = c.sqlite3_close(self.db);
85 }
86
87 /// exec is a convenience function which prepares a statement and executes it directly.
88 pub fn exec(self: *Self, comptime query: []const u8, values: anytype) !void {
89 var stmt = try self.prepare(query, values);
90 defer stmt.deinit();
91 try stmt.exec();
92 }
93
94 /// prepare prepares a statement for the `query` provided.
95 ///
96 /// The query is analysed at comptime to search for bind markers.
97 /// prepare enforces having as much fields in the `values` tuple as there are bind markers.
98 ///
99 /// Example usage:
100 ///
101 /// var stmt = try db.prepare("INSERT INTO foo(id, name) VALUES(?, ?)", .{
102 /// .id = 3540,
103 /// .name = "Eminem",
104 /// });
105 /// defer stmt.deinit();
106 ///
107 /// Note that the name of the fields in the tuple are irrelevant, only the types are.
108 pub fn prepare(self: *Self, comptime query: []const u8, values: anytype) !Statement {
109 return Statement.prepare(self, 0, query, values);
110 }
111
112 /// rowsAffected returns the number of rows affected by the last statement executed.
113 pub fn rowsAffected(self: *Self) usize {
114 return @intCast(usize, c.sqlite3_changes(self.db));
115 }
116};
117
118/// Statement is a wrapper around a SQLite statement, providing high-level functions to execute
119/// a statement and retrieve rows for SELECT queries.
120///
121/// The exec function can be used to execute a query which does not return rows:
122///
123/// var stmt = try db.prepare("UPDATE foo SET id = ? WHERE name = ?", .{
124/// .id = 200,
125/// .name = "José",
126/// });
127/// defer stmt.deinit();
128///
129/// The one function can be used to select a single row:
130///
131/// var stmt = try db.prepare("SELECT name FROM foo WHERE id = ?", .{ .id = 200 });
132/// defer stmt.deinit();
133///
134/// const Row = struct { id: usize };
135/// const row = try stmt.one(Row .{});
136///
137/// The all function can be used to select all rows:
138///
139/// var stmt = try db.prepare("SELECT name FROM foo", .{});
140/// defer stmt.deinit();
141///
142/// const Row = struct { id: usize };
143/// const rows = try stmt.all(Row .{});
144///
145/// Look at aach function for more complete documentation.
146///
147pub const Statement = struct {
148 const Self = @This();
149
150 stmt: *c.sqlite3_stmt,
151
152 fn prepare(db: *Db, flags: c_uint, comptime query: []const u8, values: anytype) !Self {
153 const StructType = @typeInfo(@TypeOf(values)).Struct;
154 comptime {
155 const bind_parameter_count = std.mem.count(u8, query, "?");
156 if (bind_parameter_count != StructType.fields.len) {
157 @compileError("bind parameter count != number of fields in tuple/struct");
158 }
159 }
160
161 // prepare
162
163 var stmt = blk: {
164 var tmp: ?*c.sqlite3_stmt = undefined;
165 const result = c.sqlite3_prepare_v3(
166 db.db,
167 query.ptr,
168 @intCast(c_int, query.len),
169 flags,
170 &tmp,
171 null,
172 );
173 if (result != c.SQLITE_OK) {
174 logger.warn("unable to prepare statement, result: {}", .{result});
175 return error.CannotPrepareStatement;
176 }
177 break :blk tmp.?;
178 };
179
180 // Bind
181
182 inline for (StructType.fields) |struct_field, _i| {
183 const i = @as(usize, _i);
184 const field_type_info = @typeInfo(struct_field.field_type);
185 const field_value = @field(values, struct_field.name);
186 const column = i + 1;
187
188 switch (struct_field.field_type) {
189 []const u8, []u8 => {
190 _ = c.sqlite3_bind_text(stmt, column, field_value.ptr, @intCast(c_int, field_value.len), null);
191 },
192 else => switch (field_type_info) {
193 .Int, .ComptimeInt => _ = c.sqlite3_bind_int64(stmt, column, @intCast(c_longlong, field_value)),
194 .Float, .ComptimeFloat => _ = c.sqlite3_bind_double(stmt, column, field_value),
195 .Array => |arr| {
196 switch (arr.child) {
197 u8 => {
198 const data: []const u8 = field_value[0..field_value.len];
199
200 _ = c.sqlite3_bind_text(stmt, column, data.ptr, @intCast(c_int, data.len), null);
201 },
202 else => @compileError("cannot populate field " ++ field.name ++ " of type array of " ++ @typeName(arr.child)),
203 }
204 },
205 else => @compileError("cannot bind field " ++ struct_field.name ++ " of type " ++ @typeName(struct_field.field_type)),
206 },
207 }
208 }
209
210 return Self{
211 .stmt = stmt,
212 };
213 }
214
215 pub fn deinit(self: *Self) void {
216 const result = c.sqlite3_finalize(self.stmt);
217 if (result != c.SQLITE_OK) {
218 logger.err("unable to finalize prepared statement, result: {}", .{result});
219 }
220 }
221
222 pub fn exec(self: *Self) !void {
223 const result = c.sqlite3_step(self.stmt);
224 switch (result) {
225 c.SQLITE_DONE => {},
226 c.SQLITE_BUSY => return error.SQLiteBusy,
227 else => std.debug.panic("invalid result {}", .{result}),
228 }
229 }
230
231 /// one reads a single row from the result set of this statement.
232 ///
233 /// The data in the row is used to populate a value of the type `Type`.
234 /// This means that `Type` must have as many fields as is returned in the query
235 /// executed by this statement.
236 /// This also means that the type of each field must be compatible with the SQLite type.
237 ///
238 /// Here is an example of how to use an anonymous struct type:
239 ///
240 /// const row = try stmt.one(
241 /// struct {
242 /// id: usize,
243 /// name: []const u8,
244 /// age: usize,
245 /// },
246 /// .{ .allocator = allocator },
247 /// );
248 ///
249 /// The `options` tuple is used to provide additional state in some cases, for example
250 /// an allocator used to read text and blobs.
251 ///
252 pub fn one(self: *Self, comptime Type: type, options: anytype) !?Type {
253 const TypeInfo = @typeInfo(Type);
254
255 var result = c.sqlite3_step(self.stmt);
256
257 switch (TypeInfo) {
258 .Int => return switch (result) {
259 c.SQLITE_ROW => try self.readInt(Type, options),
260 c.SQLITE_DONE => null,
261 else => std.debug.panic("invalid result {}", .{result}),
262 },
263 .Struct => return switch (result) {
264 c.SQLITE_ROW => try self.readStruct(Type, options),
265 c.SQLITE_DONE => null,
266 else => std.debug.panic("invalid result {}", .{result}),
267 },
268 else => @compileError("cannot read into type " ++ @typeName(Type)),
269 }
270 }
271
272 /// all reads all rows from the result set of this statement.
273 ///
274 /// The data in each row is used to populate a value of the type `Type`.
275 /// This means that `Type` must have as many fields as is returned in the query
276 /// executed by this statement.
277 /// This also means that the type of each field must be compatible with the SQLite type.
278 ///
279 /// Here is an example of how to use an anonymous struct type:
280 ///
281 /// const rows = try stmt.all(
282 /// struct {
283 /// id: usize,
284 /// name: []const u8,
285 /// age: usize,
286 /// },
287 /// .{ .allocator = allocator },
288 /// );
289 ///
290 /// The `options` tuple is used to provide additional state in some cases.
291 /// Note that for this function the allocator is mandatory.
292 ///
293 pub fn all(self: *Self, comptime Type: type, options: anytype) ![]Type {
294 const TypeInfo = @typeInfo(Type);
295
296 var rows = std.ArrayList(Type).init(options.allocator);
297
298 var result = c.sqlite3_step(self.stmt);
299 while (result == c.SQLITE_ROW) : (result = c.sqlite3_step(self.stmt)) {
300 const columns = c.sqlite3_column_count(self.stmt);
301
302 var value = switch (TypeInfo) {
303 .Int => blk: {
304 debug.assert(columns == 1);
305 break :blk try self.readInt(Type, options);
306 },
307 .Struct => blk: {
308 std.debug.assert(columns == @typeInfo(Type).Struct.fields.len);
309 break :blk try self.readStruct(Type, options);
310 },
311 else => @compileError("cannot read into type " ++ @typeName(Type)),
312 };
313
314 try rows.append(value);
315 }
316
317 if (result != c.SQLITE_DONE) {
318 logger.err("unable to iterate, result: {}", .{result});
319 return error.SQLiteStepError;
320 }
321
322 return rows.span();
323 }
324
325 fn readInt(self: *Self, comptime Type: type, options: anytype) !Type {
326 const n = c.sqlite3_column_int64(self.stmt, 0);
327 return @intCast(Type, n);
328 }
329
330 fn readStruct(self: *Self, comptime Type: type, options: anytype) !Type {
331 var value: Type = undefined;
332
333 inline for (@typeInfo(Type).Struct.fields) |field, _i| {
334 const i = @as(usize, _i);
335 const field_type_info = @typeInfo(field.field_type);
336
337 switch (field.field_type) {
338 []const u8, []u8 => {
339 const data = c.sqlite3_column_blob(self.stmt, i);
340 if (data == null) {
341 @field(value, field.name) = "";
342 } else {
343 const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i));
344
345 var tmp = try options.allocator.alloc(u8, size);
346 mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]);
347
348 @field(value, field.name) = tmp;
349 }
350 },
351 else => switch (field_type_info) {
352 .Int => {
353 const n = c.sqlite3_column_int64(self.stmt, i);
354 @field(value, field.name) = @intCast(field.field_type, n);
355 },
356 .Float => {
357 const f = c.sqlite3_column_double(self.stmt, i);
358 @field(value, field.name) = f;
359 },
360 .Void => {
361 @field(value, field.name) = {};
362 },
363 .Array => |arr| {
364 switch (arr.child) {
365 u8 => {
366 const data = c.sqlite3_column_blob(self.stmt, i);
367 const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i));
368
369 if (size > @as(usize, arr.len)) return error.ArrayTooSmall;
370
371 mem.copy(u8, @field(value, field.name)[0..], @ptrCast([*c]const u8, data)[0..size]);
372 },
373 else => @compileError("cannot populate field " ++ field.name ++ " of type array of " ++ @typeName(arr.child)),
374 }
375 },
376 else => @compileError("cannot populate field " ++ field.name ++ " of type " ++ @typeName(field.field_type)),
377 },
378 }
379 }
380
381 return value;
382 }
383};
384
385test "sqlite: statement exec" {
386 var arena = std.heap.ArenaAllocator.init(testing.allocator);
387 defer arena.deinit();
388 var allocator = &arena.allocator;
389
390 var db: Db = undefined;
391 try db.init(testing.allocator, dbMode());
392
393 // Create the tables
394
395 comptime const all_ddl = &[_][]const u8{
396 \\CREATE TABLE user(
397 \\ id integer PRIMARY KEY,
398 \\ name text,
399 \\ age integer
400 \\)
401 ,
402 \\CREATE TABLE article(
403 \\ id integer PRIMARY KEY,
404 \\ author_id integer,
405 \\ data text,
406 \\ FOREIGN KEY(author_id) REFERENCES user(id)
407 \\)
408 };
409 inline for (all_ddl) |ddl| {
410 var stmt = try db.prepare(ddl, .{});
411 defer stmt.deinit();
412 try stmt.exec();
413 }
414
415 // Add data
416
417 const User = struct {
418 id: usize,
419 name: []const u8,
420 age: usize,
421 };
422
423 const users = &[_]User{
424 .{ .id = 20, .name = "Vincent", .age = 33 },
425 .{ .id = 40, .name = "Julien", .age = 35 },
426 .{ .id = 60, .name = "José", .age = 40 },
427 };
428
429 for (users) |user| {
430 try db.exec("INSERT INTO user(id, name, age) VALUES(?, ?, ?)", user);
431
432 const rows_inserted = db.rowsAffected();
433 testing.expectEqual(@as(usize, 1), rows_inserted);
434 }
435
436 // Read a single user
437
438 {
439 var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?", .{ .id = 20 });
440 defer stmt.deinit();
441
442 var rows = try stmt.all(User, .{ .allocator = allocator });
443 for (rows) |row| {
444 testing.expectEqual(users[0].id, row.id);
445 testing.expectEqualStrings(users[0].name, row.name);
446 testing.expectEqual(users[0].age, row.age);
447 }
448 }
449
450 // Read all users
451
452 {
453 var stmt = try db.prepare("SELECT id, name, age FROM user", .{});
454 defer stmt.deinit();
455
456 var rows = try stmt.all(User, .{ .allocator = allocator });
457 testing.expectEqual(@as(usize, 3), rows.len);
458 for (rows) |row, i| {
459 const exp = users[i];
460 testing.expectEqual(exp.id, row.id);
461 testing.expectEqualStrings(exp.name, row.name);
462 testing.expectEqual(exp.age, row.age);
463 }
464 }
465
466 // Test with anonymous structs
467
468 {
469 var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?", .{ .id = 20 });
470 defer stmt.deinit();
471
472 var row = try stmt.one(
473 struct {
474 id: usize,
475 name: []const u8,
476 age: usize,
477 },
478 .{ .allocator = allocator },
479 );
480 testing.expect(row != null);
481
482 const exp = users[0];
483 testing.expectEqual(exp.id, row.?.id);
484 testing.expectEqualStrings(exp.name, row.?.name);
485 testing.expectEqual(exp.age, row.?.age);
486 }
487
488 // Test with a single integer
489
490 {
491 var stmt = try db.prepare("SELECT age FROM user WHERE id = ?", .{ .id = 20 });
492 defer stmt.deinit();
493
494 var age = try stmt.one(usize, .{});
495 testing.expect(age != null);
496
497 testing.expectEqual(@as(usize, 33), age.?);
498 }
499}
500
501pub fn dbMode() Db.Mode {
502 return if (build_options.is_ci) blk: {
503 break :blk .{ .Memory = {} };
504 } else blk: {
505 const path = "/tmp/zig-sqlite.db";
506 std.fs.cwd().deleteFile(path) catch {};
507 break :blk .{ .File = path };
508 };
509}