summaryrefslogtreecommitdiff
path: root/sqlite.zig
diff options
context:
space:
mode:
authorGravatar Vincent Rischmann2021-10-23 20:22:32 +0200
committerGravatar Vincent Rischmann2021-10-23 21:36:29 +0200
commitde708c4598c84c168e3412ca6e367b98e1fd5f9f (patch)
tree4642f06d698ad3168dfbf6640df1c79036654293 /sqlite.zig
parentuse explicit error sets everywhere (diff)
downloadzig-sqlite-de708c4598c84c168e3412ca6e367b98e1fd5f9f.tar.gz
zig-sqlite-de708c4598c84c168e3412ca6e367b98e1fd5f9f.tar.xz
zig-sqlite-de708c4598c84c168e3412ca6e367b98e1fd5f9f.zip
implement savepoint
Diffstat (limited to '')
-rw-r--r--sqlite.zig250
1 files changed, 250 insertions, 0 deletions
diff --git a/sqlite.zig b/sqlite.zig
index b1a903e..2669fce 100644
--- a/sqlite.zig
+++ b/sqlite.zig
@@ -577,6 +577,127 @@ pub const Db = struct {
577 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 { 577 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 {
578 return Blob.open(self.db, db_name, table, column, row, flags); 578 return Blob.open(self.db, db_name, table, column, row, flags);
579 } 579 }
580
581 /// savepoint starts a new named transaction.
582 ///
583 /// The returned type is a helper useful for managing commits and rollbacks, for example:
584 ///
585 /// var savepoint = try db.savepoint("foobar");
586 /// defer savepoint.rollback();
587 ///
588 /// try db.exec("INSERT INTO foo(id, name) VALUES(?, ?)", .{ 1, "foo" });
589 ///
590 /// savepoint.commit();
591 ///
592 pub fn savepoint(self: *Self, name: []const u8) Savepoint.InitError!Savepoint {
593 return Savepoint.init(self, name);
594 }
595};
596
597/// Savepoint is a helper type for managing savepoints.
598///
599/// A savepoint creates a transaction like BEGIN/COMMIT but they're named and can be nested.
600/// See https://sqlite.org/lang_savepoint.html.
601///
602/// You can create a savepoint like this:
603///
604/// var savepoint = try db.savepoint("foobar");
605/// defer savepoint.rollback();
606///
607/// ...
608///
609/// Savepoint.commit();
610///
611/// This is equivalent to BEGIN/COMMIT/ROLLBACK.
612///
613/// Savepoints are more useful for _nesting_ transactions, for example:
614///
615/// var savepoint = try db.savepoint("outer");
616/// defer savepoint.rollback();
617///
618/// try db.exec("INSERT INTO foo(id, name) VALUES(?, ?)", .{ 1, "foo" });
619///
620/// {
621/// var savepoint2 = try db.savepoint("inner");
622/// defer savepoint2.rollback();
623///
624/// var i: usize = 0;
625/// while (i < 30) : (i += 1) {
626/// try db.exec("INSERT INTO foo(id, name) VALUES(?, ?)", .{ 2, "bar" });
627/// }
628///
629/// savepoint2.commit();
630/// }
631///
632/// try db.exec("UPDATE bar SET processed = ? WHERE id = ?", .{ true, 20 });
633///
634/// savepoint.commit();
635///
636/// In this example if any query in the inner transaction fail, all previously executed queries are discarded but the outer transaction is untouched.
637///
638pub const Savepoint = struct {
639 const Self = @This();
640
641 db: *Db,
642 committed: bool,
643
644 commit_stmt: DynamicStatement,
645 rollback_stmt: DynamicStatement,
646
647 pub const InitError = error{
648 SavepointNameTooShort,
649 SavepointNameTooLong,
650 SavepointNameInvalid,
651 } || std.fmt.AllocPrintError || Error;
652
653 fn init(db: *Db, name: []const u8) InitError!Self {
654 if (name.len < 1) return error.SavepointNameTooShort;
655 if (name.len > 20) return error.SavepointNameTooLong;
656 if (!std.ascii.isAlpha(name[0])) return error.SavepointNameInvalid;
657 for (name) |b| {
658 if (b != '_' and !std.ascii.isAlNum(b)) {
659 return error.SavepointNameInvalid;
660 }
661 }
662
663 var buffer: [256]u8 = undefined;
664 var fba = std.heap.FixedBufferAllocator.init(&buffer);
665
666 const commit_query = try std.fmt.allocPrint(&fba.allocator, "RELEASE SAVEPOINT {s}", .{name});
667 const rollback_query = try std.fmt.allocPrint(&fba.allocator, "ROLLBACK TRANSACTION TO SAVEPOINT {s}", .{name});
668
669 var res = Self{
670 .db = db,
671 .committed = false,
672 .commit_stmt = try db.prepareDynamic(commit_query),
673 .rollback_stmt = try db.prepareDynamic(rollback_query),
674 };
675
676 try res.db.execDynamic(
677 try std.fmt.allocPrint(&fba.allocator, "SAVEPOINT {s}", .{name}),
678 .{},
679 .{},
680 );
681
682 return res;
683 }
684
685 pub fn commit(self: *Self) void {
686 self.commit_stmt.exec(.{}, .{}) catch |err| {
687 const detailed_error = self.db.getDetailedError();
688 logger.err("unable to release savepoint, error: {}, message: {s}", .{ err, detailed_error });
689 };
690 self.committed = true;
691 }
692
693 pub fn rollback(self: *Self) void {
694 if (self.committed) return;
695
696 self.rollback_stmt.exec(.{}, .{}) catch |err| {
697 const detailed_error = self.db.getDetailedError();
698 std.debug.panic("unable to rollback transaction, error: {}, message: {s}\n", .{ err, detailed_error });
699 };
700 }
580}; 701};
581 702
582pub const QueryOptions = struct { 703pub const QueryOptions = struct {
@@ -2572,6 +2693,135 @@ test "sqlite: exec with diags, failing statement" {
2572 try testing.expectEqualStrings("cannot rollback - no transaction is active", detailed_err.message); 2693 try testing.expectEqualStrings("cannot rollback - no transaction is active", detailed_err.message);
2573} 2694}
2574 2695
2696test "sqlite: savepoint with no failures" {
2697 var arena = std.heap.ArenaAllocator.init(testing.allocator);
2698 defer arena.deinit();
2699
2700 var db = try getTestDb();
2701 try addTestData(&db);
2702
2703 {
2704 var savepoint = try db.savepoint("outer1");
2705 defer savepoint.rollback();
2706
2707 try db.exec("INSERT INTO article(author_id, data, is_published) VALUES(?, ?, ?)", .{}, .{ 1, null, true });
2708
2709 {
2710 var savepoint2 = try db.savepoint("inner1");
2711 defer savepoint2.rollback();
2712
2713 try db.exec("INSERT INTO article(author_id, data, is_published) VALUES(?, ?, ?)", .{}, .{ 2, "foobar", true });
2714
2715 savepoint2.commit();
2716 }
2717
2718 savepoint.commit();
2719 }
2720
2721 // No failures, expect to have two rows.
2722
2723 var stmt = try db.prepare("SELECT data, author_id FROM article ORDER BY id ASC");
2724 defer stmt.deinit();
2725
2726 var rows = try stmt.all(
2727 struct {
2728 data: []const u8,
2729 author_id: usize,
2730 },
2731 &arena.allocator,
2732 .{},
2733 .{},
2734 );
2735
2736 try testing.expectEqual(@as(usize, 2), rows.len);
2737 try testing.expectEqual(@as(usize, 1), rows[0].author_id);
2738 try testing.expectEqualStrings("", rows[0].data);
2739 try testing.expectEqual(@as(usize, 2), rows[1].author_id);
2740 try testing.expectEqualStrings("foobar", rows[1].data);
2741}
2742
2743test "sqlite: two nested savepoints with inner failure" {
2744 var arena = std.heap.ArenaAllocator.init(testing.allocator);
2745 defer arena.deinit();
2746
2747 var db = try getTestDb();
2748 try addTestData(&db);
2749
2750 {
2751 var savepoint = try db.savepoint("outer2");
2752 defer savepoint.rollback();
2753
2754 try db.exec("INSERT INTO article(author_id, data, is_published) VALUES(?, ?, ?)", .{}, .{ 10, "barbaz", true });
2755
2756 inner: {
2757 var savepoint2 = try db.savepoint("inner2");
2758 defer savepoint2.rollback();
2759
2760 try db.exec("INSERT INTO article(author_id, data, is_published) VALUES(?, ?, ?)", .{}, .{ 20, null, true });
2761
2762 // Explicitly fail
2763 db.exec("INSERT INTO article(author_id, data, is_published) VALUES(?, ?)", .{}, .{ 22, null }) catch {
2764 break :inner;
2765 };
2766
2767 savepoint2.commit();
2768 }
2769
2770 savepoint.commit();
2771 }
2772
2773 // The inner transaction failed, expect to have only one row.
2774
2775 var stmt = try db.prepare("SELECT data, author_id FROM article");
2776 defer stmt.deinit();
2777
2778 var rows = try stmt.all(
2779 struct {
2780 data: []const u8,
2781 author_id: usize,
2782 },
2783 &arena.allocator,
2784 .{},
2785 .{},
2786 );
2787 try testing.expectEqual(@as(usize, 1), rows.len);
2788 try testing.expectEqual(@as(usize, 10), rows[0].author_id);
2789 try testing.expectEqualStrings("barbaz", rows[0].data);
2790}
2791
2792test "sqlite: two nested savepoints with outer failure" {
2793 var arena = std.heap.ArenaAllocator.init(testing.allocator);
2794 defer arena.deinit();
2795
2796 var db = try getTestDb();
2797 try addTestData(&db);
2798
2799 blk: {
2800 var savepoint = try db.savepoint("outer3");
2801 defer savepoint.rollback();
2802
2803 var i: usize = 100;
2804 while (i < 120) : (i += 1) {
2805 try db.exec("INSERT INTO article(author_id, data, is_published) VALUES(?, ?, ?)", .{}, .{ i, null, true });
2806 }
2807
2808 // Explicitly fail
2809 db.exec("INSERT INTO article(author_id, data, is_published) VALUES(?, ?)", .{}, .{ 2, null }) catch {
2810 break :blk;
2811 };
2812
2813 savepoint.commit();
2814 }
2815
2816 // The outer transaction failed, expect to have no rows.
2817
2818 var stmt = try db.prepare("SELECT 1 FROM article");
2819 defer stmt.deinit();
2820
2821 var rows = try stmt.all(usize, &arena.allocator, .{}, .{});
2822 try testing.expectEqual(@as(usize, 0), rows.len);
2823}
2824
2575fn getTestDb() !Db { 2825fn getTestDb() !Db {
2576 var buf: [1024]u8 = undefined; 2826 var buf: [1024]u8 = undefined;
2577 var fba = std.heap.FixedBufferAllocator.init(&buf); 2827 var fba = std.heap.FixedBufferAllocator.init(&buf);