├── .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"sth"> and return it as an array reference. Note that
157 | L"finish"> 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"sth"> 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"sth"> and return it as a hash reference. Note that
209 | L"finish"> 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"name">, 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"name">.
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"active"> to a different version, up or down, defaults to
279 | using L"latest">. 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