File Coverage

blib/lib/DBIx/DataModel/Meta/Schema.pm
Criterion Covered Total %
statement 200 203 98.5
branch 54 64 84.3
condition 28 55 50.9
subroutine 41 41 100.0
pod 7 9 77.7
total 330 372 88.7


line stmt bran cond sub pod time code
1             package DBIx::DataModel::Meta::Schema;
2 20     20   2564 use strict;
  20         41  
  20         799  
3 20     20   94 use warnings;
  20         34  
  20         1125  
4 20     20   123 use parent 'DBIx::DataModel::Meta';
  20         39  
  20         431  
5 20     20   1454 use DBIx::DataModel;
  20         42  
  20         136  
6 20     20   101 use DBIx::DataModel::Meta::Utils qw/define_class define_readonly_accessors/;
  20         40  
  20         1364  
7 20     20   12041 use DBIx::DataModel::Source::Join;
  20         88  
  20         880  
8 20     20   11344 use DBIx::DataModel::Meta::Source::Join;
  20         68  
  20         714  
9 20     20   126 use DBIx::DataModel::Carp;
  20         33  
  20         88  
10              
11 20         1875 use Params::Validate qw/validate_with SCALAR ARRAYREF CODEREF UNDEF BOOLEAN
12 20     20   1122 OBJECT HASHREF/;
  20         37  
13 20     20   118 use List::MoreUtils qw/any firstval lastval uniq/;
  20         35  
  20         141  
14 20     20   31657 use Hash::Util qw/lock_keys/;
  20         77054  
  20         215  
15 20     20   2182 use Module::Load qw/load/;
  20         43  
  20         206  
16 20     20   1407 use Try::Tiny;
  20         36  
  20         1352  
17 20     20   146 use mro qw/c3/;
  20         36  
  20         167  
18 20     20   581 use namespace::clean;
  20         36  
  20         166  
