Skip to content

Commit a8a1bbf

Browse files
CopilotFCO
andcommitted
Complete savepoint implementation with documentation and additional tests
Co-authored-by: FCO <[email protected]>
1 parent de1aaa7 commit a8a1bbf

File tree

3 files changed

+185
-1
lines changed

3 files changed

+185
-1
lines changed

docs/savepoints.pod6

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
=begin pod
2+
3+
=head1 SAVEPOINT Support in Red
4+
5+
Red now supports database savepoints for better handling of nested transactions. This addresses issues where nested transactions using separate connections could lead to isolation problems.
6+
7+
=head2 Automatic Savepoint Usage
8+
9+
When you start a nested transaction using C<begin>, Red automatically uses savepoints instead of creating new database connections:
10+
11+
=begin code :lang<raku>
12+
use Red:api<2>;
13+
14+
model Person is rw {
15+
has Int $.id is serial;
16+
has Str $.name is column;
17+
}
18+
19+
my $*RED-DB = database "SQLite";
20+
Person.^create-table;
21+
22+
# Start main transaction
23+
my $main-tx = $*RED-DB.begin;
24+
25+
{
26+
my $*RED-DB = $main-tx;
27+
my $person1 = Person.^create(:name("Alice"));
28+
29+
# Start nested transaction (creates savepoint automatically)
30+
my $nested-tx = $main-tx.begin;
31+
32+
{
33+
my $*RED-DB = $nested-tx;
34+
my $person2 = Person.^create(:name("Bob"));
35+
36+
# Rollback nested transaction (rolls back to savepoint)
37+
$nested-tx.rollback; # Only Bob is rolled back
38+
}
39+
40+
# Alice is still there
41+
$main-tx.commit;
42+
}
43+
=end code
44+
45+
=head2 Manual Savepoint Operations
46+
47+
You can also create and manage savepoints manually:
48+
49+
=begin code :lang<raku>
50+
my $tx = $*RED-DB.begin;
51+
my $*RED-DB = $tx;
52+
53+
my $person1 = Person.^create(:name("Alice"));
54+
55+
# Create a named savepoint
56+
$*RED-DB.savepoint("checkpoint1");
57+
58+
my $person2 = Person.^create(:name("Bob"));
59+
60+
# Rollback to the savepoint
61+
$*RED-DB.rollback-to-savepoint("checkpoint1"); # Bob is rolled back
62+
63+
# Or release the savepoint when no longer needed
64+
$*RED-DB.savepoint("checkpoint2");
65+
my $person3 = Person.^create(:name("Charlie"));
66+
$*RED-DB.release-savepoint("checkpoint2"); # Just removes the savepoint
67+
68+
$tx.commit; # Alice and Charlie are committed
69+
=end code
70+
71+
=head2 Database Support
72+
73+
Savepoints are supported on:
74+
75+
=item PostgreSQL - Full SAVEPOINT/ROLLBACK TO SAVEPOINT/RELEASE SAVEPOINT syntax
76+
=item MySQL - Full SAVEPOINT/ROLLBACK TO SAVEPOINT/RELEASE SAVEPOINT syntax
77+
=item SQLite - SAVEPOINT/ROLLBACK TO/RELEASE syntax (simplified)
78+
79+
=head2 API Methods
80+
81+
=head3 Automatic Transaction Context
82+
83+
=item C<$driver.begin()> - Returns a transaction context that uses savepoints for nesting
84+
=item C<$tx.begin()> - Creates a nested savepoint context
85+
=item C<$tx.commit()> - Commits transaction or releases savepoint
86+
=item C<$tx.rollback()> - Rolls back transaction or rolls back to savepoint
87+
88+
=head3 Manual Savepoint Control
89+
90+
=item C<$driver.savepoint($name)> - Creates a named savepoint
91+
=item C<$driver.rollback-to-savepoint($name)> - Rolls back to a named savepoint
92+
=item C<$driver.release-savepoint($name)> - Releases a named savepoint
93+
94+
=head2 Implementation Details
95+
96+
The savepoint implementation uses a C<TransactionContext> wrapper that:
97+
98+
=item Maintains API compatibility with existing transaction code
99+
=item Uses the same underlying database connection for all nested levels
100+
=item Automatically translates nested transactions to savepoints
101+
=item Provides promise-based coordination for cleanup
102+
=item Delegates all driver methods to the parent connection
103+
104+
This ensures that changes made in outer transactions are visible to inner transactions, solving the isolation issues that occurred with the previous approach of using separate connections.
105+
106+
=end pod

