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 attribute accepts a L object configured to connect to a L backend. The C environment variable can be used to specify the database name (defaults to "zing"). The C environment variable can be used to specify the database host (defaults to "localhost"). The C environment variable can be used to specify the database port (defaults to "3306"). The C environment variable can be used to specify the database username (defaults to "root"). The C environment variable can be used to specify the database password. The C environment variable can be used to specify the database table name (defaults to "entities"). =cut =head1 INHERITS This package inherits behaviors from: L =cut =head1 LIBRARIES This package uses type constraints from: L =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 =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 L L L L L =cut