├── .pls_cache └── index ├── examples ├── blog │ ├── blog.conf │ ├── templates │ │ ├── posts │ │ │ ├── create.html.ep │ │ │ ├── show.html.ep │ │ │ ├── edit.html.ep │ │ │ ├── index.html.ep │ │ │ └── _form.html.ep │ │ └── layouts │ │ │ └── blog.html.ep │ ├── migrations │ │ └── blog.sql │ ├── script │ │ └── blog │ └── lib │ │ ├── Blog │ │ ├── Model │ │ │ └── Posts.pm │ │ └── Controller │ │ │ └── Posts.pm │ │ └── Blog.pm └── chat.pl ├── .gitignore ├── t ├── migrations │ └── test.sql ├── mysql.t ├── mysql_auto_reconnect.t ├── mariadb.t ├── destroy.t ├── async_query_in_flight.t ├── blocking-leak.t ├── strict-mode.t ├── utf8.t ├── 00-project.t ├── sql-live.t ├── mysql_lite_app.t ├── pubsub.t ├── results_methods.t ├── json.t ├── from-string.t ├── test-dbi-async.t ├── results.t ├── crud.t ├── connection.t ├── database.t ├── migrations.t └── sql.t ├── MANIFEST.SKIP ├── .perltidyrc ├── .github └── workflows │ └── ci.yml ├── lib ├── Mojo │ ├── mysql │ │ ├── Transaction.pm │ │ ├── Results.pm │ │ ├── Migrations.pm │ │ ├── PubSub.pm │ │ └── Database.pm │ └── mysql.pm └── SQL │ └── Abstract │ └── mysql.pm ├── Makefile.PL ├── Changes └── README.md /.pls_cache/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhthorsen/mojo-mysql/HEAD/.pls_cache/index -------------------------------------------------------------------------------- /examples/blog/blog.conf: -------------------------------------------------------------------------------- 1 | {mysql => 'mysql://mysql@/test', secrets => ['s3cret']}; 2 | -------------------------------------------------------------------------------- /examples/blog/templates/posts/create.html.ep: -------------------------------------------------------------------------------- 1 | % layout 'blog', title => 'New post'; 2 |

New post

3 | %= include 'posts/_form', caption => 'Create', target => 'store_post' 4 | -------------------------------------------------------------------------------- /examples/blog/templates/posts/show.html.ep: -------------------------------------------------------------------------------- 1 | % layout 'blog', title => $post->{title}; 2 |

<%= $post->{title} %>

3 |

<%= $post->{body} %>

