├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .perltidyrc
├── .pls_cache
└── index
├── Changes
├── MANIFEST.SKIP
├── Makefile.PL
├── README.md
├── examples
├── blog
│ ├── blog.conf
│ ├── lib
│ │ ├── Blog.pm
│ │ └── Blog
│ │ │ ├── Controller
│ │ │ └── Posts.pm
│ │ │ └── Model
│ │ │ └── Posts.pm
│ ├── migrations
│ │ └── blog.sql
│ ├── script
│ │ └── blog
│ └── templates
│ │ ├── layouts
│ │ └── blog.html.ep
│ │ └── posts
│ │ ├── _form.html.ep
│ │ ├── create.html.ep
│ │ ├── edit.html.ep
│ │ ├── index.html.ep
│ │ └── show.html.ep
└── chat.pl
├── lib
├── Mojo
│ ├── mysql.pm
│ └── mysql
│ │ ├── Database.pm
│ │ ├── Migrations.pm
│ │ ├── PubSub.pm
│ │ ├── Results.pm
│ │ └── Transaction.pm
└── SQL
│ └── Abstract
│ └── mysql.pm
└── t
├── 00-project.t
├── async_query_in_flight.t
├── blocking-leak.t
├── connection.t
├── crud.t
├── database.t
├── destroy.t
├── json.t
├── mariadb.t
├── migrations.t
├── migrations
└── test.sql
├── mysql.t
├── mysql_auto_reconnect.t
├── mysql_lite_app.t
├── pubsub.t
├── results.t
├── results_methods.t
├── sql-live.t
├── sql.t
├── strict-mode.t
├── test-dbi-async.t
└── utf8.t
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.pls_cache/index:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhthorsen/mojo-mysql/550d7cfd2a7cf00c96a79fb7ca3ffc4b4947031d/.pls_cache/index
--------------------------------------------------------------------------------
/Changes:
--------------------------------------------------------------------------------
1 | Revision history for perl distribution Mojo-mysql
2 |
3 | 1.27 2023-10-26T20:59:16
4 | - Fix not cleaning up during GLOBAL_PHASE is DESTRUCT
5 | Contributor: Lasse Løvik
6 |
7 | 1.26 2022-12-09T10:41:01
8 | - Add support for "MOJO_MYSQL_PREFER_DRIVER" in Makefile.PL
9 | - Add support for autodetecting DBD::MariaDB
10 | - Fix spelling mistakes #88
11 | Contributor: Lucas Tiago de Moraes
12 | - Specified Perl version
13 | - Updated basic repository files
14 | - Updated contributors list
15 |
16 | 1.25 2021-11-22T18:13:22+0900
17 | - Changed DBD::mysql is not a dependency if DBD::MariaDB 1.21 is already installed #88
18 |
19 | 1.24 2021-10-06T07:57:45+0900
20 | - Fixed last_insert_id() and warnings_count() for MariaDB #86
21 | Contributor: Dan Book
22 |
23 | 1.23 2021-09-11T10:36:17+0200
24 | - Fix version number issues #84
25 | - Internal changes regarding MariaDB/mysql attributes
26 |
27 | 1.21 2021-04-28T12:15:39+0900
28 | - Fix uninitialized warning with expand(1) #83
29 | - Removed delay() from tests and documentation
30 |
31 | 1.20 2020-09-05T13:27:52+0900
32 | - Mojo::mysql::PubSub is less susceptible to deadlocks/timeouts #80
33 | Contributor: Larry Leszczynski
34 |
35 | 1.19 2020-05-01T06:58:48+0900
36 | - Fix documentation for Mojo::mysql::close_idle_connections()
37 | - Add documentation for SQL::Abstract::mysql::where() #78
38 | - Made SQL::Abstract::mysql more compatible with SQL::Abstract #77
39 | Contributor: Matt S Trout
40 |
41 | 1.18 2019-12-01T09:41:10+0100
42 | - Add missing code for SELECT AS
43 |
44 | 1.17 2019-08-01T09:44:57+0200
45 | - Fix leaking $sth when used in blocking mode, fixes #66
46 |
47 | 1.16 2019-06-25T06:33:55+0200
48 | - Add DBI to prerequisites
49 | Contributor: Mohammad S Anwar
50 |
51 | 1.15 2019-04-22T06:41:34+0200
52 | - Add support for NATURAL JOIN and JOIN USING #59
53 |
54 | 1.14 2019-03-23T08:07:17+0100
55 | - Correct handling of fetchall and arrays in Results
56 |
57 | 1.13 2019-03-02T11:27:01+0800
58 | - Add support for DBD::MariaDB #47
59 | - Add missing code for SQL JOIN #56
60 | Contributor: Tekki
61 | - Made it clearer that PubSub is an experiment
62 |
63 | 1.12 2019-01-05T12:34:13+0900
64 | - Bumped Mojolicious version to 8.03
65 | - Bumped SQL::Abstract version to 1.86 #49
66 |
67 | 1.11 2018-12-18T19:27:08+0900
68 | - Avoid "Gathering async_query_in_flight results for the wrong handle" warning
69 |
70 | 1.10 2018-12-18T07:25:14+0900
71 | - Add SQL::Abstract::mysql
72 | Contributor: Rolf Stöckli
73 |
74 | 1.09 2018-11-27T09:32:01+0900
75 | - Fix MariaDB/MySQL incompatibility #41
76 | Contributor: Rolf Stöckli
77 | - Fix documentation mismatch regarding "mysql_client_found_rows" #42
78 | Contributor: Yuriy Zhilovets
79 |
80 | 1.08 2018-11-13T17:31:49+0900
81 | - Fix query() with callback returns $self
82 | - Fix Gathering async_query_in_flight results for the wrong handle bug
83 | - Add close_idle_connections to Mojo::mysql
84 | - Add support for working with JSON
85 | - Add tables method to Mojo::mysql::Database
86 | - Change database name is optional in constructor #38
87 |
88 | 1.07 2018-05-03T12:25:08+0200
89 | - Fix using "mysql_socket" instead of "host" when connecting to a unix socket #34
90 | - Allow constructor to take a single hashref #37
91 |
92 | 1.06 2018-02-27T19:32:40+0100
93 | - Changed from_string() to also accept Mojo::URL objects #36
94 | Contributor: Karl Rune Nilsen
95 |
96 | 1.05 2017-11-11T10:04:40+0800
97 | - Add delete_p(), insert_p(), query_p(), select_p() and update_p()
98 |
99 | 1.04 2017-08-14T19:22:33+0200
100 | - Documented "mysql_enable_utf8" v.s. "mysql_enable_utf8mb4" #32
101 | - Can pass on attributes to new()
102 |
103 | 1.03 2017-05-21T23:19:29+0200
104 | - Add ability to set types of query parameters #31
105 | Contributor: Dan Book
106 |
107 | 1.02 2017-05-15T20:34:01+0200
108 | - Fix utf8 handling in DBD::mysql 4.042
109 | - Prevent warnings when creating the mojo_migrations table #26
110 | - Add proper quoting of table and column names #30
111 | - Add warning when using Mojo::mysql::PubSub
112 |
113 | 1.01 2017-03-25T08:24:29+0100
114 | - Add strict_mode() method and constructor #29
115 |
116 | 1.00 2017-02-12T18:30:58+0100
117 | - Add support for generating queries with SQL::Abstract
118 | - Add abstract attribute to Mojo::Pg
119 | - Add delete, insert, select and update methods to Mojo::Pg::Database
120 | - Add database_class attribute to Mojo::mysql
121 | - Add results_class attribute to Mojo::mysql::Database
122 | - Improved contextual caller information in query error messages
123 | - Compatible with Mojolicious 7.18
124 |
125 | 0.14 2016-02-15T14:06:24+0100
126 | - Add Mojo::mysql::auto_migrate
127 |
128 | 0.13 2016-01-27T21:05:37+0100
129 | - Remove deprecrated do() method
130 | - Add finish() to Mojo::mysql::Results
131 | - Fix bug where non-blocking queries could get lost after the database
132 | connection got closed unexpectedly
133 | https://github.com/kraih/mojo-pg/commit/2165b8e1131f2a5044ec2aae1c0ba8a00232b7c8
134 | - Improved Mojo::mysql::Migrations to detect if the currently active version
135 | is greater than the latest version.
136 | https://github.com/kraih/mojo-pg/commit/92bc312e725042b748950b9c61319d0256d0004a
137 |
138 | 0.12 2015-05-02T17:55:13Z
139 | - Added module Mojo::mysql::PubSub.
140 | - Added pubsub attribute to Mojo::mysql.
141 |
142 | 0.11 2015-04-06T03:38:31Z
143 | - Fixed bug in Mojo::mysql::Migrations where migrations would sometimes be
144 | executed in the wrong order.
145 |
146 | 0.10 2015-04-05T23:32:03Z
147 | - Fixed bug in Mojo::mysql::Migrations where the latest version could not
148 | always be determined correctly. (Hernan Lopes)
149 | - Updated blog example application from Mojo::Pg
150 |
151 | 0.09 2015-03-29T18:29:35Z
152 | - Fixed Mojo::mysql::Migrations to allow delimiter in comments and quoted
153 | strings
154 | - delimiter support in Mojo::mysql::Migrations, allows creation of stored
155 | procedures and triggers in migration scripts
156 | - 'quote' and 'quote_id' methods in Mojo::mysql::Database
157 |
158 | 0.08 2015-03-24T13:14:32Z
159 | - blog example from Mojo::Pg
160 | - better examples in POD
161 | - Improved Mojo::mysql::Migrations to make no changes to the database when
162 | checking the currently active version.
163 | - Fixed Mojo::mysql::Migrations to handle UTF-8 encoded files correctly
164 |
165 | 0.07 2015-03-09T13:34:31Z
166 | - Deprecated Mojo::mysql::Database::do in favour of
167 | Mojo::mysql::Database::query as in Mojo::Pg
168 | - Some new methods in Mojo::mysql::Result eliminating need to access sth
169 | - bugfix in Mojo::mysql::Migrations with trailing whitespace after last
170 | semicolon
171 |
172 | 0.06 2015-02-25T17:31:24Z
173 | - OO Mojo::Loader API is deprecated in Mojolicious 5.81
174 |
175 | 0.05 2015-01-22T00:14:11Z
176 | - Do not cache statement handles in Mojo::mysql::Database.
177 | - Synced changes from Mojo::Pg
178 | - utf8 enabled by default
179 |
180 | 0.04 2015-01-02T12:15:26Z
181 | - Add support for migrations #3 Contributor: Curt Hochwender
182 | - Add Mojo::mysql::Migrations.
183 | - Add migrations attribute to Mojo::msyql
184 | - Add db attribute to Mojo::mysql::Transaction.
185 | - Fix bug where Perl would close the DBD::mysql file descriptor after it
186 | had been used with the event loop.
187 | - Remove dbh attribute from Mojo::mysql::Transaction
188 | - Updated Mojolicious requirement to 5.49 to ensure migrations in the DATA
189 | section are not served as static files
190 |
191 | 0.03 2014-10-13T13:39:59Z
192 | - Removed commit and rollback methods from Mojo::mysql::Database.
193 | - Added Mojo::mysql::Transaction.
194 |
195 | 0.02 2014-10-12T18:14:33Z
196 | - Force mysql_auto_reconnect = 0 to avoid nasty reconnect bugs under some
197 | environments. https://metacpan.org/pod/DBD::mysql#mysql_auto_reconnect
198 |
199 | 0.01 2014-10-11T17:34:05Z
200 | - First release.
201 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NAME
2 |
3 | Mojo::mysql - Mojolicious and Async MySQL/MariaDB
4 |
5 | # SYNOPSIS
6 |
7 | use Mojo::mysql;
8 |
9 | # Connect to a local database
10 | my $mysql = Mojo::mysql->strict_mode('mysql://username@/test');
11 |
12 | # Connect to a remote database
13 | my $mysql = Mojo::mysql->strict_mode('mysql://username:password@hostname/test');
14 | # MySQL >= 8.0:
15 | my $mysql = Mojo::mysql->strict_mode('mysql://username:password@hostname/test;mysql_ssl=1');
16 |
17 | # Use DBD::MariaDB instead of DBD::mysql
18 | my $mysql = Mojo::mysql->strict_mode('mariadb://username@/test');
19 |
20 | # Create a table
21 | $mysql->db->query(
22 | 'create table names (id integer auto_increment primary key, name text)');
23 |
24 | # Insert a few rows
25 | my $db = $mysql->db;
26 | $db->query('insert into names (name) values (?)', 'Sara');
27 | $db->query('insert into names (name) values (?)', 'Stefan');
28 |
29 | # Insert more rows in a transaction
30 | eval {
31 | my $tx = $db->begin;
32 | $db->query('insert into names (name) values (?)', 'Baerbel');
33 | $db->query('insert into names (name) values (?)', 'Wolfgang');
34 | $tx->commit;
35 | };
36 | say $@ if $@;
37 |
38 | # Insert another row and return the generated id
39 | say $db->query('insert into names (name) values (?)', 'Daniel')
40 | ->last_insert_id;
41 |
42 | # Use SQL::Abstract::mysql to generate queries for you
43 | $db->insert('names', {name => 'Isabel'});
44 | say $db->select('names', undef, {name => 'Isabel'})->hash->{id};
45 | $db->update('names', {name => 'Bel'}, {name => 'Isabel'});
46 | $db->delete('names', {name => 'Bel'});
47 |
48 | # Select one row at a time
49 | my $results = $db->query('select * from names');
50 | while (my $next = $results->hash) {
51 | say $next->{name};
52 | }
53 |
54 | # Select all rows blocking
55 | $db->query('select * from names')
56 | ->hashes->map(sub { $_->{name} })->join("\n")->say;
57 |
58 | # Select all rows non-blocking
59 | $db->query('select * from names' => sub {
60 | my ($db, $err, $results) = @_;
61 | $results->hashes->map(sub { $_->{name} })->join("\n")->say;
62 | });
63 |
64 | # Concurrent non-blocking queries (synchronized with promises)
65 | my $now = $db->query_p('select now() as now');
66 | my $names = $db->query_p('select * from names');
67 | Mojo::Promise->all($now, $names)->then(sub {
68 | my ($now, $names) = @_;
69 | say $now->[0]->hash->{now};
70 | say $_->{name} for $names->[0]->hashes->each;
71 | })->catch(sub {
72 | my $err = shift;
73 | warn "Something went wrong: $err";
74 | })->wait;
75 |
76 | Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
77 |
78 | # DESCRIPTION
79 |
80 | [Mojo::mysql](https://metacpan.org/pod/Mojo%3A%3Amysql) is a tiny wrapper around [DBD::mysql](https://metacpan.org/pod/DBD%3A%3Amysql) and [DBD::MariaDB](https://metacpan.org/pod/DBD%3A%3AMariaDB) that
81 | makes [MySQL](http://www.mysql.org) and [MariaDB](https://mariadb.org/) a lot
82 | of fun to use with the [Mojolicious](http://mojolicio.us) real-time web
83 | framework.
84 |
85 | The two DBD drivers are compatible with both MySQL and MariaDB, but they offer
86 | different ["options"](#options). [DBD::MariaDB](https://metacpan.org/pod/DBD%3A%3AMariaDB) should have better unicode support
87 | though and might become the default in the future.
88 |
89 | Database and handles are cached automatically, so they can be reused
90 | transparently to increase performance. And you can handle connection timeouts
91 | gracefully by holding on to them only for short amounts of time.
92 |
93 | use Mojolicious::Lite;
94 | use Mojo::mysql;
95 |
96 | helper mysql =>
97 | sub { state $mysql = Mojo::mysql->strict_mode('mysql://sri:s3cret@localhost/db') };
98 |
99 | get '/' => sub {
100 | my $c = shift;
101 | my $db = $c->mysql->db;
102 | $c->render(json => $db->query('select now() as time')->hash);
103 | };
104 |
105 | app->start;
106 |
107 | While all I/O operations are performed blocking, you can wait for long running
108 | queries asynchronously, allowing the [Mojo::IOLoop](https://metacpan.org/pod/Mojo%3A%3AIOLoop) event loop to perform
109 | other tasks in the meantime. Since database connections usually have a very low
110 | latency, this often results in very good performance.
111 |
112 | Every database connection can only handle one active query at a time, this
113 | includes asynchronous ones. So if you start more than one, they will be put on
114 | a waiting list and performed sequentially. To perform multiple queries
115 | concurrently, you have to use multiple connections.
116 |
117 | # Performed sequentially (10 seconds)
118 | my $db = $mysql->db;
119 | $db->query('select sleep(5)' => sub {...});
120 | $db->query('select sleep(5)' => sub {...});
121 |
122 | # Performed concurrently (5 seconds)
123 | $mysql->db->query('select sleep(5)' => sub {...});
124 | $mysql->db->query('select sleep(5)' => sub {...});
125 |
126 | All cached database handles will be reset automatically if a new process has
127 | been forked, this allows multiple processes to share the same [Mojo::mysql](https://metacpan.org/pod/Mojo%3A%3Amysql)
128 | object safely.
129 |
130 | # EVENTS
131 |
132 | [Mojo::mysql](https://metacpan.org/pod/Mojo%3A%3Amysql) inherits all events from [Mojo::EventEmitter](https://metacpan.org/pod/Mojo%3A%3AEventEmitter) and can emit the
133 | following new ones.
134 |
135 | ## connection
136 |
137 | $mysql->on(connection => sub {
138 | my ($mysql, $dbh) = @_;
139 | ...
140 | });
141 |
142 | Emitted when a new database connection has been established.
143 |
144 | # ATTRIBUTES
145 |
146 | [Mojo::mysql](https://metacpan.org/pod/Mojo%3A%3Amysql) implements the following attributes.
147 |
148 | ## abstract
149 |
150 | $abstract = $mysql->abstract;
151 | $mysql = $mysql->abstract(SQL::Abstract::mysql->new);
152 |
153 | [SQL::Abstract::mysql](https://metacpan.org/pod/SQL%3A%3AAbstract%3A%3Amysql) object used to generate CRUD queries for [Mojo::mysql::Database](https://metacpan.org/pod/Mojo%3A%3Amysql%3A%3ADatabase).
154 |
155 | # Generate statements and bind values
156 | my ($stmt, @bind) = $mysql->abstract->select('names');
157 |
158 | ## auto\_migrate
159 |
160 | my $bool = $mysql->auto_migrate;
161 | $mysql = $mysql->auto_migrate($bool);
162 |
163 | Automatically migrate to the latest database schema with ["migrations"](#migrations), as
164 | soon as the first database connection has been established.
165 |
166 | Defaults to false.
167 |
168 | ## database\_class
169 |
170 | $class = $mysql->database_class;
171 | $mysql = $mysql->database_class("MyApp::Database");
172 |
173 | Class to be used by ["db"](#db), defaults to [Mojo::mysql::Database](https://metacpan.org/pod/Mojo%3A%3Amysql%3A%3ADatabase). Note that this
174 | class needs to have already been loaded before ["db"](#db) is called.
175 |
176 | ## dsn
177 |
178 | my $dsn = $mysql->dsn;
179 | $mysql = $mysql->dsn('dbi:mysql:dbname=foo');
180 |
181 | Data Source Name, defaults to `dbi:mysql:dbname=test`.
182 |
183 | ## max\_connections
184 |
185 | my $max = $mysql->max_connections;
186 | $mysql = $mysql->max_connections(3);
187 |
188 | Maximum number of idle database handles to cache for future use, defaults to
189 | `5`.
190 |
191 | ## migrations
192 |
193 | my $migrations = $mysql->migrations;
194 | $mysql = $mysql->migrations(Mojo::mysql::Migrations->new);
195 |
196 | [Mojo::mysql::Migrations](https://metacpan.org/pod/Mojo%3A%3Amysql%3A%3AMigrations) object you can use to change your database schema more
197 | easily.
198 |
199 | # Load migrations from file and migrate to latest version
200 | $mysql->migrations->from_file('/Users/sri/migrations.sql')->migrate;
201 |
202 | MySQL and MariaDB does not support nested transactions and DDL transactions.
203 | DDL statements cause implicit `COMMIT`. `ROLLBACK` will be called if any step
204 | of migration script fails, but only DML statements after the last implicit or
205 | explicit `COMMIT` can be reverted. Not all storage engines (like `MYISAM`)
206 | support transactions.
207 |
208 | This means database will most likely be left in unknown state if migration script fails.
209 | Use this feature with caution and remember to always backup your database.
210 |
211 | ## options
212 |
213 | my $options = $mysql->options;
214 | $mysql = $mysql->options({mysql_use_result => 1});
215 |
216 | Options for database handles, defaults to activating `mysql_enable_utf8` (only
217 | for [DBD::mysql](https://metacpan.org/pod/DBD%3A%3Amysql)), `AutoCommit`, `AutoInactiveDestroy` as well as
218 | `RaiseError` and deactivating `PrintError`. `AutoCommit` and `RaiseError`
219 | are considered mandatory, so deactivating them would be very dangerous.
220 |
221 | `mysql_auto_reconnect` is never enabled, [Mojo::mysql](https://metacpan.org/pod/Mojo%3A%3Amysql) takes care of dead connections.
222 |
223 | `AutoCommit` cannot not be disabled, use $db->[begin](https://metacpan.org/pod/Mojo%3A%3Amysql%3A%3ADatabase#begin) to manage transactions.
224 |
225 | `RaiseError` is enabled for blocking and disabled in event loop for non-blocking queries.
226 |
227 | About `mysql_enable_utf8`:
228 |
229 | The mysql_enable_utf8 sets the utf8 charset which only supports up to 3-byte
230 | UTF-8 encodings. mysql_enable_utf8mb4 (as of DBD::mysql 4.032) properly
231 | supports encoding unicode characters to up to 4 bytes, such as 𠜎. It means the
232 | connection charset will be utf8mb4 (supported back to at least mysql 5.5) and
233 | these unicode characters will be supported, but no other changes.
234 |
235 | See also [https://github.com/jhthorsen/mojo-mysql/pull/32](https://github.com/jhthorsen/mojo-mysql/pull/32)
236 |
237 | ## password
238 |
239 | my $password = $mysql->password;
240 | $mysql = $mysql->password('s3cret');
241 |
242 | Database password, defaults to an empty string.
243 |
244 | ## pubsub
245 |
246 | my $pubsub = $mysql->pubsub;
247 | $mysql = $mysql->pubsub(Mojo::mysql::PubSub->new);
248 |
249 | [Mojo::mysql::PubSub](https://metacpan.org/pod/Mojo%3A%3Amysql%3A%3APubSub) should be considered an EXPERIMENT! See
250 | ["DESCRIPTION" in Mojo::mysql::PubSub](https://metacpan.org/pod/Mojo%3A%3Amysql%3A%3APubSub#DESCRIPTION) for more information.
251 |
252 | ## username
253 |
254 | my $username = $mysql->username;
255 | $mysql = $mysql->username('batman');
256 |
257 | Database username, defaults to an empty string.
258 |
259 | # METHODS
260 |
261 | [Mojo::mysql](https://metacpan.org/pod/Mojo%3A%3Amysql) inherits all methods from [Mojo::EventEmitter](https://metacpan.org/pod/Mojo%3A%3AEventEmitter) and implements the
262 | following new ones.
263 |
264 | ## close\_idle\_connections
265 |
266 | $mysql = $mysql->close_idle_connections($keep);
267 |
268 | Close all connections that are not currently active, or limit the
269 | number of idle connections to `$keep`.
270 |
271 | ## db
272 |
273 | my $db = $mysql->db;
274 |
275 | Get [Mojo::mysql::Database](https://metacpan.org/pod/Mojo%3A%3Amysql%3A%3ADatabase) object for a cached or newly created database
276 | handle. The database handle will be automatically cached again when that
277 | object is destroyed, so you can handle connection timeouts gracefully by
278 | holding on to it only for short amounts of time.
279 |
280 | ## from\_string
281 |
282 | $mysql = $mysql->from_string('mysql://user@/test');
283 |
284 | Parse configuration from connection string.
285 |
286 | # Just a database
287 | $mysql->from_string('mysql:///db1');
288 |
289 | # Username and database
290 | $mysql->from_string('mysql://batman@/db2');
291 |
292 | # Username, password, host and database
293 | $mysql->from_string('mysql://batman:s3cret@localhost/db3');
294 |
295 | # Username, domain socket and database
296 | $mysql->from_string('mysql://batman@%2ftmp%2fmysql.sock/db4');
297 |
298 | # Username, database and additional options
299 | $mysql->from_string('mysql://batman@/db5?PrintError=1&RaiseError=0');
300 |
301 | ## new
302 |
303 | my $mysql = Mojo::mysql->new;
304 | my $mysql = Mojo::mysql->new(%attrs);
305 | my $mysql = Mojo::mysql->new(\%attrs);
306 | my $mysql = Mojo::mysql->new('mysql://user@/test');
307 | my $mysql = Mojo::mysql->new('mariadb://user@/test');
308 |
309 | Construct a new [Mojo::mysql](https://metacpan.org/pod/Mojo%3A%3Amysql) object either from ["ATTRIBUTES"](#attributes) and or parse
310 | connection string with ["from\_string"](#from_string) if necessary.
311 |
312 | Using the "mariadb" scheme requires the optional module [DBD::MariaDB](https://metacpan.org/pod/DBD%3A%3AMariaDB) version
313 | 1.21 (or later) to be installed.
314 |
315 | ## strict\_mode
316 |
317 | my $mysql = Mojo::mysql->strict_mode('mysql://user@/test');
318 | my $mysql = $mysql->strict_mode($boolean);
319 |
320 | This method can act as both a constructor and a method. When called as a
321 | constructor, it will be the same as:
322 |
323 | my $mysql = Mojo::mysql->new('mysql://user@/test')->strict_mode(1);
324 |
325 | Enabling strict mode will execute the following statement when a new connection
326 | is created:
327 |
328 | SET SQL_MODE = CONCAT('ANSI,TRADITIONAL,ONLY_FULL_GROUP_BY,', @@sql_mode)
329 | SET SQL_AUTO_IS_NULL = 0
330 |
331 | The idea is to set up a connection that makes it harder for MySQL to allow
332 | "invalid" data to be inserted.
333 |
334 | This method will not be removed, but the internal commands is subject to
335 | change.
336 |
337 | # DEBUGGING
338 |
339 | You can set the `DBI_TRACE` environment variable to get some advanced
340 | diagnostics information printed to `STDERR` by [DBI](https://metacpan.org/pod/DBI).
341 |
342 | DBI_TRACE=1
343 | DBI_TRACE=15
344 | DBI_TRACE=15=dbitrace.log
345 | DBI_TRACE=SQL
346 | DBI_PROFILE=2
347 |
348 | See also [https://metacpan.org/pod/DBI#DBI\_TRACE](https://metacpan.org/pod/DBI#DBI_TRACE) and
349 | [https://metacpan.org/pod/DBI#DBI\_PROFILE](https://metacpan.org/pod/DBI#DBI_PROFILE).
350 |
351 | # REFERENCE
352 |
353 | This is the class hierarchy of the [Mojo::mysql](https://metacpan.org/pod/Mojo%3A%3Amysql) distribution.
354 |
355 | - [Mojo::mysql](https://metacpan.org/pod/Mojo%3A%3Amysql)
356 | - [Mojo::mysql::Database](https://metacpan.org/pod/Mojo%3A%3Amysql%3A%3ADatabase)
357 | - [Mojo::mysql::Migrations](https://metacpan.org/pod/Mojo%3A%3Amysql%3A%3AMigrations)
358 | - [Mojo::mysql::PubSub](https://metacpan.org/pod/Mojo%3A%3Amysql%3A%3APubSub)
359 | - [Mojo::mysql::Results](https://metacpan.org/pod/Mojo%3A%3Amysql%3A%3AResults)
360 | - [Mojo::mysql::Transaction](https://metacpan.org/pod/Mojo%3A%3Amysql%3A%3ATransaction)
361 |
362 | # AUTHORS
363 |
364 | This project is highly inspired by Sebastian Riedel's [Mojo::Pg](https://metacpan.org/pod/Mojo%3A%3APg).
365 |
366 | ## Project Founder
367 |
368 | Jan Henning Thorsen - `jhthorsen@cpan.org`
369 |
370 | ## Contributors
371 |
372 | - Adam Hopkins
373 | - Alexander Karelas
374 | - Curt Hochwender
375 | - Dan Book
376 | - Doug Bell
377 | - Florian Heyer
378 | - Hernan Lopes
379 | - Karl Rune Nilsen
380 | - Larry Leszczynski
381 | - Lucas Tiago de Moraes
382 | - Matt S Trout
383 | - Mike Magowan
384 | - Mohammad S Anwar
385 | - Rolf Stöckli
386 | - Sebastian Riedel
387 | - Svetoslav Naydenov
388 | - Svetoslav Naydenov
389 | - Tekki
390 |
391 | # COPYRIGHT AND LICENSE
392 |
393 | Copyright (C) 2014-2019, Jan Henning Thorsen.
394 |
395 | This program is free software, you can redistribute it and/or modify it under
396 | the terms of the Artistic License version 2.0.
397 |
398 | # SEE ALSO
399 |
400 | [https://github.com/jhthorsen/mojo-mysql](https://github.com/jhthorsen/mojo-mysql),
401 |
402 | [Mojo::Pg](https://metacpan.org/pod/Mojo%3A%3APg) Async Connector for PostgreSQL using [DBD::Pg](https://metacpan.org/pod/DBD%3A%3APg), [https://github.com/kraih/mojo-pg](https://github.com/kraih/mojo-pg),
403 |
404 | [Mojo::MySQL5](https://metacpan.org/pod/Mojo%3A%3AMySQL5) Pure-Perl non-blocking I/O MySQL Connector, [https://github.com/harry-bix/mojo-mysql5](https://github.com/harry-bix/mojo-mysql5),
405 |
406 | [Mojolicious::Guides](https://metacpan.org/pod/Mojolicious%3A%3AGuides), [http://mojolicio.us](http://mojolicio.us).
407 |
--------------------------------------------------------------------------------
/examples/blog/blog.conf:
--------------------------------------------------------------------------------
1 | {mysql => 'mysql://mysql@/test', secrets => ['s3cret']};
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/layouts/blog.html.ep:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= title %>
5 |
14 |
15 |
16 | <%= link_to 'Blog' => 'posts' %>
17 | %= content
18 |
19 |
20 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/lib/Mojo/mysql.pm:
--------------------------------------------------------------------------------
1 | package Mojo::mysql;
2 | use Mojo::Base 'Mojo::EventEmitter';
3 |
4 | use Carp 'croak';
5 | use DBI;
6 | use File::Spec::Functions 'file_name_is_absolute';
7 | use Mojo::mysql::Database;
8 | use Mojo::mysql::Migrations;
9 | use Mojo::URL;
10 | use Scalar::Util 'weaken';
11 | use SQL::Abstract::mysql;
12 |
13 | our $VERSION = '1.27';
14 |
15 | has abstract => sub { SQL::Abstract::mysql->new(quote_char => chr(96), name_sep => '.') };
16 | has auto_migrate => 0;
17 | has database_class => 'Mojo::mysql::Database';
18 | has dsn => 'dbi:mysql:dbname=test';
19 | has max_connections => 5;
20 |
21 | has migrations => sub {
22 | my $migrations = Mojo::mysql::Migrations->new(mysql => shift);
23 | weaken $migrations->{mysql};
24 | return $migrations;
25 | };
26 |
27 | has options => sub {
28 | my $self = shift;
29 | my $options = {AutoCommit => 1, AutoInactiveDestroy => 1, PrintError => 0, RaiseError => 1};
30 | $options->{mysql_enable_utf8} = 1 if $self->dsn =~ m!^dbi:mysql:!;
31 | return $options;
32 | };
33 |
34 | has [qw(password username)] => '';
35 | has pubsub => sub {
36 | require Mojo::mysql::PubSub;
37 | my $pubsub = Mojo::mysql::PubSub->new(mysql => shift);
38 | warn "Use of Mojo::mysql::PubSub is highly EXPERIMENTAL and should be considered an experiment"
39 | unless $ENV{MOJO_PUBSUB_EXPERIMENTAL};
40 | weaken $pubsub->{mysql};
41 | return $pubsub;
42 | };
43 |
44 | sub close_idle_connections {
45 | my ($self, $keep) = (shift, $_[0] || 0);
46 | my $queue = $self->{queue} || [];
47 |
48 | # The database handle needs to be destroyed before the file handle
49 | shift(@$queue)->[0] = undef while @$queue > $keep;
50 | return $self;
51 | }
52 |
53 | sub db {
54 | my $self = shift;
55 |
56 | # Fork safety
57 | delete @$self{qw(pid queue)} unless ($self->{pid} //= $$) eq $$;
58 |
59 | my ($dbh, $handle) = @{$self->_dequeue};
60 | return $self->database_class->new(dbh => $dbh, handle => $handle, mysql => $self);
61 | }
62 |
63 | sub from_string {
64 | my ($self, $str) = @_;
65 |
66 | # Protocol
67 | return $self unless $str;
68 | my $url = UNIVERSAL::isa($str, 'Mojo::URL') ? $str : Mojo::URL->new($str);
69 | croak qq{Invalid MySQL/MariaDB connection string "$str"} unless $url->protocol =~ m!^(mariadb|mysql)$!;
70 | my $dsn = $url->protocol eq 'mariadb' ? 'dbi:MariaDB' : 'dbi:mysql';
71 |
72 | # https://github.com/jhthorsen/mojo-mysql/pull/47
73 | die "DBD::MariaDB 1.21 is required for Mojo::mysql to work properly"
74 | if $dsn eq 'dbi:MariaDB' and !eval 'use DBD::MariaDB 1.21;1';
75 |
76 | # Database
77 | $dsn .= ':dbname=' . $url->path->parts->[0] if defined $url->path->parts->[0];
78 |
79 | # Host and port
80 | if (my $host = $url->host) { $dsn .= file_name_is_absolute($host) ? ";mysql_socket=$host" : ";host=$host" }
81 | if (my $port = $url->port) { $dsn .= ";port=$port" }
82 |
83 | # Need to set the dsn before reading options
84 | $self->dsn($dsn);
85 |
86 | # Username and password
87 | if (($url->userinfo // '') =~ /^([^:]+)(?::([^:]+))?$/) {
88 | $self->username($1);
89 | $self->password($2) if defined $2;
90 | }
91 |
92 | # Options
93 | my $hash = $url->query->to_hash;
94 | @{$self->options}{keys %$hash} = values %$hash;
95 |
96 | return $self;
97 | }
98 |
99 | sub new {
100 | @_ > 2 || ref $_[-1] eq 'HASH' ? shift->SUPER::new(@_) : shift->SUPER::new->from_string(@_);
101 | }
102 |
103 | sub strict_mode {
104 | my $self = ref $_[0] ? shift : shift->new(@_);
105 | $self->{strict_mode} = $_[0] ? 1 : @_ ? 0 : 1;
106 | warn "[Mojo::mysql] strict_mode($self->{strict_mode})\n" if $ENV{DBI_TRACE};
107 | $self->close_idle_connections;
108 | return $self;
109 | }
110 |
111 | sub _dequeue {
112 | my $self = shift;
113 | my $dbh;
114 |
115 | while (my $c = shift @{$self->{queue}}) { return $c if $c->[0]->ping }
116 | $dbh = DBI->connect(map { $self->$_ } qw(dsn username password options));
117 |
118 | # batman's probably going to have more "fun" than you have ...
119 | # especially once he discovers that DBD::mysql randomly reconnects under
120 | # you, silently, but only if certain env vars are set
121 | # hint: force-set mysql_auto_reconnect or whatever it's called to 0
122 | Mojo::mysql::Database->_dbh_attr($dbh, mysql_auto_reconnect => 0);
123 |
124 | # Maintain Commits with Mojo::mysql::Transaction
125 | $dbh->{AutoCommit} = 1;
126 |
127 | $self->_set_strict_mode($dbh) if $self->{strict_mode};
128 | $self->migrations->migrate if $self->auto_migrate and !$self->{migrated}++;
129 | $self->emit(connection => $dbh);
130 | [$dbh];
131 | }
132 |
133 | sub _enqueue {
134 | my ($self, $dbh, $handle) = @_;
135 | push @{$self->{queue}}, [$dbh, $handle] if $dbh->{Active};
136 | $self->close_idle_connections($self->max_connections);
137 | }
138 |
139 | sub _set_strict_mode {
140 | $_[1]->do(q[SET SQL_MODE = CONCAT('ANSI,TRADITIONAL,ONLY_FULL_GROUP_BY,', @@sql_mode)]);
141 | $_[1]->do(q[SET SQL_AUTO_IS_NULL = 0]);
142 | }
143 |
144 | 1;
145 |
146 | =encoding utf8
147 |
148 | =head1 NAME
149 |
150 | Mojo::mysql - Mojolicious and Async MySQL/MariaDB
151 |
152 | =head1 SYNOPSIS
153 |
154 | use Mojo::mysql;
155 |
156 | # Connect to a local database
157 | my $mysql = Mojo::mysql->strict_mode('mysql://username@/test');
158 |
159 | # Connect to a remote database
160 | my $mysql = Mojo::mysql->strict_mode('mysql://username:password@hostname/test');
161 | # MySQL >= 8.0:
162 | my $mysql = Mojo::mysql->strict_mode('mysql://username:password@hostname/test;mysql_ssl=1');
163 |
164 | # Use DBD::MariaDB instead of DBD::mysql
165 | my $mysql = Mojo::mysql->strict_mode('mariadb://username@/test');
166 |
167 | # Create a table
168 | $mysql->db->query(
169 | 'create table names (id integer auto_increment primary key, name text)');
170 |
171 | # Insert a few rows
172 | my $db = $mysql->db;
173 | $db->query('insert into names (name) values (?)', 'Sara');
174 | $db->query('insert into names (name) values (?)', 'Stefan');
175 |
176 | # Insert more rows in a transaction
177 | eval {
178 | my $tx = $db->begin;
179 | $db->query('insert into names (name) values (?)', 'Baerbel');
180 | $db->query('insert into names (name) values (?)', 'Wolfgang');
181 | $tx->commit;
182 | };
183 | say $@ if $@;
184 |
185 | # Insert another row and return the generated id
186 | say $db->query('insert into names (name) values (?)', 'Daniel')
187 | ->last_insert_id;
188 |
189 | # Use SQL::Abstract::mysql to generate queries for you
190 | $db->insert('names', {name => 'Isabel'});
191 | say $db->select('names', undef, {name => 'Isabel'})->hash->{id};
192 | $db->update('names', {name => 'Bel'}, {name => 'Isabel'});
193 | $db->delete('names', {name => 'Bel'});
194 |
195 | # Select one row at a time
196 | my $results = $db->query('select * from names');
197 | while (my $next = $results->hash) {
198 | say $next->{name};
199 | }
200 |
201 | # Select all rows blocking
202 | $db->query('select * from names')
203 | ->hashes->map(sub { $_->{name} })->join("\n")->say;
204 |
205 | # Select all rows non-blocking
206 | $db->query('select * from names' => sub {
207 | my ($db, $err, $results) = @_;
208 | $results->hashes->map(sub { $_->{name} })->join("\n")->say;
209 | });
210 |
211 | # Concurrent non-blocking queries (synchronized with promises)
212 | my $now = $db->query_p('select now() as now');
213 | my $names = $db->query_p('select * from names');
214 | Mojo::Promise->all($now, $names)->then(sub {
215 | my ($now, $names) = @_;
216 | say $now->[0]->hash->{now};
217 | say $_->{name} for $names->[0]->hashes->each;
218 | })->catch(sub {
219 | my $err = shift;
220 | warn "Something went wrong: $err";
221 | })->wait;
222 |
223 | Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
224 |
225 | =head1 DESCRIPTION
226 |
227 | L is a tiny wrapper around L and L that
228 | makes L and L a lot
229 | of fun to use with the L real-time web
230 | framework.
231 |
232 | The two DBD drivers are compatible with both MySQL and MariaDB, but they offer
233 | different L. L should have better unicode support
234 | though and might become the default in the future.
235 |
236 | Database and handles are cached automatically, so they can be reused
237 | transparently to increase performance. And you can handle connection timeouts
238 | gracefully by holding on to them only for short amounts of time.
239 |
240 | use Mojolicious::Lite;
241 | use Mojo::mysql;
242 |
243 | helper mysql =>
244 | sub { state $mysql = Mojo::mysql->strict_mode('mysql://sri:s3cret@localhost/db') };
245 |
246 | get '/' => sub {
247 | my $c = shift;
248 | my $db = $c->mysql->db;
249 | $c->render(json => $db->query('select now() as time')->hash);
250 | };
251 |
252 | app->start;
253 |
254 | While all I/O operations are performed blocking, you can wait for long running
255 | queries asynchronously, allowing the L event loop to perform
256 | other tasks in the meantime. Since database connections usually have a very low
257 | latency, this often results in very good performance.
258 |
259 | Every database connection can only handle one active query at a time, this
260 | includes asynchronous ones. So if you start more than one, they will be put on
261 | a waiting list and performed sequentially. To perform multiple queries
262 | concurrently, you have to use multiple connections.
263 |
264 | # Performed sequentially (10 seconds)
265 | my $db = $mysql->db;
266 | $db->query('select sleep(5)' => sub {...});
267 | $db->query('select sleep(5)' => sub {...});
268 |
269 | # Performed concurrently (5 seconds)
270 | $mysql->db->query('select sleep(5)' => sub {...});
271 | $mysql->db->query('select sleep(5)' => sub {...});
272 |
273 | All cached database handles will be reset automatically if a new process has
274 | been forked, this allows multiple processes to share the same L
275 | object safely.
276 |
277 | =head1 EVENTS
278 |
279 | L inherits all events from L and can emit the
280 | following new ones.
281 |
282 | =head2 connection
283 |
284 | $mysql->on(connection => sub {
285 | my ($mysql, $dbh) = @_;
286 | ...
287 | });
288 |
289 | Emitted when a new database connection has been established.
290 |
291 | =head1 ATTRIBUTES
292 |
293 | L implements the following attributes.
294 |
295 | =head2 abstract
296 |
297 | $abstract = $mysql->abstract;
298 | $mysql = $mysql->abstract(SQL::Abstract::mysql->new);
299 |
300 | L object used to generate CRUD queries for L.
301 |
302 | # Generate statements and bind values
303 | my ($stmt, @bind) = $mysql->abstract->select('names');
304 |
305 | =head2 auto_migrate
306 |
307 | my $bool = $mysql->auto_migrate;
308 | $mysql = $mysql->auto_migrate($bool);
309 |
310 | Automatically migrate to the latest database schema with L"migrations">, as
311 | soon as the first database connection has been established.
312 |
313 | Defaults to false.
314 |
315 | =head2 database_class
316 |
317 | $class = $mysql->database_class;
318 | $mysql = $mysql->database_class("MyApp::Database");
319 |
320 | Class to be used by L"db">, defaults to L. Note that this
321 | class needs to have already been loaded before L"db"> is called.
322 |
323 | =head2 dsn
324 |
325 | my $dsn = $mysql->dsn;
326 | $mysql = $mysql->dsn('dbi:mysql:dbname=foo');
327 |
328 | Data Source Name, defaults to C.
329 |
330 | =head2 max_connections
331 |
332 | my $max = $mysql->max_connections;
333 | $mysql = $mysql->max_connections(3);
334 |
335 | Maximum number of idle database handles to cache for future use, defaults to
336 | C<5>.
337 |
338 | =head2 migrations
339 |
340 | my $migrations = $mysql->migrations;
341 | $mysql = $mysql->migrations(Mojo::mysql::Migrations->new);
342 |
343 | L object you can use to change your database schema more
344 | easily.
345 |
346 | # Load migrations from file and migrate to latest version
347 | $mysql->migrations->from_file('/Users/sri/migrations.sql')->migrate;
348 |
349 | MySQL and MariaDB does not support nested transactions and DDL transactions.
350 | DDL statements cause implicit C. C will be called if any step
351 | of migration script fails, but only DML statements after the last implicit or
352 | explicit C can be reverted. Not all storage engines (like C)
353 | support transactions.
354 |
355 | This means database will most likely be left in unknown state if migration script fails.
356 | Use this feature with caution and remember to always backup your database.
357 |
358 | =head2 options
359 |
360 | my $options = $mysql->options;
361 | $mysql = $mysql->options({mysql_use_result => 1});
362 |
363 | Options for database handles, defaults to activating C (only
364 | for L), C, C as well as
365 | C and deactivating C. C and C
366 | are considered mandatory, so deactivating them would be very dangerous.
367 |
368 | C is never enabled, L takes care of dead connections.
369 |
370 | C cannot not be disabled, use $db->L to manage transactions.
371 |
372 | C is enabled for blocking and disabled in event loop for non-blocking queries.
373 |
374 | About C:
375 |
376 | The mysql_enable_utf8 sets the utf8 charset which only supports up to 3-byte
377 | UTF-8 encodings. mysql_enable_utf8mb4 (as of DBD::mysql 4.032) properly
378 | supports encoding unicode characters to up to 4 bytes, such as 𠜎. It means the
379 | connection charset will be utf8mb4 (supported back to at least mysql 5.5) and
380 | these unicode characters will be supported, but no other changes.
381 |
382 | See also L
383 |
384 | =head2 password
385 |
386 | my $password = $mysql->password;
387 | $mysql = $mysql->password('s3cret');
388 |
389 | Database password, defaults to an empty string.
390 |
391 | =head2 pubsub
392 |
393 | my $pubsub = $mysql->pubsub;
394 | $mysql = $mysql->pubsub(Mojo::mysql::PubSub->new);
395 |
396 | L should be considered an EXPERIMENT! See
397 | L for more information.
398 |
399 | =head2 username
400 |
401 | my $username = $mysql->username;
402 | $mysql = $mysql->username('batman');
403 |
404 | Database username, defaults to an empty string.
405 |
406 | =head1 METHODS
407 |
408 | L inherits all methods from L and implements the
409 | following new ones.
410 |
411 | =head2 close_idle_connections
412 |
413 | $mysql = $mysql->close_idle_connections($keep);
414 |
415 | Close all connections that are not currently active, or limit the
416 | number of idle connections to C<$keep>.
417 |
418 | =head2 db
419 |
420 | my $db = $mysql->db;
421 |
422 | Get L object for a cached or newly created database
423 | handle. The database handle will be automatically cached again when that
424 | object is destroyed, so you can handle connection timeouts gracefully by
425 | holding on to it only for short amounts of time.
426 |
427 | =head2 from_string
428 |
429 | $mysql = $mysql->from_string('mysql://user@/test');
430 |
431 | Parse configuration from connection string.
432 |
433 | # Just a database
434 | $mysql->from_string('mysql:///db1');
435 |
436 | # Username and database
437 | $mysql->from_string('mysql://batman@/db2');
438 |
439 | # Username, password, host and database
440 | $mysql->from_string('mysql://batman:s3cret@localhost/db3');
441 |
442 | # Username, domain socket and database
443 | $mysql->from_string('mysql://batman@%2ftmp%2fmysql.sock/db4');
444 |
445 | # Username, database and additional options
446 | $mysql->from_string('mysql://batman@/db5?PrintError=1&RaiseError=0');
447 |
448 | =head2 new
449 |
450 | my $mysql = Mojo::mysql->new;
451 | my $mysql = Mojo::mysql->new(%attrs);
452 | my $mysql = Mojo::mysql->new(\%attrs);
453 | my $mysql = Mojo::mysql->new('mysql://user@/test');
454 | my $mysql = Mojo::mysql->new('mariadb://user@/test');
455 |
456 | Construct a new L object either from L and or parse
457 | connection string with L"from_string"> if necessary.
458 |
459 | Using the "mariadb" scheme requires the optional module L version
460 | 1.21 (or later) to be installed.
461 |
462 | =head2 strict_mode
463 |
464 | my $mysql = Mojo::mysql->strict_mode('mysql://user@/test');
465 | my $mysql = $mysql->strict_mode($boolean);
466 |
467 | This method can act as both a constructor and a method. When called as a
468 | constructor, it will be the same as:
469 |
470 | my $mysql = Mojo::mysql->new('mysql://user@/test')->strict_mode(1);
471 |
472 | Enabling strict mode will execute the following statement when a new connection
473 | is created:
474 |
475 | SET SQL_MODE = CONCAT('ANSI,TRADITIONAL,ONLY_FULL_GROUP_BY,', @@sql_mode)
476 | SET SQL_AUTO_IS_NULL = 0
477 |
478 | The idea is to set up a connection that makes it harder for MySQL to allow
479 | "invalid" data to be inserted.
480 |
481 | This method will not be removed, but the internal commands is subject to
482 | change.
483 |
484 | =head1 DEBUGGING
485 |
486 | You can set the C environment variable to get some advanced
487 | diagnostics information printed to C by L.
488 |
489 | DBI_TRACE=1
490 | DBI_TRACE=15
491 | DBI_TRACE=15=dbitrace.log
492 | DBI_TRACE=SQL
493 | DBI_PROFILE=2
494 |
495 | See also L and
496 | L.
497 |
498 | =head1 REFERENCE
499 |
500 | This is the class hierarchy of the L distribution.
501 |
502 | =over 2
503 |
504 | =item * L
505 |
506 | =item * L
507 |
508 | =item * L
509 |
510 | =item * L
511 |
512 | =item * L
513 |
514 | =item * L
515 |
516 | =back
517 |
518 | =head1 AUTHORS
519 |
520 | This project is highly inspired by Sebastian Riedel's L.
521 |
522 | =head2 Project Founder
523 |
524 | Jan Henning Thorsen - C
525 |
526 | =head2 Contributors
527 |
528 | =over 2
529 |
530 |
531 | =item * Adam Hopkins
532 |
533 | =item * Alexander Karelas
534 |
535 | =item * Curt Hochwender
536 |
537 | =item * Dan Book
538 |
539 | =item * Doug Bell
540 |
541 | =item * Florian Heyer
542 |
543 | =item * Hernan Lopes
544 |
545 | =item * Karl Rune Nilsen
546 |
547 | =item * Larry Leszczynski
548 |
549 | =item * Lucas Tiago de Moraes
550 |
551 | =item * Matt S Trout
552 |
553 | =item * Mike Magowan
554 |
555 | =item * Mohammad S Anwar
556 |
557 | =item * Rolf Stöckli
558 |
559 | =item * Sebastian Riedel
560 |
561 | =item * Svetoslav Naydenov
562 |
563 | =item * Svetoslav Naydenov
564 |
565 | =item * Tekki
566 |
567 | =back
568 |
569 | =head1 COPYRIGHT AND LICENSE
570 |
571 | Copyright (C) 2014-2019, Jan Henning Thorsen.
572 |
573 | This program is free software, you can redistribute it and/or modify it under
574 | the terms of the Artistic License version 2.0.
575 |
576 | =head1 SEE ALSO
577 |
578 | L,
579 |
580 | L Async Connector for PostgreSQL using L, L,
581 |
582 | L Pure-Perl non-blocking I/O MySQL Connector, L,
583 |
584 | L, L.
585 |
586 | =cut
587 |
--------------------------------------------------------------------------------
/lib/Mojo/mysql/Database.pm:
--------------------------------------------------------------------------------
1 | package Mojo::mysql::Database;
2 | use Mojo::Base 'Mojo::EventEmitter';
3 |
4 | use Carp;
5 | use Mojo::IOLoop;
6 | use Mojo::JSON 'to_json';
7 | use Mojo::mysql::Results;
8 | use Mojo::mysql::Transaction;
9 | use Mojo::Promise;
10 | use Mojo::Util 'monkey_patch';
11 | use Scalar::Util 'weaken';
12 |
13 | has [qw(dbh mysql)];
14 | has results_class => 'Mojo::mysql::Results';
15 |
16 | for my $name (qw(delete insert select update)) {
17 | monkey_patch __PACKAGE__, $name, sub {
18 | my $self = shift;
19 | my @cb = ref $_[-1] eq 'CODE' ? pop : ();
20 | return $self->query($self->mysql->abstract->$name(@_), @cb);
21 | };
22 | monkey_patch __PACKAGE__, "${name}_p", sub {
23 | my $self = shift;
24 | return $self->query_p($self->mysql->abstract->$name(@_));
25 | };
26 | }
27 |
28 | sub DESTROY {
29 | my $self = shift;
30 | return if ${^GLOBAL_PHASE} eq 'DESTRUCT';
31 | $self->_cleanup_sth;
32 | return unless (my $mysql = $self->mysql) and (my $dbh = $self->dbh);
33 | $mysql->_enqueue($dbh, $self->{handle});
34 | }
35 |
36 | sub backlog { scalar @{shift->{waiting} || []} }
37 |
38 | sub begin {
39 | my $self = shift;
40 | my $tx = Mojo::mysql::Transaction->new(db => $self);
41 | weaken $tx->{db};
42 | return $tx;
43 | }
44 |
45 | sub disconnect {
46 | my $self = shift;
47 | $self->_cleanup_sth;
48 | $self->_unwatch;
49 | $self->dbh->disconnect;
50 | }
51 |
52 | sub pid { shift->_dbh_attr('mysql_thread_id') }
53 |
54 | sub ping { shift->dbh->ping }
55 |
56 | sub query {
57 | my ($self, $query) = (shift, shift);
58 | my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
59 |
60 | # Blocking
61 | unless ($cb) {
62 | Carp::confess('Cannot perform blocking query, while waiting for async response') if $self->backlog;
63 | my $sth = $self->dbh->prepare($query);
64 | local $sth->{HandleError} = sub { $_[0] = Carp::shortmess($_[0]); 0 };
65 | _bind_params($sth, @_);
66 | my $rv = $sth->execute;
67 | my $res = $self->results_class->new(db => $self, is_blocking => 1, sth => $sth);
68 | $res->{affected_rows} = defined $rv && $rv >= 0 ? 0 + $rv : undef;
69 | return $res;
70 | }
71 |
72 | # Non-blocking
73 | push @{$self->{waiting}}, {args => [@_], err => Carp::shortmess('__MSG__'), cb => $cb, query => $query};
74 | $self->$_ for qw(_next _watch);
75 | return $self;
76 | }
77 |
78 | sub query_p {
79 | my $self = shift;
80 | my $promise = Mojo::Promise->new;
81 | $self->query(@_ => sub { $_[1] ? $promise->reject($_[1]) : $promise->resolve($_[2]) });
82 | return $promise;
83 | }
84 |
85 | sub quote { shift->dbh->quote(shift) }
86 |
87 | sub quote_id { shift->dbh->quote_identifier(shift) }
88 |
89 | sub tables {
90 | shift->query('show tables')->arrays->reduce(sub { push @$a, $b->[0]; $a }, []);
91 | }
92 |
93 | sub _bind_params {
94 | my $sth = shift;
95 | for my $i (0 .. $#_) {
96 | my $param = $_[$i];
97 | my %attrs;
98 | if (ref $param eq 'HASH') {
99 | if (exists $param->{json}) {
100 | $param = to_json $param->{json};
101 | }
102 | elsif (exists $param->{type} && exists $param->{value}) {
103 | ($param, $attrs{TYPE}) = @$param{qw(value type)};
104 | }
105 | }
106 |
107 | $sth->bind_param($i + 1, $param, \%attrs);
108 | }
109 | return $sth;
110 | }
111 |
112 | sub _cleanup_sth {
113 | my $self = shift;
114 | delete $self->{done_sth};
115 | $_->{cb}($self, 'Premature connection close', undef) for @{delete $self->{waiting} || []};
116 | }
117 |
118 | sub _dbh_attr {
119 | my $self = shift;
120 | my $dbh = ref $self ? $self->dbh : shift;
121 | my $name = shift;
122 | $name =~ s!^mysql!{lc $dbh->{Driver}{Name}}!e;
123 | return $dbh->{$name} = shift if @_;
124 | return $dbh->{$name};
125 | }
126 |
127 | sub _next {
128 | my $self = shift;
129 |
130 | return unless my $next = $self->{waiting}[0];
131 | return if $next->{sth};
132 |
133 | my $dbh = $self->dbh;
134 | my $flag = lc $dbh->{Driver}{Name} eq 'mariadb' ? 'mariadb_async' : 'async';
135 | my $sth = $next->{sth} = $self->dbh->prepare($next->{query}, {$flag => 1});
136 | _bind_params($sth, @{$next->{args}});
137 | $sth->execute;
138 | }
139 |
140 | sub _unwatch {
141 | Mojo::IOLoop->singleton->reactor->remove(delete $_[0]->{handle}) if $_[0]->{handle};
142 | }
143 |
144 | sub _watch {
145 | my $self = shift;
146 | return if $self->{handle};
147 |
148 | my $dbh = $self->dbh;
149 | my $driver = lc $dbh->{Driver}{Name};
150 | my $ready_method = "${driver}_async_ready";
151 | my $result_method = "${driver}_async_result";
152 | my $fd = $driver eq 'mariadb' ? $dbh->mariadb_sockfd : $dbh->mysql_fd;
153 | open $self->{handle}, '<&', $fd or die "Could not dup $driver fd: $!";
154 | Mojo::IOLoop->singleton->reactor->io(
155 | $self->{handle} => sub {
156 | return unless my $waiting = $self->{waiting};
157 | return unless @$waiting and $waiting->[0]{sth} and $waiting->[0]{sth}->$ready_method;
158 | my ($cb, $err, $sth) = @{shift @$waiting}{qw(cb err sth)};
159 |
160 | # Do not raise exceptions inside the event loop
161 | my $rv = do { local $sth->{RaiseError} = 0; $sth->$result_method };
162 | my $res = $self->results_class->new(db => $self, sth => $sth);
163 |
164 | $err = undef if defined $rv;
165 | $err =~ s!\b__MSG__\b!{$dbh->errstr}!e if defined $err;
166 | $res->{affected_rows} = defined $rv && $rv >= 0 ? 0 + $rv : undef;
167 |
168 | $self->$cb($err, $res);
169 | $self->_next;
170 | $self->_unwatch unless $self->backlog;
171 | }
172 | )->watch($self->{handle}, 1, 0);
173 | }
174 |
175 | 1;
176 |
177 | =encoding utf8
178 |
179 | =head1 NAME
180 |
181 | Mojo::mysql::Database - Database
182 |
183 | =head1 SYNOPSIS
184 |
185 | use Mojo::mysql::Database;
186 |
187 | my $db = Mojo::mysql::Database->new(mysql => $mysql, dbh => $dbh);
188 |
189 | =head1 DESCRIPTION
190 |
191 | L is a container for database handles used by L.
192 |
193 | =head1 ATTRIBUTES
194 |
195 | L implements the following attributes.
196 |
197 | =head2 dbh
198 |
199 | my $dbh = $db->dbh;
200 | $db = $db->dbh(DBI->new);
201 |
202 | Database handle used for all queries.
203 |
204 | =head2 mysql
205 |
206 | my $mysql = $db->mysql;
207 | $db = $db->mysql(Mojo::mysql->new);
208 |
209 | L object this database belongs to.
210 |
211 | =head2 results_class
212 |
213 | $class = $db->results_class;
214 | $db = $db->results_class("MyApp::Results");
215 |
216 | Class to be used by L"query">, defaults to L. Note that
217 | this class needs to have already been loaded before L"query"> is called.
218 |
219 | =head1 METHODS
220 |
221 | L inherits all methods from L and
222 | implements the following new ones.
223 |
224 | =head2 backlog
225 |
226 | my $num = $db->backlog;
227 |
228 | Number of waiting non-blocking queries.
229 |
230 | =head2 begin
231 |
232 | my $tx = $db->begin;
233 |
234 | Begin transaction and return L object, which will
235 | automatically roll back the transaction unless
236 | L has been called before it is destroyed.
237 |
238 | # Add names in a transaction
239 | eval {
240 | my $tx = $db->begin;
241 | $db->query('insert into names values (?)', 'Baerbel');
242 | $db->query('insert into names values (?)', 'Wolfgang');
243 | $tx->commit;
244 | };
245 | say $@ if $@;
246 |
247 | =head2 delete
248 |
249 | my $results = $db->delete($table, \%where);
250 |
251 | Generate a C statement with L (usually an
252 | L object) and execute it with L"query">. You can also append a
253 | callback to perform operations non-blocking.
254 |
255 | $db->delete(some_table => sub {
256 | my ($db, $err, $results) = @_;
257 | ...
258 | });
259 | Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
260 |
261 | =head2 delete_p
262 |
263 | my $promise = $db->delete_p($table, \%where, \%options);
264 |
265 | Same as L"delete">, but performs all operations non-blocking and returns a
266 | L object instead of accepting a callback.
267 |
268 | $db->delete_p('some_table')->then(sub {
269 | my $results = shift;
270 | ...
271 | })->catch(sub {
272 | my $err = shift;
273 | ...
274 | })->wait;
275 |
276 | =head2 disconnect
277 |
278 | $db->disconnect;
279 |
280 | Disconnect database handle and prevent it from getting cached again.
281 |
282 | =head2 insert
283 |
284 | my $results = $db->insert($table, \@values || \%fieldvals, \%options);
285 |
286 | Generate an C statement with L (usually an
287 | L object) and execute it with L"query">. You can also append a
288 | callback to perform operations non-blocking.
289 |
290 | $db->insert(some_table => {foo => 'bar'} => sub {
291 | my ($db, $err, $results) = @_;
292 | ...
293 | });
294 | Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
295 |
296 | =head2 insert_p
297 |
298 | my $promise = $db->insert_p($table, \@values || \%fieldvals, \%options);
299 |
300 | Same as L"insert">, but performs all operations non-blocking and returns a
301 | L object instead of accepting a callback.
302 |
303 | $db->insert_p(some_table => {foo => 'bar'})->then(sub {
304 | my $results = shift;
305 | ...
306 | })->catch(sub {
307 | my $err = shift;
308 | ...
309 | })->wait;
310 |
311 | =head2 pid
312 |
313 | my $pid = $db->pid;
314 |
315 | Return the connection id of the backend server process.
316 |
317 | =head2 ping
318 |
319 | my $bool = $db->ping;
320 |
321 | Check database connection.
322 |
323 | =head2 query
324 |
325 | my $results = $db->query('select * from foo');
326 | my $results = $db->query('insert into foo values (?, ?, ?)', @values);
327 | my $results = $db->query('insert into foo values (?)', {json => {bar => 'baz'}});
328 | my $results = $db->query('insert into foo values (?)', {type => SQL_INTEGER, value => 42});
329 |
330 | Execute a blocking statement and return a L object with the
331 | results. You can also append a callback to perform operation non-blocking.
332 |
333 | $db->query('select * from foo' => sub {
334 | my ($db, $err, $results) = @_;
335 | ...
336 | });
337 | Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
338 |
339 | Hash reference arguments containing a value named C, will be encoded to
340 | JSON text with L. To accomplish the reverse, you can use
341 | the method L, which automatically decodes data back
342 | to Perl data structures.
343 |
344 | $db->query('insert into foo values (x) values (?)', {json => {bar => 'baz'}});
345 | $db->query('select * from foo')->expand->hash->{x}{bar}; # baz
346 |
347 | Hash reference arguments containing values named C and C can be
348 | used to bind specific L data types (see L) to
349 | placeholders. This is needed to pass binary data in parameters; see
350 | L for more information.
351 |
352 | # Insert binary data
353 | use DBI ':sql_types';
354 | $db->query('insert into bar values (?)', {type => SQL_BLOB, value => $bytes});
355 |
356 | =head2 query_p
357 |
358 | my $promise = $db->query_p('select * from foo');
359 |
360 | Same as L"query">, but performs all operations non-blocking and returns a
361 | L object instead of accepting a callback.
362 |
363 | $db->query_p('insert into foo values (?, ?, ?)' => @values)->then(sub {
364 | my $results = shift;
365 | ...
366 | })->catch(sub {
367 | my $err = shift;
368 | ...
369 | })->wait;
370 |
371 | =head2 quote
372 |
373 | my $escaped = $db->quote($str);
374 |
375 | Quote a string literal for use as a literal value in an SQL statement.
376 |
377 | =head2 quote_id
378 |
379 | my $escaped = $db->quote_id($id);
380 |
381 | Quote an identifier (table name etc.) for use in an SQL statement.
382 |
383 | =head2 select
384 |
385 | my $results = $db->select($source, $fields, $where, $order);
386 |
387 | Generate a C statement with L (usually an
388 | L object) and execute it with L"query">. You can also append a
389 | callback to perform operations non-blocking.
390 |
391 | $db->select(some_table => ['foo'] => sub {
392 | my ($db, $err, $results) = @_;
393 | ...
394 | });
395 | Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
396 |
397 | =head2 select_p
398 |
399 | my $promise = $db->select_p($source, $fields, $where, $order);
400 |
401 | Same as L"select">, but performs all operations non-blocking and returns a
402 | L object instead of accepting a callback.
403 |
404 | $db->select_p(some_table => ['foo'] => {bar => 'yada'})->then(sub {
405 | my $results = shift;
406 | ...
407 | })->catch(sub {
408 | my $err = shift;
409 | ...
410 | })->wait;
411 |
412 | =head2 update
413 |
414 | my $results = $db->update($table, \%fieldvals, \%where);
415 |
416 | Generate an C statement with L (usually an
417 | L object) and execute it with L"query">. You can also append a
418 | callback to perform operations non-blocking.
419 |
420 | $db->update(some_table => {foo => 'baz'} => {foo => 'bar'} => sub {
421 | my ($db, $err, $results) = @_;
422 | ...
423 | });
424 | Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
425 |
426 | =head2 update_p
427 |
428 | my $promise = $db->update_p($table, \%fieldvals, \%where, \%options);
429 |
430 | Same as L"update">, but performs all operations non-blocking and returns a
431 | L object instead of accepting a callback.
432 |
433 | $db->update_p(some_table => {foo => 'baz'} => {foo => 'bar'})->then(sub {
434 | my $results = shift;
435 | ...
436 | })->catch(sub {
437 | my $err = shift;
438 | ...
439 | })->wait;
440 |
441 | =head2 tables
442 |
443 | my $tables = $db->tables;
444 |
445 | Return an array reference with table names for this database.
446 |
447 | =head1 SEE ALSO
448 |
449 | L.
450 |
451 | =cut
452 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/SQL/Abstract/mysql.pm:
--------------------------------------------------------------------------------
1 | package SQL::Abstract::mysql;
2 | use Mojo::Base 'SQL::Abstract';
3 |
4 | BEGIN { *puke = \&SQL::Abstract::puke }
5 |
6 | sub insert {
7 | my ($self, $options) = (shift, $_[2] || {}); # ($self, $table, $data, $options)
8 | my ($sql, @bind) = $self->SUPER::insert(@_);
9 |
10 | # options
11 | if (exists $options->{on_conflict}) {
12 | my $on_conflict = $options->{on_conflict} // '';
13 | if (ref $on_conflict eq 'HASH') {
14 | my ($s, @b) = $self->_update_set_values($on_conflict);
15 | $sql .= $self->_sqlcase(' on duplicate key update ') . $s;
16 | push @bind, @b;
17 | }
18 | elsif ($on_conflict eq 'ignore') {
19 | $sql =~ s/^(\w+)/{$self->_sqlcase('insert ignore')}/e;
20 | }
21 | elsif ($on_conflict eq 'replace') {
22 | $sql =~ s/^(\w+)/{$self->_sqlcase('replace')}/e;
23 | }
24 | else {
25 | puke qq{on_conflict value "$on_conflict" is not allowed};
26 | }
27 | }
28 |
29 | return wantarray ? ($sql, @bind) : $sql;
30 | }
31 |
32 | sub _mysql_for {
33 | my ($self, $param) = @_;
34 |
35 | return $self->_SWITCH_refkind(
36 | $param => {
37 | SCALAR => sub {
38 | return $self->_sqlcase('lock in share mode') if $param eq 'share';
39 | return $self->_sqlcase('for update') if $param eq 'update';
40 | puke qq{for value "$param" is not allowed};
41 | },
42 | SCALARREF => sub { $self->_sqlcase('for ') . $$param },
43 | }
44 | );
45 | }
46 |
47 | sub _mysql_group_by {
48 | my ($self, $param) = @_;
49 |
50 | return $self->_SWITCH_refkind(
51 | $param => {ARRAYREF => sub { join ', ', map $self->_quote($_), @$param }, SCALARREF => sub {$$param},});
52 | }
53 |
54 | sub _order_by {
55 | my ($self, $options) = @_;
56 | my ($sql, @bind) = ('');
57 |
58 | # Legacy
59 | return $self->SUPER::_order_by($options) if ref $options ne 'HASH' or grep {/^-(?:desc|asc)/i} keys %$options;
60 |
61 | # GROUP BY
62 | $sql .= $self->_sqlcase(' group by ') . $self->_mysql_group_by($options->{group_by}) if defined $options->{group_by};
63 |
64 | # HAVING
65 | if (defined($options->{having})) {
66 | my ($s, @b) = $self->_recurse_where($options->{having});
67 | $sql .= $self->_sqlcase(' having ') . $s;
68 | push @bind, @b;
69 | }
70 |
71 | # ORDER BY
72 | $sql .= $self->_order_by($options->{order_by}) if defined $options->{order_by};
73 |
74 | # LIMIT / OFFSET
75 | for my $name (qw(limit offset)) {
76 | next unless defined $options->{$name};
77 | $sql .= $self->_sqlcase(" $name ") . '?';
78 | push @bind, $options->{$name};
79 | }
80 |
81 | # FOR
82 | $sql .= ' ' . $self->_mysql_for($options->{for}) if defined $options->{for};
83 |
84 | return $sql, @bind;
85 | }
86 |
87 | sub _select_fields {
88 | my ($self, $fields) = @_;
89 |
90 | return $fields unless ref $fields eq 'ARRAY';
91 |
92 | my (@fields, @bind);
93 | for my $field (@$fields) {
94 | $self->_SWITCH_refkind(
95 | $field => {
96 | ARRAYREF => sub {
97 | puke 'field alias must be in the form [$name => $alias]' if @$field < 2;
98 | push @fields, $self->_quote($field->[0]) . $self->_sqlcase(' as ') . $self->_quote($field->[1]);
99 | },
100 | ARRAYREFREF => sub {
101 | push @fields, shift @$$field;
102 | push @bind, @$$field;
103 | },
104 | SCALARREF => sub { push @fields, $$field },
105 | FALLBACK => sub { push @fields, $self->_quote($field) }
106 | }
107 | );
108 | }
109 |
110 | return join(', ', @fields), @bind;
111 | }
112 |
113 | sub _table {
114 | my ($self, $table) = @_;
115 |
116 | return $self->SUPER::_table($table) unless ref $table eq 'ARRAY';
117 |
118 | my (@tables, @joins);
119 | for my $jt (@$table) {
120 | if (ref $jt eq 'ARRAY') { push @joins, $jt }
121 | else { push @tables, $jt }
122 | }
123 |
124 | my $sql = $self->SUPER::_table(\@tables);
125 | my $sep = $self->{name_sep} // '';
126 | for my $join (@joins) {
127 |
128 | my $type = '';
129 | if ($join->[0] =~ /^-(.+)/) {
130 | $type = " $1";
131 | shift @$join;
132 | }
133 |
134 | my $name = shift @$join;
135 | $sql .= $self->_sqlcase("$type join ") . $self->_quote($name);
136 |
137 | # NATURAL JOIN
138 | if ($type eq ' natural') {
139 | puke 'natural join must be in the form [-natural => $table]' if @$join;
140 | }
141 |
142 | # JOIN USING
143 | elsif (@$join == 1) {
144 | $sql .= $self->_sqlcase(' using (') . $self->_quote($join->[0]) . ')';
145 | }
146 |
147 | # others
148 | else {
149 | puke 'join must be in the form [$table, $fk => $pk]' if @$join < 2;
150 | puke 'join requires an even number of keys' if @$join % 2;
151 |
152 | my @keys;
153 | while (my ($fk, $pk) = splice @$join, 0, 2) {
154 | push @keys,
155 | $self->_quote(index($fk, $sep) > 0 ? $fk : "$name.$fk") . ' = '
156 | . $self->_quote(index($pk, $sep) > 0 ? $pk : "$tables[0].$pk");
157 | }
158 |
159 | $sql .= $self->_sqlcase(' on ') . '(' . join($self->_sqlcase(' and '), @keys) . ')';
160 | }
161 |
162 | }
163 |
164 | return $sql;
165 | }
166 |
167 | 1;
168 |
169 | =encoding utf8
170 |
171 | =head1 NAME
172 |
173 | SQL::Abstract::mysql - Generate SQL from Perl data structures for MySQL and MariaDB
174 |
175 | =head1 SYNOPSIS
176 |
177 | use SQL::Abstract::mysql;
178 |
179 | my $abstract = SQL::Abstract::mysql->new(quote_char => chr(96), name_sep => '.');
180 | # The same as
181 | use Mojo::mysql;
182 | my $mysql = Mojo::mysql->new;
183 | my $abstract = $mysql->abstract;
184 |
185 | say $abstract->insert('some_table', \%some_values, \%some_options);
186 | say $abstract->select('some_table');
187 |
188 | =head1 DESCRIPTION
189 |
190 | L extends L with a few MySQL / MariaDB
191 | features used by L. It was inspired by L.
192 |
193 | =head1 FUNCTIONS
194 |
195 | =head2 puke
196 |
197 | See L.
198 |
199 | =head1 METHODS
200 |
201 | L inherits all methods from L.
202 |
203 | =head2 insert
204 |
205 | my ($stmt, @bind) = $abstract->insert($table, \@values || \%fieldvals, \%options);
206 |
207 | This method extends L with the following functionality:
208 |
209 | =head3 ON CONFLICT
210 |
211 | The C option can be used to generate C, C and
212 | C queries.
213 | So far C<'ignore'> to pass C, C<'replace'> to pass C and
214 | hash references to pass C with conflict targets are supported.
215 |
216 | # "insert ignore into t (id, a) values (123, 'b')"
217 | $abstract->insert('t', {id => 123, a => 'b'}, {on_conflict => 'ignore'});
218 |
219 | # "replace into t (id, a) values (123, 'b')"
220 | $abstract->insert('t', {id => 123, a => 'b'}, {on_conflict => 'replace'});
221 |
222 | # "insert into t (id, a) values (123, 'b') on duplicate key update c='d'"
223 | $abstract->insert('t', {id => 123, a => 'b'}, {on_conflict => {c => 'd'}});
224 |
225 | =head2 select
226 |
227 | my ($stmt, @bind) = $abstract->select($source, $fields, $where, $order);
228 | my ($stmt, @bind) = $abstract->select($source, $fields, $where, \%options);
229 |
230 | This method extends L with the following functionality:
231 |
232 | =head3 AS
233 |
234 | The C<$fields> argument accepts array references containing array references
235 | with field names and aliases, as well as array references containing scalar
236 | references to pass literal SQL and array reference references to pass literal
237 | SQL with bind values.
238 |
239 | # "select foo as bar from some_table"
240 | $abstract->select('some_table', [[foo => 'bar']]);
241 |
242 | # "select foo, bar as baz, yada from some_table"
243 | $abstract->select('some_table', ['foo', [bar => 'baz'], 'yada']);
244 |
245 | # "select extract(epoch from foo) as foo, bar from some_table"
246 | $abstract->select('some_table', [\'extract(epoch from foo) as foo', 'bar']);
247 |
248 | # "select 'test' as foo, bar from some_table"
249 | $abstract->select('some_table', [\['? as foo', 'test'], 'bar']);
250 |
251 | =head3 JOIN
252 |
253 | The C<$source> argument accepts array references containing not only table
254 | names, but also array references with tables to generate C clauses for.
255 |
256 | # "select * from foo join bar on (bar.foo_id = foo.id)"
257 | $abstract->select(['foo', ['bar', foo_id => 'id']]);
258 |
259 | # "select * from foo join bar on (foo.id = bar.foo_id)"
260 | $abstract->select(['foo', ['bar', 'foo.id' => 'bar.foo_id']]);
261 |
262 | # -left, -right, -inner
263 | # "select * from foo left join bar on (bar.foo_id = foo.id)"
264 | $abstract->select(['foo', [-left => 'bar', foo_id => 'id']]);
265 |
266 | # -natural
267 | # "select * from foo natural join bar"
268 | $abstract->select(['foo', [-natural => 'bar']]);
269 |
270 | # join using
271 | # "select * from foo join bar using (foo_id)"
272 | $abstract->select(['foo', [bar => 'foo_id']]);
273 |
274 | # more than one table
275 | # "select * from foo join bar on (bar.foo_id = foo.id) join baz on (baz.foo_id = foo.id)"
276 | $abstract->select(['foo', ['bar', foo_id => 'id'], ['baz', foo_id => 'id']]);
277 |
278 | # more than one field
279 | # "select * from foo left join bar on (bar.foo_id = foo.id and bar.foo_id2 = foo.id2)"
280 | $abstract->select(['foo', [-left => 'bar', foo_id => 'id', foo_id2 => 'id2']]);
281 |
282 | =head2 where
283 |
284 | my ($stmt, @bind) = $abstract->where($where, \%options);
285 |
286 | This method extends L with the following functionality:
287 |
288 | =head3 FOR
289 |
290 | The C option can be used to generate C queries with C
291 | or C clauses. So far the scalar values C and
292 | C and scalar references to pass literal SQL are supported.
293 |
294 | # "select * from some_table for update"
295 | $abstract->select('some_table', '*', undef, {for => 'update'});
296 |
297 | # "select * from some_table lock in share mode"
298 | $abstract->select('some_table', '*', undef, {for => 'share'});
299 |
300 | # "select * from some_table for share"
301 | $abstract->select('some_table', '*', undef, {for => \'share'});
302 |
303 | # "select * from some_table for update skip locked"
304 | $abstract->select('some_table', '*', undef, {for => \'update skip locked'});
305 |
306 | =head3 GROUP BY
307 |
308 | The C option can be used to generate C queries with C clauses. So far array references to pass a list of fields and scalar
310 | references to pass literal SQL are supported.
311 |
312 | # "select * from some_table group by foo, bar"
313 | $abstract->select('some_table', '*', undef, {group_by => ['foo', 'bar']});
314 |
315 | # "select * from some_table group by foo, bar"
316 | $abstract->select('some_table', '*', undef, {group_by => \'foo, bar'});
317 |
318 | =head3 HAVING
319 |
320 | The C option can be used to generate C queries with C
321 | clauses, which takes the same values as the C<$where> argument.
322 |
323 | # "select * from t group by a having b = 'c'"
324 | $abstract->select('t', '*', undef, {group_by => ['a'], having => {b => 'c'}});
325 |
326 | =head3 ORDER BY
327 |
328 | In addition to the C<$order> argument accepted by L you can pass
329 | a hash reference with various options. This includes C, which takes
330 | the same values as the C<$order> argument.
331 |
332 | # "select * from some_table order by foo desc"
333 | $abstract->select('some_table', '*', undef, {order_by => {-desc => 'foo'}});
334 |
335 | =head3 LIMIT / OFFSET
336 |
337 | The C and C options can be used to generate C queries
338 | with C and C clauses.
339 |
340 | # "select * from some_table limit 10"
341 | $abstract->select('some_table', '*', undef, {limit => 10});
342 |
343 | # "select * from some_table offset 5"
344 | $abstract->select('some_table', '*', undef, {offset => 5});
345 |
346 | # "select * from some_table limit 10 offset 5"
347 | $abstract->select('some_table', '*', undef, {limit => 10, offset => 5});
348 |
349 | =head1 SEE ALSO
350 |
351 | L, L, L, L.
352 |
353 | =cut
354 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 })->wait;
79 | is $result, 1, 'right result';
80 | $db->delete_p('crud_test', {name => 'promise_two'})->then(sub { $result = shift->affected_rows })->wait;
81 | is $result, 1, 'right result';
82 |
83 | note 'Promises (rejected)';
84 | my $fail;
85 | $db->query_p('does_not_exist')->catch(sub { $fail = shift })->wait;
86 | like $fail, qr/does_not_exist/, 'right error';
87 |
88 | note 'cleanup';
89 | END { $db and $db->query('drop table if exists crud_test'); }
90 |
91 | done_testing;
92 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.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/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_test (
16 | id integer auto_increment primary key,
17 | name text
18 | )'
19 | );
20 | $db->query('truncate table results_test');
21 |
22 | my $res = $db->query('insert into results_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_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_test set name=? where name=?', 'foo', 'foo1')->affected_rows, 0, 'right affected rows';
38 | is $db->query('update results_test set name=? where name=?', 'foo', 'foo')->affected_rows, 0, 'right affected rows';
39 | is $db->query('update results_test set id=1 where id=1')->affected_rows, 0, 'right affected rows';
40 |
41 | $res = $db->query("select 1 + '4a'");
42 | is_deeply $res->array, [5];
43 | is $res->warnings_count, 1, 'warnings';
44 |
45 | $res = $db->query('show warnings');
46 | like $res->hashes->[0]->{Message}, qr/Truncated/, 'warning message';
47 |
48 | $db->disconnect;
49 |
50 | $mysql->options->{"${driver}_client_found_rows"} = 1;
51 | $db = $mysql->db;
52 |
53 | is $db->query('update results_test set name=? where name=?', 'foo', 'foo1')->affected_rows, 0, 'right affected rows';
54 | is $db->query('update results_test set name=? where name=?', 'foo', 'foo')->affected_rows, 1, 'right affected rows';
55 | is $db->query('update results_test set id=1 where id=1')->affected_rows, 1, 'right affected rows';
56 |
57 | $db->query('drop table results_test');
58 |
59 | my $err;
60 | $db->query('select name from results_test', sub { shift; ($err, $res) = @_; Mojo::IOLoop->stop });
61 | Mojo::IOLoop->start;
62 | like $err, qr/results_test/, 'has error';
63 | ok index($err, $res->errstr) == 0, 'same error';
64 | is length($res->state), 5, 'has state';
65 |
66 | done_testing;
67 |
--------------------------------------------------------------------------------
/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_mysql1';
10 |
11 | note 'Create table';
12 | $db->query(<query('truncate table mojo_mysql1');
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_mysql1') unless $ENV{TEST_KEEP_DB};
55 |
56 | done_testing;
57 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 results_test (
12 | id serial primary key,
13 | name varchar(255) charset utf8
14 | )'
15 | );
16 | $db->query('truncate table results_test');
17 | $db->query('insert into results_test (name) values (?)', $_) for qw(☺ ☻);
18 |
19 | note 'Result methods';
20 | is_deeply $db->query('select * from results_test')->rows, 2, 'two rows';
21 | is_deeply $db->query('select * from results_test')->columns, ['id', 'name'], 'right structure';
22 | is_deeply $db->query('select * from results_test')->array, [1, '☺'], 'right structure';
23 | is_deeply [$db->query('select * from results_test')->arrays->each], [[1, '☺'], [2, '☻']], 'right structure';
24 | is_deeply $db->query('select * from results_test')->hash, {id => 1, name => '☺'}, 'right structure';
25 | is_deeply [$db->query('select * from results_test')->hashes->each],
26 | [{id => 1, name => '☺'}, {id => 2, name => '☻'}], 'right structure';
27 | is $mysql->db->query('select * from results_test')->text, "1 ☺\n2 ☻\n", 'right text';
28 |
29 | $db->query('drop table results_test');
30 |
31 | done_testing;
32 |
--------------------------------------------------------------------------------