Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions SAVEPOINT-IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# SAVEPOINT Implementation Summary

This document summarizes the implementation of SAVEPOINT functionality in Red ORM to address issue #575.

## Problem Solved

Previously, nested transactions in Red created new database connections, which led to isolation issues where changes made in the outer transaction were not visible to the inner transaction. This caused problems in complex transaction scenarios.

## Solution Overview

The implementation introduces SAVEPOINT support that:

1. **Uses the same connection**: All nested transactions use savepoints on the same underlying database connection
2. **Maintains API compatibility**: Existing code continues to work without changes
3. **Adds manual control**: Developers can create and manage named savepoints directly

## Files Modified/Added

### New AST Classes
- `lib/Red/AST/Savepoint.rakumod` - AST for SAVEPOINT statements
- `lib/Red/AST/RollbackToSavepoint.rakumod` - AST for ROLLBACK TO SAVEPOINT statements
- `lib/Red/AST/ReleaseSavepoint.rakumod` - AST for RELEASE SAVEPOINT statements

### Core Implementation
- `lib/Red/Driver/TransactionContext.rakumod` - Transaction context wrapper for savepoint management
- `lib/Red/Driver.rakumod` - Updated base driver role with savepoint methods
- `lib/Red/Driver/CommonSQL.rakumod` - SQL translation for savepoint operations
- `lib/Red/Driver/SQLite.rakumod` - SQLite-specific syntax overrides

### Tests and Documentation
- `t/90-savepoints.rakutest` - Comprehensive test suite for savepoint functionality
- `t/91-savepoints-basic.rakutest` - Basic functionality tests
- `docs/savepoints.pod6` - User documentation
- `examples/savepoint-demo.raku` - Example usage

## Architecture

### TransactionContext Pattern

The core innovation is the `TransactionContext` class that:

1. **Wraps the parent driver**: Acts as a proxy to the real database connection
2. **Tracks nesting level**: Knows whether it's the main transaction or a savepoint
3. **Manages savepoint names**: Automatically generates names like "sp2", "sp3" for nested levels
4. **Delegates methods**: All driver methods are forwarded to the parent connection
5. **Handles promises**: Maintains promises for coordination between levels

### SQL Generation

The implementation supports different SQL dialects:

```sql
-- PostgreSQL/MySQL Standard
SAVEPOINT sp1;
ROLLBACK TO SAVEPOINT sp1;
RELEASE SAVEPOINT sp1;

-- SQLite Simplified
SAVEPOINT sp1;
ROLLBACK TO sp1;
RELEASE sp1;
```

### Method Flow

1. `$driver.begin()` → Creates `TransactionContext(level=1)` and executes `BEGIN`
2. `$tx.begin()` → Creates `TransactionContext(level=2, name="sp2")` and executes `SAVEPOINT sp2`
3. `$tx.commit()` → Executes `RELEASE SAVEPOINT sp2` or `COMMIT` depending on level
4. `$tx.rollback()` → Executes `ROLLBACK TO SAVEPOINT sp2` or `ROLLBACK` depending on level

## Key Design Decisions

### 1. API Compatibility
- `begin()` still returns a different object (TransactionContext instead of new connection)
- All existing driver methods are available through delegation
- FALLBACK method ensures any missing methods are forwarded

### 2. Automatic vs Manual
- Automatic: Nested `begin()` calls use savepoints transparently
- Manual: `savepoint()`, `rollback-to-savepoint()`, `release-savepoint()` methods for direct control

### 3. Promise Coordination
- Each transaction context maintains a Promise
- Helps coordinate cleanup between different nesting levels
- Allows for future enhancements like waiting on nested operations

### 4. Database Compatibility
- Standard SQL SAVEPOINT syntax for PostgreSQL and MySQL
- SQLite-specific overrides for its simpler syntax
- Easily extensible for other databases

## Benefits

