diff options
| author | 2021-10-23 20:22:32 +0200 | |
|---|---|---|
| committer | 2021-10-23 21:36:29 +0200 | |
| commit | de708c4598c84c168e3412ca6e367b98e1fd5f9f (patch) | |
| tree | 4642f06d698ad3168dfbf6640df1c79036654293 /sqlite.zig | |
| parent | use explicit error sets everywhere (diff) | |
| download | zig-sqlite-de708c4598c84c168e3412ca6e367b98e1fd5f9f.tar.gz zig-sqlite-de708c4598c84c168e3412ca6e367b98e1fd5f9f.tar.xz zig-sqlite-de708c4598c84c168e3412ca6e367b98e1fd5f9f.zip | |
implement savepoint
Diffstat (limited to 'sqlite.zig')
| -rw-r--r-- | sqlite.zig | 250 |
1 files changed, 250 insertions, 0 deletions
| @@ -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 | /// | ||
| 638 | pub 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 | ||
| 582 | pub const QueryOptions = struct { | 703 | pub 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 | ||
| 2696 | test "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 | |||
| 2743 | test "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 | |||
| 2792 | test "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 | |||
| 2575 | fn getTestDb() !Db { | 2825 | fn 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); |