File Coverage

blib/lib/DBIx/DataModel/Meta/Schema.pm
Criterion Covered Total %
statement 171 173 98.8
branch 42 52 80.7
condition 27 52 51.9
subroutine 30 30 100.0
pod 6 8 75.0
total 276 315 87.6


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