File Coverage

blib/lib/Mojolicious/Plugin/QuickPg.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::QuickPg;
2 1     1   14492 use Mojo::Base 'Mojolicious::Plugin';
  1         7460  
  1         6  
3            
4 1     1   1217 use Mojo::Pg;
  0            
  0            
5             use Carp;
6            
7             our $VERSION = '1.6';
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::Pg->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             $app->log->info($version->{version}) if ($conf->{debug});
24            
25             $app->helper( qselect => sub {$plugin->_quick_select(@_)});
26             $app->helper( qinsert => sub {$plugin->_quick_insert(@_)});
27             $app->helper( qupdate => sub {$plugin->_quick_update(@_)});
28             $app->helper( qdelete => sub {$plugin->_quick_delete(@_)});
29             $app->helper( qcount => sub {$plugin->_quick_count(@_)});
30             $app->helper( qcustom => sub {
31             my ($self, $sql, @params) = @_;
32             say "DEBUG: $sql" if $Debug;
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 "DEBUG: $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             # get primary key from table
112             my $PK = 'id'; # default PK column is 'id'
113             my $pkey = $self->pg->db->query(qq{SELECT a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid
114             AND a.attnum = ANY(i.indkey) WHERE i.indrelid = '$table_name'::regclass AND i.indisprimary
115             ORDER BY a.attnum DESC});
116             # if id exists in PKeys get it, else - get last elem
117             while (my $next = $pkey->hash) {
118             if ( $next->{attname} eq 'id' ) {
119             $PK = $next->{attname};
120             # Note that "finish" needs to be called if you are not fetching all the possible rows
121             $pkey->finish;
122             last;
123             }
124             else {
125             $PK = $next->{attname};
126             }
127             }
128            
129             $sql .= ' returning ' . $PK;
130            
131             eval {
132             return $self->pg->db->query($sql, @bind_params)->hash->{$PK};
133             };
134             } else {
135             # delete/update
136             eval {
137             return $self->pg->db->query($sql, @bind_params)->rows;
138             };
139             }
140            
141             }
142             }
143            
144             sub _generate_sql {
145             my ($self, $type, $table_name, $data, $where) = @_;
146            
147             my $which_cols = '*';
148             my $opts = $type eq 'SELECT' && $data ? $data : {};
149             if ($opts->{columns}) {
150             my @cols = (ref $opts->{columns})
151             ? @{ $opts->{columns} }
152             : $opts->{columns} ;
153             $which_cols = join(',', map { $self->_quote_identifier($_) } @cols);
154             }
155            
156             $table_name = $self->_quote_identifier($table_name);
157             my @bind_params;
158            
159             my $sql = {
160             SELECT => "SELECT $which_cols FROM $table_name",
161             INSERT => "INSERT INTO $table_name ",
162             UPDATE => "UPDATE $table_name SET ",
163             DELETE => "DELETE FROM $table_name ",
164             COUNT => "SELECT COUNT(*) FROM $table_name",
165             }->{$type};
166            
167             if ($type eq 'INSERT') {
168             my (@keys, @values);
169             for my $key (sort keys %$data) {
170             my $value = $data->{$key};
171             push @keys, $self->_quote_identifier($key);
172             if (ref $value eq 'SCALAR') {
173             # If it's a scalarref it goes in the SQL as it is; this is a
174             # potential SQL injection risk, but is documented as such - it
175             # allows the user to include arbitrary SQL, at their own risk.
176             push @values, $$value;
177             } else {
178             push @values, "?";
179             push @bind_params, $value;
180             }
181             }
182            
183             $sql .= sprintf "(%s) VALUES (%s)",
184             join(',', @keys), join(',', @values);
185             }
186            
187             if ($type eq 'UPDATE') {
188             my @sql;
189             for (sort keys %$data) {
190             push @sql, $self->_quote_identifier($_) . '=' .
191             (ref $data->{$_} eq 'SCALAR' ? ${$data->{$_}} : "?");
192             push @bind_params, $data->{$_} if (ref $data->{$_} ne 'SCALAR');
193             }
194             $sql .= join ',', @sql;
195             }
196            
197             if ($type eq 'UPDATE' || $type eq 'DELETE' || $type eq 'SELECT' || $type eq 'COUNT')
198             {
199             if ($where && !ref $where) {
200             $sql .= " WHERE " . $where;
201             } elsif ( ref $where eq 'HASH' ) {
202             my @stmts;
203             foreach my $k ( sort keys %$where ) {
204             my $v = $where->{$k};
205             if ( ref $v eq 'HASH' ) {
206             my $not = delete $v->{'not'};
207             while (my($op,$value) = each %$v ) {
208             my ($cond, $add_bind_param)
209             = $self->_get_where_sql($op, $not, $value);
210             push @stmts, $self->_quote_identifier($k) . $cond;
211             push @bind_params, $v->{$op} if $add_bind_param;
212             }
213             } else {
214             my $clause .= $self->_quote_identifier($k);
215             if ( ! defined $v ) {
216             $clause .= ' IS NULL';
217             }
218             elsif ( ! ref $v ) {
219             $clause .= '=?';
220             push @bind_params, $v;
221             }
222             elsif ( ref $v eq 'ARRAY' ) {
223             $clause .= ' IN (' . (join ',', map { '?' } @$v) . ')';
224             push @bind_params, @$v;
225             }
226             push @stmts, $clause;
227             }
228             }
229             $sql .= " WHERE " . join " AND ", @stmts if keys %$where;
230             } elsif (ref $where) {
231             carp "Can't handle ref " . ref $where . " for where";
232             return;
233             }
234             }
235            
236             # Add an ORDER BY clause, if we want to:
237             if (exists $opts->{order_by} and defined $opts->{order_by}) {
238             $sql .= ' ' . $self->_build_order_by_clause($opts->{order_by});
239             }
240            
241            
242             # Add a LIMIT clause if we want to:
243             if ((exists $opts->{limit} and defined $opts->{limit}) and (exists $opts->{offset} and defined $opts->{offset})) {
244             my $limit = $opts->{limit};
245             my $offset = $opts->{offset};
246             $offset =~ s/\s+//g;
247             $limit =~ s/\s+//g;
248             die "Invalid OFFSET param $opts->{offset} !" unless ($offset =~ /^\d+$/);
249             if ($limit =~ m{ ^ \d+ (?: , \d+)? $ }x) {
250             # Checked for sanity above so safe to interpolate
251             $sql .= " LIMIT $limit OFFSET $offset";
252             } else {
253             die "Invalid LIMIT param $opts->{limit} !";
254             }
255             } elsif ($type eq 'SELECT' && !wantarray) {
256             # We're only returning one row in scalar context, so don't ask for any
257             # more than that
258             $sql .= " LIMIT 1 OFFSET 0";
259             }
260            
261             return ($sql, @bind_params);
262             }
263            
264             sub _get_where_sql {
265             my ($self, $op, $not, $value) = @_;
266            
267             $op = lc $op;
268            
269             # "IS" needs special-casing, as it will be either "IS NULL" or "IS NOT NULL"
270             # - there's no need to return a bind param for that.
271             if ($op eq 'is') {
272             return $not ? 'IS NOT NULL' : 'IS NULL';
273             }
274            
275             my %st = (
276             'like' => ' LIKE ?',
277             'is' => ' IS ?',
278             'ge' => ' >= ?',
279             'gt' => ' > ?',
280             'le' => ' <= ?',
281             'lt' => ' < ?',
282             'eq' => ' = ?',
283             'ne' => ' != ?',
284             );
285            
286             # Return the appropriate SQL, and indicate that the value should be added to
287             # the bind params
288             return (($not ? ' NOT' . $st{$op} : $st{$op}), 1);
289             }
290            
291             # Given either a column name, or a hashref of e.g. { asc => 'colname' },
292             # or an arrayref of either, construct an ORDER BY clause (quoting col names)
293             # e.g.:
294             # 'foo' => ORDER BY foo
295             # { asc => 'foo' } => ORDER BY foo ASC
296             # ['foo', 'bar'] => ORDER BY foo, bar
297             # [ { asc => 'foo' }, { desc => 'bar' } ]
298             # => 'ORDER BY foo ASC, bar DESC
299             sub _build_order_by_clause {
300             my ($self, $in) = @_;
301            
302             # Input could be a straight scalar, or a hashref, or an arrayref of either
303             # straight scalars or hashrefs. Turn a straight scalar into an arrayref to
304             # avoid repeating ourselves.
305             $in = [ $in ] unless ref $in eq 'ARRAY';
306            
307             # Now, for each of the fields given, add them to the clause
308             my @sort_fields;
309             for my $field (@$in) {
310             if (!ref $field) {
311             push @sort_fields, $self->_quote_identifier($field);
312             } elsif (ref $field eq 'HASH') {
313             my ($order, $name) = %$field;
314             $order = uc $order;
315             if ($order ne 'ASC' && $order ne 'DESC') {
316             die "Invalid sort order $order used in order_by option!";
317             }
318             # $order has been checked to be 'ASC' or 'DESC' above, so safe to
319             # interpolate
320             push @sort_fields, $self->_quote_identifier($name) . " $order";
321             }
322             }
323            
324             return "ORDER BY " . join ', ', @sort_fields;
325             }
326            
327             # A wrapper around DBI's quote_identifier which first splits on ".", so that
328             # e.g. database.table gets quoted as `database`.`table`, not `database.table`
329             sub _quote_identifier {
330             my ($self, $identifier) = @_;
331             return join '.', map {
332             $self->pg->db->dbh->quote_identifier($_)
333             } split /\./, $identifier;
334             }
335            
336             1;
337             __END__