19              
20             #----------------------------------------------------------------------
21             # Params::Validate specification for new()
22             #----------------------------------------------------------------------
23              
24             # new() parameter specification (in Params::Validate format)
25             my $spec = {
26             class => {type => SCALAR },
27             isa => {type => SCALAR|ARRAYREF,
28             default => 'DBIx::DataModel::Schema'},
29              
30             sql_no_inner_after_left_join => {type => BOOLEAN, optional => 1},
31             join_with_USING => {type => BOOLEAN, optional => 1},
32             resultAs_namespaces => {type => ARRAYREF, optional => 1},
33              
34             # fields below are in common with tables (schema is a kind of "pseudo-root")
35             auto_insert_columns => {type => HASHREF, default => {}},
36             auto_update_columns => {type => HASHREF, default => {}},
37             no_update_columns => {type => HASHREF, default => {}},
38              
39             # beware: more members of %$spec are added below
40             };
41              
42             # parameters for optional subclasses of the builtin source classes
43             for my $member (qw/table join/) {
44             my $capitalized = ucfirst $member;
45             my $parent = "DBIx::DataModel::Source::$capitalized";
46             my $meta_class = "DBIx::DataModel::Meta::Source::$capitalized";
47             $spec->{$member."_parent"} = {type => SCALAR|ARRAYREF,
48             default => $parent};
49             $spec->{$member."_metaclass"} = {type => SCALAR,
50             isa => $meta_class,
51             default => $meta_class};
52             }
53              
54             # parameters for optional subclasses of the builtin metaclasses
55             for my $member (qw/association path type/) {
56             my $capitalized = ucfirst $member;
57             my $meta_class = "DBIx::DataModel::Meta::$capitalized";
58             $spec->{$member."_metaclass"} = {type => SCALAR,
59             isa => $meta_class,
60             default => $meta_class};
61             }
62              
63             # parameters for optional subclasses of builtin classes
64             my $statement_class = 'DBIx::DataModel::Statement';
65             $spec->{statement_class} = {type => SCALAR,
66             isa => $statement_class,
67             default => $statement_class};
68              
69             my $sqla_abstract_class = 'SQL::Abstract::More';
70             $spec->{sql_abstract_class} = {type => SCALAR,
71             isa => $sqla_abstract_class,
72             default => $sqla_abstract_class};
73             $spec->{sql_abstract_args} = {type => ARRAYREF,
74             default => []};
75              
76             #----------------------------------------------------------------------
77             # PUBLIC METHODS : CONSTRUCTOR AND ACCESSORS
78             #----------------------------------------------------------------------
79              
80             sub new {
81 15     15 1 93 my $class = shift;
82              
83             # check parameters
84 15         703 my $self = validate_with(
85             params => \@_,
86             spec => $spec,
87             allow_extra => 0,
88             );
89              
90             # canonical representations (arrayref) for some attributes
91 15         134 for my $attr (qw/isa table_parent parent join_parent/) {
92 60 50       313 ref $self->{$attr} or $self->{$attr} = [$self->{$attr}];
93             }
94              
95             # initial hashrefs for schema members
96 15         82 $self->{$_} = {} for qw/table association type/;
97              
98             # TODO : some checking on auto_update_columns, auto_insert, etc.
99              
100             # attributes just for initialisation, don't keep them within $self
101 15         43 my $isa = delete $self->{isa};
102              
103 15         55 bless $self, $class; # this is the metaschema instance
104              
105             # now create the Perl class for schema instances
106             define_class(
107             name => $self->{class},
108 15         172 isa => $isa,
109             metadm => $self,
110             );
111              
112             # namespaces for that class will be used for searching the 'ResultAs' classes
113 14   33     235 $self->{resultAs_namespaces} //= mro::get_linear_isa($self->{class});
114 14         46 $self->{resultAs_class_for} = {}; # cache, initially empty
115              
116 14         79 return $self;
117             }
118              
119             # accessors for args passed to new()
120             define_readonly_accessors(__PACKAGE__, grep {$_ ne 'isa'} keys %$spec);
121              
122             # accessors for internal lists of other meta-objects
123             foreach my $kind (qw/table association type join/) {
124 20     20   20068 no strict 'refs';
  20         50  
  20         15221  
125             # retrieve list of meta-objects
126             *{$kind."s"} = sub {
127 8     8   17 my $self = shift;
128 8         12 return values %{$self->{$kind}};
  8         59  
129             };
130              
131             # retrieve single named object
132             *{$kind} = sub {
133 157     157   464 my ($self, $name) = @_;
        157      
        148      
        82      
134             # remove schema prefix, if any
135 157         1432 $name =~ s/^$self->{class}:://;
136 157         962 return $self->{$kind}{$name};
137             };
138             }
139              
140              
141             sub db_table {
142 4     4 0 33 my ($self, $db_name) = @_;
143 4     10   30 return firstval {uc($_->db_from) eq uc($db_name)} $self->tables;
  10         25  
144             }
145              
146              
147             #----------------------------------------------------------------------
148             # PUBLIC FRONT-END METHODS FOR DECLARING SCHEMA MEMBERS
149             # (syntactic sugar for back-end define_table(), define_association(), etc.)
150             #----------------------------------------------------------------------
151              
152             sub Table {
153 51     51 1 91 my $self = shift;
154 51         105 my %args;
155              
156             # last member of @_ might be a hashref with named parameters
157 51 100       175 %args = %{pop @_} if ref $_[-1];
  10         39  
158              
159             # parse positional parameters (old syntax)
160 51         198 my ($class_name, $db_name, @primary_key) = @_;
161 51 50 33     258 $db_name && @primary_key
162             or croak "not enough args to \$schema->Table(); "
163             . "did you mean \$schema->table() ?";
164 51   33     323 $args{class} ||= $class_name;
165 51   33     263 $args{db_name} ||= $db_name;
166 51   50     290 $args{primary_key} ||= \@primary_key;
167              
168             # define it
169 51         274 $self->define_table(%args);
170              
171 51         256 return $self->class;
172             }
173              
174             sub View {
175 2     2 1 21 my $self = shift;
176 2         6 my %args;
177              
178             # last member of @_ might be a hashref with named parameters
179 2 50       9 %args = %{pop @_} if ref $_[-1];
  0         0  
180              
181             # parse positional parameters (old syntax)
182 2         12 my ($class_name, $default_columns, $sql, $where, @parents) = @_;
183 2   33     22 $args{class} ||= $class_name;
184 2   33     15 $args{db_name} ||= $sql;
185 2   33     13 $args{where} ||= $where;
186 2   33     14 $args{default_columns} ||= $default_columns;
187 2   50     12 $args{parents} ||= [map {$self->table($_)} @parents];
  4         37  
188              
189             # define it
190 2         15 $self->define_table(%args);
191              
192 2         9 return $self->class;
193             }
194              
195             sub Type {
196 4     4 1 24 my ($self, $type_name, %handlers) = @_;
197              
198 4         25 $self->define_type(
199             name => $type_name,
200             handlers => \%handlers,
201             );
202              
203 4         22 return $self->class;
204             }
205              
206             sub Association {
207 19     19 1 50 my $self = shift;
208              
209 19         100 $self->define_association(
210             kind => 'Association',
211             $self->_parse_association_end(A => shift),
212             $self->_parse_association_end(B => shift),
213             );
214              
215 19         152 return $self->class;
216             }
217              
218             # MAYBE TODO : sub Aggregation {} with kind => 'Aggregation'.
219             # This would be good for UML completeness, but rather useless since
220             # aggregations behave exactly like compositions, so there is nothing
221             # special to implement.
222              
223             sub Composition {
224 14     14 1 59 my $self = shift;
225              
226 14         83 $self->define_association(
227             kind => 'Composition',
228             $self->_parse_association_end(A => shift),
229             $self->_parse_association_end(B => shift),
230             );
231              
232 14         122 return $self->class;
233             }
234              
235             #----------------------------------------------------------------------
236             # PUBLIC BACK-END METHODS FOR DECLARING SCHEMA MEMBERS
237             #----------------------------------------------------------------------
238              
239             # common pattern for defining tables, associations and types
240             foreach my $kind (qw/table association type/) {
241             my $metaclass = "${kind}_metaclass";
242 20     20   156 no strict 'refs';
  20         107  
  20         5672  
243             *{"define_$kind"} = sub {
244 92     92   169 my $self = shift;
245              
246             # force metaclass to be loaded (it could be a user-defined subclass)
247 92         511 load $self->{$metaclass};
248              
249             # instanciate the metaclass
250 92         4978 unshift @_, schema => $self;
251 92         547 my $meta_obj = $self->{$metaclass}->new(@_);
252              
253             # store into our registry (except paths because they are accessed through
254             # tables or through associations)
255 92 50       538 $self->{$kind}{$meta_obj->{name}} = $meta_obj
256             unless $kind eq 'path';
257              
258 92         232 return $self;
259             };
260             }
261              
262              
263             # defining joins (different from the common pattern above)
264             sub define_join {
265 57     57 0 113 my $self = shift;
266              
267             # parse arguments
268 57         273 my ($joins, $aliased_tables, $db_table_by_source) = $self->_parse_join_path(@_);
269              
270             # build class name
271 54         129 my $subclass = join "", map {($_->{kind}, $_->{name})} @$joins;
  144         546  
272 54         228 my $class_name = "$self->{class}::AutoJoin::$subclass";
273              
274             # do nothing if join class was already loaded
275 20 100   20   150 { no strict 'refs'; return $class_name->metadm if @{$class_name.'::ISA'}; }
  20         106  
  20         41212  
  54         89  
  54         95  
  54         588  
276              
277             # otherwise, build the new class
278              
279             # prepare args for SQL::Abstract::More::join
280 32         116 my @sqla_join_args = ($joins->[0]{db_table});
281 32         134 foreach my $join (@$joins[1 .. $#$joins]) {
282             my $join_spec = {
283             operator => $join->{kind},
284             condition => $join->{condition},
285             using => $join->{using},
286 56         195 };
287 56         130 push @sqla_join_args, $join_spec, $join->{db_table};
288             }
289              
290             # install the Join
291             my %args = (
292             schema => $self,
293             class => $class_name,
294 32         93 parents => [uniq map {$_->{table}} @$joins],
  88         595  
295             sqla_join_args => \@sqla_join_args,
296             aliased_tables => $aliased_tables,
297             db_table_by_source => $db_table_by_source,
298             );
299 32 100       182 $args{primary_key} = $joins->[0]{primary_key} if $joins->[0]{primary_key};
300 32         291 my $meta_join = DBIx::DataModel::Meta::Source::Join->new(%args);
301              
302             # store into our registry
303 32         179 $self->{join}{$subclass} = $meta_join;
304              
305 32         269 return $meta_join;
306             }
307              
308              
309              
310             sub find_result_class {
311 162     162 1 376 my $self = shift;
312 162         446 my $name = ucfirst shift;
313              
314 162         422 my $from_cache = $self->{resultAs_class_for}{$name};
315 162 100       688 return $from_cache if $from_cache;
316              
317             # try to find subclass $name within namespace of schema or ancestors
318 40         76 foreach my $namespace (@{$self->{resultAs_namespaces}}) {
  40         132  
319 78         214 my $class = "${namespace}::ResultAs::${name}";
320              
321             # see if that class is already loaded (by checking for a 'get_result' method)
322 78         151 my $is_loaded = defined &{$class."::get_result"};
  78         391  
323              
324             # otherwise, try to load the module
325 76     76   4381 $is_loaded ||= try {load $class; 1}
  37         1511  
326 78 100 100 39   910 catch {die $_ if $_ !~ /^Can't locate(?! object method)/};
  39         1679  
327              
328             # if class is found, feed the cache and exit loop
329 77 100       1317 if ($is_loaded) {
330 39         176 $self->{resultAs_class_for}{$name} = $class;
331 39         285 return $class;
332             }
333             }
334              
335 0         0 return; # false : class not found
336             }
337              
338              
339              
340             #----------------------------------------------------------------------
341             # PRIVATE UTILITY METHODS
342             #----------------------------------------------------------------------
343              
344              
345             sub _parse_association_end {
346 66     66   173 my ($self, $letter, $end_params)= @_;
347              
348 66         195 my ($table, $role, $multiplicity, @cols) = @$end_params;
349              
350             # prepend schema name in table, unless it already contains "::"
351 66 50       666 $table =~ s/^/$self->{class}::/ unless $table =~ /::/;
352              
353             # if role is 0, or 'none', or '---', make it empty
354 66 100 66     502 $role = undef if $role && $role =~ /^(0|""|''|-+|none)$/;
355              
356             # pair of parameters for this association end
357 66         280 my %letter_params = (
358             table => $table->metadm,
359             role => $role,
360             multiplicity => $multiplicity,
361             );
362 66 100       187 $letter_params{join_cols} = \@cols if @cols;
363 66         353 return $letter => \%letter_params;
364             }
365              
366              
367              
368             sub _parse_join_path {
369 57     57   202 my ($self, $initial_table, @join_items) = @_;
370              
371             # check if there are enough args
372 57 50 33     383 $initial_table && @join_items
373             or croak "join: not enough arguments";
374              
375             # build first member of the join from the initial table
376 57         243 my %first_join = (kind => '', name => $initial_table);
377 57 100       361 $initial_table =~ s/\|(.+)$// and $first_join{alias} = $1;
378 57 100       225 my $table = $self->table($initial_table)
379             or croak "...->join('$initial_table', ...) : this schema has "
380             . "no table named '$initial_table'";
381 56         156 $first_join{table} = $table;
382 56         271 $first_join{primary_key} = [$table->primary_key];
383 56         248 $first_join{db_table} = $table->db_from;
384 56 100       194 $first_join{db_table} .= "|$first_join{alias}" if $first_join{alias};
385              
386             # accumulator structure for the loop below
387             my %accu = (
388             source => {($first_join{alias} || $table->name) => \%first_join},
389             joins => [\%first_join],
390             join_kind => undef,
391             seen_left_join => undef,
392 56 100 66     396 aliased_tables => {$first_join{alias} ? ($first_join{alias} => $table->name) : ()},
393             );
394 56         331 lock_keys(%accu); # just to make sure that there can be no typos in subs using this %accu
395              
396              
397             # loop over remaining join items
398 56         909 foreach my $join_item (@join_items) {
399              
400             # if it is a connector like '=>' or '<=>' or '<=' (see SQLAM syntax) ...
401 108 100       338 if ($join_item =~ /^[<>]?=[<>=]?$/) {
402 16 50       52 !$accu{join_kind} or croak "'$accu{join_kind}' can't be followed by '$join_item'";
403 16         37 $accu{join_kind} = $join_item;
404             # TODO: accept more general join syntax as recognized by SQLA::More::join
405             }
406              
407             # otherwise, it must be a path specification
408             else {
409 92         358 $self->_process_next_path_item($join_item, \%accu);
410             }
411             }
412              
413             # index to DB tables from DBIDM source names (will be used by Statement.pm)
414 54         100 my %db_table_by_source = map {($_ => $accu{source}{$_}{db_table})} keys %{$accu{source}};
  144         411  
  54         198  
415              
416 54         407 return ($accu{joins}, $accu{aliased_tables}, \%db_table_by_source);
417             }
418              
419              
420              
421             my $path_regex = qr/^(?:(.+?)\.)? # $1: optional source followed by '.'
422             (.+?) # $2: path name (mandatory)
423             (?:\|(.+))? # $3: optional alias following a '|'
424             $/x;
425              
426             sub _process_next_path_item {
427 92     92   205 my ($self, $path_item, $accu) = @_;
428              
429             # parse
430 92 50       1336 my ($source_name, $path_name, $alias) = $path_item =~ $path_regex
431             or croak "incorrect item '$path_item' in join specification";
432              
433             # find source and path information, from join elements seen so far
434             my $source_join
435             = $source_name ? $accu->{source}{$source_name}
436 92 100   73   539 : lastval {$_->{table}{path}{$path_name}} @{$accu->{joins}};
  73         344  
  68         478  
437 92 100 66     784 my $path = $source_join && $source_join->{table}{path}{$path_name}
438             or croak "couldn't find item '$path_item' in join specification";
439             # TODO: also deal with indirect paths (many-to-many)
440              
441             # if join kind was not explicit, compute it from min. multiplicity and from previous joins
442 90 100       227 if (!$accu->{join_kind}) {
443             $accu->{join_kind} = $path->{multiplicity}[0] == 0 ? '=>'
444 74 100 66     451 : ($accu->{seen_left_join} && $self->{sql_no_inner_after_left_join}) ? '=>'
    100          
445             : '<=>';
446             }
447 90 100       277 $accu->{seen_left_join} = 1 if $accu->{join_kind} eq '=>';
448              
449             # if max. multiplicity > 1, the join has no primary key
450 90 100       268 delete $accu->{joins}[0]{primary_key} if $path->{multiplicity}[1] > 1;
451              
452             # build new join hashref and insert it into appropriate structures
453             my %new_join = ( kind => $accu->{join_kind},
454             name => $path_item,
455             alias => $alias,
456             table => $path->{to},
457 90 100       380 db_table => $path->{to}->db_from . ($alias ? "|$alias" : ""),
458             condition => {}, # for joining with conditions on left and right columns
459             using => [], # for joining with a USING clause
460             );
461 90         351 lock_keys(%new_join);
462 90         1123 $self->_fill_join_condition_and_using(\%new_join, $source_join, $path, $alias);
463 90         136 push @{$accu->{joins}}, \%new_join;
  90         221  
464 90   66     378 $accu->{source}{$alias || $path_name} = \%new_join;
465              
466             # remember aliased table
467 90 100       246 $accu->{aliased_tables}{$alias} = $path->{to}->name if $alias;
468              
469             # reset join kind for next loop
470 90         285 undef $accu->{join_kind};
471             }
472              
473              
474             sub _fill_join_condition_and_using {
475 90     90   231 my ($self, $new_join, $source_join, $path, $alias) = @_;
476              
477 90   66     341 my $left_table = $source_join->{alias} || $source_join->{db_table};
478 90   66     290 my $right_table = $alias || $path->{to}->db_from;
479              
480 90         161 while (my ($left_col, $right_col) = each %{$path->{on}}) {
  180         699  
481 90 50       223 if ($left_col eq $right_col) {
482             # both cols have equal names, so they can participate in a USING clause
483 90 50       231 push @{$new_join->{using}}, $left_col if $new_join->{using};
  90         2154  
484             }
485             else {
486             # USING clause is no longer possible as soon as there are unequal names
487 0         0 undef $new_join->{using};
488             }
489              
490             # for the ON clause, prefix column names by their table names.
491             # Theoretically we should honor SQL::Abstract's "name_sep" setting .. but here there is no access to $statement->sql_abstract
492 90         511 $new_join->{condition}{"$left_table.$left_col"} = { -ident => "$right_table.$right_col" };
493             }
494             }
495              
496              
497             1;
498              
499             __END__