File Coverage

blib/lib/App/AutoCRUD/DataSource.pm
Criterion Covered Total %
statement 123 149 82.5
branch 24 58 41.3
condition 6 21 28.5
subroutine 25 26 96.1
pod 5 5 100.0
total 183 259 70.6


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