BusyBird

 view release on metacpan or  search on metacpan

lib/BusyBird/StatusStorage/SQLite.pm  view on Meta::CPAN

        $dbh->begin_work();
        my $timeline_id = $self->_get_timeline_id($dbh, $timeline) || $self->_create_timeline($dbh, $timeline);
        if(!defined($timeline_id)) {
            die "Internal error: could not create a timeline '$timeline' somehow.";
        }
        my $sth;
        my $total_count = 0;
        my $put_method = "_put_$mode";
        foreach my $status (@$statuses) {
            my $record = _to_status_record($timeline_id, $status);
            my $count;
            ($count, $sth) = $self->$put_method($dbh, $record, $sth);
            if($count > 0) {
                $total_count += $count;
            }
        }
        my $exceeding_delete_count = 0;
        if($mode ne "update" && $total_count > 0) {
            $exceeding_delete_count = $self->_delete_exceeding_statuses($dbh, $timeline_id);
        }
        $dbh->commit();
        if($exceeding_delete_count > 0) {
            $self->_add_to_delete_count($dbh, $exceeding_delete_count);
        }
        return (undef, $total_count);
    } catch {
        my $e = shift;
        if($dbh) {
            $dbh->rollback();
        }
        return ($e);
    };
    @_ = @results;
    goto $callback;
}

sub _get_timeline_id {
    my ($self, $dbh, $timeline_name) = @_;
    my ($sql, @bind) = $self->{maker}->select('timelines', ['timeline_id'], sql_eq(name => $timeline_name));
    my $record = $dbh->selectrow_arrayref($sql, undef, @bind);
    if(!defined($record)) {
        return undef;
    }
    return $record->[0];
}

sub _create_timeline {
    my ($self, $dbh, $timeline_name) = @_;
    my ($sql, @bind) = $self->{maker}->insert('timelines', [name => "$timeline_name"]);
    $dbh->do($sql, undef, @bind);
    return $self->_get_timeline_id($dbh, $timeline_name);
}

sub _to_status_record {
    my ($timeline_id, $status) = @_;
    croak "status ID must be set" if not defined $status->{id};
    croak "timeline_id must be defined" if not defined $timeline_id;
    my $record = {
        timeline_id => $timeline_id,
        status_id => $status->{id},
        level => $status->{busybird}{level} || 0,
    };
    my $acked_at = $status->{busybird}{acked_at};  ## avoid autovivification
    ($record->{utc_acked_at}, $record->{timezone_acked_at}) = _extract_utc_timestamp_and_timezone($acked_at);
    ($record->{utc_created_at}, $record->{timezone_created_at}) = _extract_utc_timestamp_and_timezone($status->{created_at});
    $record->{content} = to_json($status);
    return $record;
}

sub _from_status_record {
    my ($record) = @_;
    my $status = from_json($record->{content});
    $status->{id} = $record->{status_id};
    if($record->{level} != 0 || defined($status->{busybird}{level})) {
        $status->{busybird}{level} = $record->{level};
    }
    my $acked_at_str = _create_bb_timestamp_from_utc_timestamp_and_timezone($record->{utc_acked_at}, $record->{timezone_acked_at});
    if(defined($acked_at_str) || defined($status->{busybird}{acked_at})) {
        $status->{busybird}{acked_at} = $acked_at_str;
    }
    my $created_at_str = _create_bb_timestamp_from_utc_timestamp_and_timezone($record->{utc_created_at}, $record->{timezone_created_at});
    if(defined($created_at_str) || defined($status->{created_at})) {
        $status->{created_at} = $created_at_str;
    }
    return $status;
}

sub _extract_utc_timestamp_and_timezone {
    my ($timestamp_str) = @_;
    if(!defined($timestamp_str) || $timestamp_str eq '') {
        return ($UNDEF_TIMESTAMP, 'UTC');
    }
    my $datetime = BusyBird::DateTime::Format->parse_datetime($timestamp_str);
    croak "Invalid datetime format: $timestamp_str" if not defined $datetime;
    my $timezone_name = $datetime->time_zone->name;
    $datetime->set_time_zone('UTC');
    my $utc_timestamp = _format_datetime($datetime);
    return ($utc_timestamp, $timezone_name);
}

sub _create_bb_timestamp_from_utc_timestamp_and_timezone {
    my ($utc_timestamp_str, $timezone) = @_;
    if($utc_timestamp_str eq $UNDEF_TIMESTAMP) {
        return undef;
    }
    my $dt = _parse_datetime($utc_timestamp_str);
    $dt->set_time_zone($timezone);
    return BusyBird::DateTime::Format->format_datetime($dt);
}

