File Coverage

blib/lib/Mojolicious/Plugin/QuickMy.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::QuickMy;
2 1     1   14045 use Mojo::Base 'Mojolicious::Plugin';
  1         6863  
  1         4  
3              
4 1     1   1093 use Mojo::mysql;
  0            
  0            
5             use Carp;
6              
7             our $VERSION = '1.0';
8             our $Debug = 0;
9             has pg => sub { +{} };
10              
11             sub register {
12             my ($plugin, $app, $conf) = @_;
13             my $version = undef;
14             croak qq{DSN is empty! See perldoc...\n} unless $conf->{dsn};
15            
16             $plugin->pg(Mojo::mysql->new($conf->{dsn}));
17             $Debug = $conf->{debug};
18            
19             eval {
20             $version = $plugin->pg->db->query('select version()')->hash;
21             };
22             croak qq{Invalid dsn: $@ \n} if $@;
23              
24             $app->log->info("MySQL version: $version->{'version()'}") if ($conf->{debug});
25            
26             $app->helper( qselect => sub {$plugin->_quick_select(@_)});
27             $app->helper( qinsert => sub {$plugin->_quick_insert(@_)});
28             $app->helper( qupdate => sub {$plugin->_quick_update(@_)});
29             $app->helper( qdelete => sub {$plugin->_quick_delete(@_)});
30             $app->helper( qcount => sub {$plugin->_quick_count(@_)});
31             $app->helper( qcustom => sub {
32             my ($self, $sql, @params) = @_;
33             eval {
34             local $SIG{__WARN__} = sub {};
35             return $plugin->pg->db->query($sql,@params);
36             };
37            
38             });
39             $app->helper( qerror => sub { return $@ });
40             }
41              
42             sub _quick_insert {
43             my ($plugin, $self, $table_name, $data) = @_;
44             return $plugin->_quick_query('INSERT', $table_name, $data);
45             }
46              
47             #
48             sub _quick_update {
49             my ($plugin, $self, $table_name, $where, $data) = @_;
50             return $plugin->_quick_query('UPDATE', $table_name, $data, $where);
51             }
52             #
53             #
54             sub _quick_delete {
55             my ($plugin, $self, $table_name, $where) = @_;
56             return $plugin->_quick_query('DELETE', $table_name, undef, $where);
57             }
58            
59             sub _quick_select {
60             my ($plugin, $self, $table_name, $where, $opts) = @_;
61             $where = $where || {};
62             return $plugin->_quick_query('SELECT', $table_name, $opts, $where);
63             }
64              
65            
66             sub _quick_count {
67             my ($plugin, $self, $table_name, $where) = @_;
68             $where = $where || {};
69             my $opts = {}; #Options are irrelevant for a count.
70             return $plugin->_quick_query('COUNT', $table_name, $opts, $where);
71             }
72              
73             #############
74             sub _quick_query {
75             my ($self, $type, $table_name, $data, $where) = @_;
76             # Basic sanity checks first...
77             if ($type !~ m{^ (SELECT|INSERT|UPDATE|DELETE|COUNT) $}x) {
78             carp "Unrecognised query type $type!";
79             return;
80             }
81             if (!$table_name || ref $table_name) {
82             carp "Expected table name as a straight scalar";
83             return;
84             }
85             if (($type eq 'INSERT' || $type eq 'UPDATE')
86             && (!$data || ref $data ne 'HASH'))
87             {
88             carp "Expected a hashref of changes";
89             return;
90             }
91             if (($type =~ m{^ (SELECT|UPDATE|DELETE|COUNT) $}x)
92             && (!$where)) {
93             carp "Expected where conditions";
94             return;
95             }
96            
97             my ($sql, @bind_params) = $self->_generate_sql(
98             $type, $table_name, $data, $where
99             );
100             say $sql if $Debug;
101            
102             if ($type eq 'SELECT') {
103             return $self->pg->db->query($sql, @bind_params)->hash unless wantarray;
104             return $self->pg->db->query($sql, @bind_params)->hashes->to_array;
105             } elsif ($type eq 'COUNT') {
106             my $row = $self->pg->db->query($sql, @bind_params)->hash;
107             return $row->{count};
108             } else {
109             # INSERT (default field name = id)
110             if ($type eq 'INSERT') {
111             eval {
112             return $self->pg->db->query($sql, @bind_params)->last_insert_id;
113             };
114             } else {
115             # delete/update
116             eval {
117             return $self->pg->db->query($sql, @bind_params)->rows;
118             };
119             }
120            
121             }
122             }
123            
124             sub _generate_sql {
125             my ($self, $type, $table_name, $data, $where) = @_;
126            
127             my $which_cols = '*';
128             my $opts = $type eq 'SELECT' && $data ? $data : {};
129             if ($opts->{columns}) {
130             my @cols = (ref $opts->{columns})
131             ? @{ $opts->{columns} }
132             : $opts->{columns} ;
133             $which_cols = join(',', map { $self->_quote_identifier($_) } @cols);
134             }
135            
136             $table_name = $self->_quote_identifier($table_name);
137             my @bind_params;
138            
139             my $sql = {
140             SELECT => "SELECT $which_cols FROM $table_name",
141             INSERT => "INSERT INTO $table_name ",
142             UPDATE => "UPDATE $table_name SET ",
143             DELETE => "DELETE FROM $table_name ",
144             COUNT => "SELECT COUNT(*) FROM $table_name",
145             }->{$type};
146            
147             if ($type eq 'INSERT') {
148             my (@keys, @values);
149             for my $key (sort keys %$data) {
150             my $value = $data->{$key};
151             push @keys, $self->_quote_identifier($key);
152             if (ref $value eq 'SCALAR') {
153             # If it's a scalarref it goes in the SQL as it is; this is a
154             # potential SQL injection risk, but is documented as such - it
155             # allows the user to include arbitrary SQL, at their own risk.
156             push @values, $$value;
157             } else {
158             push @values, "?";
159             push @bind_params, $value;
160             }
161             }
162            
163             $sql .= sprintf "(%s) VALUES (%s)",
164             join(',', @keys), join(',', @values);
165             }
166            
167             if ($type eq 'UPDATE') {
168             my @sql;
169             for (sort keys %$data) {
170             push @sql, $self->_quote_identifier($_) . '=' .
171             (ref $data->{$_} eq 'SCALAR' ? ${$data->{$_}} : "?");
172             push @bind_params, $data->{$_} if (ref $data->{$_} ne 'SCALAR');
173             }
174             $sql .= join ',', @sql;
175             }
176            
177             if ($type eq 'UPDATE' || $type eq 'DELETE' || $type eq 'SELECT' || $type eq 'COUNT')
178             {
179             if ($where && !ref $where) {
180             $sql .= " WHERE " . $where;
181             } elsif ( ref $where eq 'HASH' ) {
182             my @stmts;
183             foreach my $k ( sort keys %$where ) {
184             my $v = $where->{$k};
185             if ( ref $v eq 'HASH' ) {
186             my $not = delete $v->{'not'};
187             while (my($op,$value) = each %$v ) {
188             my ($cond, $add_bind_param)
189             = $self->_get_where_sql($op, $not, $value);
190             push @stmts, $self->_quote_identifier($k) . $cond;
191             push @bind_params, $v->{$op} if $add_bind_param;
192             }
193             } else {
194             my $clause .= $self->_quote_identifier($k);
195             if ( ! defined $v ) {
196             $clause .= ' IS NULL';
197             }
198             elsif ( ! ref $v ) {
199             $clause .= '=?';
200             push @bind_params, $v;
201             }
202             elsif ( ref $v eq 'ARRAY' ) {
203             $clause .= ' IN (' . (join ',', map { '?' } @$v) . ')';
204             push @bind_params, @$v;
205             }
206             push @stmts, $clause;
207             }
208             }
209             $sql .= " WHERE " . join " AND ", @stmts if keys %$where;
210             } elsif (ref $where) {
211             carp "Can't handle ref " . ref $where . " for where";
212             return;
213             }
214             }
215            
216             # Add an ORDER BY clause, if we want to:
217             if (exists $opts->{order_by} and defined $opts->{order_by}) {
218             $sql .= ' ' . $self->_build_order_by_clause($opts->{order_by});
219             }
220            
221            
222             # Add a LIMIT clause if we want to:
223             if (exists $opts->{limit} and defined $opts->{limit}) {
224             my $limit = $opts->{limit};
225             $limit =~ s/\s+//g;
226             # Check the limit clause is sane - just a number, or two numbers with a
227             # comma between (if using offset,limit )
228             if ($limit =~ m{ ^ \d+ (?: , \d+)? $ }x) {
229             # Checked for sanity above so safe to interpolate
230             $sql .= " LIMIT $limit";
231             } else {
232             die "Invalid LIMIT param $opts->{limit} !";
233             }
234             } elsif ($type eq 'SELECT' && !wantarray) {
235             # We're only returning one row in scalar context, so don't ask for any
236             # more than that
237             $sql .= " LIMIT 1";
238             }
239            
240             if (exists $opts->{offset} and defined $opts->{offset}) {
241             my $offset = $opts->{offset};
242             $offset =~ s/\s+//g;
243             if ($offset =~ /^\d+$/) {
244             $sql .= " OFFSET $offset";
245             } else {
246             die "Invalid OFFSET param $opts->{offset} !";
247             }
248             }
249             return ($sql, @bind_params);
250             }
251            
252             sub _get_where_sql {
253             my ($self, $op, $not, $value) = @_;
254            
255             $op = lc $op;
256            
257             # "IS" needs special-casing, as it will be either "IS NULL" or "IS NOT NULL"
258             # - there's no need to return a bind param for that.
259             if ($op eq 'is') {
260             return $not ? 'IS NOT NULL' : 'IS NULL';
261             }
262            
263             my %st = (
264             'like' => ' LIKE ?',
265             'is' => ' IS ?',
266             'ge' => ' >= ?',
267             'gt' => ' > ?',
268             'le' => ' <= ?',
269             'lt' => ' < ?',
270             'eq' => ' = ?',
271             'ne' => ' != ?',
272             );
273            
274             # Return the appropriate SQL, and indicate that the value should be added to
275             # the bind params
276             return (($not ? ' NOT' . $st{$op} : $st{$op}), 1);
277             }
278            
279             # Given either a column name, or a hashref of e.g. { asc => 'colname' },
280             # or an arrayref of either, construct an ORDER BY clause (quoting col names)
281             # e.g.:
282             # 'foo' => ORDER BY foo
283             # { asc => 'foo' } => ORDER BY foo ASC
284             # ['foo', 'bar'] => ORDER BY foo, bar
285             # [ { asc => 'foo' }, { desc => 'bar' } ]
286             # => 'ORDER BY foo ASC, bar DESC
287             sub _build_order_by_clause {
288             my ($self, $in) = @_;
289            
290             # Input could be a straight scalar, or a hashref, or an arrayref of either
291             # straight scalars or hashrefs. Turn a straight scalar into an arrayref to
292             # avoid repeating ourselves.
293             $in = [ $in ] unless ref $in eq 'ARRAY';
294            
295             # Now, for each of the fields given, add them to the clause
296             my @sort_fields;
297             for my $field (@$in) {
298             if (!ref $field) {
299             push @sort_fields, $self->_quote_identifier($field);
300             } elsif (ref $field eq 'HASH') {
301             my ($order, $name) = %$field;
302             $order = uc $order;
303             if ($order ne 'ASC' && $order ne 'DESC') {
304             die "Invalid sort order $order used in order_by option!";
305             }
306             # $order has been checked to be 'ASC' or 'DESC' above, so safe to
307             # interpolate
308             push @sort_fields, $self->_quote_identifier($name) . " $order";
309             }
310             }
311            
312             return "ORDER BY " . join ', ', @sort_fields;
313             }
314              
315             # A wrapper around DBI's quote_identifier which first splits on ".", so that
316             # e.g. database.table gets quoted as `database`.`table`, not `database.table`
317             sub _quote_identifier {
318             my ($self, $identifier) = @_;
319             return join '.', map {
320             qq{$_}
321             } split /\./, $identifier;
322             }
323              
324             1;
325             __END__