File Coverage

blib/lib/App/AutoCRUD/DataSource.pm
Criterion Covered Total %
statement 133 161 82.6
branch 27 64 42.1
condition 6 21 28.5
subroutine 26 27 96.3
pod 5 5 100.0
total 197 278 70.8


line stmt bran cond sub pod time code
1             package App::AutoCRUD::DataSource;
2              
3 2     2   1058 use strict;
  2         3  
  2         56  
4 2     2   6 use warnings;
  2         2  
  2         47  
5              
6 2     2   6 use Moose;
  2         3  
  2         10  
7 2     2   5590 use Carp;
  2         2  
  2         122  
8 2     2   7 use DBI;
  2         2  
  2         127  
9 2     2   8 use Clone qw/clone/;
  2         2  
  2         73  
10 2     2   7 use List::MoreUtils qw/part/;
  2         2  
  2         17  
11 2     2   903 use Scalar::Does qw/does/;
  2         2  
  2         12  
12 2     2   678 use Data::Reach qw/reach/;
  2         3  
  2         16  
13 2     2   1013 use SQL::Abstract::FromQuery 0.10;
  2         81701  
  2         77  
14              
15 2     2   18 use namespace::clean -except => 'meta';
  2         4  
  2         17  
16              
17             has 'app' => (is => 'ro', isa => 'App::AutoCRUD', required => 1,
18             weak_ref => 1, handles => [qw/ dir/]);
19             has 'name' => (is => 'ro', isa => 'Str',
20             builder => '_name', lazy => 1);
21             has 'config' => (is => 'ro', isa => 'HashRef', reader => 'config_data',
22             builder => '_config', lazy => 1);
23             has 'dbh' => (is => 'ro', isa => 'DBI::db',
24             builder => '_dbh', lazy => 1);
25             has 'schema' => (is => 'ro', isa => 'Str|Object',
26             builder => '_schema', lazy => 1);
27             has 'query_parser' => (is => 'ro', isa => 'SQL::Abstract::FromQuery',
28             builder => '_query_parser', lazy => 1);
29             has 'tablegroups' => (is => 'ro', isa => 'ArrayRef',
30             builder => '_tablegroups', lazy => 1);
31              
32             # indirectly generated through the _schema builder method
33             has 'generated_schema' => (is => 'ro', isa => 'Str', init_arg => undef);
34             has 'loaded_class' => (is => 'ro', isa => 'Str', init_arg => undef);
35              
36              
37              
38             #======================================================================
39             # ATTRIBUTE BUILDERS
40             #======================================================================
41              
42             sub _dbh {
43 2     2   3 my $self = shift;
44 2         3 my $dbh;
45              
46             # create a connection from specifications found in config
47 2 50       6 if (my $connect_spec = $self->config(qw/dbh connect/)) {
    0          
48 2 50       48 if (does($connect_spec, 'ARRAY')) {
    50          
    0          
49             # regular DBI connect using the given list of arguments
50 0 0       0 $dbh = DBI->connect(@$connect_spec)
51             or die "can't connect to " . join(", ", @$connect_spec);
52             }
53             elsif (does($connect_spec, 'CODE')) {
54 2 50       96 $dbh = $connect_spec->()
55             or die "coderef connection to " . self->name . " failed";
56             }
57             elsif (does($connect_spec, '""')) {
58             # config was a string : treat it as a line of Perl code
59 0         0 local $@;
60 0 0       0 $dbh = eval $connect_spec
61             or die $@;
62             }
63             else {
64 0         0 die "can't connect to " . $self->name . " (wrong config/dbh info)";
65             }
66             }
67              
68             # or recover existing connection in schema
69             elsif (my $schema = $self->{schema}) { # bypass encapsulation to avoid
70             # circular calls with ->_schema()
71 0         0 $dbh = $schema->dbh;
72             }
73              
74             # report failure if no connection found
75             $dbh
76 2 50       16088 or die "no DBI/connect information in config for " . $self->name;
77              
78 2         71 return $dbh;
79             }
80              
81             sub _schema {
82 2     2   4 my $self = shift;
83              
84 2         6 my $required_class = $self->config('require');
85 2   33     36 my $schema_class = $self->config('schema_class') || $required_class;
86              
87             # if external code is required, load it
88 2 50 0     32 if ($required_class &&
      33        
89             !($schema_class && $self->app->is_class_loaded($schema_class))) {
90 0 0       0 $self->{loaded_class} = $self->app->try_load_class($required_class)
91             or die "Can't locate $required_class";
92 0   0     0 $schema_class = $self->config('schema_class') || $self->{loaded_class};
93             }
94              
95             # generate class on the fly if needed
96 2 50       5 if (!$schema_class) {
97 2         52 $schema_class = (ref $self) . "::_Auto_Schema::" . $self->name;
98              
99 2 50       49 if (! $self->app->is_class_loaded($schema_class)) {
100             # build a schema generator from the DBI connection
101 2         70 my $dbh = $self->dbh;
102 2         895 require DBIx::DataModel::Schema::Generator;
103 2         131344 my $generator = DBIx::DataModel::Schema::Generator->new(
104             -schema => $schema_class,
105             );
106 2         44 my @args = map {$self->config('dbh', $_)} qw/db_catalog db_schema db_type/;
  6         97  
107 2         90 $generator->parse_DBI($self->dbh, @args);
108              
109             # generate and store perl code
110 2         333996 $self->{generated_schema} = $generator->perl_code;
111              
112             # eval source code on the fly
113 2     2   16 eval $self->{generated_schema};
  2     2   2  
  2     2   52  
  2         10  
  2         3  
  2         61  
  2         1220  
  2         741  
  2         10  
  2         1098  
114             }
115             }
116              
117 2         93 return $schema_class;
118             }
119              
120              
121              
122              
123             sub _query_parser {
124 2     2   4 my $self = shift;
125              
126 2         21 return SQL::Abstract::FromQuery->new;
127             }
128              
129              
130             sub _tablegroups {
131 1     1   2 my ($self) = @_;
132              
133             # get table info from database
134 1         24 my $dbh = $self->dbh;
135 1   50     4 my $sth = $dbh->table_info($self->config(qw/dbh db_catalog/),
136             $self->config(qw/dbh db_schema/),
137             undef,
138             $self->config(qw/dbh db_type/) || 'TABLE',
139             );
140 1         467 my $tables = $sth->fetchall_hashref('TABLE_NAME');
141              
142             # merge with descriptions from config
143 1         195 foreach my $table (keys %$tables) {
144 12         16 my $descr = $self->config(tables => $table => 'descr');
145 12 50       122 $tables->{$table}{descr} = $descr if $descr;
146             }
147              
148             # grouping: merge with table info from config
149 1   50     4 my $tablegroups = clone $self->config('tablegroups') || [];
150 1         21 foreach my $group (@$tablegroups) {
151             # tables declared in this group are removed from the global %$tables ..
152 0         0 my @declared_table_names = @{$group->{tables}};
  0         0  
153 0         0 my @extracted_tables = map {delete $tables->{$_}} @declared_table_names;
  0         0  
154              
155             # .. and their full definitions take place of the declared names
156 0         0 $group->{tables} = [ grep {$_} @extracted_tables ];
  0         0  
157             }
158              
159             # deal with remaining tables (
160 1 50       15 if (my @other_tables = sort keys %$tables) {
161              
162             # Filter out based on the regexps in filters include & exclude
163 1 50       4 if (my $filter_include = $self->config(qw/filters include/)) {
164 1         20 @other_tables = grep { $_ =~ /$filter_include/ } @other_tables;
  12         28  
165             }
166 1 50       4 if (my $filter_exclude = $self->config(qw/filters exclude/)) {
167 1         19 @other_tables = grep { $_ !~ /$filter_exclude/ } @other_tables;
  10         23  
168             }
169              
170             # if some unclassified tables remain after the filtering
171 1 50       4 if (@other_tables) {
172             push @$tablegroups, {
173             name => 'Unclassified tables',
174             descr => 'Present in database but unlisted in config',
175 1         3 tables => [ @{$tables}{@other_tables} ],
  1         9  
176             };
177             }
178             }
179              
180 1         48 return $tablegroups;
181             }
182              
183              
184             sub _config {
185 2     2   3 my $self = shift;
186 2 50       53 my $config = $self->app->config(datasources => $self->name)
187             or die "no config for datasource " . $self->name;
188              
189             # shallow copy
190 2         49 $config = { %$config };
191              
192 2 50       7 if (my $struct = $config->{structure}) {
193             # get the structure config
194 0 0       0 my $struct_config = $self->app->config(structures => $struct)
195             or die "no config for structure $struct";
196              
197             # copy structure into datasource config
198 0         0 $config->{$_} = $struct_config->{$_} foreach keys %$struct_config;
199            
200             }
201              
202 2         54 return $config;
203             }
204              
205              
206              
207             #======================================================================
208             # METHODS
209             #======================================================================
210              
211             sub config {
212 55     55 1 129 my ($self, @path) = @_;
213 55         1600 return reach $self->config_data, @path;
214             }
215              
216              
217             sub descr {
218 2     2 1 4 my ($self) = @_;
219 2         7 return $self->config('descr');
220             }
221              
222             sub prepare_for_request {
223 22     22 1 27 my ($self, $req) = @_;
224              
225             # if schema is in single-schema mode, make sure it is connected to
226             # the proper database
227 22         523 my $schema = $self->schema;
228 22 50       562 $schema->dbh($self->dbh) unless ref $schema;
229             }
230              
231              
232             sub primary_key {
233 11     11 1 19 my ($self, $table) = @_;
234              
235 11         29 return $self->_meta_table($table)->primary_key;
236             }
237              
238              
239             sub colgroups {
240 11     11 1 14 my ($self, $table) = @_;
241              
242             # if info already in cache, return it
243 11         35 my $colgroups = $self->{colgroups}{$table};
244 11 100       45 return $colgroups if $colgroups;
245              
246             # paths from this table
247 4         15 my $meta_table = $self->_meta_table($table);
248 4         32 my %paths = $meta_table->path;
249              
250             # primary_key
251 4         202 my @pk = $meta_table->primary_key;
252              
253             # get column info from database
254 4         42 my $db_catalog = $self->config(qw/dbh db_catalog/);
255 4         84 my $db_schema = $self->config(qw/dbh db_schema/);
256 4         165 my $sth = $self->dbh->column_info($db_catalog, $db_schema,
257             $table, undef);
258 4         3231 my $columns = $sth->fetchall_hashref('COLUMN_NAME');
259              
260             # TMP HACK, Oracle-specific. Q: How to design a good abstraction for this ?
261             $columns = $self->_columns_from_Oracle_synonym($db_schema, $table)
262 4 50 33     671 if ! keys %$columns and $self->dbh->{Driver}{Name} eq 'Oracle';
263              
264             # mark primary keys
265 4         19 $columns->{$_}{is_pk} = 1 foreach @pk;
266              
267             # attach paths to relevant columns
268 4         13 foreach my $path (values %paths) {
269             # name of column(s) from which this path starts
270 6         28 my %path_on = $path->on;
271 6         97 my ($col_name, @others) = keys %path_on;
272              
273             # for the moment, don't handle assoc on multiple columns (TODO)
274 6 50       16 next if @others;
275              
276 6 50       20 my $col = $columns->{$col_name} or next;
277             my $path_subdata = { name => $path->name,
278             to_table => $path->to->db_from,
279 6         19 foreign_key => $path_on{$col_name} };
280 6         118 push @{$col->{paths}}, $path_subdata;
  6         23  
281             }
282              
283             # grouping: merge with column info from config
284 4   50     16 $colgroups = clone $self->config(tables => $table => 'colgroups') || [];
285 4         104 foreach my $group (@$colgroups) {
286 0         0 my @columns;
287 0         0 foreach my $column (@{$group->{columns}}) {
  0         0  
288 0         0 my $col_name = $column->{name};
289 0 0       0 my $db_col = delete $columns->{$col_name} or next;
290 0         0 push @columns, {%$db_col, %$column};
291             }
292 0         0 $group->{columns} = \@columns;
293             }
294              
295             # deal with remaining columns (present in database but unlisted in
296             # config); sorted with primary keys first, then alphabetically.
297             my $sort_pk = sub { $columns->{$a}{is_pk} ? -1
298 7 100   7   46 : $columns->{$b}{is_pk} ? 1
    100          
299 4         20 : $a cmp $b};
300 4 50       26 if (my @other_cols = sort $sort_pk keys %$columns) {
301             # build colgroup
302             push @$colgroups, {name => 'Unclassified columns',
303 4         12 columns => [ @{$columns}{@other_cols} ]};
  4         22  
304             }
305              
306             # cache result and return
307 4         13 $self->{colgroups}{$table} = $colgroups;
308 4         197 return $colgroups;
309             }
310              
311              
312              
313              
314             sub _columns_from_Oracle_synonym {
315 0     0   0 my ($self, $db_schema, $syn_name) = @_;
316              
317 0         0 my $dbh = $self->dbh;
318 0         0 my $sql = "SELECT TABLE_OWNER, TABLE_NAME FROM ALL_SYNONYMS "
319             . "WHERE OWNER=? AND SYNONYM_NAME=?";
320 0 0       0 my ($owner, $table) = $dbh->selectrow_array($sql, {}, $db_schema, $syn_name)
321             or return {};
322              
323 0         0 my $sth = $dbh->column_info(undef, $owner, $table, undef);
324 0         0 return $sth->fetchall_hashref('COLUMN_NAME')
325             }
326              
327              
328              
329             sub _meta_table {
330 15     15   23 my ($self, $table) = @_;
331              
332 15 50       408 my $meta_table = $self->schema->metadm->db_table($table)
333             or die "no table in schema corresponds to '$table'";
334 15         681 return $meta_table;
335             }
336              
337              
338              
339              
340             1;
341              
342             __END__
343              
344             =head1 NAME
345              
346             App::AutoCRUD::DataSource -
347              
348             =head1 DESCRIPTION
349              
350             This class encapsulates all information needed by the AutoCRUD application
351             for communicating with one particular I<datasource>. The information
352             comes partly from the configuration file, and partly from the
353             requests made to the database schema.
354              
355              
356             =head1 ATTRIBUTES
357              
358             =head2 app
359              
360             Weak reference to the application that hosts this datasource.
361              
362             =head2 name
363              
364             Unique name identifying this datasource within the AutoCRUD application.
365             This name will be part of URLs addressing this datasource.
366              
367             =head2 config
368              
369             Copy of the configuration tree (see L<App::AutoCRUD::ConfigDomain>)
370             for this specific datasource.
371              
372             =head2 dbh
373              
374             L<DBI> database handle, which encapsulates the connection to the
375             database. The dbh is created on demand, from connection parameters or
376             from a coderef specified in the configuration tree (see
377             L<App::AutoCRUD::ConfigDomain/dbh>); alternatively, it
378             can also be supplied from the calling program, or grabbed from the
379             schema. Once created, the dbh is readonly and cannot be changed (even
380             if the schema itself was bound to another dbh by a remote module -- the
381             dbh will be forced again before processing the HTTP request).
382              
383              
384             =head2 schema
385              
386             An instance or a subclass of L<DBIx::DataModel::Schema>.
387             Usually this is loaded from parameters specified in the configuration tree;
388             if such parameters are absent, the fallback behavior is to generate
389             a class on the fly, using L<DBIx::DataModel::Schema::Generator>.
390              
391              
392             =head2 query_parser
393              
394             An instance of L<SQL::Abstract::FromQuery>, for parsing the content
395             of search forms.
396              
397             =head2 tablegroups
398              
399             Information about tables in that datasource. This is an ordered list
400             of I<tablegroups>, where each tablegroup is a hashref with a B<name>,
401             a B<descr> (description), and an ordered list of I<tables>.
402             Each table in that list contains information as returned by
403             the L<DBI/table_info> method, plus an additional B<descr> field.
404              
405             The tablegroups structure comes from the configuration data. If tables
406             are found in the database, but not mentioned in the configuration, they are
407             automatically inserted into a group called "Unclassified".
408              
409              
410             =head1 METHODS
411              
412             =head2 config
413              
414             my $data = $datasource->config(@path);
415              
416             Returns the config subtree at location C<@path> under this datasource.
417              
418             =head2 descr
419              
420             Returns the description string for this datasource, as specified in config.
421              
422             =head2 prepare_for_request
423              
424             $datasource->prepare_for_request($req);
425              
426             Called from L<App::AutoCRUD/call> before serving
427             a request. This is a hook for subclasses to provide application-specific
428             behaviour if needed (like for example resetting the database connection
429             or supplying user credentials from the HTTP request).
430             The argument C<$req> is an instance of L<Plack::Request>.
431              
432             =head2 primary_key
433              
434             Proxy method to L<DBIx::DataModel::Meta::Source/primary_key>.
435              
436             =head2 colgroups
437              
438             my $colgroups = $datasource->colgroups($table_name);
439              
440             Returns an arrayref of I<column groups>, as specified in config (or guessed
441             from the database meta-information, if the config says nothing).
442              
443             Each column group is a hashref with keys C<name> (containing a string)
444             and C<columns> (containing an arrayref of I<columns>).
445              
446             Each column is a hashref as returned from L<DBI/column_info>, i.e. containing
447             keys C<TABLE_NAME>, C<COLUMN_NAME>, C<DATA_TYPE>, C<COLUMN_SIZE>, etc.
448             In addition, some other keys are inserted into this hashref :
449              
450             =over
451              
452             =item is_pkey
453              
454             Boolean indicating that this column is part of the primary key
455              
456             =item paths
457              
458             An arrayref of I<paths> to other tables. Each path is a hashref with
459             keys C<name> (name of this path), C<to_table> (name of the associated table),
460             C<foreign_key> (name of the associated column in the remote table).
461              
462             =back
463              
464              
465