diff options
| author | 2020-12-30 15:23:56 +0100 | |
|---|---|---|
| committer | 2021-02-27 12:43:22 +0100 | |
| commit | b4957c7b0c1de8786b4e4f356650925422e6808d (patch) | |
| tree | eed70eddddefb7dc6a481a0ed62a77e01f347288 /sqlite.zig | |
| parent | store the database handle in Iterator and Statement (diff) | |
| download | zig-sqlite-b4957c7b0c1de8786b4e4f356650925422e6808d.tar.gz zig-sqlite-b4957c7b0c1de8786b4e4f356650925422e6808d.tar.xz zig-sqlite-b4957c7b0c1de8786b4e4f356650925422e6808d.zip | |
add Diagnostics
Diffstat (limited to 'sqlite.zig')
| -rw-r--r-- | sqlite.zig | 141 |
1 files changed, 130 insertions, 11 deletions
| @@ -192,6 +192,31 @@ pub const ThreadingMode = enum { | |||
| 192 | Serialized, | 192 | Serialized, |
| 193 | }; | 193 | }; |
| 194 | 194 | ||
| 195 | /// Diagnostics can be used by the library to give more information in case of failures. | ||
| 196 | pub const Diagnostics = struct { | ||
| 197 | message: []const u8 = "", | ||
| 198 | err: ?DetailedError = null, | ||
| 199 | |||
| 200 | pub fn format(self: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { | ||
| 201 | if (self.err) |err| { | ||
| 202 | if (self.message.len > 0) { | ||
| 203 | _ = try writer.print("{{message: {s}, error: {s}}}", .{ self.message, err.message }); | ||
| 204 | return; | ||
| 205 | } | ||
| 206 | |||
| 207 | _ = try writer.write(err.message); | ||
| 208 | return; | ||
| 209 | } | ||
| 210 | |||
| 211 | if (self.message.len > 0) { | ||
| 212 | _ = try writer.write(self.message); | ||
| 213 | return; | ||
| 214 | } | ||
| 215 | |||
| 216 | _ = try writer.write("none"); | ||
| 217 | } | ||
| 218 | }; | ||
| 219 | |||
| 195 | pub const InitOptions = struct { | 220 | pub const InitOptions = struct { |
| 196 | /// mode controls how the database is opened. | 221 | /// mode controls how the database is opened. |
| 197 | /// | 222 | /// |
| @@ -207,6 +232,9 @@ pub const InitOptions = struct { | |||
| 207 | /// | 232 | /// |
| 208 | /// Defaults to Serialized. | 233 | /// Defaults to Serialized. |
| 209 | threading_mode: ThreadingMode = .Serialized, | 234 | threading_mode: ThreadingMode = .Serialized, |
| 235 | |||
| 236 | /// if provided, diags will be populated in case of failures. | ||
| 237 | diags: ?*Diagnostics = null, | ||
| 210 | }; | 238 | }; |
| 211 | 239 | ||
| 212 | /// DetailedError contains a SQLite error code and error message. | 240 | /// DetailedError contains a SQLite error code and error message. |
| @@ -267,8 +295,11 @@ pub const Db = struct { | |||
| 267 | create: bool = false, | 295 | create: bool = false, |
| 268 | }; | 296 | }; |
| 269 | 297 | ||
| 270 | /// init creates a database with the provided `mode`. | 298 | /// init creates a database with the provided options. |
| 271 | pub fn init(self: *Self, options: InitOptions) !void { | 299 | pub fn init(self: *Self, options: InitOptions) !void { |
| 300 | var dummy_diags = Diagnostics{}; | ||
| 301 | var diags = options.diags orelse &dummy_diags; | ||
| 302 | |||
| 272 | // Validate the threading mode | 303 | // Validate the threading mode |
| 273 | if (options.threading_mode != .SingleThread and !isThreadSafe()) { | 304 | if (options.threading_mode != .SingleThread and !isThreadSafe()) { |
| 274 | return error.CannotUseSingleThreadedSQLite; | 305 | return error.CannotUseSingleThreadedSQLite; |
| @@ -293,6 +324,11 @@ pub const Db = struct { | |||
| 293 | var db: ?*c.sqlite3 = undefined; | 324 | var db: ?*c.sqlite3 = undefined; |
| 294 | const result = c.sqlite3_open_v2(path, &db, flags, null); | 325 | const result = c.sqlite3_open_v2(path, &db, flags, null); |
| 295 | if (result != c.SQLITE_OK or db == null) { | 326 | if (result != c.SQLITE_OK or db == null) { |
| 327 | if (db) |v| { | ||
| 328 | diags.err = getLastDetailedErrorFromDb(v); | ||
| 329 | } else { | ||
| 330 | diags.err = getDetailedErrorFromResultCode(result); | ||
| 331 | } | ||
| 296 | return errorFromResultCode(result); | 332 | return errorFromResultCode(result); |
| 297 | } | 333 | } |
| 298 | 334 | ||
| @@ -306,6 +342,11 @@ pub const Db = struct { | |||
| 306 | var db: ?*c.sqlite3 = undefined; | 342 | var db: ?*c.sqlite3 = undefined; |
| 307 | const result = c.sqlite3_open_v2(":memory:", &db, flags, null); | 343 | const result = c.sqlite3_open_v2(":memory:", &db, flags, null); |
| 308 | if (result != c.SQLITE_OK or db == null) { | 344 | if (result != c.SQLITE_OK or db == null) { |
| 345 | if (db) |v| { | ||
| 346 | diags.err = getLastDetailedErrorFromDb(v); | ||
| 347 | } else { | ||
| 348 | diags.err = getDetailedErrorFromResultCode(result); | ||
| 349 | } | ||
| 309 | return errorFromResultCode(result); | 350 | return errorFromResultCode(result); |
| 310 | } | 351 | } |
| 311 | 352 | ||
| @@ -371,7 +412,7 @@ pub const Db = struct { | |||
| 371 | comptime var buf: [1024]u8 = undefined; | 412 | comptime var buf: [1024]u8 = undefined; |
| 372 | comptime var query = getPragmaQuery(&buf, name, arg); | 413 | comptime var query = getPragmaQuery(&buf, name, arg); |
| 373 | 414 | ||
| 374 | var stmt = try self.prepare(query); | 415 | var stmt = try self.prepareWithDiags(query, options); |
| 375 | defer stmt.deinit(); | 416 | defer stmt.deinit(); |
| 376 | 417 | ||
| 377 | return try stmt.one(Type, options, .{}); | 418 | return try stmt.one(Type, options, .{}); |
| @@ -385,19 +426,26 @@ pub const Db = struct { | |||
| 385 | } | 426 | } |
| 386 | 427 | ||
| 387 | /// one is a convenience function which prepares a statement and reads a single row from the result set. | 428 | /// one is a convenience function which prepares a statement and reads a single row from the result set. |
| 388 | pub fn one(self: *Self, comptime Type: type, comptime query: []const u8, options: anytype, values: anytype) !?Type { | 429 | pub fn one(self: *Self, comptime Type: type, comptime query: []const u8, options: QueryOptions, values: anytype) !?Type { |
| 389 | var stmt = try self.prepare(query); | 430 | var stmt = try self.prepareWithDiags(query, options); |
| 390 | defer stmt.deinit(); | 431 | defer stmt.deinit(); |
| 391 | return try stmt.one(Type, options, values); | 432 | return try stmt.one(Type, options, values); |
| 392 | } | 433 | } |
| 393 | 434 | ||
| 394 | /// oneAlloc is like `one` but can allocate memory. | 435 | /// oneAlloc is like `one` but can allocate memory. |
| 395 | pub fn oneAlloc(self: *Self, comptime Type: type, allocator: *mem.Allocator, comptime query: []const u8, options: anytype, values: anytype) !?Type { | 436 | pub fn oneAlloc(self: *Self, comptime Type: type, allocator: *mem.Allocator, comptime query: []const u8, options: QueryOptions, values: anytype) !?Type { |
| 396 | var stmt = try self.prepare(query); | 437 | var stmt = try self.prepareWithDiags(query, options); |
| 397 | defer stmt.deinit(); | 438 | defer stmt.deinit(); |
| 398 | return try stmt.oneAlloc(Type, allocator, options, values); | 439 | return try stmt.oneAlloc(Type, allocator, options, values); |
| 399 | } | 440 | } |
| 400 | 441 | ||
| 442 | /// prepareWithDiags is like `prepare` but takes an additional options argument. | ||
| 443 | pub fn prepareWithDiags(self: *Self, comptime query: []const u8, options: QueryOptions) !Statement(.{}, ParsedQuery.from(query)) { | ||
| 444 | @setEvalBranchQuota(10000); | ||
| 445 | const parsed_query = ParsedQuery.from(query); | ||
| 446 | return Statement(.{}, comptime parsed_query).prepare(self, options, 0); | ||
| 447 | } | ||
| 448 | |||
| 401 | /// prepare prepares a statement for the `query` provided. | 449 | /// prepare prepares a statement for the `query` provided. |
| 402 | /// | 450 | /// |
| 403 | /// The query is analysed at comptime to search for bind markers. | 451 | /// The query is analysed at comptime to search for bind markers. |
| @@ -411,10 +459,11 @@ pub const Db = struct { | |||
| 411 | /// The statement returned is only compatible with the number of bind markers in the input query. | 459 | /// The statement returned is only compatible with the number of bind markers in the input query. |
| 412 | /// This is done because we type check the bind parameters when executing the statement later. | 460 | /// This is done because we type check the bind parameters when executing the statement later. |
| 413 | /// | 461 | /// |
| 462 | /// If you want additional error information in case of failures, use `prepareWithDiags`. | ||
| 414 | pub fn prepare(self: *Self, comptime query: []const u8) !Statement(.{}, ParsedQuery.from(query)) { | 463 | pub fn prepare(self: *Self, comptime query: []const u8) !Statement(.{}, ParsedQuery.from(query)) { |
| 415 | @setEvalBranchQuota(10000); | 464 | @setEvalBranchQuota(10000); |
| 416 | const parsed_query = ParsedQuery.from(query); | 465 | const parsed_query = ParsedQuery.from(query); |
| 417 | return Statement(.{}, comptime parsed_query).prepare(self, 0); | 466 | return Statement(.{}, comptime parsed_query).prepare(self, .{}, 0); |
| 418 | } | 467 | } |
| 419 | 468 | ||
| 420 | /// rowsAffected returns the number of rows affected by the last statement executed. | 469 | /// rowsAffected returns the number of rows affected by the last statement executed. |
| @@ -428,6 +477,11 @@ pub const Db = struct { | |||
| 428 | } | 477 | } |
| 429 | }; | 478 | }; |
| 430 | 479 | ||
| 480 | pub const QueryOptions = struct { | ||
| 481 | /// if provided, diags will be populated in case of failures. | ||
| 482 | diags: ?*Diagnostics = null, | ||
| 483 | }; | ||
| 484 | |||
| 431 | /// Iterator allows iterating over a result set. | 485 | /// Iterator allows iterating over a result set. |
| 432 | /// | 486 | /// |
| 433 | /// Each call to `next` returns the next row of the result set, or null if the result set is exhausted. | 487 | /// Each call to `next` returns the next row of the result set, or null if the result set is exhausted. |
| @@ -463,12 +517,16 @@ pub fn Iterator(comptime Type: type) type { | |||
| 463 | // If it returns null iterating is done. | 517 | // If it returns null iterating is done. |
| 464 | // | 518 | // |
| 465 | // This cannot allocate memory. If you need to read TEXT or BLOB columns you need to use arrays or alternatively call nextAlloc. | 519 | // This cannot allocate memory. If you need to read TEXT or BLOB columns you need to use arrays or alternatively call nextAlloc. |
| 466 | pub fn next(self: *Self, options: anytype) !?Type { | 520 | pub fn next(self: *Self, options: QueryOptions) !?Type { |
| 521 | var dummy_diags = Diagnostics{}; | ||
| 522 | var diags = options.diags orelse &dummy_diags; | ||
| 523 | |||
| 467 | var result = c.sqlite3_step(self.stmt); | 524 | var result = c.sqlite3_step(self.stmt); |
| 468 | if (result == c.SQLITE_DONE) { | 525 | if (result == c.SQLITE_DONE) { |
| 469 | return null; | 526 | return null; |
| 470 | } | 527 | } |
| 471 | if (result != c.SQLITE_ROW) { | 528 | if (result != c.SQLITE_ROW) { |
| 529 | diags.err = getLastDetailedErrorFromDb(self.db); | ||
| 472 | return errorFromResultCode(result); | 530 | return errorFromResultCode(result); |
| 473 | } | 531 | } |
| 474 | 532 | ||
| @@ -503,12 +561,16 @@ pub fn Iterator(comptime Type: type) type { | |||
| 503 | } | 561 | } |
| 504 | 562 | ||
| 505 | // nextAlloc is like `next` but can allocate memory. | 563 | // nextAlloc is like `next` but can allocate memory. |
| 506 | pub fn nextAlloc(self: *Self, allocator: *mem.Allocator, options: anytype) !?Type { | 564 | pub fn nextAlloc(self: *Self, allocator: *mem.Allocator, options: QueryOptions) !?Type { |
| 565 | var dummy_diags = Diagnostics{}; | ||
| 566 | var diags = options.diags orelse &dummy_diags; | ||
| 567 | |||
| 507 | var result = c.sqlite3_step(self.stmt); | 568 | var result = c.sqlite3_step(self.stmt); |
| 508 | if (result == c.SQLITE_DONE) { | 569 | if (result == c.SQLITE_DONE) { |
| 509 | return null; | 570 | return null; |
| 510 | } | 571 | } |
| 511 | if (result != c.SQLITE_ROW) { | 572 | if (result != c.SQLITE_ROW) { |
| 573 | diags.err = getLastDetailedErrorFromDb(self.db); | ||
| 512 | return errorFromResultCode(result); | 574 | return errorFromResultCode(result); |
| 513 | } | 575 | } |
| 514 | 576 | ||
| @@ -847,7 +909,10 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) t | |||
| 847 | db: *c.sqlite3, | 909 | db: *c.sqlite3, |
| 848 | stmt: *c.sqlite3_stmt, | 910 | stmt: *c.sqlite3_stmt, |
| 849 | 911 | ||
| 850 | fn prepare(db: *Db, flags: c_uint) !Self { | 912 | fn prepare(db: *Db, options: QueryOptions, flags: c_uint) !Self { |
| 913 | var dummy_diags = Diagnostics{}; | ||
| 914 | var diags = options.diags orelse &dummy_diags; | ||
| 915 | |||
| 851 | var stmt = blk: { | 916 | var stmt = blk: { |
| 852 | const real_query = query.getQuery(); | 917 | const real_query = query.getQuery(); |
| 853 | 918 | ||
| @@ -861,6 +926,7 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) t | |||
| 861 | null, | 926 | null, |
| 862 | ); | 927 | ); |
| 863 | if (result != c.SQLITE_OK) { | 928 | if (result != c.SQLITE_OK) { |
| 929 | diags.err = getLastDetailedErrorFromDb(db.db); | ||
| 864 | return errorFromResultCode(result); | 930 | return errorFromResultCode(result); |
| 865 | } | 931 | } |
| 866 | break :blk tmp.?; | 932 | break :blk tmp.?; |
| @@ -1795,18 +1861,25 @@ test "sqlite: blob open, reopen" { | |||
| 1795 | } | 1861 | } |
| 1796 | 1862 | ||
| 1797 | test "sqlite: failing open" { | 1863 | test "sqlite: failing open" { |
| 1864 | var diags: Diagnostics = undefined; | ||
| 1865 | |||
| 1798 | var db: Db = undefined; | 1866 | var db: Db = undefined; |
| 1799 | const res = db.init(.{ | 1867 | const res = db.init(.{ |
| 1868 | .diags = &diags, | ||
| 1800 | .open_flags = .{}, | 1869 | .open_flags = .{}, |
| 1801 | .mode = .{ .File = "/tmp/not_existing.db" }, | 1870 | .mode = .{ .File = "/tmp/not_existing.db" }, |
| 1802 | }); | 1871 | }); |
| 1803 | testing.expectError(error.SQLiteCantOpen, res); | 1872 | testing.expectError(error.SQLiteCantOpen, res); |
| 1873 | testing.expectEqual(@as(usize, 14), diags.err.?.code); | ||
| 1874 | testing.expectEqualStrings("unable to open database file", diags.err.?.message); | ||
| 1804 | } | 1875 | } |
| 1805 | 1876 | ||
| 1806 | test "sqlite: failing prepare statement" { | 1877 | test "sqlite: failing prepare statement" { |
| 1807 | var db = try getTestDb(); | 1878 | var db = try getTestDb(); |
| 1808 | 1879 | ||
| 1809 | const result = db.prepare("SELECT id FROM foobar"); | 1880 | var diags: Diagnostics = undefined; |
| 1881 | |||
| 1882 | const result = db.prepareWithDiags("SELECT id FROM foobar", .{ .diags = &diags }); | ||
| 1810 | testing.expectError(error.SQLiteError, result); | 1883 | testing.expectError(error.SQLiteError, result); |
| 1811 | 1884 | ||
| 1812 | const detailed_err = db.getDetailedError(); | 1885 | const detailed_err = db.getDetailedError(); |
| @@ -1814,6 +1887,52 @@ test "sqlite: failing prepare statement" { | |||
| 1814 | testing.expectEqualStrings("no such table: foobar", detailed_err.message); | 1887 | testing.expectEqualStrings("no such table: foobar", detailed_err.message); |
| 1815 | } | 1888 | } |
| 1816 | 1889 | ||
| 1890 | test "sqlite: diagnostics format" { | ||
| 1891 | const TestCase = struct { | ||
| 1892 | input: Diagnostics, | ||
| 1893 | exp: []const u8, | ||
| 1894 | }; | ||
| 1895 | |||
| 1896 | const testCases = &[_]TestCase{ | ||
| 1897 | .{ | ||
| 1898 | .input = .{}, | ||
| 1899 | .exp = "my diagnostics: none", | ||
| 1900 | }, | ||
| 1901 | .{ | ||
| 1902 | .input = .{ | ||
| 1903 | .message = "foobar", | ||
| 1904 | }, | ||
| 1905 | .exp = "my diagnostics: foobar", | ||
| 1906 | }, | ||
| 1907 | .{ | ||
| 1908 | .input = .{ | ||
| 1909 | .err = .{ | ||
| 1910 | .code = 20, | ||
| 1911 | .message = "barbaz", | ||
| 1912 | }, | ||
| 1913 | }, | ||
| 1914 | .exp = "my diagnostics: barbaz", | ||
| 1915 | }, | ||
| 1916 | .{ | ||
| 1917 | .input = .{ | ||
| 1918 | .message = "foobar", | ||
| 1919 | .err = .{ | ||
| 1920 | .code = 20, | ||
| 1921 | .message = "barbaz", | ||
| 1922 | }, | ||
| 1923 | }, | ||
| 1924 | .exp = "my diagnostics: {message: foobar, error: barbaz}", | ||
| 1925 | }, | ||
| 1926 | }; | ||
| 1927 | |||
| 1928 | inline for (testCases) |tc| { | ||
| 1929 | var buf: [1024]u8 = undefined; | ||
| 1930 | const str = try std.fmt.bufPrint(&buf, "my diagnostics: {s}", .{tc.input}); | ||
| 1931 | |||
| 1932 | testing.expectEqualStrings(tc.exp, str); | ||
| 1933 | } | ||
| 1934 | } | ||
| 1935 | |||
| 1817 | fn getTestDb() !Db { | 1936 | fn getTestDb() !Db { |
| 1818 | var buf: [1024]u8 = undefined; | 1937 | var buf: [1024]u8 = undefined; |
| 1819 | var fba = std.heap.FixedBufferAllocator.init(&buf); | 1938 | var fba = std.heap.FixedBufferAllocator.init(&buf); |