sub get_statuses {
    my ($self, %args) = @_;
    my $timeline = $args{timeline};
    croak "timeline parameter is mandatory" if not defined $timeline;
    my $callback = $args{callback};
    croak "callback parameter is mandatory" if not defined $callback;
    croak "callback parameter must be a CODEREF" if ref($callback) ne "CODE";
    my $ack_state = defined($args{ack_state}) ? $args{ack_state} : "any";
    if($ack_state ne "any" && $ack_state ne "unacked" && $ack_state ne "acked") {
        croak "ack_state parameter must be either 'any' or 'acked' or 'unacked'";
    }
    my $max_id = $args{max_id};
    my $count = defined($args{count}) ? $args{count} : 'all';
    if($count ne 'all' && !looks_like_number($count)) {
        croak "count parameter must be either 'all' or number";
    }
    my @results = try {
        my $dbh = $self->_get_my_dbh();
        my $timeline_id = $self->_get_timeline_id($dbh, $timeline);
        if(!defined($timeline_id)) {
            return (undef, []);
        }
        my $cond = $self->_create_base_condition($timeline_id, $ack_state);
        if(defined($max_id)) {
            my $max_id_cond = $self->_create_max_id_condition($dbh, $timeline_id, $max_id, $ack_state);
            if(!defined($max_id_cond)) {
                return (undef, []);
            }
            $cond = sql_and([$cond, $max_id_cond]);

lib/BusyBird/StatusStorage/SQLite.pm  view on Meta::CPAN

                next;
            }
            my ($sql, @bind) = $self->{maker}->select(
                'statuses', ['timeline_id', 'status_id'],
                sql_and([sql_eq(timeline_id => $timeline_id), sql_eq(status_id => $status_id)])
            );
            if(!$sth) {
                $sth = $dbh->prepare($sql);
            }
            $sth->execute(@bind);
            my $result = $sth->fetchall_arrayref();
            if(!defined($result)) {
                confess "Statement handle is inactive. Something is wrong.";
            }
            if(@$result) {
                push @ret_contained, $query_elem;
            }else {
                push @ret_not_contained, $query_elem;
            }
        }
        return (undef, \@ret_contained, \@ret_not_contained);
    }catch {
        my $e = shift;
        return ($e);
    };
    @_ = @method_result;
    goto $callback;
}

sub get_timeline_names {
    my ($self) = @_;
    my $dbh = $self->_get_my_dbh();
    my ($sql, @bind) = $self->{maker}->select(
        'timelines', ['name']
    );
    my $result = $dbh->selectall_arrayref($sql, undef, @bind);
    my @return = map { $_->[0] } @$result;
    return @return;
}

1;

__END__

=pod

=head1 NAME

BusyBird::StatusStorage::SQLite - status storage in SQLite database

=head1 SYNOPSIS

    use BusyBird;
    use BusyBird::StatusStorage::SQLite;
    
    my $storage = BusyBird::StatusStorage::SQLite->new(
        path => 'path/to/storage.sqlite3',
        max_status_num => 5000
    );
    
    busybird->set_config(
        default_status_storage => $storage
    );

=head1 DESCRIPTION

This is an implementation of L<BusyBird::StatusStorage> interface.
It stores statuses in an SQLite database.

This storage is synchronous, i.e., all operations block the thread
and the callback is called before the method returns.

=head1 CLASS METHOD

=head2 $storage = BusyBird::StatusStorage::SQLite->new(%args)

The constructor.

Fields in C<%args> are:

=over

=item C<path> => FILE_PATH (mandatory)

Path string to the SQLite database file.
If C<':memory:'> is specified, it creates a temporary in-memory storage.

=item C<max_status_num> => INT (optional, default: 2000)

The maximum number of statuses the storage guarantees to store per timeline.
You cannot expect a timeline to keep more statuses than this number.

=item C<hard_max_status_num> => INT (optional, default: 120% of max_status_num)

The hard limit max number of statuses the storage is able to store per timeline.
When the number of statuses in a timeline exceeds this number,
it deletes old statuses from the timeline so that the timeline has C<max_status_num> statuses.

=item C<vacuum_on_delete> => INT (optional, default: 200% of max_status_num)

The status storage automatically executes C<VACUUM> every time this number of statuses are
deleted from the storage. B<The number is for the whole storage, not per timeline>.

If you set this option less than or equal to 0, it never C<VACUUM> itself.


=back

=head1 OBJECT METHODS

L<BusyBird::StatusStorage::SQLite> implements all object methods in L<BusyBird::StatusStorage>.
In addition to it, it has the following methods.

=head2 $storage->vacuum()

Executes SQL C<VACUUM> on the database.

=head2 @timeline_names = $storage->get_timeline_names()

Returns all timeline names in the C<$storage>.



( run in 0.428 second using v1.01-cache-2.11-cpan-39bf76dae61 )