summaryrefslogtreecommitdiff
path: root/sqlite.zig
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--sqlite.zig240
1 files changed, 240 insertions, 0 deletions
diff --git a/sqlite.zig b/sqlite.zig
index b911e0e..e1e3216 100644
--- a/sqlite.zig
+++ b/sqlite.zig
@@ -1,6 +1,7 @@
1const std = @import("std"); 1const std = @import("std");
2const build_options = @import("build_options"); 2const build_options = @import("build_options");
3const debug = std.debug; 3const debug = std.debug;
4const io = std.io;
4const mem = std.mem; 5const mem = std.mem;
5const testing = std.testing; 6const testing = std.testing;
6 7
@@ -13,6 +14,171 @@ usingnamespace @import("error.zig");
13 14
14const logger = std.log.scoped(.sqlite); 15const logger = std.log.scoped(.sqlite);
15 16
17pub const ZeroBlob = struct {
18 length: usize,
19};
20
21/// Blob is a wrapper for a sqlite BLOB.
22///
23/// This type is useful when reading or binding data and for doing incremental i/o.
24pub const Blob = struct {
25 const Self = @This();
26
27 pub const OpenFlags = struct {
28 read: bool = true,
29 write: bool = false,
30 };
31
32 pub const DatabaseName = union(enum) {
33 main,
34 temp,
35 attached: [:0]const u8,
36
37 fn toString(self: @This()) [:0]const u8 {
38 return switch (self) {
39 .main => "main",
40 .temp => "temp",
41 .attached => |name| name,
42 };
43 }
44 };
45
46 // Used when reading or binding data.
47 data: []const u8,
48
49 // Used for incremental i/o.
50 handle: *c.sqlite3_blob = undefined,
51 offset: c_int = 0,
52 size: c_int = 0,
53
54 /// close closes the blob.
55 pub fn close(self: *Self) !void {
56 const result = c.sqlite3_blob_close(self.handle);
57 if (result != c.SQLITE_OK) {
58 return errorFromResultCode(result);
59 }
60 }
61
62 pub const Reader = io.Reader(*Self, Error, read);
63
64 /// reader returns a io.Reader.
65 pub fn reader(self: *Self) Reader {
66 return .{ .context = self };
67 }
68
69 fn read(self: *Self, buffer: []u8) Error!usize {
70 if (self.offset >= self.size) {
71 return 0;
72 }
73
74 var tmp_buffer = blk: {
75 const remaining = @intCast(usize, self.size) - @intCast(usize, self.offset);
76 break :blk if (buffer.len > remaining) buffer[0..remaining] else buffer;
77 };
78
79 const result = c.sqlite3_blob_read(
80 self.handle,
81 tmp_buffer.ptr,
82 @intCast(c_int, tmp_buffer.len),
83 self.offset,
84 );
85 if (result != c.SQLITE_OK) {
86 return errorFromResultCode(result);
87 }
88
89 self.offset += @intCast(c_int, tmp_buffer.len);
90
91 return tmp_buffer.len;
92 }
93
94 pub const Writer = io.Writer(*Self, Error, write);
95
96 /// writer returns a io.Writer.
97 pub fn writer(self: *Self) Writer {
98 return .{ .context = self };
99 }
100
101 fn write(self: *Self, data: []const u8) Error!usize {
102 const result = c.sqlite3_blob_write(
103 self.handle,
104 data.ptr,
105 @intCast(c_int, data.len),
106 self.offset,
107 );
108 if (result != c.SQLITE_OK) {
109 return errorFromResultCode(result);
110 }
111
112 self.offset += @intCast(c_int, data.len);
113
114 return data.len;
115 }
116
117 /// Reset the offset used for reading and writing.
118 pub fn reset(self: *Self) void {
119 self.offset = 0;
120 }
121
122 /// reopen moves this blob to another row of the same table.
123 ///
124 /// See https://sqlite.org/c3ref/blob_reopen.html.
125 pub fn reopen(self: *Self, row: i64) !void {
126 const result = c.sqlite3_blob_reopen(self.handle, row);
127 if (result != c.SQLITE_OK) {
128 return error.CannotReopenBlob;
129 }
130
131 self.size = c.sqlite3_blob_bytes(self.handle);
132 self.offset = 0;
133 }
134
135 /// open opens a blob for incremental i/o.
136 ///
137 /// You can get a std.io.Writer to write data to the blob:
138 ///
139 /// var blob = try db.openBlob(.main, "mytable", "mycolumn", 1, .{ .write = true });
140 /// var blob_writer = blob.writer();
141 ///
142 /// try blob_writer.writeAll(my_data);
143 ///
144 /// Note that a blob is not extensible, if you want to change the blob size you must use an UPDATE statement.
145 ///
146 /// You can get a std.io.Reader to read the blob data:
147 ///
148 /// var blob = try db.openBlob(.main, "mytable", "mycolumn", 1, .{});
149 /// var blob_reader = blob.reader();
150 ///
151 /// const data = try blob_reader.readAlloc(allocator);
152 ///
153 fn open(db: *c.sqlite3, db_name: DatabaseName, table: [:0]const u8, column: [:0]const u8, row: i64, comptime flags: OpenFlags) !Blob {
154 comptime if (!flags.read and !flags.write) {
155 @compileError("must open a blob for either read, write or both");
156 };
157
158 const open_flags: c_int = if (flags.write) 1 else 0;
159
160 var blob: Blob = undefined;
161 const result = c.sqlite3_blob_open(
162 db,
163 db_name.toString(),
164 table,
165 column,
166 row,
167 open_flags,
168 @ptrCast([*c]?*c.sqlite3_blob, &blob.handle),
169 );
170 if (result == c.SQLITE_MISUSE) debug.panic("sqlite misuse while opening a blob", .{});
171 if (result != c.SQLITE_OK) {
172 return error.CannotOpenBlob;
173 }
174
175 blob.size = c.sqlite3_blob_bytes(blob.handle);
176 blob.offset = 0;
177
178 return blob;
179 }
180};
181
16/// ThreadingMode controls the threading mode used by SQLite. 182/// ThreadingMode controls the threading mode used by SQLite.
17/// 183///
18/// See https://sqlite.org/threadsafe.html 184/// See https://sqlite.org/threadsafe.html
@@ -255,6 +421,11 @@ pub const Db = struct {
255 pub fn rowsAffected(self: *Self) usize { 421 pub fn rowsAffected(self: *Self) usize {
256 return @intCast(usize, c.sqlite3_changes(self.db)); 422 return @intCast(usize, c.sqlite3_changes(self.db));
257 } 423 }
424
425 /// openBlob opens a blob.
426 pub fn openBlob(self: *Self, db_name: Blob.DatabaseName, table: [:0]const u8, column: [:0]const u8, row: i64, comptime flags: Blob.OpenFlags) !Blob {
427 return Blob.open(self.db, db_name, table, column, row, flags);
428 }
258}; 429};
259 430
260/// Iterator allows iterating over a result set. 431/// Iterator allows iterating over a result set.
@@ -764,6 +935,7 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) t
764 switch (FieldType) { 935 switch (FieldType) {
765 Text => _ = c.sqlite3_bind_text(self.stmt, column, field.data.ptr, @intCast(c_int, field.data.len), null), 936 Text => _ = c.sqlite3_bind_text(self.stmt, column, field.data.ptr, @intCast(c_int, field.data.len), null),
766 Blob => _ = c.sqlite3_bind_blob(self.stmt, column, field.data.ptr, @intCast(c_int, field.data.len), null), 937 Blob => _ = c.sqlite3_bind_blob(self.stmt, column, field.data.ptr, @intCast(c_int, field.data.len), null),
938 ZeroBlob => _ = c.sqlite3_bind_zeroblob64(self.stmt, column, field.length),
767 else => switch (field_type_info) { 939 else => switch (field_type_info) {
768 .Int, .ComptimeInt => _ = c.sqlite3_bind_int64(self.stmt, column, @intCast(c_longlong, field)), 940 .Int, .ComptimeInt => _ = c.sqlite3_bind_int64(self.stmt, column, @intCast(c_longlong, field)),
769 .Float, .ComptimeFloat => _ = c.sqlite3_bind_double(self.stmt, column, field), 941 .Float, .ComptimeFloat => _ = c.sqlite3_bind_double(self.stmt, column, field),
@@ -949,6 +1121,9 @@ const test_users = &[_]TestUser{
949 1121
950fn createTestTables(db: *Db) !void { 1122fn createTestTables(db: *Db) !void {
951 const AllDDL = &[_][]const u8{ 1123 const AllDDL = &[_][]const u8{
1124 "DROP TABLE IF EXISTS user",
1125 "DROP TABLE IF EXISTS article",
1126 "DROP TABLE IF EXISTS test_blob",
952 \\CREATE TABLE user( 1127 \\CREATE TABLE user(
953 \\ id integer PRIMARY KEY, 1128 \\ id integer PRIMARY KEY,
954 \\ name text, 1129 \\ name text,
@@ -1554,6 +1729,67 @@ test "sqlite: statement iterator" {
1554 } 1729 }
1555} 1730}
1556 1731
1732test "sqlite: blob open, reopen" {
1733 var arena = std.heap.ArenaAllocator.init(testing.allocator);
1734 defer arena.deinit();
1735 var allocator = &arena.allocator;
1736
1737 var db = try getTestDb();
1738 defer db.deinit();
1739
1740 const blob_data1 = "\xDE\xAD\xBE\xEFabcdefghijklmnopqrstuvwxyz0123456789";
1741 const blob_data2 = "\xCA\xFE\xBA\xBEfoobar";
1742
1743 // Insert two blobs with a set length
1744 try db.exec("CREATE TABLE test_blob(id integer primary key, data blob)", .{});
1745
1746 try db.exec("INSERT INTO test_blob(data) VALUES(?)", .{
1747 .data = ZeroBlob{ .length = blob_data1.len * 2 },
1748 });
1749 const rowid1 = db.getLastInsertRowID();
1750
1751 try db.exec("INSERT INTO test_blob(data) VALUES(?)", .{
1752 .data = ZeroBlob{ .length = blob_data2.len * 2 },
1753 });
1754 const rowid2 = db.getLastInsertRowID();
1755
1756 // Open the blob in the first row
1757 var blob = try db.openBlob(.main, "test_blob", "data", rowid1, .{ .write = true });
1758
1759 {
1760 // Write the first blob data
1761 var blob_writer = blob.writer();
1762 try blob_writer.writeAll(blob_data1);
1763 try blob_writer.writeAll(blob_data1);
1764
1765 blob.reset();
1766
1767 var blob_reader = blob.reader();
1768 const data = try blob_reader.readAllAlloc(allocator, 8192);
1769
1770 testing.expectEqualSlices(u8, blob_data1 ** 2, data);
1771 }
1772
1773 // Reopen the blob in the second row
1774 try blob.reopen(rowid2);
1775
1776 {
1777 // Write the second blob data
1778 var blob_writer = blob.writer();
1779 try blob_writer.writeAll(blob_data2);
1780 try blob_writer.writeAll(blob_data2);
1781
1782 blob.reset();
1783
1784 var blob_reader = blob.reader();
1785 const data = try blob_reader.readAllAlloc(allocator, 8192);
1786
1787 testing.expectEqualSlices(u8, blob_data2 ** 2, data);
1788 }
1789
1790 try blob.close();
1791}
1792
1557test "sqlite: failing open" { 1793test "sqlite: failing open" {
1558 var db: Db = undefined; 1794 var db: Db = undefined;
1559 const res = db.init(.{ 1795 const res = db.init(.{
@@ -1610,6 +1846,10 @@ fn dbMode(allocator: *mem.Allocator) Db.Mode {
1610 return if (build_options.in_memory) blk: { 1846 return if (build_options.in_memory) blk: {
1611 break :blk .{ .Memory = {} }; 1847 break :blk .{ .Memory = {} };
1612 } else blk: { 1848 } else blk: {
1849 if (build_options.dbfile) |dbfile| {
1850 return .{ .File = allocator.dupeZ(u8, dbfile) catch unreachable };
1851 }
1852
1613 const path = tmpDbPath(allocator) catch unreachable; 1853 const path = tmpDbPath(allocator) catch unreachable;
1614 1854
1615 std.fs.cwd().deleteFile(path) catch {}; 1855 std.fs.cwd().deleteFile(path) catch {};