1. **Solves Isolation Issues**: Outer transaction changes are visible to inner transactions
2. **Performance**: No connection overhead for nested transactions
3. **Correctness**: Proper transaction semantics across nesting levels
4. **Flexibility**: Both automatic and manual savepoint management
5. **Compatibility**: Works with existing Red transaction code

## Future Enhancements

Potential improvements that could build on this foundation:

1. **Named Transaction Blocks**: Allow developers to name transaction levels
2. **Rollback Retry Logic**: Automatic retry of operations after savepoint rollbacks
3. **Transaction Metrics**: Tracking of savepoint usage and nesting depth
4. **Deadlock Detection**: Enhanced handling of database deadlocks with savepoints
5. **Async Coordination**: Better integration with async/await patterns

## Testing

The implementation includes comprehensive tests covering:

- Basic savepoint SQL generation
- Nested transaction scenarios
- Manual savepoint operations
- Transaction isolation behavior
- API compatibility

Run tests with: `prove6 -lv t/90-savepoints.rakutest t/91-savepoints-basic.rakutest`

## Maintenance Notes

- The TransactionContext class delegates all methods to maintain compatibility
- SQL translation is handled in the driver hierarchy (CommonSQL → specific drivers)
- New database drivers need to implement savepoint translation methods
- Tests should be updated when adding new transaction-related features
106 changes: 106 additions & 0 deletions docs/savepoints.pod6
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
=begin pod

=head1 SAVEPOINT Support in Red

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.

=head2 Automatic Savepoint Usage

When you start a nested transaction using C<begin>, Red automatically uses savepoints instead of creating new database connections:

=begin code :lang<raku>
use Red:api<2>;

model Person is rw {
has Int $.id is serial;
has Str $.name is column;
}

my $*RED-DB = database "SQLite";
Person.^create-table;

# Start main transaction
my $main-tx = $*RED-DB.begin;

{
my $*RED-DB = $main-tx;
my $person1 = Person.^create(:name("Alice"));

# Start nested transaction (creates savepoint automatically)
my $nested-tx = $main-tx.begin;

{
my $*RED-DB = $nested-tx;
my $person2 = Person.^create(:name("Bob"));

# Rollback nested transaction (rolls back to savepoint)
$nested-tx.rollback; # Only Bob is rolled back
}

# Alice is still there
$main-tx.commit;
}
=end code

=head2 Manual Savepoint Operations

You can also create and manage savepoints manually:

=begin code :lang<raku>
my $tx = $*RED-DB.begin;
my $*RED-DB = $tx;

my $person1 = Person.^create(:name("Alice"));

# Create a named savepoint
$*RED-DB.savepoint("checkpoint1");

my $person2 = Person.^create(:name("Bob"));

# Rollback to the savepoint
$*RED-DB.rollback-to-savepoint("checkpoint1"); # Bob is rolled back

# Or release the savepoint when no longer needed
$*RED-DB.savepoint("checkpoint2");
my $person3 = Person.^create(:name("Charlie"));
$*RED-DB.release-savepoint("checkpoint2"); # Just removes the savepoint

$tx.commit; # Alice and Charlie are committed
=end code

=head2 Database Support

Savepoints are supported on:

=item PostgreSQL - Full SAVEPOINT/ROLLBACK TO SAVEPOINT/RELEASE SAVEPOINT syntax
=item MySQL - Full SAVEPOINT/ROLLBACK TO SAVEPOINT/RELEASE SAVEPOINT syntax
=item SQLite - SAVEPOINT/ROLLBACK TO/RELEASE syntax (simplified)

=head2 API Methods

=head3 Automatic Transaction Context

=item C<$driver.begin()> - Returns a transaction context that uses savepoints for nesting
=item C<$tx.begin()> - Creates a nested savepoint context
=item C<$tx.commit()> - Commits transaction or releases savepoint
=item C<$tx.rollback()> - Rolls back transaction or rolls back to savepoint

=head3 Manual Savepoint Control

