package Zing::Store::Mysql;
use 5.014;
use strict;
use warnings;
use registry 'Zing::Types';
use routines;
use Data::Object::Class;
use Data::Object::ClassHas;
extends 'Zing::Store';
our $VERSION = '0.03'; # VERSION
# ATTRIBUTES
has client => (
is => 'ro',
isa => 'InstanceOf["DBI::db"]',
new => 1,
);
fun new_client($self) {
my $dbname = $ENV{ZING_DBNAME} || 'zing';
my $dbhost = $ENV{ZING_DBHOST} || 'localhost';
my $dbport = $ENV{ZING_DBPORT} || '3306';
my $dbuser = $ENV{ZING_DBUSER} || 'root';
my $dbpass = $ENV{ZING_DBPASS};
require DBI; DBI->connect(
join(';',
"dbi:mysql:database=$dbname",
$dbhost ? join('=', 'host', $dbhost) : (),
$dbport ? join('=', 'port', $dbport) : (),
),
$dbuser, $dbpass,
{
AutoCommit => 1,
PrintError => 0,
RaiseError => 1
}
);
}
has meta => (
is => 'ro',
isa => 'Str',
new => 1,
);
fun new_meta($self) {
require Zing::ID; Zing::ID->new->string
}
has table => (
is => 'ro',
isa => 'Str',
new => 1,
);
fun new_table($self) {
$ENV{ZING_DBZONE} || 'entities'
}
# BUILDERS
fun new_encoder($self) {
require Zing::Encoder::Dump; Zing::Encoder::Dump->new;
}
fun BUILD($self) {
my $client = $self->client;
my $table = $self->table;
local $@; eval {
$client->do(qq{
create table if not exists `$table` (
`id` int not null auto_increment primary key,
`key` varchar(255) not null,
`value` mediumtext not null,
`index` int default 0,
`meta` varchar(255) null
) engine = innodb
});
}
unless (defined(do{
local $@;
local $client->{RaiseError} = 0;
local $client->{PrintError} = 0;
eval {
$client->do(qq{
select 1 from `$table` where 1 = 1
})
}
}));
return $self;
}
fun DESTROY($self) {
$self->client->disconnect;
return $self;
}
# METHODS
my $retries = 10;
method drop(Str $key) {
my $table = $self->table;
my $client = $self->client;
my $sth = $client->prepare(
qq{delete from `$table` where `key` = ?}
);
$sth->execute($key);
return $sth->rows > 0 ? 1 : 0;
}
method keys(Str $query) {
$query =~ s/\*/%/g;
my $table = $self->table;
my $client = $self->client;
my $data = $client->selectall_arrayref(
qq{select distinct(`key`) from `$table` where `key` like ?},
{},
$query,
);
return [map $$_[0], @$data];
}
method lpull(Str $key) {
my $table = $self->table;
my $client = $self->client;
for my $attempt (1..$retries) {
local $@; eval {
my $sth = $client->prepare(
qq{
update `$table` set `meta` = ? where `id` = (
select `s1`.`id` from (
select `s0`.`id` from `$table` `s0`
where `s0`.`key` = ? and `s0`.`meta` is null
order by `s0`.`index` asc limit 1
) as `s1`
)
}
);
$sth->execute($self->meta, $key);
};
if ($@) {
die $@ if $attempt == $retries;
}
else {
last;
}
}
my $data = $client->selectrow_arrayref(
qq{
select `id`, `value`
from `$table` where `meta` = ? and `key` = ? order by `index` asc limit 1
},
{},
$self->meta, $key,
);
if ($data) {
my $sth = $client->prepare(
qq{delete from `$table` where `id` = ?}
);
$sth->execute($data->[0]);
}
return $data ? $self->decode($data->[1]) : undef;
}
method lpush(Str $key, HashRef $val) {
my $table = $self->table;
my $client = $self->client;
my $sth = $client->prepare(
qq{
insert into `$table` (`key`, `value`, `index`) values (?, ?, (
select ifnull(min(`s0`.`index`), 0) - 1
from `$table` `s0` where `s0`.`key` = ?
))
}
);
for my $attempt (1..$retries) {
local $@; eval {
$sth->execute($key, $self->encode($val), $key);
};
if ($@) {
die $@ if $attempt == $retries;
}
else {
last;
}
}
return $sth->rows;
}
method read(Str $key) {
my $table = $self->table;
my $client = $self->client;
my $data = $client->selectrow_arrayref(
qq{
select `value` from `$table`
where `key` = ? order by `id` desc limit 1
},
{},
$key,
);
return $data ? $data->[0] : undef;
}
method recv(Str $key) {
my $data = $self->read($key);
return $data ? $self->decode($data) : $data;
}
method rpull(Str $key) {
my $table = $self->table;
my $client = $self->client;
for my $attempt (1..$retries) {
local $@; eval {
my $sth = $client->prepare(
qq{
update `$table` set `meta` = ? where `id` = (
select `s1`.`id` from (
select `s0`.`id` from `$table` `s0`
where `s0`.`key` = ? and `s0`.`meta` is null
order by `s0`.`index` desc limit 1
) as `s1`
)
}
);
$sth->execute($self->meta, $key);
};
if ($@) {
die $@ if $attempt == $retries;
}
else {
last;
}
}
my $data = $client->selectrow_arrayref(
qq{
select `id`, `value`
from `$table` where `meta` = ? and `key` = ? order by `index` desc limit 1
},
{},
$self->meta, $key,
);
if ($data) {
my $sth = $client->prepare(
qq{delete from `$table` where `id` = ?}
);
$sth->execute($data->[0]);
}
return $data ? $self->decode($data->[1]) : undef;
}
method rpush(Str $key, HashRef $val) {
my $table = $self->table;
my $client = $self->client;
my $sth = $client->prepare(
qq{
insert into `$table` (`key`, `value`, `index`) values (?, ?, (
select ifnull(max(`s0`.`index`), 0) + 1
from `$table` `s0` where `s0`.`key` = ?
))
}
);
for my $attempt (1..$retries) {
local $@; eval {
$sth->execute($key, $self->encode($val), $key);
};
if ($@) {
die $@ if $attempt == $retries;
}
else {
last;
}
}
return $sth->rows;
}
method send(Str $key, HashRef $val) {
my $set = $self->encode($val);
$self->write($key, $set);
return 'OK';
}
method size(Str $key) {
my $table = $self->table;
my $client = $self->client;
my $data = $client->selectrow_arrayref(
qq{select count(`key`) from `$table` where `key` = ?},
{},
$key,
);
return $data->[0];
}
method slot(Str $key, Int $pos) {
my $table = $self->table;
my $client = $self->client;
my $data = $client->selectrow_arrayref(
qq{
select `value` from `$table`
where `key` = ? order by `index` asc limit ?, 1
},
{},
$key, $pos
);
return $data ? $self->decode($data->[0]) : undef;
}
method test(Str $key) {
my $table = $self->table;
my $client = $self->client;
my $data = $client->selectrow_arrayref(
qq{select count(`id`) from `$table` where `key` = ?},
{},
$key,
);
return $data->[0] ? 1 : 0;
}
method write(Str $key, Str $data) {
my $table = $self->table;
my $client = $self->client;
$client->prepare(
qq{delete from `$table` where `key` = ?}
)->execute($key);
$client->prepare(
qq{insert into `$table` (`key`, `value`) values (?, ?)}
)->execute($key, $data);
return $self;
}
1;
=encoding utf8
=head1 NAME
Zing::Store::Mysql - Mysql Storage
=cut
=head1 ABSTRACT
Mysql Storage Abstraction
=cut
=head1 SYNOPSIS
use Test::DB::Mysql;
use Zing::Encoder::Dump;
use Zing::Store::Mysql;
my $testdb = Test::DB::Mysql->new;
my $store = Zing::Store::Mysql->new(
client => $testdb->create->dbh,
encoder => Zing::Encoder::Dump->new
);
# $store->drop;
=cut
=head1 DESCRIPTION
This package provides a MySQL-specific storage adapter for use with data
persistence abstractions. The L</client> attribute accepts a L<DBI> object
configured to connect to a L<DBD::mysql> backend. The C<ZING_DBNAME>
environment variable can be used to specify the database name (defaults to
"zing"). The C<ZING_DBHOST> environment variable can be used to specify the
database host (defaults to "localhost"). The C<ZING_DBPORT> environment
variable can be used to specify the database port (defaults to "3306"). The
C<ZING_DBUSER> environment variable can be used to specify the database
username (defaults to "root"). The C<ZING_DBPASS> environment variable can
be used to specify the database password. The C<ZING_DBZONE> environment
variable can be used to specify the database table name (defaults to
"entities").
=cut
=head1 INHERITS
This package inherits behaviors from:
L<Zing::Store>
=cut
=head1 LIBRARIES
This package uses type constraints from:
L<Zing::Types>
=cut
=head1 ATTRIBUTES
This package has the following attributes:
=cut
=head2 client
client(InstanceOf["DBI::db"])
This attribute is read-only, accepts C<(InstanceOf["DBI::db"])> values, and is optional.
=cut
=head1 METHODS
This package implements the following methods:
=cut
=head2 decode
decode(Str $data) : HashRef
The decode method decodes the JSON data provided and returns the data as a hashref.
=over 4
=item decode example #1
# given: synopsis
$store->decode('{"status"=>"ok"}');
=back
=cut
=head2 drop
drop(Str $key) : Int
The drop method removes (drops) the item from the datastore.
=over 4
=item drop example #1
# given: synopsis
$store->drop('zing:main:global:model:temp');
=back
=cut
=head2 encode
encode(HashRef $data) : Str
The encode method encodes and returns the data provided as JSON.
=over 4
=item encode example #1
# given: synopsis
$store->encode({ status => 'ok' });
=back
=cut
=head2 keys
keys(Str @keys) : ArrayRef[Str]
The keys method returns a list of keys under the namespace of the datastore or
provided key.
=over 4
=item keys example #1
# given: synopsis
my $keys = $store->keys('zing:main:global:model:temp');
=back
=over 4
=item keys example #2
# given: synopsis
$store->send('zing:main:global:model:temp', { status => 'ok' });
my $keys = $store->keys('zing:main:global:model:temp');
=back
=cut
=head2 lpull
lpull(Str $key) : Maybe[HashRef]
The lpull method pops data off of the top of a list in the datastore.
=over 4
=item lpull example #1
# given: synopsis
$store->lpull('zing:main:global:model:items');
=back
=over 4
=item lpull example #2
# given: synopsis
$store->rpush('zing:main:global:model:items', { status => 'ok' });
$store->lpull('zing:main:global:model:items');
=back
=cut
=head2 lpush
lpush(Str $key, HashRef $val) : Int
The lpush method pushed data onto the top of a list in the datastore.
=over 4
=item lpush example #1
# given: synopsis
$store->lpush('zing:main:global:model:items', { status => '1' });
=back
=over 4
=item lpush example #2
# given: synopsis
$store->lpush('zing:main:global:model:items', { status => '0' });
$store->lpush('zing:main:global:model:items', { status => '0' });
=back
=cut
=head2 recv
recv(Str $key) : Maybe[HashRef]
The recv method fetches and returns data from the datastore by its key.
=over 4
=item recv example #1
# given: synopsis
$store->recv('zing:main:global:model:temp');
=back
=over 4
=item recv example #2
# given: synopsis
$store->send('zing:main:global:model:temp', { status => 'ok' });
$store->recv('zing:main:global:model:temp');
=back
=cut
=head2 rpull
rpull(Str $key) : Maybe[HashRef]
The rpull method pops data off of the bottom of a list in the datastore.
=over 4
=item rpull example #1
# given: synopsis
$store->rpull('zing:main:global:model:items');
=back
=over 4
=item rpull example #2
# given: synopsis
$store->rpush('zing:main:global:model:items', { status => 1 });
$store->rpush('zing:main:global:model:items', { status => 2 });
$store->rpull('zing:main:global:model:items');
=back
=cut
=head2 rpush
rpush(Str $key, HashRef $val) : Int
The rpush method pushed data onto the bottom of a list in the datastore.
=over 4
=item rpush example #1
# given: synopsis
$store->rpush('zing:main:global:model:items', { status => 'ok' });
=back
=over 4
=item rpush example #2
# given: synopsis
$store->rpush('zing:main:global:model:items', { status => 'ok' });
$store->rpush('zing:main:global:model:items', { status => 'ok' });
=back
=cut
=head2 send
send(Str $key, HashRef $val) : Str
The send method commits data to the datastore with its key and returns truthy.
=over 4
=item send example #1
# given: synopsis
$store->send('zing:main:global:model:temp', { status => 'ok' });
=back
=cut
=head2 size
size(Str $key) : Int
The size method returns the size of a list in the datastore.
=over 4
=item size example #1
# given: synopsis
my $size = $store->size('zing:main:global:model:items');
=back
=over 4
=item size example #2
# given: synopsis
$store->rpush('zing:main:global:model:items', { status => 'ok' });
my $size = $store->size('zing:main:global:model:items');
=back
=cut
=head2 slot
slot(Str $key, Int $pos) : Maybe[HashRef]
The slot method returns the data from a list in the datastore by its index.
=over 4
=item slot example #1
# given: synopsis
my $model = $store->slot('zing:main:global:model:items', 0);
=back
=over 4
=item slot example #2
# given: synopsis
$store->rpush('zing:main:global:model:items', { status => 'ok' });
my $model = $store->slot('zing:main:global:model:items', 0);
=back
=cut
=head2 test
test(Str $key) : Int
The test method returns truthy if the specific key (or datastore) exists.
=over 4
=item test example #1
# given: synopsis
$store->rpush('zing:main:global:model:items', { status => 'ok' });
$store->test('zing:main:global:model:items');
=back
=over 4
=item test example #2
# given: synopsis
$store->drop('zing:main:global:model:items');
$store->test('zing:main:global:model:items');
=back
=cut
=head1 AUTHOR
Al Newkirk, C<awncorp@cpan.org>
=head1 LICENSE
Copyright (C) 2011-2019, Al Newkirk, et al.
This is free software; you can redistribute it and/or modify it under the terms
of the The Apache License, Version 2.0, as elucidated in the L<"license
file"|https://github.com/iamalnewkirk/zing-store-mysql/blob/master/LICENSE>.
=head1 PROJECT
L<Wiki|https://github.com/iamalnewkirk/zing-store-mysql/wiki>
L<Project|https://github.com/iamalnewkirk/zing-store-mysql>
L<Initiatives|https://github.com/iamalnewkirk/zing-store-mysql/projects>
L<Milestones|https://github.com/iamalnewkirk/zing-store-mysql/milestones>
L<Contributing|https://github.com/iamalnewkirk/zing-store-mysql/blob/master/CONTRIBUTE.md>
L<Issues|https://github.com/iamalnewkirk/zing-store-mysql/issues>
=cut