File Coverage

blib/lib/DBIx/PgLink/Adapter.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 DBIx::PgLink::Adapter;
2              
3 5     5   506240 use Carp;
  5         14  
  5         739  
4 5     5   6727 use Moose;
  0            
  0            
5             use MooseX::Method;
6             use DBI qw(:sql_types);
7             use DBIx::PgLink::Logger qw/trace_msg trace_level/;
8             use DBIx::PgLink::Types;
9             use Data::Dumper;
10              
11             extends 'Moose::Object';
12              
13             our $VERSION = '0.01';
14              
15             has 'connector' => (
16             is => 'ro',
17             isa => 'DBIx::PgLink::Connector',
18             required => 0,
19             weak_ref => 1,
20             );
21              
22             has 'dbh' => (
23             isa => 'Object', # could be ::db or any DBIx wrapper
24             is => 'rw',
25             # delegation bug #1: wrong context for list-returning methods
26             # delegation bug #2: reconnection hook cannot use wrapped method, core dump at subsequent call of $next->()
27             handles => [ qw/
28             err errstr state set_err func
29             data_sources do last_insert_id
30             selectrow_array selectrow_arrayref selectrow_hashref
31             selectall_arrayref selectall_hashref selectcol_arrayref
32             prepare prepare_cached
33             commit rollback begin_work
34             disconnect ping
35             get_info table_info column_info primary_key_info primary_key
36             foreign_key_info statistics_info tables
37             type_info_all type_info
38             quote quote_identifier
39             /],
40             );
41              
42             has 'statement_roles' => (
43             isa => 'ArrayRef',
44             is => 'rw',
45             auto_deref => 1,
46             default => sub { [] },
47             );
48              
49              
50             with 'DBIx::PgLink::RoleInstaller';
51             has '+role_prefix' => ( default => __PACKAGE__ . '::Roles::' );
52              
53              
54             has 'are_transactions_supported' => (
55             isa => 'Bool',
56             is => 'ro',
57             lazy => 1,
58             default => sub {
59             # borrowed from DBIx::SQLEngine
60             my $self = shift;
61             my $dbh = $self->dbh;
62             eval {
63             local $SIG{__DIE__};
64             $dbh->begin_work;
65             $dbh->rollback;
66             };
67             return ( $@ ) ? 0 : 1;
68             },
69             );
70              
71             has 'are_routines_supported' => (is=>'ro', isa=>'Bool', default=>0);
72             has 'routine_can_be_overloaded' => (is=>'ro', isa=>'Bool', default=>0);
73             has 'include_catalog_to_qualified_name' => (is=>'ro', isa=>'Bool', default=>0);
74             has 'include_schema_to_qualified_name' => (is=>'ro', isa=>'Bool', default=>1);
75             has 'require_parameter_type' => (is=>'ro', isa=>'Bool', default=>1); # performance option, typed binding ~2x times slower
76              
77             sub connect {
78             my $self = shift;
79             my $attr = $_[-1];
80             if (ref $attr ne 'HASH') {
81             $attr = {};
82             push @_, $attr;
83             }
84             $attr->{RaiseError} = 1;
85             $attr->{PrintError} = 0;
86             $attr->{AutoCommit} = 1 unless exists $attr->{AutoCommit};
87             # appends a stack trace to all errors
88             $attr->{HandleError} = sub { $_[0]=Carp::longmess($_[0]); 0; };
89              
90             trace_msg('INFO', 'connect') if trace_level >= 2;
91             $self->dbh( DBI->connect(@_) );
92             $self->initialize_session;
93             return $self->dbh;
94             }
95              
96              
97             sub dbi_method {
98             my $self = shift;
99             my $dbi_handle = shift; # dbh or sth
100             my $method = shift;
101             return $dbi_handle->$method(@_);
102             }
103              
104             # protected statement-returning methods
105             for my $func (qw/
106             prepare prepare_cached
107             table_info column_info primary_key_info foreign_key_info statistics_info
108             /) {
109             around $func => sub {
110             my $next = shift;
111             my $self = shift;
112             trace_msg('INFO', "$func") if trace_level >= 3;
113             my $sth = $self->dbi_method($self->dbh, $func, @_);
114             return unless $sth;
115             my $st = $self->new_statement(
116             class => 'DBIx::PgLink::Adapter::st',
117             parent => $self,
118             sth => $sth,
119             );
120             return $st;
121             };
122             }
123              
124             sub new_statement {
125             my ($self, %p) = @_;
126             my $class = $p{class};
127             my $st = $class->new(%p);
128             for my $role ($self->statement_roles) {
129             $role->meta->apply($st);
130             }
131             return $st;
132             }
133              
134              
135             # list-returning methods and other protected methods
136             for my $func (qw/
137             data_sources func do primary_key tables type_info
138             selectrow_array selectrow_arrayref selectrow_hashref
139             selectall_arrayref selectall_hashref selectcol_arrayref
140             commit rollback begin_work
141             /) {
142             around $func => sub {
143             my $next = shift;
144             my $self = shift;
145             trace_msg('INFO', "$func") if trace_level >= 3;
146             return $self->dbi_method($self->dbh, $func, @_);
147             };
148             }
149              
150             sub is_transaction_active {
151             my $self = shift;
152             return ! $self->dbh->{'AutoCommit'};
153             }
154              
155             sub initialize_session { 1 }
156              
157             # for Reconnect role
158             sub always_valid_query {
159             "SELECT 1"
160             }
161              
162             sub check_where_condition { 1 }
163              
164              
165             has 'is_plperl' => (
166             is => 'ro',
167             isa => 'Bool',
168             lazy => 1,
169             default => sub {
170             eval "main::NOTICE";
171             return !$@;
172             }
173             );
174              
175             sub require_plperl {
176             my ($self, $who) = @_;
177             die "$who can be used in PL/Perl environment only"
178             unless $self->is_plperl;
179             }
180              
181              
182             # most of DBI catalog methods returns statement handle
183             # here we define wrapper subs that returns reference to array of hashes
184             # and call expand_xxx method on every hash item
185             # Expanded metadata may contain additional fields consumed by Accessor
186              
187             sub table_info_arrayref {
188             my $self = shift;
189             my $sth = $self->table_info(@_);
190             return [] unless $sth;
191             my @result = ();
192             while (my $i = $sth->fetchrow_hashref) {
193             $self->expand_table_info($i)
194             and push @result, $i;
195             }
196             $sth->finish;
197             return \@result;
198             }
199              
200              
201             sub routine_info_arrayref {
202             my $self = shift;
203             my $sth = $self->routine_info(@_);
204             return [] unless $sth;
205             my @result = ();
206             while (my $i = $sth->fetchrow_hashref) {
207             $self->expand_routine_info($i)
208             and push @result, $i;
209             }
210             $sth->finish;
211             return \@result;
212             }
213              
214              
215             sub column_info_arrayref {
216             my $self = shift;
217             my $sth = $self->column_info(@_);
218             return [] unless $sth;
219             my @result = ();
220             while (my $i = $sth->fetchrow_hashref) {
221             $self->expand_column_info($i)
222             and push @result, $i;
223             }
224             $sth->finish;
225             return \@result;
226             }
227              
228              
229             # create column_info-like structure from statement description, returns refarray of hashes
230             # NOTE: some drivers cannot have more than one open statement
231             # call type_info() once *before* this method
232             sub column_info_from_statement_arrayref {
233             my ($self, $catalog, $schema, $table, $sth) = @_;
234             my @result;
235             my %ti;
236             if ($sth->isa('DBIx::PgLink::Adapter::st')) {
237             $sth = $sth->sth; # get the real DBI::st
238             }
239             for my $f (0..$sth->{NUM_OF_FIELDS}-1) {
240             my $type = $sth->{TYPE}->[$f];
241             unless (defined $ti{$type}) {
242             $ti{$type} = ($self->type_info($type))[0]; #!!! first row only
243             }
244             push @result, {
245             TABLE_CAT => $catalog,
246             TABLE_SCHEM => $schema,
247             TABLE_NAME => $table,
248             COLUMN_NAME => $sth->{NAME}->[$f],
249             DATA_TYPE => $type,
250             TYPE_NAME => $ti{$type}->{TYPE_NAME},
251             COLUMN_SIZE => $sth->{PRECISION}->[$f],
252             DECIMAL_DIGITS => $sth->{SCALE}->[$f],
253             NUM_PREC_RADIX => $sth->{SCALE}->[$f],
254             NULLABLE => $sth->{NULLABLE}->[$f],
255             ORDINAL_POSITION => $f+1,
256             IS_NULLABLE => $sth->{NULLABLE}->[$f] ? 'YES' : 'NO',
257             };
258             }
259             return \@result;
260             }
261              
262              
263             sub primary_key_info_arrayref {
264             my $self = shift;
265             my $sth = $self->primary_key_info(@_) or return;
266             return [] unless $sth;
267             my @result = ();
268             while (my $i = $sth->fetchrow_hashref) {
269             $self->expand_primary_key_info($i)
270             and push @result, $i;
271             }
272             $sth->finish;
273             return \@result;
274             }
275              
276             # add or fix catalog information for Accessor
277             sub expand_table_info { 1 }
278             sub expand_routine_info { 1 }
279             sub expand_column_info { 1 }
280             sub expand_primary_key_info { 1 }
281             sub expand_routine_argument_info { 1 }
282              
283             sub unquote_identifier {
284             my ($self, $i) = @_;
285             # don't support full-qualified name with schema!
286             if ($i =~ /^"(.*)"$/) {
287             $i = $1;
288             $i =~ s/""/"/g;
289             }
290             return $i;
291             }
292              
293             sub trim_trailing_spaces {
294             $_[0] =~ s/ +$//;
295             }
296              
297              
298             sub routine_info {
299             my ($self, $catalog, $schema, $routine, $type) = @_;
300             # generic INFORMATION_SCHEMA (supported by Pg and MSSQL, but not very useful)
301              
302             my $type_cond = do {
303             if (!defined $type || $type eq '%') {
304             ''
305             } elsif ($type =~ /('\w+',)*('\w+')/) {
306             "AND ROUTINE_TYPE IN ($type)"
307             } else {
308             "AND ROUTINE_TYPE IN ('" . join("','", split /,/, $type) . "')"
309             }
310             };
311              
312             my $sth = eval {
313             $self->prepare(<<END_OF_SQL);
314             SELECT
315             SPECIFIC_CATALOG,
316             SPECIFIC_SCHEMA,
317             SPECIFIC_NAME,
318             ROUTINE_CATALOG,
319             ROUTINE_SCHEMA,
320             ROUTINE_NAME,
321             ROUTINE_TYPE,
322             DATA_TYPE
323             FROM INFORMATION_SCHEMA.ROUTINES
324             WHERE SPECIFIC_CATALOG like ?
325             AND SPECIFIC_SCHEMA like ?
326             AND SPECIFIC_NAME like ?
327             $type_cond
328             ORDER BY 1,2,3
329             END_OF_SQL
330             };
331             return undef if $@;
332             $sth->execute($catalog, $schema, $routine);
333             return $sth;
334             }
335              
336              
337             sub routine_argument_info_arrayref {
338             my ($self, $routine_info) = @_;
339             # no INFORMATION_SCHEMA catalog for routine input arguments
340             # NOTE: should returns AoH for single routine
341             return [];
342             }
343              
344              
345             sub routine_column_info_arrayref {
346             my ($self, $routine_info) = @_;
347             # NOTE: should returns AoH for single routine
348             # NOTE: not supported by Pg
349             my $sth = eval {
350             $self->prepare(<<END_OF_SQL);
351             SELECT *
352             FROM INFORMATION_SCHEMA.ROUTINE_COLUMNS
353             WHERE TABLE_CATALOG = ?
354             AND TABLE_SCHEMA = ?
355             AND TABLE_NAME = ?
356             ORDER BY 1,2,3
357             END_OF_SQL
358             };
359             return [] if $@;
360             $sth->execute(
361             $routine_info->{SPECIFIC_CATALOG},
362             $routine_info->{SPECIFIC_SCHEMA},
363             $routine_info->{SPECIFIC_NAME},
364             );
365             my @result = ();
366             while (my $i = $sth->fetchrow_hashref) {
367             $self->expand_column_info($i)
368             and push @result, $i;
369             }
370             $sth->finish;
371             return \@result;
372             }
373              
374              
375             method 'format_routine_call' => named(
376             catalog => {isa=>'StrNull',required=>0},
377             schema => {isa=>'StrNull',required=>0},
378             routine => {isa=>'Str',required=>1},
379             routine_type => {isa=>'Str',required=>1},
380             returns_set => {isa=>'Str',required=>0},
381             arguments => {isa=>'ArrayRef',required=>0, default=>[]},
382             ) => sub {
383             my ($self, $p) = @_;
384              
385             my $result = 'SELECT ';
386             $result .= '* FROM ' if $p->{returns_set};
387             $result .= $self->quote_identifier($p->{catalog}, $p->{schema}, $p->{routine})
388             . '(' . join(',', map { '?' } @{$p->{arguments}} ) . ')';
389             return $result;
390             };
391              
392              
393             sub get_number_of_rows {
394             my ($self, $catalog, $schema, $object, $type) = @_;
395             return selectrow_array("SELECT count(*) FROM " . $self->quote_identifier($schema, $object) );
396             # descendants can use estimated row count
397             }
398              
399             # -------------- conversion
400              
401              
402             # octal escape all bytes (PL/Perl cannot return raw binary data)
403             # SLOW!
404             my @byte_oct;
405             sub to_pg_bytea {
406             return unless defined $_[1] && length($_[1]) > 0;
407             unless (@byte_oct) {
408             for my $c (0..255) {
409             $byte_oct[$c] = sprintf('\%03o', $c);
410             }
411             }
412             my $b = '';
413             # use bytes;
414             for (my $i = 0; $i < length($_[1]); $i++) {
415             $b .= $byte_oct[ord(substr($_[1],$i,1))];
416             }
417             $_[1] = $b;
418             }
419              
420              
421             1;
422              
423              
424              
425             package DBIx::PgLink::Adapter::st;
426              
427             use Moose;
428             use DBIx::PgLink::Logger qw/trace_msg trace_level/;
429              
430             our $VERSION = '0.01';
431              
432             extends 'Moose::Object';
433              
434             has 'parent' => (
435             isa => 'DBIx::PgLink::Adapter',
436             is => 'ro',
437             required => 1,
438             is_weak => 1,
439             );
440              
441             has 'sth' => (
442             isa => 'Object', #'DBI::st',
443             is => 'ro',
444             handles => [ qw/
445             err errstr state set_err func
446             bind_param bind_param_inout bind_param_array
447             execute execute_array execute_for_fetch
448             fetch fetchrow_arrayref fetchrow_array fetchrow_hashref fetchall_arrayref fetchall_hashref
449             finish rows
450             bind_col bind_columns dump_results
451             /],
452             );
453              
454             # protected methods
455             for my $func (qw/
456             func execute execute_array execute_for_fetch
457             fetch fetchrow_arrayref fetchrow_array fetchrow_hashref fetchall_arrayref fetchall_hashref
458             /) {
459             around $func => sub {
460             my $next = shift;
461             my $self = shift;
462             trace_msg('INFO', "$func") if trace_level >= 3;
463             return $self->parent->dbi_method($self->sth, $func, @_);
464             };
465             }
466              
467             #sub DESTROY {
468             # my $self = shift;
469             # warn "destroing sth for $self->{sth}->{Statement}\n";
470             #}
471              
472             1;
473              
474             __END__
475              
476             =pod
477              
478             =head1 NAME
479              
480             DBIx::PgLink::Adapter - DBI wrapper for DBIx::PgLink suite
481              
482              
483             =head1 SYNOPSIS
484              
485              
486             use DBIx::PgLink::Adapter;
487             $db = DBIx::PgLink::Adapter->new();
488              
489             $db->install_roles(qw/NestedTransaction TraceDBI/);
490              
491             $db->install_roles('Reconnect');
492             $db->reconnect_retries(10);
493              
494             $db->connect("dbi:Pg:host=127.0.0.1;db=postgres", "postgres", "", { RaiseError=>1, AutoCommit=>1 });
495              
496             $db->do("SET client_min_messages=INFO");
497              
498             $db->dbh->{'pg_enable_utf8'} = 1;
499              
500             $st = $db->prepare("SELECT * FROM pg_database");
501             $st->execute;
502             @row = $st->fetchrow_array;
503              
504             See also L<DBIx::PgLink>
505              
506              
507             =head1 DESCRIPTION
508              
509             Class wraps DBI database handle and provides base for further extending.
510              
511             Used L<Moose> object system.
512              
513             =head2 Extending
514              
515             Extending can be made by subclassing for specific data source type
516             and/or by adding roles.
517              
518             Subclasses of C<DBIx::PgLink::Adapter> may implement missing or broken functionality
519             of DBD driver or underlying driver/database.
520              
521             Roles (a.k.a. traits or mixins) supply additional functionality
522             and may be composed in any combinations (in theory).
523             Adapter can load role:
524             1) in compile-time via C<with> clause
525             2) in run-time via C<install_role> subroutine or via direct meta-class manipulation.
526              
527             Descendant adapter classes and extra roles can have any name.
528              
529              
530             =head1 DATABASE OBJECT
531              
532             =head2 METHODS
533              
534             =over
535              
536             =item new(%attr)
537              
538             Default constructor.
539              
540             =item connect($data_source, $user, $password, \%attr)
541              
542             Connect to DBI datasource. Returns database handle.
543              
544             =item C<install_roles>
545              
546             Apply roles to current object.
547             Role name can be full package name or just last portion,
548             which defaults to 'DBIx::PgLink::Roles::' namespace.
549              
550             =item err errstr state set_err func
551             data_sources do last_insert_id
552             selectrow_array selectrow_arrayref selectrow_hashref
553             selectall_arrayref selectall_hashref selectcol_arrayref
554             prepare prepare_cached
555             commit rollback begin_work
556             disconnect ping
557             get_info table_info column_info primary_key_info primary_key
558             foreign_key_info statistics_info tables
559             type_info_all type_info
560             quote quote_identifier
561              
562             Methods of DBI database handle. Can be overrided and extended.
563              
564             All methods that should return statement handle returns
565             instance of <DBIx::PgLink::Adapter::st> class instead.
566              
567             =item C<is_transaction_active>
568              
569             Utility function. Return true if connection is in transaction.
570              
571             =item C<format_routine_call>
572              
573             $sql = $adapter->format_routine_call($catalog, $schema, $routine, $returns_set, \@args);
574              
575             Generate SQL query for routine call.
576              
577             C<$returns_set> is boolean, pass true if routine returns set.
578              
579             C<\@args> is array of hashes for routine arguments.
580             For database that supports named arguments each entry must contains 'arg_name' value.
581              
582             Generic implementation use 'SELECT' keyword with positional call syntax (PostgreSQL-compatible).
583              
584             =back
585              
586              
587             =head2 ATTRIBUTES
588              
589             B<NOTE:> DBI attributes are not imported. Use C<dbh> attribute for direct access.
590              
591             =over
592              
593             =item connector
594              
595             Weak reference to optional parent of L<DBIx::PgLink::Connector> class.
596             Read only.
597              
598             =item dbh
599              
600             Wrapped DBI database handle.
601              
602             =back
603              
604              
605             =head1 STATEMENT OBJECT
606              
607             Statement object created by C<prepare> database method.
608              
609             =head2 METHODS
610              
611             =over
612              
613             =item err errstr state set_err trace trace_msg func
614             bind_param bind_param_inout bind_param_array
615             execute execute_array execute_for_fetch
616             fetchrow_arrayref fetchrow_array fetchrow_hashref
617             fetchall_arrayref fetchall_hashref
618             finish rows
619             bind_col bind_columns dump_results
620              
621             Methods of DBI statement handle. Can be overrided and extended.
622              
623              
624             =back
625              
626             =head2 ATTRIBUTES
627              
628             =over
629              
630             =item parent
631              
632             Link to I<Adapter> instance. Read only.
633              
634             =item sth
635              
636             Wrapped DBI statement handle. Read only.
637              
638             =back
639              
640             =head1 Why another DBIx wrapper?
641              
642             I need this features:
643              
644             1) Cross-database support
645             2) Easy extending
646             3) Mixin/trait-like composing of functionality in run time
647             4) Set of ready pluggable modules. Particular interest in disconnection handling.
648              
649             =over
650              
651             =item DBIx::SQLEngine with DBIx::AnyDBD
652              
653             + Good cross-database support
654             - Too ORM-ish. Overkill for data access from one relational engine to another RDBMS.
655              
656             =item DBIx::Roles
657              
658             + Good set of predifined roles
659             - No cross-database support
660              
661             =back
662              
663             =head1 CAVEATS
664              
665             Class construction is really SLOW. It is a price for extensibility. See L<Moose::Cookbook::WTF>.
666              
667             =head1 SEE ALSO
668              
669             L<DBI>,
670             L<DBIx::PgLink>
671             L<Moose>
672              
673             =head1 AUTHOR
674              
675             Alexey Sharafutdinov E<lt>alexey.s.v.br@gmail.comE<gt>
676              
677             =head1 COPYRIGHT AND LICENSE
678              
679             This library is free software; you can redistribute it and/or modify
680             it under the same terms as Perl itself, either Perl version 5.8.8 or,
681             at your option, any later version of Perl 5 you may have available.
682              
683             =cut