=item C<$driver.savepoint($name)> - Creates a named savepoint
=item C<$driver.rollback-to-savepoint($name)> - Rolls back to a named savepoint
=item C<$driver.release-savepoint($name)> - Releases a named savepoint

=head2 Implementation Details

The savepoint implementation uses a C<TransactionContext> wrapper that:

=item Maintains API compatibility with existing transaction code
=item Uses the same underlying database connection for all nested levels
=item Automatically translates nested transactions to savepoints
=item Provides promise-based coordination for cleanup
=item Delegates all driver methods to the parent connection

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.

=end pod
82 changes: 82 additions & 0 deletions examples/savepoint-demo.raku
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env raku

use lib 'lib';
use Red:api<2>;

model Person is rw {
has Int $.id is serial;
has Str $.name is column;
}

# Set up database
my $*RED-DB = database "SQLite";
Person.^create-table;

say "=== SAVEPOINT Demo ===";
say "";

# Demonstrate nested transactions with savepoints
say "1. Starting main transaction...";
my $main-tx = $*RED-DB.begin;

{
my $*RED-DB = $main-tx;
my $person1 = Person.^create(:name("Alice"));
say " Created person: { $person1.name } (ID: { $person1.id })";

say "2. Starting nested transaction (savepoint)...";
my $nested-tx = $main-tx.begin;

{
my $*RED-DB = $nested-tx;
my $person2 = Person.^create(:name("Bob"));
say " Created person: { $person2.name } (ID: { $person2.id })";

say " Current count in nested transaction: { Person.^all.elems }";

say "3. Rolling back nested transaction...";
$nested-tx.rollback;
}

say " Current count after rollback: { Person.^all.elems }";
say " Remaining people: { Person.^all.map(*.name).join(', ') }";

say "4. Committing main transaction...";
$main-tx.commit;
}

say " Final count: { Person.^all.elems }";
say " Final people: { Person.^all.map(*.name).join(', ') }";

say "";
say "=== Manual Savepoint Demo ===";

# Demonstrate manual savepoint operations
{
my $tx = $*RED-DB.begin;
my $*RED-DB = $tx;

my $person3 = Person.^create(:name("Charlie"));
say "Created { $person3.name }";

# Create a manual savepoint
$*RED-DB.savepoint("manual_sp");
say "Created savepoint 'manual_sp'";

my $person4 = Person.^create(:name("David"));
say "Created { $person4.name }";

say "Count before rollback: { Person.^all.elems }";

# Rollback to savepoint
$*RED-DB.rollback-to-savepoint("manual_sp");
say "Rolled back to savepoint";

say "Count after rollback: { Person.^all.elems }";
say "People: { Person.^all.map(*.name).join(', ') }";

$tx.commit;
}

say "";
say "Demo complete!";
3 changes: 3 additions & 0 deletions lib/Red.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ use Red::DB;
use Red::Schema;
use Red::Formatter;
use Red::AST::Infixes;
use Red::AST::Savepoint;
use Red::AST::RollbackToSavepoint;
use Red::AST::ReleaseSavepoint;

class Red:ver<0.2.3>:api<2> {
our %experimentals;
Expand Down
11 changes: 11 additions & 0 deletions lib/Red/AST/ReleaseSavepoint.rakumod
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use Red::AST;
#| AST to release a savepoint
unit class Red::AST::ReleaseSavepoint does Red::AST;

has Str $.name is required;

method args {}

method returns {}

method find-column-name {}
11 changes: 11 additions & 0 deletions lib/Red/AST/RollbackToSavepoint.rakumod
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use Red::AST;
#| AST to rollback to a savepoint
unit class Red::AST::RollbackToSavepoint does Red::AST;

has Str $.name is required;

method args {}

method returns {}

method find-column-name {}
11 changes: 11 additions & 0 deletions lib/Red/AST/Savepoint.rakumod
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use Red::AST;
#| AST to create a savepoint
unit class Red::AST::Savepoint does Red::AST;

has Str $.name is required;

method args {}

method returns {}

method find-column-name {}
Loading
Loading