4 | %= link_to 'Edit' => edit_post => {id => $post->{id}} 5 | -------------------------------------------------------------------------------- /examples/blog/migrations/blog.sql: -------------------------------------------------------------------------------- 1 | -- 1 up 2 | create table if not exists posts ( 3 | id integer auto_increment primary key, 4 | title text, 5 | body text 6 | ); 7 | 8 | -- 1 down 9 | drop table if exists posts; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ~$ 2 | *.bak 3 | *.o 4 | *.old 5 | *.swp 6 | /*.tar.gz 7 | /blib/ 8 | /cover_db 9 | /inc/ 10 | /local 11 | /Makefile 12 | /Makefile.old 13 | /MANIFEST 14 | /MANIFEST.bak 15 | /META* 16 | /MYMETA* 17 | /nytprof* 18 | /pm_to_blib 19 | -------------------------------------------------------------------------------- /examples/blog/script/blog: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use lib 'lib'; 7 | 8 | # Start command line interface for application 9 | require Mojolicious::Commands; 10 | Mojolicious::Commands->start_app('Blog'); 11 | -------------------------------------------------------------------------------- /examples/blog/templates/posts/edit.html.ep: -------------------------------------------------------------------------------- 1 | % layout 'blog', title => 'Edit post'; 2 |

Edit post

3 | %= include 'posts/_form', caption => 'Update', target => 'update_post' 4 | %= form_for remove_post => {id => $post->{id}} => begin 5 | %= submit_button 'Remove' 6 | % end 7 | -------------------------------------------------------------------------------- /examples/blog/templates/posts/index.html.ep: -------------------------------------------------------------------------------- 1 | % layout 'blog', title => 'Blog'; 2 | % for my $post (@$posts) { 3 |

4 |

<%= link_to $post->{title} => show_post => {id => $post->{id}} %>

5 | %= $post->{body} 6 |

7 | % } 8 | %= link_to 'New post' => 'create_post' 9 | -------------------------------------------------------------------------------- /examples/blog/templates/posts/_form.html.ep: -------------------------------------------------------------------------------- 1 | %= form_for $target => begin 2 | %= label_for title => 'Title' 3 |
4 | %= text_field title => $post->{title} 5 |
6 | %= label_for body => 'Body' 7 |
8 | %= text_area body => $post->{body} 9 |
10 | %= submit_button $caption 11 | % end 12 | -------------------------------------------------------------------------------- /t/migrations/test.sql: -------------------------------------------------------------------------------- 1 | -- 1 up 2 | create table if not exists migration_test_three (baz varchar(255)); 3 | -- 1 down 4 | drop table if exists migration_test_three; 5 | -- 2 up 6 | insert into migration_test_three values ('just'); 7 | insert into migration_test_three values ('works ♥'); 8 | -- 3 up 9 | -- 4 up 10 | does_not_exist; 11 | -------------------------------------------------------------------------------- /t/mysql.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | use Test::More; 3 | use Mojo::mysql; 4 | 5 | plan skip_all => 'TEST_ONLINE=mysql://root@/test' unless $ENV{TEST_ONLINE} and $ENV{TEST_ONLINE} =~ m!^mysql:!; 6 | 7 | my $mysql = Mojo::mysql->new($ENV{TEST_ONLINE}); 8 | 9 | ok $mysql->db->ping, 'connected'; 10 | is $mysql->db->dbh->{Driver}{Name}, 'mysql', 'driver name'; 11 | 12 | done_testing; 13 | -------------------------------------------------------------------------------- /t/mysql_auto_reconnect.t: -------------------------------------------------------------------------------- 1 | BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } 2 | use Mojo::Base -strict; 3 | use Mojo::IOLoop; 4 | use Mojo::mysql; 5 | use Test::More; 6 | 7 | plan skip_all => 'TEST_ONLINE=mysql://root@/test' unless $ENV{TEST_ONLINE}; 8 | 9 | $ENV{MOD_PERL} = 1; 10 | 11 | my $mysql = Mojo::mysql->new($ENV{TEST_ONLINE}); 12 | ok $mysql->db->ping, 'connected'; 13 | ok !$mysql->db->_dbh_attr('mysql_auto_reconnect'), 'auto_reconnect'; 14 | 15 | done_testing; 16 | -------------------------------------------------------------------------------- /MANIFEST.SKIP: -------------------------------------------------------------------------------- 1 | ~$ 2 | \#$ 3 | \.# 4 | \.bak$ 5 | \.old$ 6 | \.perltidyrc$ 7 | \.swp$ 8 | \.tmp$ 9 | \B\.DS_Store 10 | \B\._ 11 | \B\.git\b 12 | \B\.gitattributes\b 13 | \B\.github\b 14 | \B\.gitignore\b 15 | \B\.pls_cache\b 16 | \B\.vstags\b 17 | \bMANIFEST\.bak 18 | \bMakeMaker-\d 19 | \bMakefile$ 20 | \b\.# 21 | \bblib/ 22 | \bblibdirs\.ts$ # 6.18 through 6.25 generated this 23 | \bcover_db\b 24 | \bcovered\b 25 | \bnode_modules\b 26 | \bpm_to_blib$ 27 | \bpm_to_blib\.ts$ 28 | ^MANIFEST\.SKIP 29 | ^MYMETA\. 30 | ^README\.md 31 | ^README\.pod 32 | ^local/ 33 | -------------------------------------------------------------------------------- /examples/blog/templates/layouts/blog.html.ep: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= title %> 5 | 14 | 15 | 16 |

<%= link_to 'Blog' => 'posts' %>

17 | %= content 18 | 19 | 20 | -------------------------------------------------------------------------------- /t/mariadb.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | use Test::More; 3 | use Mojo::mysql; 4 | 5 | # special case for travis 6 | $ENV{TEST_ONLINE} =~ s!mysql:!mariadb:! 7 | if $ENV{TEST_ONLINE} and $ENV{TEST_ONLINE} eq 'mysql://root@/tarvis_ci_mojo_mysql'; 8 | 9 | plan skip_all => 'TEST_ONLINE=mariadb://root@/test' unless $ENV{TEST_ONLINE} and $ENV{TEST_ONLINE} =~ m!^mariadb:!; 10 | plan skip_all => $@ unless my $mysql = eval { Mojo::mysql->new($ENV{TEST_ONLINE}) }; 11 | 12 | ok $mysql->db->ping, 'connected'; 13 | is $mysql->db->dbh->{Driver}{Name}, 'MariaDB', 'driver name'; 14 | 15 | done_testing; 16 | -------------------------------------------------------------------------------- /.perltidyrc: -------------------------------------------------------------------------------- 1 | -pbp # Start with Perl Best Practices 2 | -w # Show all warnings 3 | -iob # Ignore old breakpoints 4 | -l=120 # 120 characters per line 5 | -mbl=2 # No more than 2 blank lines 6 | -i=2 # Indentation is 2 columns 7 | -ci=2 # Continuation indentation is 2 columns 8 | -vt=0 # Less vertical tightness 9 | -pt=2 # High parenthesis tightness 10 | -bt=2 # High brace tightness 11 | -sbt=2 # High square bracket tightness 12 | -isbc # Don't indent comments without leading space 13 | -nst # Don't output to STDOUT 14 | -wn # Opening and closing containers to be "welded" together 15 | -------------------------------------------------------------------------------- /t/destroy.t: -------------------------------------------------------------------------------- 1 | use Test::More; 2 | use Mojo::Base -strict; 3 | use File::Temp (); 4 | use Mojo::IOLoop; 5 | 6 | plan skip_all => 'TEST_ONLINE=mysql://root@/test' unless $ENV{TEST_ONLINE}; 7 | 8 | my $stderr = File::Temp->new; 9 | 10 | die $! unless defined(my $pid = fork); 11 | unless ($pid) { 12 | open STDERR, '>&', fileno($stderr) or die $!; 13 | require Mojo::mysql; 14 | my $mysql = Mojo::mysql->new($ENV{TEST_ONLINE}); 15 | Mojo::Promise->all(Mojo::Promise->timeout(0.2), $mysql->db->query_p('select sleep(1)'))->wait; 16 | exit; 17 | } 18 | 19 | wait; 20 | $stderr->seek(0, 0); 21 | $stderr = join '', <$stderr>; 22 | $stderr =~ s/^\s*//g; 23 | like $stderr, qr{^Unhandled rejected}s, 24 | q(Avoid: Can't call method "next_tick" on an undefined value at Mojo/Promise.pm); 25 | 26 | done_testing; 27 | -------------------------------------------------------------------------------- /examples/blog/lib/Blog/Model/Posts.pm: -------------------------------------------------------------------------------- 1 | package Blog::Model::Posts; 2 | use Mojo::Base -base; 3 | 4 | has 'mysql'; 5 | 6 | sub add { 7 | my ($self, $post) = @_; 8 | my $sql = 'insert into posts (title, body) values (?, ?)'; 9 | return $self->mysql->db->query($sql, $post->{title}, $post->{body})->last_insert_id; 10 | } 11 | 12 | sub all { shift->mysql->db->query('select * from posts')->hashes->to_array } 13 | 14 | sub find { 15 | my ($self, $id) = @_; 16 | return $self->mysql->db->query('select * from posts where id = ?', $id)->hash; 17 | } 18 | 19 | sub remove { shift->mysql->db->query('delete from posts where id = ?', shift) } 20 | 21 | sub save { 22 | my ($self, $id, $post) = @_; 23 | my $sql = 'update posts set title = ?, body = ? where id = ?'; 24 | $self->mysql->db->query($sql, $post->{title}, $post->{body}, $id); 25 | } 26 | 27 | 1; 28 | -------------------------------------------------------------------------------- /t/async_query_in_flight.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | use Mojo::mysql; 3 | use Test::More; 4 | 5 | plan skip_all => 'TEST_FOR=500 TEST_ONLINE=mysql://root@/test' unless $ENV{TEST_FOR} and $ENV{TEST_ONLINE}; 6 | 7 | my $mysql = Mojo::mysql->new($ENV{TEST_ONLINE})->max_connections(int($ENV{TEST_FOR} / 3)); 8 | my $in_flight = 0; 9 | my $n = $ENV{TEST_FOR}; 10 | my @err; 11 | 12 | my $cb = sub { 13 | my ($db, $err, $res) = @_; 14 | push @err, $err if $err; 15 | Mojo::IOLoop->stop unless --$in_flight; 16 | }; 17 | 18 | Mojo::IOLoop->recurring( 19 | 0.01, 20 | sub { 21 | return unless $n-- > 0; 22 | $in_flight += 2; 23 | $mysql->db->query("SELECT NOW()", $cb); 24 | $mysql->db->query("SELECT SLEEP(0.1)", $cb); 25 | }, 26 | ); 27 | 28 | Mojo::IOLoop->start; 29 | is_deeply \@err, [], 'gathering async_query_in_flight results for the wrong handle was not seen'; 30 | 31 | done_testing; 32 | -------------------------------------------------------------------------------- /t/blocking-leak.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | use Mojo::mysql; 3 | use Test::More; 4 | 5 | plan skip_all => 'TEST_ONLINE=mysql://root@/test' unless $ENV{TEST_ONLINE}; 6 | 7 | my $n_times = $ENV{N_TIMES} || 10; 8 | my $mysql = Mojo::mysql->new($ENV{TEST_ONLINE}); 9 | my $db = $mysql->db; 10 | 11 | $db->query('create table if not exists test_mojo_mysql_blocking_leak (id serial primary key, name text)'); 12 | $db->query('insert into test_mojo_mysql_blocking_leak (name) values (?)', $_) for $0, $$; 13 | 14 | note '$db->query(...)'; 15 | for (1 .. $n_times) { 16 | $db->query('select name from test_mojo_mysql_blocking_leak')->hash; 17 | } 18 | 19 | is $db->backlog, 0, 'zero since blocking'; 20 | is @{$db->{done_sth}}, 0, 'done_sth'; 21 | 22 | note '$mysql->db->query(...)'; 23 | for (1 .. $n_times) { 24 | $mysql->db->query('select name from test_mojo_mysql_blocking_leak')->hash; 25 | } 26 | 27 | is @{$db->{done_sth}}, 0, 'done_sth'; 28 | 29 | $db->query('drop table test_mojo_mysql_blocking_leak'); 30 | 31 | done_testing; 32 | -------------------------------------------------------------------------------- /examples/chat.pl: -------------------------------------------------------------------------------- 1 | use Mojolicious::Lite; 2 | use Mojo::mysql; 3 | 4 | helper mysql => sub { state $mysql = Mojo::mysql->new('mysql://mysql@/test') }; 5 | 6 | get '/' => 'chat'; 7 | 8 | websocket '/channel' => sub { 9 | my $c = shift; 10 | 11 | $c->inactivity_timeout(3600); 12 | 13 | # Forward messages from the browser to MySQL 14 | $c->on(message => sub { shift->mysql->pubsub->notify(mojochat => shift) }); 15 | 16 | # Forward messages from MySQL to the browser 17 | my $cb = $c->mysql->pubsub->listen(mojochat => sub { $c->send(pop) }); 18 | $c->on(finish => sub { shift->mysql->pubsub->unlisten(mojochat => $cb) }); 19 | }; 20 | 21 | app->start; 22 | __DATA__ 23 | 24 | @@ chat.html.ep 25 |
26 |
27 | 34 | -------------------------------------------------------------------------------- /examples/blog/lib/Blog.pm: -------------------------------------------------------------------------------- 1 | package Blog; 2 | use Mojo::Base 'Mojolicious'; 3 | 4 | use Blog::Model::Posts; 5 | use Mojo::mysql; 6 | 7 | sub startup { 8 | my $self = shift; 9 | 10 | # Configuration 11 | $self->plugin('Config'); 12 | $self->secrets($self->config('secrets')); 13 | 14 | # Model 15 | $self->helper(mysql => sub { state $mysql = Mojo::mysql->new(shift->config('mysql')) }); 16 | $self->helper( 17 | posts => sub { state $posts = Blog::Model::Posts->new(mysql => shift->mysql) }); 18 | 19 | # Migrate to latest version if necessary 20 | my $path = $self->home->rel_file('migrations/blog.sql'); 21 | $self->mysql->migrations->name('blog')->from_file($path)->migrate; 22 | 23 | # Controller 24 | my $r = $self->routes; 25 | $r->get('/' => sub { shift->redirect_to('posts') }); 26 | $r->get('/posts')->to('posts#index'); 27 | $r->get('/posts/create')->to('posts#create')->name('create_post'); 28 | $r->post('/posts')->to('posts#store')->name('store_post'); 29 | $r->get('/posts/:id')->to('posts#show')->name('show_post'); 30 | $r->get('/posts/:id/edit')->to('posts#edit')->name('edit_post'); 31 | $r->put('/posts/:id')->to('posts#update')->name('update_post'); 32 | $r->delete('/posts/:id')->to('posts#remove')->name('remove_post'); 33 | } 34 | 35 | 1; 36 | -------------------------------------------------------------------------------- /t/strict-mode.t: -------------------------------------------------------------------------------- 1 | BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } 2 | use Mojo::Base -strict; 3 | use Test::More; 4 | use Mojo::IOLoop; 5 | use Mojo::mysql; 6 | 7 | plan skip_all => 'TEST_ONLINE=mysql://root@/test' unless $ENV{TEST_ONLINE}; 8 | 9 | my $mysql = Mojo::mysql->new($ENV{TEST_ONLINE}); 10 | ok $mysql->db->ping, 'connected'; 11 | 12 | my $db = $mysql->db; 13 | 14 | $db->query('drop table if exists strict_mode_test_table'); 15 | $db->query('create table strict_mode_test_table (foo varchar(5))'); 16 | 17 | $db->query('SET SQL_MODE = ""'); # make sure this fails, even in mysql 5.7 18 | $db->insert(strict_mode_test_table => {foo => 'too_long'}); 19 | is $db->select('strict_mode_test_table')->hash->{foo}, 'too_l', 'fetch invalid data'; 20 | 21 | is $mysql->strict_mode, $mysql, 'enabled strict mode'; 22 | eval { $mysql->db->insert(strict_mode_test_table => {foo => 'too_long'}) }; 23 | like $@, qr{Data too long.*foo}, 'too long string'; 24 | 25 | is $mysql->strict_mode(0), $mysql, 'disable strict mode'; 26 | 27 | $mysql = Mojo::mysql->strict_mode($ENV{TEST_ONLINE}); 28 | isa_ok($mysql, 'Mojo::mysql'); 29 | eval { $mysql->db->insert(strict_mode_test_table => {foo => 'too_long'}) }; 30 | like $@, qr{Data too long.*foo}, 'constructed Mojo::mysql from strict_mode()'; 31 | 32 | $db->query('drop table if exists strict_mode_test_table'); 33 | 34 | done_testing; 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - "**" 7 | jobs: 8 | perl: 9 | name: "Perl ${{matrix.perl}} on ${{matrix.os}}" 10 | strategy: 11 | matrix: 12 | os: ["ubuntu-latest"] 13 | perl: ["5.32", "5.26", "5.16"] 14 | runs-on: "${{matrix.os}}" 15 | steps: 16 | - name: Install and start mysql 17 | run: | 18 | sudo apt-get update 19 | sudo apt-get install -y mysql-client 20 | sudo systemctl start mysql.service 21 | mysql -e 'create database test' -uroot -proot 22 | - run: mysql -V 23 | - uses: shogo82148/actions-setup-perl@v1 24 | with: 25 | perl-version: "${{matrix.perl}}" 26 | - run: perl -V 27 | - uses: actions/checkout@v2 28 | - name: Fix ExtUtils::MakeMaker for Perl 5.16 29 | run: cpanm -n App::cpanminus ExtUtils::MakeMaker 30 | - name: Install dependencies 31 | run: | 32 | cpanm -n Test::CPAN::Changes Test::Pod::Coverage Test::Pod Test::Spelling 33 | cpanm -n --installdeps . 34 | - name: Run tests 35 | run: prove -l t/*.t 36 | env: 37 | HARNESS_OPTIONS: j4 38 | TEST_FOR: 500 39 | TEST_ONLINE: mysql://root:root@localhost:3306/test 40 | TEST_POD: 1 41 | TEST_PUBSUB: 1 42 | -------------------------------------------------------------------------------- /t/utf8.t: -------------------------------------------------------------------------------- 1 | BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } 2 | use Mojo::Base -strict; 3 | use Mojo::mysql; 4 | use Test::More; 5 | 6 | plan skip_all => 'TEST_ONLINE=mysql://root@/test' unless $ENV{TEST_ONLINE}; 7 | 8 | my $mysql = Mojo::mysql->new($ENV{TEST_ONLINE}); 9 | my $db = $mysql->db; 10 | $db->query( 11 | 'create table if not exists utf8_test ( 12 | id serial primary key, 13 | name varchar(255) charset utf8 14 | )' 15 | ); 16 | $db->query('truncate table utf8_test'); 17 | $db->query('insert into utf8_test (name) values (?)', $_) for qw(☺ ☻); 18 | 19 | note 'Result methods'; 20 | is_deeply $db->query('select * from utf8_test')->rows, 2, 'two rows'; 21 | is_deeply $db->query('select * from utf8_test')->columns, ['id', 'name'], 'right structure'; 22 | is_deeply $db->query('select * from utf8_test')->array, [1, '☺'], 'right structure'; 23 | is_deeply [$db->query('select * from utf8_test')->arrays->each], [[1, '☺'], [2, '☻']], 'right structure'; 24 | is_deeply $db->query('select * from utf8_test')->hash, {id => 1, name => '☺'}, 'right structure'; 25 | is_deeply [$db->query('select * from utf8_test')->hashes->each], [{id => 1, name => '☺'}, {id => 2, name => '☻'}], 26 | 'right structure'; 27 | is $mysql->db->query('select * from utf8_test')->text, "1 ☺\n2 ☻\n", 'right text'; 28 | 29 | $db->query('drop table utf8_test'); 30 | 31 | done_testing; 32 | -------------------------------------------------------------------------------- /examples/blog/lib/Blog/Controller/Posts.pm: -------------------------------------------------------------------------------- 1 | package Blog::Controller::Posts; 2 | use Mojo::Base 'Mojolicious::Controller'; 3 | 4 | sub create { shift->stash(post => {}) } 5 | 6 | sub edit { 7 | my $self = shift; 8 | $self->stash(post => $self->posts->find($self->param('id'))); 9 | } 10 | 11 | sub index { 12 | my $self = shift; 13 | $self->stash(posts => $self->posts->all); 14 | } 15 | 16 | sub remove { 17 | my $self = shift; 18 | $self->posts->remove($self->param('id')); 19 | $self->redirect_to('posts'); 20 | } 21 | 22 | sub show { 23 | my $self = shift; 24 | $self->stash(post => $self->posts->find($self->param('id'))); 25 | } 26 | 27 | sub store { 28 | my $self = shift; 29 | 30 | my $validation = $self->_validation; 31 | return $self->render(action => 'create', post => {}) 32 | if $validation->has_error; 33 | 34 | my $id = $self->posts->add($validation->output); 35 | $self->redirect_to('show_post', id => $id); 36 | } 37 | 38 | sub update { 39 | my $self = shift; 40 | 41 | my $validation = $self->_validation; 42 | return $self->render(action => 'edit', post => {}) if $validation->has_error; 43 | 44 | my $id = $self->param('id'); 45 | $self->posts->save($id, $validation->output); 46 | $self->redirect_to('show_post', id => $id); 47 | } 48 | 49 | sub _validation { 50 | my $self = shift; 51 | 52 | my $validation = $self->validation; 53 | $validation->required('title'); 54 | $validation->required('body'); 55 | 56 | return $validation; 57 | } 58 | 59 | 1; 60 | -------------------------------------------------------------------------------- /lib/Mojo/mysql/Transaction.pm: -------------------------------------------------------------------------------- 1 | package Mojo::mysql::Transaction; 2 | use Mojo::Base -base; 3 | 4 | has 'db'; 5 | 6 | sub DESTROY { 7 | my $self = shift; 8 | if ($self->{rollback} && (my $dbh = $self->{dbh})) { $dbh->rollback } 9 | } 10 | 11 | sub commit { 12 | my $self = shift; 13 | $self->{dbh}->commit if delete $self->{rollback}; 14 | } 15 | 16 | sub new { 17 | my $self = shift->SUPER::new(@_, rollback => 1); 18 | $self->{dbh} = $self->db->dbh; 19 | $self->{dbh}->begin_work; 20 | return $self; 21 | } 22 | 23 | 1; 24 | 25 | =encoding utf8 26 | 27 | =head1 NAME 28 | 29 | Mojo::mysql::Transaction - Transaction 30 | 31 | =head1 SYNOPSIS 32 | 33 | use Mojo::mysql::Transaction; 34 | 35 | my $tx = Mojo::mysql::Transaction->new(db => $db); 36 | $tx->commit; 37 | 38 | =head1 DESCRIPTION 39 | 40 | L is a cope guard for L transactions used by 41 | L. 42 | 43 | =head1 ATTRIBUTES 44 | 45 | L implements the following attributes. 46 | 47 | =head2 db 48 | 49 | my $db = $tx->db; 50 | $tx = $tx->db(Mojo::mysql::Database->new); 51 | 52 | L object this transaction belongs to. 53 | 54 | =head1 METHODS 55 | 56 | L inherits all methods from L and 57 | implements the following new ones. 58 | 59 | =head2 commit 60 | 61 | $tx = $tx->commit; 62 | 63 | Commit transaction. 64 | 65 | =head2 new 66 | 67 | my $tx = Mojo::mysql::Transaction->new; 68 | 69 | Construct a new L object. 70 | 71 | =head1 SEE ALSO 72 | 73 | L. 74 | 75 | =cut 76 | -------------------------------------------------------------------------------- /t/00-project.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use Test::More; 4 | use File::Find; 5 | 6 | plan skip_all => 'No such directory: .git' unless $ENV{TEST_ALL} or -d '.git'; 7 | plan skip_all => 'HARNESS_PERL_SWITCHES =~ /Devel::Cover/' if +($ENV{HARNESS_PERL_SWITCHES} || '') =~ /Devel::Cover/; 8 | 9 | for (qw( 10 | Test::CPAN::Changes::changes_file_ok+VERSION!4 11 | Test::Pod::Coverage::pod_coverage_ok+VERSION!1 12 | Test::Pod::pod_file_ok+VERSION!1 13 | Test::Spelling::pod_file_spelling_ok+has_working_spellchecker!1 14 | )) 15 | { 16 | my ($fqn, $module, $sub, $check, $skip_n) = /^((.*)::(\w+))\+(\w+)!(\d+)$/; 17 | next if eval "use $module;$module->$check"; 18 | no strict qw(refs); 19 | *$fqn = sub { 20 | SKIP: { skip "$sub(@_) ($module is required)", $skip_n } 21 | }; 22 | } 23 | 24 | my @files; 25 | find({wanted => sub { /\.pm$/ and push @files, $File::Find::name }, no_chdir => 1}, -e 'blib' ? 'blib' : 'lib'); 26 | plan tests => @files * 4 + 4; 27 | 28 | Test::Spelling::add_stopwords() 29 | if Test::Spelling->can('has_working_spellchecker') && Test::Spelling->has_working_spellchecker; 30 | 31 | for my $file (@files) { 32 | my $module = $file; 33 | $module =~ s,\.pm$,,; 34 | $module =~ s,.*/?lib/,,; 35 | $module =~ s,/,::,g; 36 | ok eval "use $module; 1", "use $module" or diag $@; 37 | Test::Pod::pod_file_ok($file); 38 | Test::Pod::Coverage::pod_coverage_ok($module, {also_private => [qr/^[A-Z_]+$/]}); 39 | Test::Spelling::pod_file_spelling_ok($file); 40 | } 41 | 42 | Test::CPAN::Changes::changes_file_ok(); 43 | 44 | __DATA__ 45 | Anwar 46 | Async 47 | DBD 48 | DDL 49 | DML 50 | Florian 51 | Henning 52 | Hernan 53 | Heyer 54 | Hochwender 55 | Karelas 56 | Leszczynski 57 | Magowan 58 | Mojolicious 59 | Moraes 60 | Naydenov 61 | Nilsen 62 | Riedel 63 | Rolf 64 | Stöckli 65 | Svetoslav 66 | Tekki 67 | Thorsen 68 | Tiago 69 | dbh 70 | de 71 | dsn 72 | errstr 73 | mariadb 74 | pubsub 75 | puke 76 | schemas 77 | sql 78 | sth 79 | unicode 80 | unlisten 81 | -------------------------------------------------------------------------------- /t/sql-live.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | use Mojo::mysql; 3 | use Test::More; 4 | 5 | plan skip_all => 'TEST_ONLINE=mysql://root@/test' unless $ENV{TEST_ONLINE}; 6 | 7 | my $mysql = Mojo::mysql->new($ENV{TEST_ONLINE}); 8 | my $db = $mysql->db; 9 | my $dbname = 'mojo_live_test'; 10 | 11 | note 'Create table'; 12 | $db->query(<query('truncate table mojo_live_test'); 22 | 23 | note 'Insert values'; 24 | my @testdata = ( 25 | {id => 1, f1 => 'one first', f2 => 'two first', f3 => 'three first'}, 26 | {id => 2, f1 => 'one second', f2 => 'two second', f3 => 'three second'}, 27 | {id => 3, f1 => 'one third', f2 => 'two third', f3 => 'three third'}, 28 | ); 29 | 30 | for my $data (@testdata) { 31 | is $db->insert($dbname, $data)->rows, 1, 'insert values'; 32 | } 33 | 34 | is $db->select($dbname)->rows, scalar(@testdata), 'size of db'; 35 | 36 | note 'On conflict'; 37 | my $conflict = {id => 1, f1 => 'one conflict'}; 38 | 39 | eval { $db->insert($dbname, $conflict); }; 40 | like $@, qr/Duplicate entry/, 'unable to insert conflict'; 41 | 42 | is $db->insert($dbname, $conflict, {on_conflict => 'ignore'})->rows, 0, 'ignore conflict'; 43 | 44 | is $db->insert($dbname, $conflict, {on_conflict => 'replace'})->rows, 2, 'replace'; 45 | is $db->select($dbname)->rows, scalar(@testdata), 'size of db'; 46 | is $db->select($dbname, 'f1', {id => 1})->hash->{f1}, 'one conflict', 'value replaced'; 47 | 48 | $conflict->{f1} = 'another conflict'; 49 | my $msg = 'we had a conflict'; 50 | is $db->insert($dbname, $conflict, {on_conflict => {f3 => $msg}})->rows, 2, 'update'; 51 | is_deeply $db->select($dbname, ['f1', 'f3'])->hash, {f1 => 'one conflict', f3 => $msg}, 'value updated'; 52 | 53 | note 'Cleanup'; 54 | $db->query('drop table mojo_live_test') unless $ENV{TEST_KEEP_DB}; 55 | 56 | done_testing; 57 | -------------------------------------------------------------------------------- /t/mysql_lite_app.t: -------------------------------------------------------------------------------- 1 | BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } 2 | use Mojo::Base -strict; 3 | use Mojo::mysql; 4 | use Mojolicious::Lite; 5 | use Test::Mojo; 6 | use Test::More; 7 | 8 | plan skip_all => 'TEST_ONLINE=mysql://root@/test' unless $ENV{TEST_ONLINE}; 9 | 10 | helper mysql => sub { state $mysql = Mojo::mysql->new($ENV{TEST_ONLINE}) }; 11 | 12 | app->mysql->migrations->name('app_test')->from_data->migrate; 13 | 14 | get '/blocking' => sub { 15 | my $c = shift; 16 | my $db = $c->mysql->db; 17 | $c->res->headers->header('X-PID' => $db->pid); 18 | $c->render(text => $db->query('call mojo_app_test()')->hash->{stuff}); 19 | }; 20 | 21 | get '/non-blocking' => sub { 22 | my $c = shift; 23 | my $class; 24 | $class = ref $c->mysql->db->query( 25 | 'select * from app_test' => sub { 26 | my ($db, $err, $results) = @_; 27 | $c->res->headers->header('X-PID' => $db->pid); 28 | $c->render(text => sprintf '%s=%s', $class, $results->hash->{stuff}); 29 | } 30 | ); 31 | }; 32 | 33 | my $t = Test::Mojo->new; 34 | 35 | note 'Make sure migrations are not served as static files'; 36 | $t->get_ok('/app_test')->status_is(404); 37 | 38 | note 'Blocking select (with connection reuse)'; 39 | $t->get_ok('/blocking')->status_is(200)->content_is('I ♥ Mojolicious!'); 40 | my $pid = $t->tx->res->headers->header('X-PID'); 41 | $t->get_ok('/blocking')->status_is(200)->header_is('X-PID', $pid)->content_is('I ♥ Mojolicious!'); 42 | 43 | note 'Non-blocking select (with connection reuse)'; 44 | $t->get_ok('/non-blocking')->status_is(200)->header_is('X-PID', $pid) 45 | ->content_is('Mojo::mysql::Database=I ♥ Mojolicious!'); 46 | $t->get_ok('/non-blocking')->status_is(200)->header_is('X-PID', $pid) 47 | ->content_is('Mojo::mysql::Database=I ♥ Mojolicious!'); 48 | $t->app->mysql->migrations->migrate(0); 49 | 50 | done_testing; 51 | 52 | __DATA__ 53 | @@ app_test 54 | -- 1 up 55 | create table if not exists app_test (stuff text); 56 | delimiter // 57 | create procedure mojo_app_test() 58 | deterministic reads sql data 59 | begin 60 | select * from app_test; 61 | end 62 | // 63 | 64 | -- 2 up 65 | insert into app_test values ('I ♥ Mojolicious!'); 66 | 67 | -- 1 down 68 | drop table app_test; 69 | drop procedure mojo_app_test; 70 | -------------------------------------------------------------------------------- /t/pubsub.t: -------------------------------------------------------------------------------- 1 | BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } 2 | use Mojo::Base -strict; 3 | use Mojo::mysql; 4 | use Test::More; 5 | 6 | plan skip_all => 'TEST_PUBSUB=1 TEST_ONLINE=mysql://root@/test' unless $ENV{TEST_PUBSUB} && $ENV{TEST_ONLINE}; 7 | 8 | my $mysql = Mojo::mysql->new($ENV{TEST_ONLINE}); 9 | my (@pids, @payload); 10 | 11 | { 12 | my @warn; 13 | local $SIG{__WARN__} = sub { push @warn, $_[0] }; 14 | $mysql->pubsub->on(reconnect => sub { push @pids, pop->pid }); 15 | like "@warn", qr{EXPERIMENTAL}, 'pubsub() will warn'; 16 | } 17 | 18 | $ENV{MOJO_PUBSUB_EXPERIMENTAL} = 1; 19 | 20 | $mysql->pubsub->notify(test => 'skipped_message'); 21 | my $sa = $mysql->pubsub->listen(test => sub { push @payload, a => pop }); 22 | $mysql->pubsub->notify(test => 'm1'); 23 | wait_for(1 => 'one subscriber'); 24 | is_deeply \@payload, [a => 'm1'], 'right message m1'; 25 | 26 | $mysql->db->query('insert into mojo_pubsub_notify (channel, payload) values (?, ?)', 'test', 'm2'); 27 | wait_for(1 => 'one subscriber'); 28 | is_deeply \@payload, [a => 'm2'], 'right message m2'; 29 | 30 | $mysql->db->query('insert into mojo_pubsub_notify (channel, payload) values (?, ?), (?, ?), (?, ?), (?, ?)', 31 | 'test', 'm3', 'test', 'm4', 'skipped_channel', 'x1', 'test', 'm5'); 32 | wait_for(3 => 'skipped channel'); 33 | is_deeply \@payload, [map { (a => "m$_") } 3 .. 5], 'right messages 3..5'; 34 | 35 | my $sb = $mysql->pubsub->listen(test => sub { push @payload, b => pop }); 36 | $mysql->pubsub->notify(test => undef)->notify(test => 'd2'); 37 | wait_for(4, 'two subscribers'); 38 | is_deeply \@payload, [map { (a => $_, b => $_) } ('', 'd2')], 'right messages undef + d2'; 39 | 40 | $mysql->pubsub->unlisten(test => $sa)->notify(test => 'u1'); 41 | wait_for(1 => 'unlisten'); 42 | is_deeply \@payload, [b => 'u1'], 'right message after unlisten'; 43 | 44 | $mysql->pubsub->{db}{dbh}{Warn} = 0; 45 | $mysql->db->query('kill ?', $pids[0]); 46 | $mysql->pubsub->notify(test => 'k1'); 47 | wait_for(1 => 'reconnect'); 48 | isnt $pids[0], $pids[1], 'different database pids'; 49 | is_deeply \@payload, [b => 'k1'], 'right message after reconnect'; 50 | 51 | $mysql->migrations->name('pubsub')->from_data('Mojo::mysql::PubSub')->migrate(0); 52 | 53 | done_testing; 54 | 55 | sub wait_for { 56 | my ($n, $descr) = @_; 57 | note "[$n] $descr"; 58 | @payload = (); 59 | my $tid = Mojo::IOLoop->recurring(0.05 => sub { @payload == $n * 2 and Mojo::IOLoop->stop }); 60 | Mojo::IOLoop->start; 61 | Mojo::IOLoop->remove($tid); 62 | } 63 | -------------------------------------------------------------------------------- /t/results_methods.t: -------------------------------------------------------------------------------- 1 | BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } 2 | use Mojo::Base -strict; 3 | use Test::More; 4 | 5 | plan skip_all => 'TEST_ONLINE=mysql://root@/test' unless $ENV{TEST_ONLINE}; 6 | 7 | use Mojo::IOLoop; 8 | use Mojo::mysql; 9 | 10 | my $mysql = Mojo::mysql->new($ENV{TEST_ONLINE}); 11 | my $driver = $ENV{TEST_ONLINE} =~ m!^(\w+):! ? $1 : 'mysql'; 12 | $mysql->options->{"${driver}_client_found_rows"} = 0; 13 | my $db = $mysql->db; 14 | $db->query( 15 | 'create table if not exists results_methods_test ( 16 | id integer auto_increment primary key, 17 | name text 18 | )' 19 | ); 20 | $db->query('truncate table results_methods_test'); 21 | 22 | my $res = $db->query('insert into results_methods_test (name) values (?)', 'foo'); 23 | is $res->affected_rows, 1, 'right affected_rows'; 24 | is $res->last_insert_id, 1, 'right last_insert_id'; 25 | is $res->warnings_count, 0, 'no warnings'; 26 | is $res->err, undef, 'no error'; 27 | is $res->errstr, undef, 'no error'; 28 | is $res->state, '', 'no state'; 29 | 30 | 31 | $res = $db->query('insert into results_methods_test (name) values (?)', 'bar'); 32 | is $res->affected_rows, 1, 'right affected_rows'; 33 | is $res->last_insert_id, 2, 'right last_insert_id'; 34 | is $res->warnings_count, 0, 'no warnings'; 35 | 36 | 37 | is $db->query('update results_methods_test set name=? where name=?', 'foo', 'foo1')->affected_rows, 0, 38 | 'right affected rows'; 39 | is $db->query('update results_methods_test set name=? where name=?', 'foo', 'foo')->affected_rows, 0, 40 | 'right affected rows'; 41 | is $db->query('update results_methods_test set id=1 where id=1')->affected_rows, 0, 'right affected rows'; 42 | 43 | $res = $db->query("select 1 + '4a'"); 44 | is_deeply $res->array, [5]; 45 | is $res->warnings_count, 1, 'warnings'; 46 | 47 | $res = $db->query('show warnings'); 48 | like $res->hashes->[0]->{Message}, qr/Truncated/, 'warning message'; 49 | 50 | $db->disconnect; 51 | 52 | $mysql->options->{"${driver}_client_found_rows"} = 1; 53 | $db = $mysql->db; 54 | 55 | is $db->query('update results_methods_test set name=? where name=?', 'foo', 'foo1')->affected_rows, 0, 56 | 'right affected rows'; 57 | is $db->query('update results_methods_test set name=? where name=?', 'foo', 'foo')->affected_rows, 1, 58 | 'right affected rows'; 59 | is $db->query('update results_methods_test set id=1 where id=1')->affected_rows, 1, 'right affected rows'; 60 | 61 | $db->query('drop table results_methods_test'); 62 | 63 | my $err; 64 | $db->query('select name from results_methods_test', sub { shift; ($err, $res) = @_; Mojo::IOLoop->stop }); 65 | Mojo::IOLoop->start; 66 | like $err, qr/results_methods_test/, 'has error'; 67 | ok index($err, $res->errstr) == 0, 'same error'; 68 | is length($res->state), 5, 'has state'; 69 | 70 | done_testing; 71 | -------------------------------------------------------------------------------- /Makefile.PL: -------------------------------------------------------------------------------- 1 | use 5.016; 2 | use strict; 3 | use warnings; 4 | use utf8; 5 | use ExtUtils::MakeMaker; 6 | 7 | my @DRIVER = $ENV{MOJO_MYSQL_PREFER_DRIVER} ? (split /=/, $ENV{MOJO_MYSQL_PREFER_DRIVER}) : (); 8 | $DRIVER[0] ||= eval('use DBD::MariaDB 1.21;1') ? 'DBD::MariaDB' : 'DBD::mysql'; 9 | $DRIVER[1] ||= $DRIVER[0] eq 'DBD::mysql' ? '4.050' : $DRIVER[0] eq 'DBD::MariaDB' ? '1.21' : '0'; 10 | 11 | my $GITHUB_URL = 'https://github.com/jhthorsen/mojo-mysql'; 12 | my %WriteMakefileArgs = ( 13 | NAME => 'Mojo::mysql', 14 | AUTHOR => 'Jan Henning Thorsen ', 15 | LICENSE => 'artistic_2', 16 | ABSTRACT_FROM => 'lib/Mojo/mysql.pm', 17 | VERSION_FROM => 'lib/Mojo/mysql.pm', 18 | TEST_REQUIRES => {'Test::More' => '0.90'}, 19 | PREREQ_PM => {@DRIVER, 'DBI' => '1.643', 'Mojolicious' => '8.03', 'SQL::Abstract' => '1.86'}, 20 | META_MERGE => { 21 | 'dynamic_config' => 0, 22 | 'meta-spec' => {version => 2}, 23 | 'no_index' => {directory => [qw(examples t)]}, 24 | 'prereqs' => {runtime => {requires => {perl => '5.016'}}}, 25 | 'resources' => { 26 | bugtracker => {web => "$GITHUB_URL/issues"}, 27 | homepage => $GITHUB_URL, 28 | license => ['http://www.opensource.org/licenses/artistic-license-2.0'], 29 | repository => {type => 'git', url => "$GITHUB_URL.git", web => $GITHUB_URL}, 30 | x_IRC => {url => 'irc://irc.libera.chat/#convos', web => 'https://web.libera.chat/#convos'}, 31 | }, 32 | 'x_contributors' => [ 33 | 'Adam Hopkins ', 34 | 'Alexander Karelas ', 35 | 'Curt Hochwender ', 36 | 'Dan Book ', 37 | 'Doug Bell ', 38 | 'Florian Heyer ', 39 | 'Hernan Lopes ', 40 | 'Jan Henning Thorsen ', 41 | 'Karl Rune Nilsen ', 42 | 'Larry Leszczynski ', 43 | 'Lucas Tiago de Moraes ', 44 | 'Matt S Trout ', 45 | 'Mike Magowan ', 46 | 'Mohammad S Anwar ', 47 | 'Rolf Stöckli ', 48 | 'Sebastian Riedel ', 49 | 'Svetoslav Naydenov ', 50 | 'Svetoslav Naydenov ', 51 | 'Tekki ', 52 | ], 53 | }, 54 | test => {TESTS => (-e 'META.yml' ? 't/*.t' : 't/*.t xt/*.t')}, 55 | ); 56 | 57 | unless (eval { ExtUtils::MakeMaker->VERSION('6.63_03') }) { 58 | my $test_requires = delete $WriteMakefileArgs{TEST_REQUIRES}; 59 | @{$WriteMakefileArgs{PREREQ_PM}}{keys %$test_requires} = values %$test_requires; 60 | } 61 | 62 | WriteMakefile(%WriteMakefileArgs); 63 | -------------------------------------------------------------------------------- /t/json.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | use Mojo::mysql; 3 | use Test::More; 4 | 5 | plan skip_all => 'TEST_ONLINE=mysql://root@/test' unless $ENV{TEST_ONLINE}; 6 | 7 | my $mysql = Mojo::mysql->new($ENV{TEST_ONLINE}); 8 | my $db = $mysql->db; 9 | 10 | eval { $db->query('select json_type("[]")'); } or do { 11 | plan skip_all => $@; 12 | }; 13 | 14 | $db->query('create table if not exists mojo_json_test (id int(10), name varchar(60), j json)'); 15 | $db->query('truncate table mojo_json_test'); 16 | $db->query('insert into mojo_json_test (id, name, j) values (?, ?, ?)', $$, $0, {json => {foo => 42}}); 17 | 18 | is $db->query('select json_type(j) from mojo_json_test')->array->[0], 'OBJECT', 'json_type'; 19 | is $db->query('select json_extract(j, "$.foo") from mojo_json_test')->array->[0], '42', 'json_extract'; 20 | is_deeply $db->query('select id, name, j from mojo_json_test where json_extract(j, "$.foo") = 42')->expand->hash, 21 | {id => $$, name => $0, j => {foo => 42}}, 'expand json'; 22 | 23 | my $value_hash = {nick => 'supergirl'}; 24 | my $value_json = Mojo::JSON::to_json($value_hash); 25 | my $query = 'select name from mojo_json_test where name like "%supergirl%"'; 26 | $db->query('insert into mojo_json_test (name) values (?)', {json => {nick => 'supergirl'}}); 27 | 28 | is_deeply $db->query($query)->expand->hash, {name => $value_json}, 'hash: name as string'; 29 | is_deeply $db->query($query)->expand(1)->hash, {name => $value_hash}, 'hash: name as hash'; 30 | is_deeply $db->query($query)->expand->hashes, [{name => $value_json}], 'hashes: name as string'; 31 | is_deeply $db->query($query)->expand(1)->hashes, [{name => $value_hash}], 'hashes: name as hash'; 32 | is_deeply $db->query($query)->expand->array, [$value_json], 'array: name as string'; 33 | is_deeply $db->query($query)->expand(1)->array, [$value_hash], 'array: name as hash'; 34 | is_deeply $db->query($query)->expand->arrays, [[$value_json]], 'arrays: name as string'; 35 | is_deeply $db->query($query)->expand(1)->arrays, [[$value_hash]], 'arrays: name as hash'; 36 | 37 | $db->query('insert into mojo_json_test (name) values (?)', undef); 38 | is_deeply $db->query('select name from mojo_json_test where name is null')->expand->hash->{name}, undef, 'name is null'; 39 | is_deeply $db->query('select name from mojo_json_test where name is null')->expand(0)->hash->{name}, undef, 40 | 'name is null'; 41 | is_deeply $db->query('select name from mojo_json_test where name is null')->expand(1)->hash->{name}, undef, 42 | 'name is null'; 43 | is_deeply $db->query('select name from mojo_json_test where name is null')->expand->array->[0], undef, 'name is null'; 44 | is_deeply $db->query('select name from mojo_json_test where name is null')->expand(0)->array->[0], undef, 45 | 'name is null'; 46 | is_deeply $db->query('select name from mojo_json_test where name is null')->expand(1)->array->[0], undef, 47 | 'name is null'; 48 | 49 | $db->query('drop table mojo_json_test') unless $ENV{TEST_KEEP_DB}; 50 | 51 | done_testing; 52 | -------------------------------------------------------------------------------- /t/from-string.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | use Mojo::mysql; 3 | use Test::More; 4 | 5 | my %options = (AutoCommit => 1, AutoInactiveDestroy => 1, PrintError => 0, RaiseError => 1); 6 | 7 | for my $engine (qw(MariaDB mysql)) { 8 | for my $class (qw(str Mojo::URL URI::db)) { 9 | subtest "$engine - $class" => sub { 10 | plan skip_all => 'URI::db not installed' unless Mojo::mysql::URI; 11 | my $str = lc "$engine://user:pass\@localhost:42/test?a=b"; 12 | my $url = $class eq 'str' ? $str : $class->new($str); 13 | my $mysql = Mojo::mysql->new($url); 14 | is $mysql->dsn, "dbi:${engine}:dbname=test;host=localhost;port=42", 'dsn'; 15 | is $mysql->username, 'user', 'username'; 16 | is $mysql->password, 'pass', 'password'; 17 | local $options{mysql_enable_utf8} = 1 if $engine eq 'mysql'; 18 | is_deeply $mysql->options, {%options, a => 'b'}, 'options'; 19 | }; 20 | } 21 | 22 | subtest "$engine - ssl" => sub { 23 | my $str = lc 24 | "$engine://localhost:42/test?mysql_ssl=1&mysql_ssl_verify=1&mysql_ssl_verify_server_cert=1&mysql_ssl_client_key=key.pem&mysql_ssl_client_cert=crt.pem&mysql_ssl_ca_file=ca.pem"; 25 | my $mysql = Mojo::mysql->new($str); 26 | is $mysql->dsn, "dbi:${engine}:dbname=test;host=localhost;port=42", 'dsn'; 27 | is $mysql->username, '', 'username'; 28 | is $mysql->password, '', 'password'; 29 | local $options{mysql_enable_utf8} = 1 if $engine eq 'mysql'; 30 | is_deeply $mysql->options, 31 | { 32 | %options, 33 | mysql_ssl => 1, 34 | mysql_ssl_ca_file => 'ca.pem', 35 | mysql_ssl_client_cert => 'crt.pem', 36 | mysql_ssl_client_key => 'key.pem', 37 | mysql_ssl_verify => 1, 38 | mysql_ssl_verify_server_cert => 1, 39 | }, 40 | 'options'; 41 | }; 42 | } 43 | 44 | subtest 'MariaDB 1.21 is required' => sub { 45 | plan skip_all => 'MariaDB is installed' if Mojo::mysql::MARIADB; 46 | my $mysql = Mojo::mysql->new('mariadb://localhost:42/test'); 47 | eval { $mysql->_dequeue }; 48 | like $@, qr/DBD::MariaDB.*is required/, 'error'; 49 | }; 50 | 51 | subtest 'username and password' => sub { 52 | my $mysql = Mojo::mysql->new('mysql://@localhost'); 53 | is $mysql->username, '', 'empty username'; 54 | is $mysql->password, '', 'empty password'; 55 | 56 | $mysql = Mojo::mysql->new('mysql://simple:case@localhost'); 57 | is $mysql->username, 'simple', 'simple username'; 58 | is $mysql->password, 'case', 'case password'; 59 | 60 | $mysql = Mojo::mysql->new('mysql://only@localhost'); 61 | is $mysql->username, 'only', 'only username'; 62 | is $mysql->password, '', 'empty password'; 63 | 64 | $mysql = Mojo::mysql->new('mysql://colon:@localhost'); 65 | is $mysql->username, 'colon', 'colon username'; 66 | is $mysql->password, '', 'empty password'; 67 | 68 | $mysql = Mojo::mysql->new('mysql://user:and:password@localhost'); 69 | is $mysql->username, 'user', 'user username'; 70 | is $mysql->password, 'and:password', 'and:password password'; 71 | }; 72 | 73 | done_testing; 74 | -------------------------------------------------------------------------------- /t/test-dbi-async.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Mojo::Base -strict; 3 | 4 | use DBI; 5 | use Mojo::IOLoop; 6 | use Test::More; 7 | 8 | # This is not a real test for Mojo::mysql, but it's a test to see if I have 9 | # understood how async works. 10 | # - jhthorsen 11 | 12 | plan skip_all => 'TEST_DBI_ASYNC=1' unless $ENV{TEST_DBI_ASYNC}; 13 | 14 | my ($dbh, $dsn, $rv); 15 | 16 | # Check if there's a difference between the MySQL and MariaDB driver 17 | for (qw(dbi:mysql: dbi:MariaDB:)) { 18 | $dsn = $_; 19 | 20 | note "$dsn connect"; 21 | $dbh = DBI->connect($dsn, 'root', undef, {PrintError => 0, PrintWarn => 1, RaiseError => 1}); 22 | 23 | note "$dsn should not fail, since the driver is not yet in async mode"; 24 | test_sync_select(40); 25 | 26 | note "$dsn should not fail, since the sync request is done"; 27 | my @sth = ($dbh->prepare('SELECT SLEEP(1), 11', $dsn =~ /MariaDB/ ? {mariadb_async => 1} : {async => 1})); 28 | $sth[0]->execute; 29 | 30 | note "$dsn fails with: We cannot switch to blocking, when async is in process"; 31 | test_sync_select(41); 32 | 33 | my $fd_method = $dsn =~ /MariaDB/ ? 'mariadb_sockfd' : 'mysql_fd'; 34 | my $ready_method = $dsn =~ /MariaDB/ ? 'mariadb_async_ready' : 'mysql_async_ready'; 35 | my $result_method = $dsn =~ /MariaDB/ ? 'mariadb_async_result' : 'mysql_async_result'; 36 | 37 | open my $fd, '<&', $dbh->$fd_method or die "Dup mariadb_sockfd: $!"; 38 | Mojo::IOLoop->singleton->reactor->io( 39 | $fd => sub { 40 | return unless $sth[-1]->$ready_method; 41 | 42 | # DBD::mysql::st mysql_async_result failed: Gathering async_query_in_flight results for the wrong handle 43 | # $sth[0]->$result_method; 44 | 45 | $rv = do { local $sth[-1]->{RaiseError} = 0; $sth[-1]->$result_method; }; 46 | return Mojo::IOLoop->stop if @sth == 2; 47 | 48 | note "$dsn need to prepare/execute after the first is ready"; 49 | push @sth, $dbh->prepare('SELECT SLEEP(1), 22', $dsn =~ /MariaDB/ ? {mariadb_async => 1} : {async => 1}); 50 | $sth[1]->execute; 51 | } 52 | )->watch($fd, 1, 0); 53 | 54 | Mojo::IOLoop->start; 55 | 56 | note "$dsn sync works as long as the async is done"; 57 | test_sync_select(42); 58 | 59 | note "$dsn async fetchrow_arrayref+finish order does not matter"; 60 | is_deeply $sth[1]->fetchrow_arrayref, [0, 22], "$dsn SELECT SLEEP(1), 22"; 61 | ok eval { $sth[1]->finish; 1 }, 'finish is also successful' or diag "$dsn: $@"; 62 | 63 | note "$dsn async fetchrow_arrayref works afterwards"; 64 | is_deeply $sth[0]->fetchrow_arrayref, [0, 11], "$dsn SELECT SLEEP(1), 11"; 65 | ok eval { $sth[0]->finish; 1 }, 'finish is also successful' or diag "$dsn: $@"; 66 | 67 | test_sync_select(42); 68 | 69 | note "$dsn need to clean up the sth before dbh"; 70 | @sth = (); 71 | undef $dbh; 72 | } 73 | 74 | done_testing; 75 | 76 | sub test_sync_select { 77 | my $num = shift; 78 | eval { 79 | my $sth_sync = $dbh->prepare("SELECT $num as num"); 80 | $sth_sync->execute; 81 | is $sth_sync->fetchrow_arrayref->[0], $num, "$dsn SELECT $num as num"; 82 | 1; 83 | } or do { 84 | if ($num eq '41') { 85 | like $@, qr{Calling a synchronous function on an asynchronous handle}, "$dsn cannot switch from async to sync"; 86 | } 87 | else { 88 | is $@, $num, "$dsn SELECT $num as num"; 89 | } 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /t/results.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | use Mojo::mysql; 3 | use Test::More; 4 | 5 | plan skip_all => 'TEST_ONLINE=mysql://root@/test' unless $ENV{TEST_ONLINE}; 6 | 7 | my $mysql = Mojo::mysql->new($ENV{TEST_ONLINE}); 8 | my $db = $mysql->db; 9 | $db->query( 10 | 'create table if not exists results_test ( 11 | id serial primary key, 12 | name text 13 | )' 14 | ); 15 | $db->query('insert into results_test (name) values (?)', $_) for qw(foo bar); 16 | 17 | note 'Result methods'; 18 | is_deeply $db->query('select * from results_test')->rows, 2, 'two rows'; 19 | is_deeply $db->query('select * from results_test')->columns, ['id', 'name'], 'right structure'; 20 | is_deeply $db->query('select * from results_test')->array, [1, 'foo'], 'right structure'; 21 | is_deeply $db->query('select * from results_test')->arrays->to_array, [[1, 'foo'], [2, 'bar']], 'right structure'; 22 | is_deeply $db->query('select * from results_test')->hash, {id => 1, name => 'foo'}, 'right structure'; 23 | is_deeply $db->query('select * from results_test')->hashes->to_array, 24 | [{id => 1, name => 'foo'}, {id => 2, name => 'bar'}], 'right structure'; 25 | is $mysql->db->query('select * from results_test')->text, "1 foo\n2 bar\n", 'right text'; 26 | 27 | note 'Iterate'; 28 | my $results = $db->query('select * from results_test'); 29 | is_deeply $results->array, [1, 'foo'], 'right structure'; 30 | is_deeply $results->array, [2, 'bar'], 'right structure'; 31 | is $results->array, undef, 'no more results'; 32 | 33 | note 'Non-blocking query where not all results have been fetched'; 34 | my ($fail, $result); 35 | $db->query_p('select name from results_test')->then(sub { 36 | push @$result, shift->array; 37 | $results->finish; 38 | return $db->query_p('select name from results_test'); 39 | })->then(sub { 40 | push @$result, shift->array; 41 | $results->finish; 42 | return $db->query_p('select name from results_test'); 43 | })->then(sub { 44 | push @$result, shift->array; 45 | })->catch(sub { $fail = shift })->wait; 46 | ok !$fail, 'no error'; 47 | is_deeply $result, [['foo'], ['foo'], ['foo']], 'right structure'; 48 | 49 | note 'Transactions'; 50 | { 51 | my $tx = $db->begin; 52 | $db->query("insert into results_test (name) values ('tx1')"); 53 | $db->query("insert into results_test (name) values ('tx1')"); 54 | $tx->commit; 55 | }; 56 | is_deeply $db->query('select * from results_test where name = ?', 'tx1')->hashes->to_array, 57 | [{id => 3, name => 'tx1'}, {id => 4, name => 'tx1'}], 'right structure'; 58 | { 59 | my $tx = $db->begin; 60 | $db->query("insert into results_test (name) values ('tx2')"); 61 | $db->query("insert into results_test (name) values ('tx2')"); 62 | }; 63 | is_deeply $db->query('select * from results_test where name = ?', 'tx2')->hashes->to_array, [], 'no results'; 64 | eval { 65 | my $tx = $db->begin; 66 | $db->query("insert into results_test (name) values ('tx3')"); 67 | $db->query("insert into results_test (name) values ('tx3')"); 68 | $db->query('does_not_exist'); 69 | $tx->commit; 70 | }; 71 | like $@, qr/does_not_exist/, 'right error'; 72 | is_deeply $db->query('select * from results_test where name = ?', 'tx3')->hashes->to_array, [], 'no results'; 73 | 74 | { 75 | my $n_rows = -1; 76 | my $tx = $db->begin; 77 | $fail = 'no error'; 78 | $db->query_p("insert into results_test (name) values ('txc')")->then(sub { 79 | undef $tx; 80 | return $db->query_p("select name from results_test where name = 'txc'"); 81 | })->then(sub { 82 | $n_rows = shift->arrays->size; 83 | })->catch(sub { $fail = shift })->wait; 84 | 85 | is $n_rows, 0, 'async rollback works - nothing inserted'; 86 | is $fail, 'no error', 'async rollback works - no error'; 87 | } 88 | 89 | $db->query('drop table results_test'); 90 | 91 | done_testing; 92 | -------------------------------------------------------------------------------- /t/crud.t: -------------------------------------------------------------------------------- 1 | BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } 2 | use Mojo::Base -strict; 3 | use Test::More; 4 | use Mojo::IOLoop; 5 | use Mojo::Promise; 6 | use Mojo::mysql; 7 | 8 | plan skip_all => 'TEST_ONLINE=mysql://root@/test' unless $ENV{TEST_ONLINE}; 9 | 10 | my $mysql = Mojo::mysql->new($ENV{TEST_ONLINE}); 11 | my $db = $mysql->db; 12 | 13 | ok $db->ping, 'connected'; 14 | 15 | $db->query( 16 | 'create table if not exists crud_test ( 17 | id serial primary key, 18 | name text 19 | )' 20 | ); 21 | 22 | note 'Create'; 23 | $db->insert('crud_test', {name => 'foo'}); 24 | is_deeply $db->select('crud_test')->hashes->to_array, [{id => 1, name => 'foo'}], 'right structure'; 25 | is $db->insert('crud_test', {name => 'bar'})->_sth_attr('mysql_insertid'), 2, 'right value'; 26 | is_deeply $db->select('crud_test')->hashes->to_array, [{id => 1, name => 'foo'}, {id => 2, name => 'bar'}], 27 | 'right structure'; 28 | 29 | note 'Read'; 30 | is_deeply $db->select('crud_test')->hashes->to_array, [{id => 1, name => 'foo'}, {id => 2, name => 'bar'}], 31 | 'right structure'; 32 | is_deeply $db->select('crud_test', ['name'])->hashes->to_array, [{name => 'foo'}, {name => 'bar'}], 'right structure'; 33 | is_deeply $db->select('crud_test', ['name'], {name => 'foo'})->hashes->to_array, [{name => 'foo'}], 'right structure'; 34 | is_deeply $db->select('crud_test', ['name'], undef, {-desc => 'id'})->hashes->to_array, 35 | [{name => 'bar'}, {name => 'foo'}], 'right structure'; 36 | 37 | note 'Non-blocking read'; 38 | my $result; 39 | $db->select_p('crud_test')->then(sub { $result = pop->hashes->to_array })->wait; 40 | is_deeply $result, [{id => 1, name => 'foo'}, {id => 2, name => 'bar'}], 'right structure'; 41 | $result = undef; 42 | Mojo::Promise->timer(0.1 => sub { $result = pop->hashes->to_array })->wait; 43 | 44 | note 'Update'; 45 | $db->update('crud_test', {name => 'baz'}, {name => 'foo'}); 46 | is_deeply $db->select('crud_test', undef, undef, {-asc => 'id'})->hashes->to_array, 47 | [{id => 1, name => 'baz'}, {id => 2, name => 'bar'}], 'right structure'; 48 | 49 | note 'Delete'; 50 | $db->delete('crud_test', {name => 'baz'}); 51 | is_deeply $db->select('crud_test', undef, undef, {-asc => 'id'})->hashes->to_array, [{id => 2, name => 'bar'}], 52 | 'right structure'; 53 | $db->delete('crud_test'); 54 | is_deeply $db->select('crud_test')->hashes->to_array, [], 'right structure'; 55 | 56 | note 'Promises'; 57 | $result = undef; 58 | my $curid = undef; 59 | $db->insert_p('crud_test', {name => 'promise'})->then(sub { $result = shift->last_insert_id })->wait; 60 | is $result, 3, 'right result'; 61 | $curid = $result; 62 | $result = undef; 63 | $db->select_p('crud_test', ['id', 'name'], {name => 'promise'})->then(sub { $result = shift->hash })->wait; 64 | is_deeply $result, {name => 'promise', id => $curid}, 'right result'; 65 | $result = undef; 66 | my $first = $db->query_p("select * from crud_test where name = 'promise'"); 67 | my $second = $db->query_p("update crud_test set name = 'another_promise' where name = 'promise'"); 68 | my $third = $db->select_p('crud_test', '*', {id => 3}); 69 | Mojo::Promise->all($first, $second, $third)->then(sub { 70 | my ($first, $second, $third) = @_; 71 | $result = [$first->[0]->hash, $second->[0]->affected_rows, $third->[0]->hash]; 72 | })->wait; 73 | is $result->[0]{name}, 'promise', 'right result'; 74 | is $result->[1], 1, 'right result'; 75 | is $result->[2]{name}, 'another_promise', 'right result'; 76 | $result = undef; 77 | $db->update_p('crud_test', {name => 'promise_two'}, {name => 'another_promise'},) 78 | ->then(sub { $result = shift->affected_rows }) 79 | ->wait; 80 | is $result, 1, 'right result'; 81 | $db->delete_p('crud_test', {name => 'promise_two'})->then(sub { $result = shift->affected_rows })->wait; 82 | is $result, 1, 'right result'; 83 | 84 | note 'Promises (rejected)'; 85 | my $fail; 86 | $db->query_p('does_not_exist')->catch(sub { $fail = shift })->wait; 87 | like $fail, qr/does_not_exist/, 'right error'; 88 | 89 | note 'cleanup'; 90 | END { $db and $db->query('drop table if exists crud_test'); } 91 | 92 | done_testing; 93 | -------------------------------------------------------------------------------- /t/connection.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | use Test::More; 3 | use Mojo::mysql; 4 | use Mojo::Util 'url_escape'; 5 | 6 | note 'Defaults'; 7 | my $mysql = Mojo::mysql->new; 8 | is $mysql->dsn, 'dbi:mysql:dbname=test', 'right data source'; 9 | is $mysql->username, '', 'no username'; 10 | is $mysql->password, '', 'no password'; 11 | my $options = {mysql_enable_utf8 => 1, AutoCommit => 1, AutoInactiveDestroy => 1, PrintError => 0, RaiseError => 1}; 12 | is_deeply $mysql->options, $options, 'right options'; 13 | 14 | note 'Without database name'; 15 | $mysql = Mojo::mysql->new('mysql://root@'); 16 | is $mysql->dsn, 'dbi:mysql', 'right data source'; 17 | 18 | note 'Minimal connection string with database'; 19 | $mysql = Mojo::mysql->new('mysql:///test1'); 20 | is $mysql->dsn, 'dbi:mysql:dbname=test1', 'right data source'; 21 | is $mysql->username, '', 'no username'; 22 | is $mysql->password, '', 'no password'; 23 | $options = {mysql_enable_utf8 => 1, AutoCommit => 1, AutoInactiveDestroy => 1, PrintError => 0, RaiseError => 1}; 24 | is_deeply $mysql->options, $options, 'right options'; 25 | 26 | note 'Connection string with host and port'; 27 | $mysql = Mojo::mysql->new('mysql://127.0.0.1:8080/test2'); 28 | is $mysql->dsn, 'dbi:mysql:dbname=test2;host=127.0.0.1;port=8080', 'right data source'; 29 | is $mysql->username, '', 'no username'; 30 | is $mysql->password, '', 'no password'; 31 | $options = {mysql_enable_utf8 => 1, AutoCommit => 1, AutoInactiveDestroy => 1, PrintError => 0, RaiseError => 1}; 32 | is_deeply $mysql->options, $options, 'right options'; 33 | 34 | note 'Connection string username but without host'; 35 | $mysql = Mojo::mysql->new('mysql://mysql@/test3'); 36 | is $mysql->dsn, 'dbi:mysql:dbname=test3', 'right data source'; 37 | is $mysql->username, 'mysql', 'right username'; 38 | is $mysql->password, '', 'no password'; 39 | $options = {mysql_enable_utf8 => 1, AutoCommit => 1, AutoInactiveDestroy => 1, PrintError => 0, RaiseError => 1}; 40 | is_deeply $mysql->options, $options, 'right options'; 41 | 42 | note 'Connection string with unix domain socket and options'; 43 | my $dummy_socket = File::Spec->rel2abs(__FILE__); 44 | $mysql = Mojo::mysql->new("mysql://x1:y2\@@{[url_escape $dummy_socket]}/test4?PrintError=1&RaiseError=0"); 45 | is $mysql->dsn, "dbi:mysql:dbname=test4;mysql_socket=$dummy_socket", 'right data source'; 46 | is $mysql->username, 'x1', 'right username'; 47 | is $mysql->password, 'y2', 'right password'; 48 | $options = {mysql_enable_utf8 => 1, AutoCommit => 1, AutoInactiveDestroy => 1, PrintError => 1, RaiseError => 0}; 49 | is_deeply $mysql->options, $options, 'right options'; 50 | 51 | note 'Mojo::URL object with credentials'; 52 | my $url_obj = Mojo::URL->new('mysql://x2:y3@/test5?PrintError=1'); 53 | $mysql = Mojo::mysql->new($url_obj); 54 | is $mysql->dsn, 'dbi:mysql:dbname=test5', 'right data source with Mojo::URL object'; 55 | is $mysql->username, 'x2', 'right username'; 56 | is $mysql->password, 'y3', 'right password'; 57 | $options = {mysql_enable_utf8 => 1, AutoCommit => 1, AutoInactiveDestroy => 1, PrintError => 1, RaiseError => 1}; 58 | is_deeply $mysql->options, $options, 'right options'; 59 | 60 | note 'Connection string with lots of zeros'; 61 | $mysql = Mojo::mysql->new('mysql://0:0@/0?RaiseError=0'); 62 | is $mysql->dsn, 'dbi:mysql:dbname=0', 'right data source'; 63 | is $mysql->username, '0', 'right username'; 64 | is $mysql->password, '0', 'right password'; 65 | $options = {mysql_enable_utf8 => 1, AutoCommit => 1, AutoInactiveDestroy => 1, PrintError => 0, RaiseError => 0}; 66 | is_deeply $mysql->options, $options, 'right options'; 67 | 68 | note 'Invalid connection string'; 69 | eval { Mojo::mysql->new('http://localhost:3000/test') }; 70 | like $@, qr{Invalid MySQL/MariaDB connection string}, 'right error'; 71 | 72 | note 'Quote fieldnames correctly'; 73 | like $mysql->abstract->select("foo", ['binary']), qr{`binary}, 'quoted correct binary'; 74 | like $mysql->abstract->select("foo", ['foo.binary']), qr{`foo`.`binary}, 'quoted correct foo.binary'; 75 | 76 | $mysql = Mojo::mysql->new(dsn => 'dbi:mysql:mysql_read_default_file=~/.cpanstats.cnf'); 77 | is $mysql->dsn, 'dbi:mysql:mysql_read_default_file=~/.cpanstats.cnf', 'correct dsn'; 78 | 79 | $mysql = Mojo::mysql->new({dsn => 'dbi:mysql:mysql_read_default_file=~/.cpanstats.cnf'}); 80 | is $mysql->dsn, 'dbi:mysql:mysql_read_default_file=~/.cpanstats.cnf', 'correct dsn'; 81 | 82 | done_testing; 83 | -------------------------------------------------------------------------------- /t/database.t: -------------------------------------------------------------------------------- 1 | BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } 2 | use Mojo::Base -strict; 3 | use Test::More; 4 | use DBI ':sql_types'; 5 | use Mojo::IOLoop; 6 | use Mojo::mysql; 7 | 8 | plan skip_all => 'TEST_ONLINE=mysql://root@/test' unless $ENV{TEST_ONLINE}; 9 | 10 | my $mysql = Mojo::mysql->new($ENV{TEST_ONLINE}); 11 | ok $mysql->db->ping, 'connected'; 12 | 13 | note 'Blocking select'; 14 | is_deeply $mysql->db->query('select 1 as one, 2 as two, 3 as three')->hash, {one => 1, two => 2, three => 3}, 15 | 'right structure'; 16 | 17 | note 'Non-blocking select'; 18 | my ($err, $res); 19 | my $db = $mysql->db; 20 | is $db->backlog, 0, 'no operations waiting'; 21 | $db->query('select 1 as one, 2 as two, 3 as three' => sub { ($err, $res) = ($_[1], $_[2]->hash); Mojo::IOLoop->stop; }); 22 | is $db->backlog, 1, 'one operation waiting'; 23 | Mojo::IOLoop->start; 24 | is $db->backlog, 0, 'no operations waiting'; 25 | ok !$err, 'no error' or diag "err=$err"; 26 | is_deeply $res, {one => 1, two => 2, three => 3}, 'right structure'; 27 | 28 | note 'Concurrent non-blocking selects'; 29 | ($err, $res) = (); 30 | $db = $mysql->db; 31 | Mojo::Promise->all($db->query_p('select 1 as one'), $db->query_p('select 2 as two'), $db->query_p('select 2 as two')) 32 | ->then(sub { 33 | $res = [map { $_->[0]->hashes->first } @_]; 34 | })->catch(sub { $err = shift })->wait; 35 | ok !$err, 'no error' or diag "err=$err"; 36 | is_deeply $res, [{one => 1}, {two => 2}, {two => 2}], 'concurrent non-blocking selects' or diag explain $res; 37 | 38 | note 'Sequential and Concurrent non-blocking selects'; 39 | ($err, $res) = (0, []); 40 | $db->query_p('select 1 as one')->then(sub { 41 | push @$res, shift->hashes->first; 42 | return Mojo::Promise->all($db->query_p('select 2 as two'), $db->query_p('select 2 as two')); 43 | })->then(sub { 44 | push @$res, $db->query('select 1 as one')->hashes->first; 45 | push @$res, map { $_->[0]->hashes->first } @_; 46 | return $db->query_p('select 3 as three'); 47 | })->then(sub { 48 | push @$res, shift->hashes->first; 49 | })->catch(sub { $err = shift })->wait; 50 | 51 | ok !$err, 'no error' or diag "err=$err"; 52 | is_deeply $res, [{one => 1}, {one => 1}, {two => 2}, {two => 2}, {three => 3}], 'right structure'; 53 | 54 | note 'Connection cache'; 55 | is $mysql->max_connections, 5, 'right default'; 56 | my @pids = sort map { $_->pid } $mysql->db, $mysql->db, $mysql->db, $mysql->db, $mysql->db; 57 | is_deeply \@pids, [sort map { $_->pid } $mysql->db, $mysql->db, $mysql->db, $mysql->db, $mysql->db], 58 | 'same database pids'; 59 | my $pid = $mysql->max_connections(1)->db->pid; 60 | is $mysql->db->pid, $pid, 'same database pid'; 61 | isnt $mysql->db->pid, $mysql->db->pid, 'different database pids'; 62 | is $mysql->db->pid, $pid, 'different database pid'; 63 | $pid = $mysql->db->pid; 64 | is $mysql->db->pid, $pid, 'same database pid'; 65 | $mysql->db->disconnect; 66 | isnt $mysql->db->pid, $pid, 'different database pid'; 67 | my $dbh = $mysql->db->dbh; 68 | is $mysql->db->dbh, $dbh, 'same database handle'; 69 | isnt $mysql->close_idle_connections->db->dbh, $dbh, 'different database handles'; 70 | 71 | note 'Binary data'; 72 | $db = $mysql->db; 73 | my $bytes = "\xF0\xF1\xF2\xF3"; 74 | is_deeply $db->query('select binary ? as foo', {type => SQL_BLOB, value => $bytes})->hash, {foo => $bytes}, 75 | 'right data'; 76 | 77 | note 'Fork safety'; 78 | $pid = $mysql->db->pid; 79 | { 80 | local $$ = -23; 81 | isnt $mysql->db->pid, $pid, 'different database handles'; 82 | }; 83 | 84 | note 'Blocking error'; 85 | eval { $mysql->db->query('does_not_exist') }; 86 | like $@, qr/does_not_exist/, 'does_not_exist sync'; 87 | 88 | note 'Non-blocking error'; 89 | ($err, $res) = (); 90 | $mysql->db->query('does_not_exist' => sub { ($err, $res) = @_[1, 2]; Mojo::IOLoop->stop; }); 91 | Mojo::IOLoop->start; 92 | like $err, qr/does_not_exist/, 'does_not_exist async'; 93 | 94 | note 'Clean up non-blocking queries'; 95 | ($err, $res) = (); 96 | $db = $mysql->db; 97 | $db->query('select 1' => sub { ($err, $res) = @_[1, 2] }); 98 | $db->disconnect; 99 | undef $db; 100 | is $err, 'Premature connection close', 'Premature connection close'; 101 | 102 | note 'Error context'; 103 | ($err, $res) = (); 104 | eval { $mysql->db->query('select * from table_does_not_exist') }; 105 | like $@, qr/database\.t line/, 'error context blocking'; 106 | $mysql->db->query( 107 | 'select * from table_does_not_exist', 108 | sub { 109 | (my $db, $err, $res) = @_; 110 | Mojo::IOLoop->stop; 111 | } 112 | ); 113 | 114 | Mojo::IOLoop->start; 115 | like $err, qr/database\.t line/, 'error context non-blocking'; 116 | 117 | note 'Avoid Gathering async_query_in_flight warning'; 118 | $mysql->max_connections(20); 119 | $db = $mysql->db; 120 | my @results = ($db->query('select 1')); 121 | my @warnings; 122 | local $SIG{__WARN__} = sub { push @warnings, $_[0] =~ /DBD::mysql/ ? Carp::longmess($_[0]) : $_[0] }; 123 | my $cb = sub { 124 | push @results, pop; 125 | Mojo::IOLoop->stop if @results == 3; 126 | }; 127 | $db->query('select 2', $cb); 128 | $db->query('select 3', sub { }); # Results in "Gathering async_query_in_flight results for the wrong handle" warning 129 | $db->query('select 4', $cb); 130 | Mojo::IOLoop->start; 131 | delete $SIG{__WARN__}; 132 | 133 | is_deeply [map { $_->array->[0] } reverse @results], [4, 2, 1], 'select 1, 2, 4'; 134 | is join('', @warnings), '', 'no warnings'; 135 | 136 | note 'Avoid "Calling a synchronous function on an asynchronous handle"'; 137 | my $p = $db->query_p('select 11'); 138 | eval { $db->query('select 22') }; 139 | like $@, qr{Cannot perform blocking query, while waiting for async response}, 140 | 'Cannot perform blocking and non-blocking at the same time'; 141 | 142 | done_testing; 143 | -------------------------------------------------------------------------------- /t/migrations.t: -------------------------------------------------------------------------------- 1 | BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' } 2 | use File::Spec::Functions 'catfile'; 3 | use FindBin; 4 | use Mojo::Base -strict; 5 | use Mojo::mysql; 6 | use Test::More; 7 | 8 | plan skip_all => 'TEST_ONLINE=mysql://root@/test' unless $ENV{TEST_ONLINE}; 9 | 10 | note 'Clean up before start'; 11 | my $mysql = Mojo::mysql->new($ENV{TEST_ONLINE}); 12 | $mysql->db->query('drop table if exists mojo_migrations'); 13 | 14 | note 'Defaults'; 15 | is $mysql->migrations->name, 'migrations', 'right name'; 16 | is $mysql->migrations->latest, 0, 'latest version is 0'; 17 | is $mysql->migrations->active, 0, 'active version is 0'; 18 | 19 | note 'Create migrations table'; 20 | ok !(grep {/mojo_migrations/} @{$mysql->db->tables}), 'migrations table does not exist'; 21 | 22 | is $mysql->migrations->migrate->active, 0, 'active version is 0'; 23 | ok !!(grep {/mojo_migrations/} @{$mysql->db->tables}), 'migrations table exists'; 24 | 25 | note 'Migrations from DATA section'; 26 | is $mysql->migrations->from_data->latest, 0, 'latest version is 0'; 27 | is $mysql->migrations->from_data(__PACKAGE__)->latest, 0, 'latest version is 0'; 28 | is $mysql->migrations->name('test1')->from_data->latest, 10, 'latest version is 10'; 29 | is $mysql->migrations->name('test2')->from_data->latest, 2, 'latest version is 2'; 30 | is $mysql->migrations->name('migrations')->from_data(__PACKAGE__, 'test1')->latest, 10, 'latest version is 10'; 31 | is $mysql->migrations->name('test2')->from_data(__PACKAGE__)->latest, 2, 'latest version is 2'; 32 | 33 | note 'Different syntax variations'; 34 | $mysql->migrations->name('migrations_test')->from_string(<migrations->latest, 10, 'latest version is 10'; 62 | is $mysql->migrations->active, 0, 'active version is 0'; 63 | is $mysql->migrations->migrate->active, 10, 'active version is 10'; 64 | is_deeply $mysql->db->query('select * from migration_test_one')->hash, {foo => 'works ♥'}, 'right structure'; 65 | is $mysql->migrations->migrate->active, 10, 'active version is 10'; 66 | is $mysql->migrations->migrate(1)->active, 1, 'active version is 1'; 67 | is $mysql->db->query('select * from migration_test_one')->hash, undef, 'no result'; 68 | is $mysql->migrations->migrate(3)->active, 3, 'active version is 3'; 69 | is $mysql->db->query('select * from migration_test_two')->hash, undef, 'no result'; 70 | is $mysql->migrations->migrate->active, 10, 'active version is 10'; 71 | is_deeply $mysql->db->query('select * from migration_test_two')->hash, {bar => 'works too'}, 'right structure'; 72 | is $mysql->migrations->migrate(0)->active, 0, 'active version is 0'; 73 | 74 | note 'Bad and concurrent migrations'; 75 | my $mysql2 = Mojo::mysql->new($ENV{TEST_ONLINE}); 76 | $mysql2->migrations->name('migrations_test2')->from_file(catfile($FindBin::Bin, 'migrations', 'test.sql')); 77 | is $mysql2->migrations->latest, 4, 'latest version is 4'; 78 | is $mysql2->migrations->active, 0, 'active version is 0'; 79 | eval { $mysql2->migrations->migrate }; 80 | like $@, qr/does_not_exist/, 'right error'; 81 | is $mysql2->migrations->migrate(3)->active, 3, 'active version is 3'; 82 | is $mysql2->migrations->migrate(2)->active, 2, 'active version is 3'; 83 | is $mysql->migrations->active, 0, 'active version is still 0'; 84 | is $mysql->migrations->migrate->active, 10, 'active version is 10'; 85 | is_deeply $mysql2->db->query('select * from migration_test_three')->hashes->to_array, 86 | [{baz => 'just'}, {baz => 'works ♥'}], 'right structure'; 87 | is $mysql->migrations->migrate(0)->active, 0, 'active version is 0'; 88 | is $mysql2->migrations->migrate(0)->active, 0, 'active version is 0'; 89 | 90 | note 'Migrate automatically'; 91 | $mysql = Mojo::mysql->new($ENV{TEST_ONLINE}); 92 | $mysql->migrations->name('migrations_test')->from_string(<auto_migrate(1)->db; 103 | is $mysql->migrations->active, 6, 'active version is 6'; 104 | is_deeply $mysql->db->query('select * from migration_test_six')->hashes, [{foo => 'works!'}], 'right structure'; 105 | is $mysql->migrations->migrate(5)->active, 5, 'active version is 5'; 106 | is_deeply $mysql->db->query('select * from migration_test_six')->hashes, [], 'right structure'; 107 | is $mysql->migrations->migrate(0)->active, 0, 'active version is 0'; 108 | 109 | note 'Unknown version'; 110 | eval { $mysql->migrations->migrate(23) }; 111 | like $@, qr/Version 23 has no migration/, 'right error'; 112 | 113 | note 'Version mismatch'; 114 | my $newer = <migrations->name('migrations_test3')->from_string($newer); 122 | is $mysql->migrations->migrate->active, 2, 'active version is 2'; 123 | 124 | $mysql->migrations->from_string(<migrations->migrate }; 130 | like $@, qr/Active version 2 is greater than the latest version 1/, 'right error'; 131 | eval { $mysql->migrations->migrate(0) }; 132 | like $@, qr/Active version 2 is greater than the latest version 1/, 'right error'; 133 | is $mysql->migrations->from_string($newer)->migrate(0)->active, 0, 'active version is 0'; 134 | 135 | done_testing; 136 | 137 | __DATA__ 138 | @@ test1 139 | -- 7 up 140 | create table migration_test_four (test int)); 141 | 142 | -- 10 up 143 | insert into migration_test_four values (10); 144 | 145 | @@ test2 146 | -- 2 up 147 | create table migration_test_five (test int); 148 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | Revision history for perl distribution Mojo-mysql 2 | 3 | 1.28 2025-10-30T15:07:00 4 | - Fix only specifying username in userinfo url part 5 | 6 | 1.27 2023-10-26T20:59:16 7 | - Fix not cleaning up during GLOBAL_PHASE is DESTRUCT 8 | Contributor: Lasse Løvik 9 | 10 | 1.26 2022-12-09T10:41:01 11 | - Add support for "MOJO_MYSQL_PREFER_DRIVER" in Makefile.PL 12 | - Add support for autodetecting DBD::MariaDB 13 | - Fix spelling mistakes #88 14 | Contributor: Lucas Tiago de Moraes 15 | - Specified Perl version 16 | - Updated basic repository files 17 | - Updated contributors list 18 | 19 | 1.25 2021-11-22T18:13:22+0900 20 | - Changed DBD::mysql is not a dependency if DBD::MariaDB 1.21 is already installed #88 21 | 22 | 1.24 2021-10-06T07:57:45+0900 23 | - Fixed last_insert_id() and warnings_count() for MariaDB #86 24 | Contributor: Dan Book 25 | 26 | 1.23 2021-09-11T10:36:17+0200 27 | - Fix version number issues #84 28 | - Internal changes regarding MariaDB/mysql attributes 29 | 30 | 1.21 2021-04-28T12:15:39+0900 31 | - Fix uninitialized warning with expand(1) #83 32 | - Removed delay() from tests and documentation 33 | 34 | 1.20 2020-09-05T13:27:52+0900 35 | - Mojo::mysql::PubSub is less susceptible to deadlocks/timeouts #80 36 | Contributor: Larry Leszczynski 37 | 38 | 1.19 2020-05-01T06:58:48+0900 39 | - Fix documentation for Mojo::mysql::close_idle_connections() 40 | - Add documentation for SQL::Abstract::mysql::where() #78 41 | - Made SQL::Abstract::mysql more compatible with SQL::Abstract #77 42 | Contributor: Matt S Trout 43 | 44 | 1.18 2019-12-01T09:41:10+0100 45 | - Add missing code for SELECT AS 46 | 47 | 1.17 2019-08-01T09:44:57+0200 48 | - Fix leaking $sth when used in blocking mode, fixes #66 49 | 50 | 1.16 2019-06-25T06:33:55+0200 51 | - Add DBI to prerequisites 52 | Contributor: Mohammad S Anwar 53 | 54 | 1.15 2019-04-22T06:41:34+0200 55 | - Add support for NATURAL JOIN and JOIN USING #59 56 | 57 | 1.14 2019-03-23T08:07:17+0100 58 | - Correct handling of fetchall and arrays in Results 59 | 60 | 1.13 2019-03-02T11:27:01+0800 61 | - Add support for DBD::MariaDB #47 62 | - Add missing code for SQL JOIN #56 63 | Contributor: Tekki 64 | - Made it clearer that PubSub is an experiment 65 | 66 | 1.12 2019-01-05T12:34:13+0900 67 | - Bumped Mojolicious version to 8.03 68 | - Bumped SQL::Abstract version to 1.86 #49 69 | 70 | 1.11 2018-12-18T19:27:08+0900 71 | - Avoid "Gathering async_query_in_flight results for the wrong handle" warning 72 | 73 | 1.10 2018-12-18T07:25:14+0900 74 | - Add SQL::Abstract::mysql 75 | Contributor: Rolf Stöckli 76 | 77 | 1.09 2018-11-27T09:32:01+0900 78 | - Fix MariaDB/MySQL incompatibility #41 79 | Contributor: Rolf Stöckli 80 | - Fix documentation mismatch regarding "mysql_client_found_rows" #42 81 | Contributor: Yuriy Zhilovets 82 | 83 | 1.08 2018-11-13T17:31:49+0900 84 | - Fix query() with callback returns $self 85 | - Fix Gathering async_query_in_flight results for the wrong handle bug 86 | - Add close_idle_connections to Mojo::mysql 87 | - Add support for working with JSON 88 | - Add tables method to Mojo::mysql::Database 89 | - Change database name is optional in constructor #38 90 | 91 | 1.07 2018-05-03T12:25:08+0200 92 | - Fix using "mysql_socket" instead of "host" when connecting to a unix socket #34 93 | - Allow constructor to take a single hashref #37 94 | 95 | 1.06 2018-02-27T19:32:40+0100 96 | - Changed from_string() to also accept Mojo::URL objects #36 97 | Contributor: Karl Rune Nilsen 98 | 99 | 1.05 2017-11-11T10:04:40+0800 100 | - Add delete_p(), insert_p(), query_p(), select_p() and update_p() 101 | 102 | 1.04 2017-08-14T19:22:33+0200 103 | - Documented "mysql_enable_utf8" v.s. "mysql_enable_utf8mb4" #32 104 | - Can pass on attributes to new() 105 | 106 | 1.03 2017-05-21T23:19:29+0200 107 | - Add ability to set types of query parameters #31 108 | Contributor: Dan Book 109 | 110 | 1.02 2017-05-15T20:34:01+0200 111 | - Fix utf8 handling in DBD::mysql 4.042 112 | - Prevent warnings when creating the mojo_migrations table #26 113 | - Add proper quoting of table and column names #30 114 | - Add warning when using Mojo::mysql::PubSub 115 | 116 | 1.01 2017-03-25T08:24:29+0100 117 | - Add strict_mode() method and constructor #29 118 | 119 | 1.00 2017-02-12T18:30:58+0100 120 | - Add support for generating queries with SQL::Abstract 121 | - Add abstract attribute to Mojo::Pg 122 | - Add delete, insert, select and update methods to Mojo::Pg::Database 123 | - Add database_class attribute to Mojo::mysql 124 | - Add results_class attribute to Mojo::mysql::Database 125 | - Improved contextual caller information in query error messages 126 | - Compatible with Mojolicious 7.18 127 | 128 | 0.14 2016-02-15T14:06:24+0100 129 | - Add Mojo::mysql::auto_migrate 130 | 131 | 0.13 2016-01-27T21:05:37+0100 132 | - Remove deprecrated do() method 133 | - Add finish() to Mojo::mysql::Results 134 | - Fix bug where non-blocking queries could get lost after the database 135 | connection got closed unexpectedly 136 | https://github.com/kraih/mojo-pg/commit/2165b8e1131f2a5044ec2aae1c0ba8a00232b7c8 137 | - Improved Mojo::mysql::Migrations to detect if the currently active version 138 | is greater than the latest version. 139 | https://github.com/kraih/mojo-pg/commit/92bc312e725042b748950b9c61319d0256d0004a 140 | 141 | 0.12 2015-05-02T17:55:13Z 142 | - Added module Mojo::mysql::PubSub. 143 | - Added pubsub attribute to Mojo::mysql. 144 | 145 | 0.11 2015-04-06T03:38:31Z 146 | - Fixed bug in Mojo::mysql::Migrations where migrations would sometimes be 147 | executed in the wrong order. 148 | 149 | 0.10 2015-04-05T23:32:03Z 150 | - Fixed bug in Mojo::mysql::Migrations where the latest version could not 151 | always be determined correctly. (Hernan Lopes) 152 | - Updated blog example application from Mojo::Pg 153 | 154 | 0.09 2015-03-29T18:29:35Z 155 | - Fixed Mojo::mysql::Migrations to allow delimiter in comments and quoted 156 | strings 157 | - delimiter support in Mojo::mysql::Migrations, allows creation of stored 158 | procedures and triggers in migration scripts 159 | - 'quote' and 'quote_id' methods in Mojo::mysql::Database 160 | 161 | 0.08 2015-03-24T13:14:32Z 162 | - blog example from Mojo::Pg 163 | - better examples in POD 164 | - Improved Mojo::mysql::Migrations to make no changes to the database when 165 | checking the currently active version. 166 | - Fixed Mojo::mysql::Migrations to handle UTF-8 encoded files correctly 167 | 168 | 0.07 2015-03-09T13:34:31Z 169 | - Deprecated Mojo::mysql::Database::do in favour of 170 | Mojo::mysql::Database::query as in Mojo::Pg 171 | - Some new methods in Mojo::mysql::Result eliminating need to access sth 172 | - bugfix in Mojo::mysql::Migrations with trailing whitespace after last 173 | semicolon 174 | 175 | 0.06 2015-02-25T17:31:24Z 176 | - OO Mojo::Loader API is deprecated in Mojolicious 5.81 177 | 178 | 0.05 2015-01-22T00:14:11Z 179 | - Do not cache statement handles in Mojo::mysql::Database. 180 | - Synced changes from Mojo::Pg 181 | - utf8 enabled by default 182 | 183 | 0.04 2015-01-02T12:15:26Z 184 | - Add support for migrations #3 Contributor: Curt Hochwender 185 | - Add Mojo::mysql::Migrations. 186 | - Add migrations attribute to Mojo::msyql 187 | - Add db attribute to Mojo::mysql::Transaction. 188 | - Fix bug where Perl would close the DBD::mysql file descriptor after it 189 | had been used with the event loop. 190 | - Remove dbh attribute from Mojo::mysql::Transaction 191 | - Updated Mojolicious requirement to 5.49 to ensure migrations in the DATA 192 | section are not served as static files 193 | 194 | 0.03 2014-10-13T13:39:59Z 195 | - Removed commit and rollback methods from Mojo::mysql::Database. 196 | - Added Mojo::mysql::Transaction. 197 | 198 | 0.02 2014-10-12T18:14:33Z 199 | - Force mysql_auto_reconnect = 0 to avoid nasty reconnect bugs under some 200 | environments. https://metacpan.org/pod/DBD::mysql#mysql_auto_reconnect 201 | 202 | 0.01 2014-10-11T17:34:05Z 203 | - First release. 204 | -------------------------------------------------------------------------------- /lib/Mojo/mysql/Results.pm: -------------------------------------------------------------------------------- 1 | package Mojo::mysql::Results; 2 | use Mojo::Base -base; 3 | 4 | use Mojo::Collection; 5 | use Mojo::JSON 'from_json'; 6 | use Mojo::Util 'tablify'; 7 | 8 | has [qw(db sth)]; 9 | 10 | sub array { ($_[0]->_expand({list => 0, type => 'array'}))[0] } 11 | 12 | sub arrays { _c($_[0]->_expand({list => 1, type => 'array'})) } 13 | 14 | sub columns { shift->sth->{NAME} } 15 | 16 | sub expand { $_[0]{expand} = defined $_[1] ? 2 : 1 and return $_[0] } 17 | 18 | sub finish { shift->sth->finish } 19 | 20 | sub hash { ($_[0]->_expand({list => 0, type => 'hash'}))[0] } 21 | 22 | sub hashes { _c($_[0]->_expand({list => 1, type => 'hash'})) } 23 | 24 | sub rows { shift->sth->rows } 25 | 26 | sub text { tablify shift->arrays } 27 | 28 | sub more_results { shift->sth->more_results } 29 | 30 | sub affected_rows { shift->{affected_rows} } 31 | 32 | sub err { shift->sth->err } 33 | 34 | sub errstr { shift->sth->errstr } 35 | 36 | sub last_insert_id { shift->_sth_attr('mysql_insertid') } 37 | 38 | sub state { shift->sth->state } 39 | 40 | sub warnings_count { shift->_sth_attr('mysql_warning_count') } 41 | 42 | sub _c { Mojo::Collection->new(@_) } 43 | 44 | sub _expand { 45 | my ($self, $to) = @_; 46 | 47 | # Get field names and types, needs to be done before reading from sth 48 | my $mode = $self->{expand} || 0; 49 | my ($idx, $names) = $mode == 1 ? $self->_types : (); 50 | 51 | # Fetch sql data 52 | my $hash = $to->{type} eq 'hash'; 53 | my $sql_data 54 | = $to->{list} && $hash ? $self->sth->fetchall_arrayref({}) 55 | : $to->{list} ? $self->sth->fetchall_arrayref 56 | : $hash ? [$self->sth->fetchrow_hashref] 57 | : [$self->sth->fetchrow_arrayref]; 58 | 59 | # Optionally expand 60 | if ($mode) { 61 | my $from_json = __PACKAGE__->can(sprintf '_from_json_mode_%s_%s', $mode, $to->{type}); 62 | $from_json->($_, $idx, $names) for @$sql_data; 63 | } 64 | 65 | return @$sql_data; 66 | } 67 | 68 | sub _from_json_mode_1_array { 69 | my ($r, $idx, $names) = @_; 70 | $r->[$_] = from_json $r->[$_] for grep { defined $r->[$_] } @$idx; 71 | } 72 | 73 | sub _from_json_mode_1_hash { 74 | my ($r, $idx, $names) = @_; 75 | $r->{$_} = from_json $r->{$_} for grep { defined $r->{$_} } @$names; 76 | } 77 | 78 | sub _from_json_mode_2_array { 79 | my ($r, $idx, $names) = @_; 80 | $_ = from_json $_ for grep defined && /^[\[{].*[}\]]$/, @$r; 81 | } 82 | 83 | sub _from_json_mode_2_hash { 84 | my ($r, $idx, $names) = @_; 85 | $_ = from_json $_ for grep defined && /^[\[{].*[}\]]$/, values %$r; 86 | } 87 | 88 | sub _sth_attr { 89 | my ($self, $name) = @_; 90 | $name =~ s!^mysql!{lc $self->db->dbh->{Driver}{Name}}!e; 91 | return $self->sth->{$name}; 92 | } 93 | 94 | sub _types { 95 | my $self = shift; 96 | return @$self{qw(idx names)} if $self->{idx}; 97 | 98 | my $types = $self->_sth_attr('mysql_type'); 99 | my @idx = grep { $types->[$_] == 245 or $types->[$_] == 252 } 0 .. $#$types; # 245 = MySQL, 252 = MariaDB 100 | 101 | return ($self->{idx} = \@idx, $self->{names} = [@{$self->columns}[@idx]]); 102 | } 103 | 104 | sub DESTROY { 105 | my $self = shift; 106 | return unless my $db = $self->{db} and my $sth = $self->{sth}; 107 | push @{$db->{done_sth}}, $sth unless $self->{is_blocking}; 108 | } 109 | 110 | 1; 111 | 112 | =encoding utf8 113 | 114 | =head1 NAME 115 | 116 | Mojo::mysql::Results - Results 117 | 118 | =head1 SYNOPSIS 119 | 120 | use Mojo::mysql::Results; 121 | 122 | my $results = Mojo::mysql::Results->new(db => $db, sth => $sth); 123 | 124 | =head1 DESCRIPTION 125 | 126 | L is a container for statement handles used by 127 | L. 128 | 129 | =head1 ATTRIBUTES 130 | 131 | L implements the following attributes. 132 | 133 | =head2 db 134 | 135 | my $db = $results->db; 136 | $results = $results->db(Mojo::mysql::Database->new); 137 | 138 | L object these results belong to. 139 | 140 | =head2 sth 141 | 142 | my $sth = $results->sth; 143 | $results = $results->sth($sth); 144 | 145 | Statement handle results are fetched from. 146 | 147 | =head1 METHODS 148 | 149 | L inherits all methods from L and implements 150 | the following new ones. 151 | 152 | =head2 array 153 | 154 | my $array = $results->array; 155 | 156 | Fetch next row from L and return it as an array reference. Note that 157 | L needs to be called if you are not fetching all the possible rows. 158 | 159 | # Process one row at a time 160 | while (my $next = $results->array) { 161 | say $next->[3]; 162 | } 163 | 164 | =head2 arrays 165 | 166 | my $collection = $results->arrays; 167 | 168 | Fetch all rows and return them as a L object containing 169 | array references. 170 | 171 | # Process all rows at once 172 | say $results->arrays->reduce(sub { $a->[3] + $b->[3] }); 173 | 174 | =head2 columns 175 | 176 | my $columns = $results->columns; 177 | 178 | Return column names as an array reference. 179 | 180 | =head2 expand 181 | 182 | $results = $results->expand; 183 | $results = $results->expand(1) 184 | 185 | Decode C fields automatically to Perl values for all rows. Passing in "1" 186 | as an argument will force expanding all columns that looks like a JSON array or 187 | object. 188 | 189 | # Expand JSON 190 | $results->expand->hashes->map(sub { $_->{foo}{bar} })->join("\n")->say; 191 | 192 | Note that this method is EXPERIMENTAL. 193 | 194 | See also L for more details 195 | on how to work with JSON in MySQL. 196 | 197 | =head2 finish 198 | 199 | $results->finish; 200 | 201 | Indicate that you are finished with L and will not be fetching all the 202 | remaining rows. 203 | 204 | =head2 hash 205 | 206 | my $hash = $results->hash; 207 | 208 | Fetch next row from L and return it as a hash reference. Note that 209 | L needs to be called if you are not fetching all the possible rows. 210 | 211 | # Process one row at a time 212 | while (my $next = $results->hash) { 213 | say $next->{money}; 214 | } 215 | 216 | =head2 hashes 217 | 218 | my $collection = $results->hashes; 219 | 220 | Fetch all rows and return them as a L object containing hash 221 | references. 222 | 223 | # Process all rows at once 224 | say $results->hashes->reduce(sub { $a->{money} + $b->{money} }); 225 | 226 | =head2 new 227 | 228 | my $results = Mojo::mysql::Results->new(db => $db, sth => $sth); 229 | my $results = Mojo::mysql::Results->new({db => $db, sth => $sth}); 230 | 231 | Construct a new L object. 232 | 233 | =head2 rows 234 | 235 | my $num = $results->rows; 236 | 237 | Number of rows. 238 | 239 | =head2 text 240 | 241 | my $text = $results->text; 242 | 243 | Fetch all rows and turn them into a table with L. 244 | 245 | =head2 more_results 246 | 247 | do { 248 | my $columns = $results->columns; 249 | my $arrays = $results->arrays; 250 | } while ($results->more_results); 251 | 252 | Handle multiple results. 253 | 254 | =head2 affected_rows 255 | 256 | my $affected = $results->affected_rows; 257 | 258 | Number of affected rows by the query. The number reported is dependant from 259 | C or C option in 260 | L. For example 261 | 262 | UPDATE $table SET id = 1 WHERE id = 1 263 | 264 | would return 1 if C or L is 265 | set, and 0 otherwise. 266 | 267 | =head2 last_insert_id 268 | 269 | my $last_id = $results->last_insert_id; 270 | 271 | That value of C column if executed query was C in a table with 272 | C column. 273 | 274 | =head2 warnings_count 275 | 276 | my $warnings = $results->warnings_count; 277 | 278 | Number of warnings raised by the executed query. 279 | 280 | =head2 err 281 | 282 | my $err = $results->err; 283 | 284 | Error code received. 285 | 286 | =head2 state 287 | 288 | my $state = $results->state; 289 | 290 | Error state received. 291 | 292 | =head2 errstr 293 | 294 | my $errstr = $results->errstr; 295 | 296 | Error message received. 297 | 298 | =head1 SEE ALSO 299 | 300 | L. 301 | 302 | =cut 303 | -------------------------------------------------------------------------------- /t/sql.t: -------------------------------------------------------------------------------- 1 | use Mojo::Base -strict; 2 | 3 | use Test::More; 4 | use Mojo::mysql; 5 | use SQL::Abstract::Test import => ['is_same_sql_bind']; 6 | 7 | note 'Basics'; 8 | my $mysql = Mojo::mysql->new; 9 | my $abstract = $mysql->abstract; 10 | is_query([$abstract->insert('foo', {bar => 'baz'})], ['INSERT INTO `foo` ( `bar`) VALUES ( ? )', 'baz'], 'right query'); 11 | is_query([$abstract->select('foo', '*')], ['SELECT * FROM `foo`'], 'right query'); 12 | is_query([$abstract->select(['foo', 'bar', 'baz'])], ['SELECT * FROM `foo`, `bar`, `baz`'], 'right query'); 13 | is_query( 14 | [$abstract->select(['wibble.foo', 'wobble.bar', 'wubble.baz'])], 15 | ['SELECT * FROM `wibble`.`foo`, `wobble`.`bar`, `wubble`.`baz`'], 16 | 'right query' 17 | ); 18 | 19 | my (@sql, $result); 20 | 21 | note 'on conflict: INSERT IGNORE'; 22 | @sql = $abstract->insert('foo', {bar => 'baz'}, {on_conflict => 'ignore'}); 23 | is_query(\@sql, ['INSERT IGNORE INTO `foo` ( `bar`) VALUES ( ? )', 'baz'], 'right query'); 24 | 25 | note 'on conflict: REPLACE'; 26 | @sql = $abstract->insert('foo', {bar => 'baz'}, {on_conflict => 'replace'}); 27 | is_query(\@sql, ['REPLACE INTO `foo` ( `bar`) VALUES ( ? )', 'baz'], 'right query'); 28 | 29 | note 'on conflict: ON DUPLICATE KEY UPDATE'; 30 | @sql = $abstract->insert('foo', {bar => 'baz'}, {on_conflict => {c => 'd'}}); 31 | is_query(\@sql, ['INSERT INTO `foo` ( `bar`) VALUES ( ? ) ON DUPLICATE KEY UPDATE `c` = ?', 'baz', 'd'], 'right query'); 32 | 33 | note 'on conflict (unsupported value)'; 34 | eval { $abstract->insert('foo', {bar => 'baz'}, {on_conflict => 'do something'}) }; 35 | like $@, qr/on_conflict value "do something" is not allowed/, 'right error'; 36 | 37 | eval { $abstract->insert('foo', {bar => 'baz'}, {on_conflict => undef}) }; 38 | like $@, qr/on_conflict value "" is not allowed/, 'right error'; 39 | 40 | note 'SELECT AS'; 41 | is_query( 42 | [$abstract->select('foo', [[bar => 'wibble'], [baz => 'wobble'], 'yada'])], 43 | ['SELECT `bar` AS `wibble`, `baz` AS `wobble`, `yada` FROM `foo`'], 44 | 'right query' 45 | ); 46 | 47 | note 'ORDER BY'; 48 | @sql = $abstract->select('foo', '*', {bar => 'baz'}, {-desc => 'yada'}); 49 | is_query(\@sql, ['SELECT * FROM `foo` WHERE ( `bar` = ? ) ORDER BY `yada` DESC', 'baz'], 'right query'); 50 | 51 | @sql = $abstract->select('foo', '*', {bar => 'baz'}, {order_by => {-desc => 'yada'}}); 52 | is_query(\@sql, ['SELECT * FROM `foo` WHERE ( `bar` = ? ) ORDER BY `yada` DESC', 'baz'], 'right query'); 53 | 54 | note 'LIMIT, OFFSET'; 55 | @sql = $abstract->select('foo', '*', undef, {limit => 10, offset => 5}); 56 | is_query(\@sql, ['SELECT * FROM `foo` LIMIT ? OFFSET ?', 10, 5], 'right query'); 57 | 58 | note 'GROUP BY'; 59 | @sql = $abstract->select('foo', '*', undef, {group_by => \'bar, baz'}); 60 | is_query(\@sql, ['SELECT * FROM `foo` GROUP BY bar, baz'], 'right query'); 61 | 62 | @sql = $abstract->select('foo', '*', undef, {group_by => ['bar', 'baz']}); 63 | is_query(\@sql, ['SELECT * FROM `foo` GROUP BY `bar`, `baz`'], 'right query'); 64 | 65 | note 'HAVING'; 66 | @sql = $abstract->select('foo', '*', undef, {group_by => ['bar'], having => {baz => 'yada'}}); 67 | is_query(\@sql, ['SELECT * FROM `foo` GROUP BY `bar` HAVING `baz` = ?', 'yada'], 'right query'); 68 | 69 | @sql = $abstract->select('foo', '*', {bar => {'>' => 'baz'}}, {group_by => ['bar'], having => {baz => {'<' => 'bar'}}}); 70 | $result = ['SELECT * FROM `foo` WHERE ( `bar` > ? ) GROUP BY `bar` HAVING `baz` < ?', 'baz', 'bar']; 71 | is_query(\@sql, $result, 'right query'); 72 | 73 | note 'GROUP BY (unsupported value)'; 74 | eval { $abstract->select('foo', '*', undef, {group_by => {}}) }; 75 | like $@, qr/HASHREF/, 'right error'; 76 | 77 | note 'for: FOR UPDATE'; 78 | @sql = $abstract->select('foo', '*', undef, {for => 'update'}); 79 | is_query(\@sql, ['SELECT * FROM `foo` FOR UPDATE'], 'right query'); 80 | 81 | note 'for: LOCK IN SHARE MODE'; 82 | @sql = $abstract->select('foo', '*', undef, {for => 'share'}); 83 | is_query(\@sql, ['SELECT * FROM `foo` LOCK IN SHARE MODE'], 'right query'); 84 | 85 | @sql = $abstract->select('foo', '*', undef, {for => \'SHARE'}); 86 | is_query(\@sql, ['SELECT * FROM `foo` FOR SHARE'], 'right query'); 87 | 88 | note 'for (unsupported value)'; 89 | eval { $abstract->select('foo', '*', undef, {for => 'update skip locked'}) }; 90 | like $@, qr/for value "update skip locked" is not allowed/, 'right error'; 91 | 92 | eval { $abstract->select('foo', '*', undef, {for => []}) }; 93 | like $@, qr/ARRAYREF/, 'right error'; 94 | 95 | note 'JOIN: single field'; 96 | @sql = $abstract->select(['foo', ['bar', foo_id => 'id']]); 97 | is_query(\@sql, ['SELECT * FROM `foo` JOIN `bar` ON (`bar`.`foo_id` = `foo`.`id`)'], 'right query'); 98 | 99 | @sql = $abstract->select(['foo', ['bar', 'foo.id' => 'bar.foo_id']]); 100 | is_query(\@sql, ['SELECT * FROM `foo` JOIN `bar` ON (`foo`.`id` = `bar`.`foo_id`)'], 'right query'); 101 | 102 | note 'JOIN: multiple fields'; 103 | @sql = $abstract->select(['foo', ['bar', 'foo.id' => 'bar.foo_id', 'foo.id2' => 'bar.foo_id2']]); 104 | is_query(\@sql, 105 | ['SELECT * FROM `foo` JOIN `bar` ON (`foo`.`id` = `bar`.`foo_id`' . ' AND `foo`.`id2` = `bar`.`foo_id2`' . ')'], 106 | 'right query'); 107 | 108 | note 'JOIN: multiple tables'; 109 | @sql = $abstract->select(['foo', ['bar', foo_id => 'id'], ['baz', foo_id => 'id']]); 110 | is_query(\@sql, 111 | ['SELECT * FROM `foo` JOIN `bar` ON (`bar`.`foo_id` = `foo`.`id`) JOIN `baz` ON (`baz`.`foo_id` = `foo`.`id`)'], 112 | 'right query'); 113 | 114 | note 'LEFT JOIN'; 115 | @sql = $abstract->select(['foo', [-left => 'bar', foo_id => 'id']]); 116 | is_query(\@sql, ['SELECT * FROM `foo` LEFT JOIN `bar` ON (`bar`.`foo_id` = `foo`.`id`)'], 'right query'); 117 | 118 | note 'LEFT JOIN: multiple fields'; 119 | @sql = $abstract->select(['foo', [-left => 'bar', foo_id => 'id', foo_id2 => 'id2', foo_id3 => 'id3']]); 120 | is_query( 121 | \@sql, 122 | [ 123 | 'SELECT * FROM `foo` LEFT JOIN `bar` ON (`bar`.`foo_id` = `foo`.`id`' 124 | . ' AND `bar`.`foo_id2` = `foo`.`id2` AND `bar`.`foo_id3` = `foo`.`id3`)' 125 | ], 126 | 'right query' 127 | ); 128 | 129 | note 'RIGHT JOIN'; 130 | @sql = $abstract->select(['foo', [-right => 'bar', foo_id => 'id']]); 131 | is_query(\@sql, ['SELECT * FROM `foo` RIGHT JOIN `bar` ON (`bar`.`foo_id` = `foo`.`id`)'], 'right query'); 132 | 133 | note 'INNER JOIN'; 134 | @sql = $abstract->select(['foo', [-inner => 'bar', foo_id => 'id']]); 135 | is_query(\@sql, ['SELECT * FROM `foo` INNER JOIN `bar` ON (`bar`.`foo_id` = `foo`.`id`)'], 'right query'); 136 | 137 | note 'NATURAL JOIN'; 138 | @sql = $abstract->select(['foo', [-natural => 'bar']]); 139 | is_query(\@sql, ['SELECT * FROM `foo` NATURAL JOIN `bar`'], 'right query'); 140 | 141 | note 'JOIN USING'; 142 | @sql = $abstract->select(['foo', [bar => 'foo_id']]); 143 | is_query(\@sql, ['SELECT * FROM `foo` JOIN `bar` USING (`foo_id`)'], 'right query'); 144 | 145 | note 'JOIN: unsupported value'; 146 | eval { $abstract->select(['foo', ['bar']]) }; 147 | like $@, qr/join must be in the form \[\$table, \$fk => \$pk\]/, 'right error for missing keys'; 148 | 149 | eval { $abstract->select(['foo', ['bar', foo_id => 'id', 'id2']]) }; 150 | like $@, qr/join requires an even number of keys/, 'right error for uneven number of keys'; 151 | 152 | eval { $abstract->select(['foo', [-natural => 'bar', 'foo_id']]) }; 153 | like $@, qr/natural join must be in the form \[-natural => \$table\]/, 'right error for wrong natural join'; 154 | 155 | note 'where'; 156 | @sql = $abstract->where( 157 | {user => {-like => 'r%'}}, 158 | { 159 | for => 'share', 160 | group_by => ['user'], 161 | having => {max_connections => {'<' => 100}}, 162 | limit => 10, 163 | offset => 42, 164 | order_by => 'user', 165 | } 166 | ); 167 | 168 | is_query( 169 | \@sql, 170 | [ 171 | join(' ', 172 | 'WHERE ( `user` LIKE ? )', 173 | 'GROUP BY `user`', 174 | 'HAVING `max_connections` < ?', 175 | 'ORDER BY `user`', 176 | 'LIMIT ? OFFSET ?', 177 | 'LOCK IN SHARE MODE', 178 | ), 179 | 'r%', 100, 10, 42 180 | ], 181 | 'right where' 182 | ); 183 | 184 | done_testing; 185 | 186 | sub is_query { 187 | my ($got, $want, $msg) = @_; 188 | my $got_sql = shift @$got; 189 | my $want_sql = shift @$want; 190 | local $Test::Builder::Level = $Test::Builder::Level + 1; 191 | is_same_sql_bind $got_sql, $got, $want_sql, $want, $msg; 192 | } 193 | -------------------------------------------------------------------------------- /lib/Mojo/mysql/Migrations.pm: -------------------------------------------------------------------------------- 1 | package Mojo::mysql::Migrations; 2 | use Mojo::Base -base; 3 | 4 | use Carp 'croak'; 5 | use Mojo::File; 6 | use Mojo::Loader 'data_section'; 7 | use Mojo::Util 'decode'; 8 | 9 | use constant DEBUG => $ENV{MOJO_MIGRATIONS_DEBUG} || 0; 10 | 11 | has name => 'migrations'; 12 | has 'mysql'; 13 | 14 | sub active { $_[0]->_active($_[0]->mysql->db) } 15 | 16 | sub from_data { 17 | my ($self, $class, $name) = @_; 18 | return $self->from_string(data_section($class //= caller, $name // $self->name)); 19 | } 20 | 21 | sub from_file { shift->from_string(decode 'UTF-8', Mojo::File->new(pop)->slurp) } 22 | 23 | sub from_string { 24 | my ($self, $sql) = @_; 25 | return $self unless defined $sql; 26 | my ($version, $way); 27 | my ($new, $last, $delimiter) = (1, '', ';'); 28 | my $migrations = $self->{migrations} = {up => {}, down => {}}; 29 | 30 | while (length($sql) > 0) { 31 | my $token; 32 | 33 | if ($sql =~ /^$delimiter/x) { 34 | ($new, $token) = (1, $delimiter); 35 | } 36 | elsif ($sql =~ /^delimiter\s+(\S+)\s*(?:\n|\z)/ip) { 37 | ($new, $token, $delimiter) = (1, ${^MATCH}, $1); 38 | } 39 | elsif ( 40 | $sql =~ /^(\s+)/s # whitespace 41 | or $sql =~ /^(\w+)/ # general name 42 | ) 43 | { 44 | $token = $1; 45 | } 46 | elsif ( 47 | $sql =~ /^--.*(?:\n|\z)/p # double-dash comment 48 | or $sql =~ /^\#.*(?:\n|\z)/p # hash comment 49 | or $sql =~ /^\/\*(?:[^\*]|\*[^\/])*(?:\*\/|\*\z|\z)/p # C-style comment 50 | or $sql =~ /^'(?:[^'\\]*|\\(?:.|\n)|'')*(?:'|\z)/p # single-quoted literal text 51 | or $sql =~ /^"(?:[^"\\]*|\\(?:.|\n)|"")*(?:"|\z)/p # double-quoted literal text 52 | or $sql =~ /^`(?:[^`]*|``)*(?:`|\z)/p 53 | ) 54 | { # schema-quoted literal text 55 | $token = ${^MATCH}; 56 | } 57 | else { 58 | $token = substr($sql, 0, 1); 59 | } 60 | 61 | # chew token 62 | substr($sql, 0, length($token), ''); 63 | 64 | if ($token =~ /^--\s+(\d+)\s*(up|down)/i) { 65 | my ($new_version, $new_way) = ($1, lc $2); 66 | push @{$migrations->{$way}{$version} //= []}, $last if $version and $last !~ /^\s*$/s; 67 | ($version, $way) = ($new_version, $new_way); 68 | ($new, $last, $delimiter) = (0, '', ';'); 69 | } 70 | 71 | if ($new) { 72 | push @{$migrations->{$way}{$version} //= []}, $last if $version and $last !~ /^\s*$/s; 73 | ($new, $last) = (0, ''); 74 | } 75 | else { 76 | $last .= $token; 77 | } 78 | } 79 | push @{$migrations->{$way}{$version} //= []}, $last if $version and $last !~ /^\s*$/s; 80 | 81 | return $self; 82 | } 83 | 84 | sub latest { 85 | (sort { $a <=> $b } keys %{shift->{migrations}{up}})[-1] || 0; 86 | } 87 | 88 | sub migrate { 89 | my ($self, $target) = @_; 90 | my $latest = $self->latest; 91 | $target //= $latest; 92 | 93 | # Unknown version 94 | my ($up, $down) = @{$self->{migrations}}{qw(up down)}; 95 | croak "Version $target has no migration" if $target != 0 && !$up->{$target}; 96 | 97 | # Already the right version (make sure migrations table exists) 98 | my $db = $self->mysql->db; 99 | return $self if $self->_active($db, 1) == $target; 100 | 101 | # Check version again 102 | my $tx = $db->begin; 103 | return $self if (my $active = $self->_active($db, 1)) == $target; 104 | 105 | # Newer version 106 | croak "Active version $active is greater than the latest version $latest" if $active > $latest; 107 | 108 | # Up 109 | my @sql; 110 | if ($active < $target) { 111 | foreach (sort { $a <=> $b } keys %$up) { 112 | push @sql, @{$up->{$_}} if $_ <= $target && $_ > $active; 113 | } 114 | } 115 | 116 | # Down 117 | else { 118 | foreach (reverse sort { $a <=> $b } keys %$down) { 119 | push @sql, @{$down->{$_}} if $_ > $target && $_ <= $active; 120 | } 121 | } 122 | 123 | warn "-- Migrate ($active -> $target)\n", join("\n", @sql), "\n" if DEBUG; 124 | eval { 125 | $db->query($_) for @sql; 126 | $db->query("update mojo_migrations set version = ? where name = ?", $target, $self->name); 127 | }; 128 | if (my $error = $@) { 129 | undef $tx; 130 | die $error; 131 | } 132 | $tx->commit; 133 | return $self; 134 | } 135 | 136 | sub _active { 137 | my ($self, $db, $create) = @_; 138 | 139 | my $name = $self->name; 140 | my $results = eval { $db->query('select version from mojo_migrations where name = ?', $name) }; 141 | my $error = $@; 142 | return 0 if !$create and !$results; 143 | if ($results and my $next = $results->array) { return $next->[0] } 144 | 145 | $db->query( 146 | 'create table if not exists mojo_migrations ( 147 | name varchar(128) unique not null, 148 | version bigint not null 149 | )' 150 | ) if $error; 151 | $db->query('insert into mojo_migrations values (?, ?)', $name, 0); 152 | 153 | return 0; 154 | } 155 | 156 | 1; 157 | 158 | =encoding utf8 159 | 160 | =head1 NAME 161 | 162 | Mojo::mysql::Migrations - Migrations 163 | 164 | =head1 SYNOPSIS 165 | 166 | use Mojo::mysql::Migrations; 167 | 168 | my $migrations = Mojo::mysql::Migrations->new(mysql => $mysql); 169 | $migrations->from_file('/home/sri/migrations.sql')->migrate; 170 | 171 | =head1 DESCRIPTION 172 | 173 | L is used by L to allow database schemas to 174 | evolve easily over time. A migration file is just a collection of sql blocks, 175 | with one or more statements, separated by comments of the form 176 | C<-- VERSION UP/DOWN>. 177 | 178 | -- 1 up 179 | create table messages (message text); 180 | insert into messages values ('I ♥ Mojolicious!'); 181 | delimiter // 182 | create procedure mojo_test() 183 | begin 184 | select text from messages; 185 | end 186 | // 187 | -- 1 down 188 | drop table messages; 189 | drop procedure mojo_test; 190 | 191 | -- 2 up (...you can comment freely here...) 192 | create table stuff (whatever int); 193 | -- 2 down 194 | drop table stuff; 195 | 196 | The idea is to let you migrate from any version, to any version, up and down. 197 | Migrations are very safe, because they are performed in transactions and only 198 | one can be performed at a time. If a single statement fails, the whole 199 | migration will fail and get rolled back. Every set of migrations has a 200 | L, which is stored together with the currently active version in an 201 | automatically created table named C. 202 | 203 | =head1 ATTRIBUTES 204 | 205 | L implements the following attributes. 206 | 207 | =head2 name 208 | 209 | my $name = $migrations->name; 210 | $migrations = $migrations->name('foo'); 211 | 212 | Name for this set of migrations, defaults to C. 213 | 214 | =head2 mysql 215 | 216 | my $mysql = $migrations->mysql; 217 | $migrations = $migrations->mysql(Mojo::mysql->new); 218 | 219 | L object these migrations belong to. 220 | 221 | =head1 METHODS 222 | 223 | L inherits all methods from L and implements 224 | the following new ones. 225 | 226 | =head2 active 227 | 228 | my $version = $migrations->active; 229 | 230 | Currently active version. 231 | 232 | =head2 from_data 233 | 234 | $migrations = $migrations->from_data; 235 | $migrations = $migrations->from_data('main'); 236 | $migrations = $migrations->from_data('main', 'file_name'); 237 | 238 | Extract migrations from a file in the DATA section of a class with 239 | L, defaults to using the caller class and 240 | L. 241 | 242 | __DATA__ 243 | @@ migrations 244 | -- 1 up 245 | create table messages (message text); 246 | insert into messages values ('I ♥ Mojolicious!'); 247 | -- 1 down 248 | drop table messages; 249 | 250 | =head2 from_file 251 | 252 | $migrations = $migrations->from_file('/home/sri/migrations.sql'); 253 | 254 | Extract migrations from a file. 255 | 256 | =head2 from_string 257 | 258 | $migrations = $migrations->from_string( 259 | '-- 1 up 260 | create table foo (bar int); 261 | -- 1 down 262 | drop table foo;' 263 | ); 264 | 265 | Extract migrations from string. 266 | 267 | =head2 latest 268 | 269 | my $version = $migrations->latest; 270 | 271 | Latest version available. 272 | 273 | =head2 migrate 274 | 275 | $migrations = $migrations->migrate; 276 | $migrations = $migrations->migrate(3); 277 | 278 | Migrate from L to a different version, up or down, defaults to 279 | using L. All version numbers need to be positive, with version C<0> 280 | representing an empty database. 281 | 282 | # Reset database 283 | $migrations->migrate(0)->migrate; 284 | 285 | =head1 DEBUGGING 286 | 287 | You can set the C environment variable to get some 288 | advanced diagnostics information printed to C. 289 | 290 | MOJO_MIGRATIONS_DEBUG=1 291 | 292 | =head1 SEE ALSO 293 | 294 | L, L, L. 295 | 296 | =cut 297 | -------------------------------------------------------------------------------- /lib/Mojo/mysql/PubSub.pm: -------------------------------------------------------------------------------- 1 | package Mojo::mysql::PubSub; 2 | use Mojo::Base 'Mojo::EventEmitter'; 3 | 4 | use Carp qw(croak); 5 | use Scalar::Util qw(weaken); 6 | 7 | use constant DEBUG => $ENV{MOJO_PUBSUB_DEBUG} || 0; 8 | use constant RETRIES => $ENV{MOJO_MYSQL_PUBSUB_RETRIES} // 1; 9 | 10 | has 'mysql'; 11 | 12 | sub DESTROY { 13 | my $self = shift; 14 | return unless $self->{wait_db} and $self->mysql; 15 | _query_with_retry($self->mysql->db, 'delete from mojo_pubsub_subscribe where pid = ?', $self->{wait_db}->pid); 16 | } 17 | 18 | sub listen { 19 | my ($self, $channel, $cb) = @_; 20 | my $sync_db = $self->mysql->db; 21 | my $wait_pid = $self->_wait_db($sync_db)->pid; 22 | warn qq|[PubSub] (@{[$wait_pid]}) listen "$channel"\n| if DEBUG; 23 | _query_with_retry($sync_db, 24 | 'insert into mojo_pubsub_subscribe (pid, channel) values (?, ?) on duplicate key update ts=current_timestamp', 25 | $wait_pid, $channel); 26 | push @{$self->{chans}{$channel}}, $cb; 27 | return $cb; 28 | } 29 | 30 | sub notify { 31 | my ($self, $channel, $payload) = @_; 32 | my $sync_db = $self->mysql->db; 33 | warn qq|[PubSub] channel:$channel <<< "@{[$payload // '']}"\n| if DEBUG; 34 | $self->_init($sync_db) unless $self->{init}; 35 | _query_with_retry($sync_db, 'insert into mojo_pubsub_notify (channel, payload) values (?, ?)', 36 | $channel, $payload // ''); 37 | return $self; 38 | } 39 | 40 | sub unlisten { 41 | my ($self, $channel, $cb) = @_; 42 | 43 | my $chan = $self->{chans}{$channel}; 44 | @$chan = grep { $cb ne $_ } @$chan; 45 | return $self if @$chan; 46 | 47 | my $sync_db = $self->mysql->db; 48 | my $wait_pid = $self->_wait_db($sync_db)->pid; 49 | warn qq|[PubSub] ($wait_pid) unlisten "$channel"\n| if DEBUG; 50 | _query_with_retry($sync_db, 'delete from mojo_pubsub_subscribe where pid = ? and channel = ?', $wait_pid, $channel); 51 | delete $self->{chans}{$channel}; 52 | return $self; 53 | } 54 | 55 | sub _init { 56 | my ($self, $sync_db) = @_; 57 | $self->mysql->migrations->name('pubsub')->from_data->migrate; 58 | _query_with_retry($sync_db, 59 | 'delete from mojo_pubsub_notify where ts < date_add(current_timestamp, interval -10 minute)'); 60 | _query_with_retry($sync_db, 61 | 'delete from mojo_pubsub_subscribe where ts < date_add(current_timestamp, interval -1 hour)'); 62 | $self->{init} = 1; 63 | } 64 | 65 | sub _notifications { 66 | my ($self, $sync_db) = @_; 67 | my $result 68 | = _query_with_retry($sync_db, 'select id, channel, payload from mojo_pubsub_notify where id > ? order by id', 69 | $self->{last_id}); 70 | while (my $row = $result->array) { 71 | my ($id, $channel, $payload) = @$row; 72 | $self->{last_id} = $id; 73 | next unless exists $self->{chans}{$channel}; 74 | warn qq/[PubSub] channel:$channel >>> "$payload"\n/ if DEBUG; 75 | for my $cb (@{$self->{chans}{$channel}}) { $self->$cb($payload) } 76 | } 77 | } 78 | 79 | sub _wait_db { 80 | my ($self, $sync_db) = @_; 81 | 82 | # Fork-safety 83 | delete @$self{qw(wait_db chans pid)} if ($self->{pid} //= $$) ne $$; 84 | 85 | return $self->{wait_db} if $self->{wait_db}; 86 | 87 | $self->_init($sync_db) unless $self->{init}; 88 | my $wait_db = $self->{wait_db} = $self->mysql->db; 89 | my $wait_db_pid = $wait_db->pid; 90 | _query_with_retry($sync_db, 91 | 'insert into mojo_pubsub_subscribe (pid, channel) values (?, ?) on duplicate key update ts=current_timestamp', 92 | $wait_db_pid, $_) 93 | for keys %{$self->{chans}}; 94 | 95 | if ($self->{last_id}) { 96 | $self->_notifications($sync_db); 97 | } 98 | else { 99 | my $last = _query_with_retry($sync_db, 'select id from mojo_pubsub_notify order by id desc limit 1')->array; 100 | $self->{last_id} = defined $last ? $last->[0] : 0; 101 | } 102 | 103 | weaken $wait_db->{mysql}; 104 | weaken $self; 105 | my $cb; 106 | $cb = sub { 107 | my ($db, $err, $res) = @_; 108 | return unless $self; 109 | warn qq|[PubSub] (@{[$db->pid]}) sleep(600) @{[$err ? "!!! $err" : $res->array->[0]]}\n| if DEBUG; 110 | my $sync_db = $self->mysql->db; 111 | return (delete $self->{wait_db}, $self->_wait_db($sync_db)) if $err; 112 | $res->finish; 113 | _query_with_retry($db, 'select sleep(600)', $cb); 114 | _query_with_retry($sync_db, 'update mojo_pubsub_subscribe set ts = current_timestamp where pid = ?', $db->pid); 115 | $self->_notifications($self->mysql->db); 116 | }; 117 | 118 | warn qq|[PubSub] (@{[$wait_db->pid]}) reconnect\n| if DEBUG; 119 | $self->emit(reconnect => $wait_db); 120 | return _query_with_retry($wait_db, 'select sleep(600)', $cb); 121 | } 122 | 123 | sub _query_with_retry { 124 | my ($db, $sql, @bind) = @_; 125 | 126 | my $result; 127 | 128 | my $remaining_attempts = RETRIES + 1; # including initial attempt 129 | while ($remaining_attempts--) { 130 | local $@; 131 | eval { $result = $db->query($sql, @bind) }; 132 | last unless $@; # success 133 | croak $@ unless $remaining_attempts; # rethrow $@ if no remaining attempts 134 | 135 | # If we are allowed to retry, check if the error message looks 136 | # like it refers to something retryable. Only look within the 137 | # first line to avoid potential spurious matches if the error 138 | # e.g. contains a stack trace. 139 | my $err = $@; # avoid stringifying $@ ... 140 | croak $@ unless $err =~ /^\V*(?:retry|timeout)/i; # ... and maybe rethrow it 141 | 142 | # If we got here, we are retrying the query: 143 | warn qq|[PubSub] (@{[$db->pid]}) retry ($sql) !!! $err\n| if DEBUG; 144 | } 145 | 146 | return $result; 147 | } 148 | 149 | 1; 150 | 151 | =encoding utf8 152 | 153 | =head1 NAME 154 | 155 | Mojo::mysql::PubSub - Publish/Subscribe 156 | 157 | =head1 SYNOPSIS 158 | 159 | use Mojo::mysql::PubSub; 160 | 161 | my $pubsub = Mojo::mysql::PubSub->new(mysql => $mysql); 162 | my $cb = $pubsub->listen(foo => sub { 163 | my ($pubsub, $payload) = @_; 164 | say "Received: $payload"; 165 | }); 166 | $pubsub->notify(foo => 'bar'); 167 | $pubsub->unlisten(foo => $cb); 168 | 169 | =head1 DESCRIPTION 170 | 171 | L is implementation of the publish/subscribe pattern used 172 | by L. The implementation should be considered an EXPERIMENT and 173 | might be removed without warning! 174 | 175 | Although MySQL does not have C like PostgreSQL and other RDBMs, 176 | this module implements similar feature. 177 | 178 | Single Database connection waits for notification by executing C on server. 179 | C and subscribed channels in stored in C table. 180 | Inserting new row in C table triggers C for 181 | all connections waiting for notification. 182 | 183 | C privilege is needed for MySQL user to see other users processes. 184 | C privilege is needed to be able to execute C for statements 185 | started by other users. 186 | C privilege may be needed to be able to define trigger. 187 | 188 | If your applications use this module using different MySQL users it is important 189 | the migration script to be executed by user having C privilege on the database. 190 | 191 | =head1 EVENTS 192 | 193 | L inherits all events from L and can 194 | emit the following new ones. 195 | 196 | =head2 reconnect 197 | 198 | $pubsub->on(reconnect => sub { 199 | my ($pubsub, $db) = @_; 200 | ... 201 | }); 202 | 203 | Emitted after switching to a new database connection for sending and receiving 204 | notifications. 205 | 206 | =head1 ATTRIBUTES 207 | 208 | L implements the following attributes. 209 | 210 | =head2 mysql 211 | 212 | my $mysql = $pubsub->mysql; 213 | $pubsub = $pubsub->mysql(Mojo::mysql->new); 214 | 215 | L object this publish/subscribe container belongs to. 216 | 217 | =head1 METHODS 218 | 219 | L inherits all methods from L and 220 | implements the following new ones. 221 | 222 | =head2 listen 223 | 224 | my $cb = $pubsub->listen(foo => sub {...}); 225 | 226 | Subscribe to a channel, there is no limit on how many subscribers a channel can 227 | have. 228 | 229 | # Subscribe to the same channel twice 230 | $pubsub->listen(foo => sub { 231 | my ($pubsub, $payload) = @_; 232 | say "One: $payload"; 233 | }); 234 | $pubsub->listen(foo => sub { 235 | my ($pubsub, $payload) = @_; 236 | say "Two: $payload"; 237 | }); 238 | 239 | =head2 notify 240 | 241 | $pubsub = $pubsub->notify('foo'); 242 | $pubsub = $pubsub->notify(foo => 'bar'); 243 | 244 | Notify a channel. 245 | 246 | =head2 unlisten 247 | 248 | $pubsub = $pubsub->unlisten(foo => $cb); 249 | 250 | Unsubscribe from a channel. 251 | 252 | =head1 DEBUGGING 253 | 254 | You can set the C environment variable to get some 255 | advanced diagnostics information printed to C. 256 | 257 | MOJO_PUBSUB_DEBUG=1 258 | 259 | =head1 SEE ALSO 260 | 261 | L, L, L. 262 | 263 | =cut 264 | 265 | __DATA__ 266 | 267 | @@ pubsub 268 | -- 1 down 269 | drop table mojo_pubsub_subscribe; 270 | drop table mojo_pubsub_notify; 271 | 272 | -- 1 up 273 | drop table if exists mojo_pubsub_subscribe; 274 | drop table if exists mojo_pubsub_notify; 275 | 276 | create table mojo_pubsub_subscribe ( 277 | id integer auto_increment primary key, 278 | pid integer not null, 279 | channel varchar(64) not null, 280 | ts timestamp not null default current_timestamp, 281 | unique key subs_idx(pid, channel), 282 | key ts_idx(ts) 283 | ); 284 | 285 | create table mojo_pubsub_notify ( 286 | id integer auto_increment primary key, 287 | channel varchar(64) not null, 288 | payload text, 289 | ts timestamp not null default current_timestamp, 290 | key channel_idx(channel), 291 | key ts_idx(ts) 292 | ); 293 | 294 | delimiter // 295 | 296 | create trigger mojo_pubsub_notify_kill after insert on mojo_pubsub_notify 297 | for each row 298 | begin 299 | declare done boolean default false; 300 | declare t_pid integer; 301 | 302 | declare subs_c cursor for 303 | select pid from mojo_pubsub_subscribe where channel = NEW.channel; 304 | 305 | declare continue handler for not found set done = true; 306 | 307 | open subs_c; 308 | 309 | repeat 310 | fetch subs_c into t_pid; 311 | 312 | if not done and exists ( 313 | select 1 314 | from INFORMATION_SCHEMA.PROCESSLIST 315 | where ID = t_pid and STATE = 'User sleep') 316 | then 317 | kill query t_pid; 318 | end if; 319 | 320 | until done end repeat; 321 | 322 | close subs_c; 323 | end 324 | // 325 | 326 | delimiter ; 327 | -------------------------------------------------------------------------------- /lib/SQL/Abstract/mysql.pm: -------------------------------------------------------------------------------- 1 | package SQL::Abstract::mysql; 2 | use Mojo::Base 'SQL::Abstract'; 3 | 4 | BEGIN { *puke = \&SQL::Abstract::puke } 5 | 6 | sub insert { 7 | my ($self, $options) = (shift, $_[2] || {}); # ($self, $table, $data, $options) 8 | my ($sql, @bind) = $self->SUPER::insert(@_); 9 | 10 | # options 11 | if (exists $options->{on_conflict}) { 12 | my $on_conflict = $options->{on_conflict} // ''; 13 | if (ref $on_conflict eq 'HASH') { 14 | my ($s, @b) = $self->_update_set_values($on_conflict); 15 | $sql .= $self->_sqlcase(' on duplicate key update ') . $s; 16 | push @bind, @b; 17 | } 18 | elsif ($on_conflict eq 'ignore') { 19 | $sql =~ s/^(\w+)/{$self->_sqlcase('insert ignore')}/e; 20 | } 21 | elsif ($on_conflict eq 'replace') { 22 | $sql =~ s/^(\w+)/{$self->_sqlcase('replace')}/e; 23 | } 24 | else { 25 | puke qq{on_conflict value "$on_conflict" is not allowed}; 26 | } 27 | } 28 | 29 | return wantarray ? ($sql, @bind) : $sql; 30 | } 31 | 32 | sub _mysql_for { 33 | my ($self, $param) = @_; 34 | 35 | return $self->_SWITCH_refkind( 36 | $param => { 37 | SCALAR => sub { 38 | return $self->_sqlcase('lock in share mode') if $param eq 'share'; 39 | return $self->_sqlcase('for update') if $param eq 'update'; 40 | puke qq{for value "$param" is not allowed}; 41 | }, 42 | SCALARREF => sub { $self->_sqlcase('for ') . $$param }, 43 | } 44 | ); 45 | } 46 | 47 | sub _mysql_group_by { 48 | my ($self, $param) = @_; 49 | 50 | return $self->_SWITCH_refkind( 51 | $param => {ARRAYREF => sub { join ', ', map $self->_quote($_), @$param }, SCALARREF => sub {$$param},}); 52 | } 53 | 54 | sub _order_by { 55 | my ($self, $options) = @_; 56 | my ($sql, @bind) = (''); 57 | 58 | # Legacy 59 | return $self->SUPER::_order_by($options) if ref $options ne 'HASH' or grep {/^-(?:desc|asc)/i} keys %$options; 60 | 61 | # GROUP BY 62 | $sql .= $self->_sqlcase(' group by ') . $self->_mysql_group_by($options->{group_by}) if defined $options->{group_by}; 63 | 64 | # HAVING 65 | if (defined($options->{having})) { 66 | my ($s, @b) = $self->_recurse_where($options->{having}); 67 | $sql .= $self->_sqlcase(' having ') . $s; 68 | push @bind, @b; 69 | } 70 | 71 | # ORDER BY 72 | $sql .= $self->_order_by($options->{order_by}) if defined $options->{order_by}; 73 | 74 | # LIMIT / OFFSET 75 | for my $name (qw(limit offset)) { 76 | next unless defined $options->{$name}; 77 | $sql .= $self->_sqlcase(" $name ") . '?'; 78 | push @bind, $options->{$name}; 79 | } 80 | 81 | # FOR 82 | $sql .= ' ' . $self->_mysql_for($options->{for}) if defined $options->{for}; 83 | 84 | return $sql, @bind; 85 | } 86 | 87 | sub _select_fields { 88 | my ($self, $fields) = @_; 89 | 90 | return $fields unless ref $fields eq 'ARRAY'; 91 | 92 | my (@fields, @bind); 93 | for my $field (@$fields) { 94 | $self->_SWITCH_refkind( 95 | $field => { 96 | ARRAYREF => sub { 97 | puke 'field alias must be in the form [$name => $alias]' if @$field < 2; 98 | push @fields, $self->_quote($field->[0]) . $self->_sqlcase(' as ') . $self->_quote($field->[1]); 99 | }, 100 | ARRAYREFREF => sub { 101 | push @fields, shift @$$field; 102 | push @bind, @$$field; 103 | }, 104 | SCALARREF => sub { push @fields, $$field }, 105 | FALLBACK => sub { push @fields, $self->_quote($field) } 106 | } 107 | ); 108 | } 109 | 110 | return join(', ', @fields), @bind; 111 | } 112 | 113 | sub _table { 114 | my ($self, $table) = @_; 115 | 116 | return $self->SUPER::_table($table) unless ref $table eq 'ARRAY'; 117 | 118 | my (@tables, @joins); 119 | for my $jt (@$table) { 120 | if (ref $jt eq 'ARRAY') { push @joins, $jt } 121 | else { push @tables, $jt } 122 | } 123 | 124 | my $sql = $self->SUPER::_table(\@tables); 125 | my $sep = $self->{name_sep} // ''; 126 | for my $join (@joins) { 127 | 128 | my $type = ''; 129 | if ($join->[0] =~ /^-(.+)/) { 130 | $type = " $1"; 131 | shift @$join; 132 | } 133 | 134 | my $name = shift @$join; 135 | $sql .= $self->_sqlcase("$type join ") . $self->_quote($name); 136 | 137 | # NATURAL JOIN 138 | if ($type eq ' natural') { 139 | puke 'natural join must be in the form [-natural => $table]' if @$join; 140 | } 141 | 142 | # JOIN USING 143 | elsif (@$join == 1) { 144 | $sql .= $self->_sqlcase(' using (') . $self->_quote($join->[0]) . ')'; 145 | } 146 | 147 | # others 148 | else { 149 | puke 'join must be in the form [$table, $fk => $pk]' if @$join < 2; 150 | puke 'join requires an even number of keys' if @$join % 2; 151 | 152 | my @keys; 153 | while (my ($fk, $pk) = splice @$join, 0, 2) { 154 | push @keys, 155 | $self->_quote(index($fk, $sep) > 0 ? $fk : "$name.$fk") . ' = ' 156 | . $self->_quote(index($pk, $sep) > 0 ? $pk : "$tables[0].$pk"); 157 | } 158 | 159 | $sql .= $self->_sqlcase(' on ') . '(' . join($self->_sqlcase(' and '), @keys) . ')'; 160 | } 161 | 162 | } 163 | 164 | return $sql; 165 | } 166 | 167 | 1; 168 | 169 | =encoding utf8 170 | 171 | =head1 NAME 172 | 173 | SQL::Abstract::mysql - Generate SQL from Perl data structures for MySQL and MariaDB 174 | 175 | =head1 SYNOPSIS 176 | 177 | use SQL::Abstract::mysql; 178 | 179 | my $abstract = SQL::Abstract::mysql->new(quote_char => chr(96), name_sep => '.'); 180 | # The same as 181 | use Mojo::mysql; 182 | my $mysql = Mojo::mysql->new; 183 | my $abstract = $mysql->abstract; 184 | 185 | say $abstract->insert('some_table', \%some_values, \%some_options); 186 | say $abstract->select('some_table'); 187 | 188 | =head1 DESCRIPTION 189 | 190 | L extends L with a few MySQL / MariaDB 191 | features used by L. It was inspired by L. 192 | 193 | =head1 FUNCTIONS 194 | 195 | =head2 puke 196 | 197 | See L. 198 | 199 | =head1 METHODS 200 | 201 | L inherits all methods from L. 202 | 203 | =head2 insert 204 | 205 | my ($stmt, @bind) = $abstract->insert($table, \@values || \%fieldvals, \%options); 206 | 207 | This method extends L with the following functionality: 208 | 209 | =head3 ON CONFLICT 210 | 211 | The C option can be used to generate C, C and 212 | C queries. 213 | So far C<'ignore'> to pass C, C<'replace'> to pass C and 214 | hash references to pass C with conflict targets are supported. 215 | 216 | # "insert ignore into t (id, a) values (123, 'b')" 217 | $abstract->insert('t', {id => 123, a => 'b'}, {on_conflict => 'ignore'}); 218 | 219 | # "replace into t (id, a) values (123, 'b')" 220 | $abstract->insert('t', {id => 123, a => 'b'}, {on_conflict => 'replace'}); 221 | 222 | # "insert into t (id, a) values (123, 'b') on duplicate key update c='d'" 223 | $abstract->insert('t', {id => 123, a => 'b'}, {on_conflict => {c => 'd'}}); 224 | 225 | =head2 select 226 | 227 | my ($stmt, @bind) = $abstract->select($source, $fields, $where, $order); 228 | my ($stmt, @bind) = $abstract->select($source, $fields, $where, \%options); 229 | 230 | This method extends L with the following functionality: 231 | 232 | =head3 AS 233 | 234 | The C<$fields> argument accepts array references containing array references 235 | with field names and aliases, as well as array references containing scalar 236 | references to pass literal SQL and array reference references to pass literal 237 | SQL with bind values. 238 | 239 | # "select foo as bar from some_table" 240 | $abstract->select('some_table', [[foo => 'bar']]); 241 | 242 | # "select foo, bar as baz, yada from some_table" 243 | $abstract->select('some_table', ['foo', [bar => 'baz'], 'yada']); 244 | 245 | # "select extract(epoch from foo) as foo, bar from some_table" 246 | $abstract->select('some_table', [\'extract(epoch from foo) as foo', 'bar']); 247 | 248 | # "select 'test' as foo, bar from some_table" 249 | $abstract->select('some_table', [\['? as foo', 'test'], 'bar']); 250 | 251 | =head3 JOIN 252 | 253 | The C<$source> argument accepts array references containing not only table 254 | names, but also array references with tables to generate C clauses for. 255 | 256 | # "select * from foo join bar on (bar.foo_id = foo.id)" 257 | $abstract->select(['foo', ['bar', foo_id => 'id']]); 258 | 259 | # "select * from foo join bar on (foo.id = bar.foo_id)" 260 | $abstract->select(['foo', ['bar', 'foo.id' => 'bar.foo_id']]); 261 | 262 | # -left, -right, -inner 263 | # "select * from foo left join bar on (bar.foo_id = foo.id)" 264 | $abstract->select(['foo', [-left => 'bar', foo_id => 'id']]); 265 | 266 | # -natural 267 | # "select * from foo natural join bar" 268 | $abstract->select(['foo', [-natural => 'bar']]); 269 | 270 | # join using 271 | # "select * from foo join bar using (foo_id)" 272 | $abstract->select(['foo', [bar => 'foo_id']]); 273 | 274 | # more than one table 275 | # "select * from foo join bar on (bar.foo_id = foo.id) join baz on (baz.foo_id = foo.id)" 276 | $abstract->select(['foo', ['bar', foo_id => 'id'], ['baz', foo_id => 'id']]); 277 | 278 | # more than one field 279 | # "select * from foo left join bar on (bar.foo_id = foo.id and bar.foo_id2 = foo.id2)" 280 | $abstract->select(['foo', [-left => 'bar', foo_id => 'id', foo_id2 => 'id2']]); 281 | 282 | =head2 where 283 | 284 | my ($stmt, @bind) = $abstract->where($where, \%options); 285 | 286 | This method extends L with the following functionality: 287 | 288 | =head3 FOR 289 | 290 | The C option can be used to generate C queries with C clauses. So far array references to pass a list of fields and scalar 310 | references to pass literal SQL are supported. 311 | 312 | # "select * from some_table group by foo, bar" 313 | $abstract->select('some_table', '*', undef, {group_by => ['foo', 'bar']}); 314 | 315 | # "select * from some_table group by foo, bar" 316 | $abstract->select('some_table', '*', undef, {group_by => \'foo, bar'}); 317 | 318 | =head3 HAVING 319 | 320 | The C option can be used to generate C queries 338 | with C and C clauses. 339 | 340 | # "select * from some_table limit 10" 341 | $abstract->select('some_table', '*', undef, {limit => 10}); 342 | 343 | # "select * from some_table offset 5" 344 | $abstract->select('some_table', '*', undef, {offset => 5}); 345 | 346 | # "select * from some_table limit 10 offset 5" 347 | $abstract->select('some_table', '*', undef, {limit => 10, offset => 5}); 348 | 349 | =head1 SEE ALSO 350 | 351 | L, L, L, L. 352 | 353 | =cut 354 | -------------------------------------------------------------------------------- /lib/Mojo/mysql/Database.pm: -------------------------------------------------------------------------------- 1 | package Mojo::mysql::Database; 2 | use Mojo::Base 'Mojo::EventEmitter'; 3 | 4 | use Carp; 5 | use Mojo::IOLoop; 6 | use Mojo::JSON 'to_json'; 7 | use Mojo::mysql::Results; 8 | use Mojo::mysql::Transaction; 9 | use Mojo::Promise; 10 | use Mojo::Util 'monkey_patch'; 11 | use Scalar::Util 'weaken'; 12 | 13 | has [qw(dbh mysql)]; 14 | has results_class => 'Mojo::mysql::Results'; 15 | 16 | for my $name (qw(delete insert select update)) { 17 | monkey_patch __PACKAGE__, $name, sub { 18 | my $self = shift; 19 | my @cb = ref $_[-1] eq 'CODE' ? pop : (); 20 | return $self->query($self->mysql->abstract->$name(@_), @cb); 21 | }; 22 | monkey_patch __PACKAGE__, "${name}_p", sub { 23 | my $self = shift; 24 | return $self->query_p($self->mysql->abstract->$name(@_)); 25 | }; 26 | } 27 | 28 | sub DESTROY { 29 | my $self = shift; 30 | return if ${^GLOBAL_PHASE} eq 'DESTRUCT'; 31 | $self->_cleanup_sth; 32 | return unless (my $mysql = $self->mysql) and (my $dbh = $self->dbh); 33 | $mysql->_enqueue($dbh, $self->{handle}); 34 | } 35 | 36 | sub backlog { scalar @{shift->{waiting} || []} } 37 | 38 | sub begin { 39 | my $self = shift; 40 | my $tx = Mojo::mysql::Transaction->new(db => $self); 41 | weaken $tx->{db}; 42 | return $tx; 43 | } 44 | 45 | sub disconnect { 46 | my $self = shift; 47 | $self->_cleanup_sth; 48 | $self->_unwatch; 49 | $self->dbh->disconnect; 50 | } 51 | 52 | sub pid { shift->_dbh_attr('mysql_thread_id') } 53 | 54 | sub ping { shift->dbh->ping } 55 | 56 | sub query { 57 | my ($self, $query) = (shift, shift); 58 | my $cb = ref $_[-1] eq 'CODE' ? pop : undef; 59 | 60 | # Blocking 61 | unless ($cb) { 62 | Carp::confess('Cannot perform blocking query, while waiting for async response') if $self->backlog; 63 | my $sth = $self->dbh->prepare($query); 64 | local $sth->{HandleError} = sub { $_[0] = Carp::shortmess($_[0]); 0 }; 65 | _bind_params($sth, @_); 66 | my $rv = $sth->execute; 67 | my $res = $self->results_class->new(db => $self, is_blocking => 1, sth => $sth); 68 | $res->{affected_rows} = defined $rv && $rv >= 0 ? 0 + $rv : undef; 69 | return $res; 70 | } 71 | 72 | # Non-blocking 73 | push @{$self->{waiting}}, {args => [@_], err => Carp::shortmess('__MSG__'), cb => $cb, query => $query}; 74 | $self->$_ for qw(_next _watch); 75 | return $self; 76 | } 77 | 78 | sub query_p { 79 | my $self = shift; 80 | my $promise = Mojo::Promise->new; 81 | $self->query(@_ => sub { $_[1] ? $promise->reject($_[1]) : $promise->resolve($_[2]) }); 82 | return $promise; 83 | } 84 | 85 | sub quote { shift->dbh->quote(shift) } 86 | 87 | sub quote_id { shift->dbh->quote_identifier(shift) } 88 | 89 | sub tables { 90 | shift->query('show tables')->arrays->reduce(sub { push @$a, $b->[0]; $a }, []); 91 | } 92 | 93 | sub _bind_params { 94 | my $sth = shift; 95 | for my $i (0 .. $#_) { 96 | my $param = $_[$i]; 97 | my %attrs; 98 | if (ref $param eq 'HASH') { 99 | if (exists $param->{json}) { 100 | $param = to_json $param->{json}; 101 | } 102 | elsif (exists $param->{type} && exists $param->{value}) { 103 | ($param, $attrs{TYPE}) = @$param{qw(value type)}; 104 | } 105 | } 106 | 107 | $sth->bind_param($i + 1, $param, \%attrs); 108 | } 109 | return $sth; 110 | } 111 | 112 | sub _cleanup_sth { 113 | my $self = shift; 114 | delete $self->{done_sth}; 115 | $_->{cb}($self, 'Premature connection close', undef) for @{delete $self->{waiting} || []}; 116 | } 117 | 118 | sub _dbh_attr { 119 | my $self = shift; 120 | my $dbh = ref $self ? $self->dbh : shift; 121 | my $name = shift; 122 | $name =~ s!^mysql!{lc $dbh->{Driver}{Name}}!e; 123 | return $dbh->{$name} = shift if @_; 124 | return $dbh->{$name}; 125 | } 126 | 127 | sub _next { 128 | my $self = shift; 129 | 130 | return unless my $next = $self->{waiting}[0]; 131 | return if $next->{sth}; 132 | 133 | my $dbh = $self->dbh; 134 | my $flag = lc $dbh->{Driver}{Name} eq 'mariadb' ? 'mariadb_async' : 'async'; 135 | my $sth = $next->{sth} = $self->dbh->prepare($next->{query}, {$flag => 1}); 136 | _bind_params($sth, @{$next->{args}}); 137 | $sth->execute; 138 | } 139 | 140 | sub _unwatch { 141 | Mojo::IOLoop->singleton->reactor->remove(delete $_[0]->{handle}) if $_[0]->{handle}; 142 | } 143 | 144 | sub _watch { 145 | my $self = shift; 146 | return if $self->{handle}; 147 | 148 | my $dbh = $self->dbh; 149 | my $driver = lc $dbh->{Driver}{Name}; 150 | my $ready_method = "${driver}_async_ready"; 151 | my $result_method = "${driver}_async_result"; 152 | my $fd = $driver eq 'mariadb' ? $dbh->mariadb_sockfd : $dbh->mysql_fd; 153 | open $self->{handle}, '<&', $fd or die "Could not dup $driver fd: $!"; 154 | Mojo::IOLoop->singleton->reactor->io( 155 | $self->{handle} => sub { 156 | return unless my $waiting = $self->{waiting}; 157 | return unless @$waiting and $waiting->[0]{sth} and $waiting->[0]{sth}->$ready_method; 158 | my ($cb, $err, $sth) = @{shift @$waiting}{qw(cb err sth)}; 159 | 160 | # Do not raise exceptions inside the event loop 161 | my $rv = do { local $sth->{RaiseError} = 0; $sth->$result_method }; 162 | my $res = $self->results_class->new(db => $self, sth => $sth); 163 | 164 | $err = undef if defined $rv; 165 | $err =~ s!\b__MSG__\b!{$dbh->errstr}!e if defined $err; 166 | $res->{affected_rows} = defined $rv && $rv >= 0 ? 0 + $rv : undef; 167 | 168 | $self->$cb($err, $res); 169 | $self->_next; 170 | $self->_unwatch unless $self->backlog; 171 | } 172 | )->watch($self->{handle}, 1, 0); 173 | } 174 | 175 | 1; 176 | 177 | =encoding utf8 178 | 179 | =head1 NAME 180 | 181 | Mojo::mysql::Database - Database 182 | 183 | =head1 SYNOPSIS 184 | 185 | use Mojo::mysql::Database; 186 | 187 | my $db = Mojo::mysql::Database->new(mysql => $mysql, dbh => $dbh); 188 | 189 | =head1 DESCRIPTION 190 | 191 | L is a container for database handles used by L. 192 | 193 | =head1 ATTRIBUTES 194 | 195 | L implements the following attributes. 196 | 197 | =head2 dbh 198 | 199 | my $dbh = $db->dbh; 200 | $db = $db->dbh(DBI->new); 201 | 202 | Database handle used for all queries. 203 | 204 | =head2 mysql 205 | 206 | my $mysql = $db->mysql; 207 | $db = $db->mysql(Mojo::mysql->new); 208 | 209 | L object this database belongs to. 210 | 211 | =head2 results_class 212 | 213 | $class = $db->results_class; 214 | $db = $db->results_class("MyApp::Results"); 215 | 216 | Class to be used by L, defaults to L. Note that 217 | this class needs to have already been loaded before L is called. 218 | 219 | =head1 METHODS 220 | 221 | L inherits all methods from L and 222 | implements the following new ones. 223 | 224 | =head2 backlog 225 | 226 | my $num = $db->backlog; 227 | 228 | Number of waiting non-blocking queries. 229 | 230 | =head2 begin 231 | 232 | my $tx = $db->begin; 233 | 234 | Begin transaction and return L object, which will 235 | automatically roll back the transaction unless 236 | L has been called before it is destroyed. 237 | 238 | # Add names in a transaction 239 | eval { 240 | my $tx = $db->begin; 241 | $db->query('insert into names values (?)', 'Baerbel'); 242 | $db->query('insert into names values (?)', 'Wolfgang'); 243 | $tx->commit; 244 | }; 245 | say $@ if $@; 246 | 247 | =head2 delete 248 | 249 | my $results = $db->delete($table, \%where); 250 | 251 | Generate a C statement with L (usually an 252 | L object) and execute it with L. You can also append a 253 | callback to perform operations non-blocking. 254 | 255 | $db->delete(some_table => sub { 256 | my ($db, $err, $results) = @_; 257 | ... 258 | }); 259 | Mojo::IOLoop->start unless Mojo::IOLoop->is_running; 260 | 261 | =head2 delete_p 262 | 263 | my $promise = $db->delete_p($table, \%where, \%options); 264 | 265 | Same as L, but performs all operations non-blocking and returns a 266 | L object instead of accepting a callback. 267 | 268 | $db->delete_p('some_table')->then(sub { 269 | my $results = shift; 270 | ... 271 | })->catch(sub { 272 | my $err = shift; 273 | ... 274 | })->wait; 275 | 276 | =head2 disconnect 277 | 278 | $db->disconnect; 279 | 280 | Disconnect database handle and prevent it from getting cached again. 281 | 282 | =head2 insert 283 | 284 | my $results = $db->insert($table, \@values || \%fieldvals, \%options); 285 | 286 | Generate an C statement with L (usually an 287 | L object) and execute it with L. You can also append a 288 | callback to perform operations non-blocking. 289 | 290 | $db->insert(some_table => {foo => 'bar'} => sub { 291 | my ($db, $err, $results) = @_; 292 | ... 293 | }); 294 | Mojo::IOLoop->start unless Mojo::IOLoop->is_running; 295 | 296 | =head2 insert_p 297 | 298 | my $promise = $db->insert_p($table, \@values || \%fieldvals, \%options); 299 | 300 | Same as L, but performs all operations non-blocking and returns a 301 | L object instead of accepting a callback. 302 | 303 | $db->insert_p(some_table => {foo => 'bar'})->then(sub { 304 | my $results = shift; 305 | ... 306 | })->catch(sub { 307 | my $err = shift; 308 | ... 309 | })->wait; 310 | 311 | =head2 pid 312 | 313 | my $pid = $db->pid; 314 | 315 | Return the connection id of the backend server process. 316 | 317 | =head2 ping 318 | 319 | my $bool = $db->ping; 320 | 321 | Check database connection. 322 | 323 | =head2 query 324 | 325 | my $results = $db->query('select * from foo'); 326 | my $results = $db->query('insert into foo values (?, ?, ?)', @values); 327 | my $results = $db->query('insert into foo values (?)', {json => {bar => 'baz'}}); 328 | my $results = $db->query('insert into foo values (?)', {type => SQL_INTEGER, value => 42}); 329 | 330 | Execute a blocking statement and return a L object with the 331 | results. You can also append a callback to perform operation non-blocking. 332 | 333 | $db->query('select * from foo' => sub { 334 | my ($db, $err, $results) = @_; 335 | ... 336 | }); 337 | Mojo::IOLoop->start unless Mojo::IOLoop->is_running; 338 | 339 | Hash reference arguments containing a value named C, will be encoded to 340 | JSON text with L. To accomplish the reverse, you can use 341 | the method L, which automatically decodes data back 342 | to Perl data structures. 343 | 344 | $db->query('insert into foo values (x) values (?)', {json => {bar => 'baz'}}); 345 | $db->query('select * from foo')->expand->hash->{x}{bar}; # baz 346 | 347 | Hash reference arguments containing values named C and C can be 348 | used to bind specific L data types (see L) to 349 | placeholders. This is needed to pass binary data in parameters; see 350 | L for more information. 351 | 352 | # Insert binary data 353 | use DBI ':sql_types'; 354 | $db->query('insert into bar values (?)', {type => SQL_BLOB, value => $bytes}); 355 | 356 | =head2 query_p 357 | 358 | my $promise = $db->query_p('select * from foo'); 359 | 360 | Same as L, but performs all operations non-blocking and returns a 361 | L object instead of accepting a callback. 362 | 363 | $db->query_p('insert into foo values (?, ?, ?)' => @values)->then(sub { 364 | my $results = shift; 365 | ... 366 | })->catch(sub { 367 | my $err = shift; 368 | ... 369 | })->wait; 370 | 371 | =head2 quote 372 | 373 | my $escaped = $db->quote($str); 374 | 375 | Quote a string literal for use as a literal value in an SQL statement. 376 | 377 | =head2 quote_id 378 | 379 | my $escaped = $db->quote_id($id); 380 | 381 | Quote an identifier (table name etc.) for use in an SQL statement. 382 | 383 | =head2 select 384 | 385 | my $results = $db->select($source, $fields, $where, $order); 386 | 387 | Generate a C