lib/Red/Driver/TransactionContext.rakumod

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,19 @@ method is-valid-table-name($name) { $!parent.is-valid-table-name($name) }
4646
method type-by-name($name) { $!parent.type-by-name($name) }
4747
method map-exception($exception) { $!parent.map-exception($exception) }
4848
method inflate($value, *%args) { $!parent.inflate($value, |%args) }
49+
method deflate($value) { $!parent.deflate($value) }
4950
method optimize($ast) { $!parent.optimize($ast) }
5051
method debug(*@args) { $!parent.debug(|@args) }
5152
method events { $!parent.events }
5253
method emit($data) { $!parent.emit($data) }
5354
method auto-register() { $!parent.auto-register() }
5455
method should-drop-cascade() { $!parent.should-drop-cascade() }
56+
method ping() { $!parent.ping() }
57+
58+
# Delegate any missing methods via FALLBACK
59+
method FALLBACK($name, |c) {
60+
$!parent."$name"(|c)
61+
}
5562

5663
# Override transaction methods for savepoint behavior
5764
method begin {
@@ -82,4 +89,9 @@ method rollback {
8289
$!promise.break("Transaction rolled back");
8390
}
8491
self
85-
}
92+
}
93+
94+
# Delegate savepoint methods to parent
95+
method savepoint(Str $name) { $!parent.savepoint($name) }
96+
method rollback-to-savepoint(Str $name) { $!parent.rollback-to-savepoint($name) }
97+
method release-savepoint(Str $name) { $!parent.release-savepoint($name) }

t/91-savepoints-basic.rakutest

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use Test;
2+
use Red:api<2>;
3+
4+
model SimpleTest is rw {
5+
has Int $.id is serial;
6+
has Str $.name is column;
7+
}
8+
9+
# Set up in-memory SQLite database for testing
10+
my $*RED-DB = database "SQLite";
11+
12+
plan 3;
13+
14+
# Create test table
15+
SimpleTest.^create-table;
16+
17+
subtest "Basic transaction context creation", {
18+
plan 2;
19+
20+
# Test that begin returns a different object
21+
my $tx = $*RED-DB.begin;
22+
ok $tx, "Transaction context created";
23+
ok $tx !=== $*RED-DB, "Transaction context is different from original DB";
24+
25+
$tx.rollback;
26+
};
27+
28+
subtest "SQL generation", {
29+
plan 3;
30+
31+
# Test that the correct SQL is generated for savepoint operations
32+
my $driver = $*RED-DB;
33+
34+
# Test savepoint creation
35+
my $savepoint-sql = $driver.translate(Red::AST::Savepoint.new(:name("test")));
36+
is $savepoint-sql.key, "SAVEPOINT test", "Correct savepoint SQL generated";
37+
38+
# Test rollback to savepoint
39+
my $rollback-sql = $driver.translate(Red::AST::RollbackToSavepoint.new(:name("test")));
40+
is $rollback-sql.key, "ROLLBACK TO test", "Correct rollback to savepoint SQL generated (SQLite)";
41+
42+
# Test release savepoint
43+
my $release-sql = $driver.translate(Red::AST::ReleaseSavepoint.new(:name("test")));
44+
is $release-sql.key, "RELEASE test", "Correct release savepoint SQL generated (SQLite)";
45+
};
46+
47+
subtest "Basic nested transaction", {
48+
plan 4;
49+
50+
# Start main transaction
51+
my $main-tx = $*RED-DB.begin;
52+
ok $main-tx, "Started main transaction";
53+
54+
# Start nested transaction
55+
my $nested-tx = $main-tx.begin;
56+
ok $nested-tx, "Started nested transaction";
57+
ok $nested-tx !=== $main-tx, "Nested transaction is different object";
58+
59+
# Test that we can call methods on both
60+
lives-ok {
61+
$nested-tx.rollback;
62+
$main-tx.rollback;
63+
}, "Can call rollback on both transaction contexts";
64+
};
65+
66+
done-testing;

0 commit comments

Comments
 (0)