Group
Extension

DBD-libsql/lib/DBD/libsql/Hrana.pm

package DBD::libsql::Hrana;
use strict;
use warnings;
use LWP::UserAgent;
use HTTP::Request;
use JSON;
use Protocol::WebSocket;
use IO::Socket::SSL;
use Carp;

our $VERSION = "0.05";

# Hrana Protocol Client for libSQL
# Based on the Hrana protocol specification used by libsql-client-ts

sub new {
    my ($class, %options) = @_;
    
    my $self = bless {
        url => $options{url} || 'http://127.0.0.1:8080',
        auth_token => $options{auth_token},
        user_agent => LWP::UserAgent->new(
            timeout => $options{timeout} || 30,
            agent => "DBD::libsql/$VERSION",
        ),
        json => JSON->new->utf8,
        connection_id => undef,
        closed => 0,
    }, $class;
    
    # Detect protocol type from URL
    if ($self->{url} =~ /^ws/) {
        $self->{protocol} = 'websocket';
    } else {
        $self->{protocol} = 'http';
    }
    
    return $self;
}

# HTTP-based Hrana implementation
sub connect_http {
    my $self = shift;
    
    # For HTTP, we don't need a persistent connection
    # Each request is stateless
    return 1;
}

# WebSocket-based Hrana implementation  
sub connect_websocket {
    my $self = shift;
    
    # Convert HTTP URL to WebSocket URL for local dev server
    my $ws_url = $self->{url};
    $ws_url =~ s/^http/ws/;
    $ws_url .= '/v2' unless $ws_url =~ /\/v2$/;
    
    eval {
        # For now, we'll implement WebSocket later
        # Just mark as connected for HTTP fallback
        $self->{connection_id} = $self->_generate_connection_id();
        warn "WebSocket connection would connect to: $ws_url" if $ENV{DBD_LIBSQL_DEBUG};
    };
    
    if ($@) {
        croak "Failed to connect via WebSocket: $@";
    }
    
    return 1;
}

sub connect {
    my $self = shift;
    
    if ($self->{protocol} eq 'websocket') {
        return $self->connect_websocket();
    } else {
        return $self->connect_http();
    }
}

# Execute SQL via HTTP Hrana
sub execute_http {
    my ($self, $sql, $params) = @_;
    
    my $request_body = {
        type => 'execute',
        stmt => {
            sql => $sql,
            args => $params || [],
        }
    };
    
    my $url = $self->{url} . '/v2/pipeline';
    my $request = HTTP::Request->new('POST', $url);
    $request->header('Content-Type' => 'application/json');
    
    if ($self->{auth_token}) {
        $request->header('Authorization' => 'Bearer ' . $self->{auth_token});
    }
    
    $request->content($self->{json}->encode({
        requests => [$request_body]
    }));
    
    my $response = $self->{user_agent}->request($request);
    
    unless ($response->is_success) {
        croak "HTTP request failed: " . $response->status_line;
    }
    
    my $result = eval { $self->{json}->decode($response->content) };
    if ($@) {
        croak "Failed to parse JSON response: $@";
    }
    
    return $self->_parse_execute_result($result);
}

# Execute SQL via WebSocket Hrana
sub execute_websocket {
    my ($self, $sql, $params) = @_;
    
    # TODO: Implement WebSocket-based execution
    # This would use the Hrana WebSocket protocol
    croak "WebSocket execution not yet implemented";
}

sub execute {
    my ($self, $sql, $params) = @_;
    
    if ($self->{closed}) {
        croak "Connection is closed";
    }
    
    if ($self->{protocol} eq 'websocket') {
        return $self->execute_websocket($sql, $params);
    } else {
        return $self->execute_http($sql, $params);
    }
}

# Execute batch of SQL statements
sub batch {
    my ($self, $statements) = @_;
    
    my @requests;
    for my $stmt (@$statements) {
        if (ref $stmt eq 'HASH') {
            push @requests, {
                type => 'execute',
                stmt => {
                    sql => $stmt->{sql},
                    args => $stmt->{args} || [],
                }
            };
        } else {
            push @requests, {
                type => 'execute', 
                stmt => {
                    sql => $stmt,
                    args => [],
                }
            };
        }
    }
    
    my $url = $self->{url} . '/v2/pipeline';
    my $request = HTTP::Request->new('POST', $url);
    $request->header('Content-Type' => 'application/json');
    
    if ($self->{auth_token}) {
        $request->header('Authorization' => 'Bearer ' . $self->{auth_token});
    }
    
    $request->content($self->{json}->encode({
        requests => \@requests
    }));
    
    my $response = $self->{user_agent}->request($request);
    
    unless ($response->is_success) {
        croak "Batch request failed: " . $response->status_line;
    }
    
    my $result = eval { $self->{json}->decode($response->content) };
    if ($@) {
        croak "Failed to parse JSON response: $@";
    }
    
    return $result;
}

# Parse execution result from Hrana response
sub _parse_execute_result {
    my ($self, $result) = @_;
    
    return undef unless $result && $result->{results};
    
    my $first_result = $result->{results}->[0];
    return undef unless $first_result && $first_result->{response};
    
    my $response = $first_result->{response};
    
    if ($response->{type} eq 'ok') {
        return {
            columns => $response->{result}->{cols} || [],
            rows => $response->{result}->{rows} || [],
            rows_affected => $response->{result}->{affected_row_count} || 0,
            last_insert_rowid => $response->{result}->{last_insert_rowid},
        };
    } elsif ($response->{type} eq 'error') {
        croak "SQL execution error: " . $response->{error}->{message};
    }
    
    return undef;
}

sub _generate_connection_id {
    return sprintf("%016x", int(rand(0xffffffff)));
}

sub close {
    my $self = shift;
    
    if ($self->{ws_client}) {
        # Close WebSocket connection
        $self->{ws_client} = undef;
    }
    
    $self->{closed} = 1;
    return 1;
}

sub DESTROY {
    my $self = shift;
    $self->close() unless $self->{closed};
}

1;

__END__

=head1 NAME

DBD::libsql::Hrana - Hrana protocol client for libSQL

=head1 DESCRIPTION

This module implements the Hrana protocol for communicating with libSQL servers.
It supports both HTTP and WebSocket transports.

=head1 METHODS

=head2 new(%options)

Creates a new Hrana client instance.

Options:
- url: Server URL (default: http://127.0.0.1:8080)
- auth_token: Authentication token for remote servers
- timeout: Request timeout in seconds (default: 30)

=head2 connect()

Establishes connection to the libSQL server.

=head2 execute($sql, $params)

Executes an SQL statement with optional parameters.

=head2 batch($statements)

Executes multiple SQL statements in a batch.

=head2 close()

Closes the connection to the server.

=head1 AUTHOR

ytnobody E<lt>ytnobody@gmail.comE<gt>

=cut

Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.