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   1107 use strict;
  18         43  
  18         532  
3 18     18   104 use warnings;
  18         38  
  18         481  
4 18     18   101 use parent 'DBIx::DataModel::Meta';
  18         63  
  18         141  
5 18     18   1032 use DBIx::DataModel;
  18         40  
  18         126  
6 18     18   124 use DBIx::DataModel::Meta::Utils qw/define_class define_readonly_accessors/;
  18         36  
  18         985  
7 18     18   7965 use DBIx::DataModel::Source::Join;
  18         63  
  18         637  
8 18     18   9159 use DBIx::DataModel::Meta::Source::Join;
  18         59  
  18         697  
9              
10 18         1291 use Params::Validate qw/validate_with SCALAR ARRAYREF CODEREF UNDEF BOOLEAN
11 18     18   122 OBJECT HASHREF/;
  18         37  
12 18     18   107 use List::MoreUtils qw/any firstval lastval uniq/;
  18         43  
  18         91  
13 18     18   15586 use Module::Load qw/load/;
  18         43  
  18         108  
14 18     18   1094 use Carp::Clan qw[^(DBIx::DataModel::|SQL::Abstract)];
  18         41  
  18         101  
15 18     18   1529 use namespace::clean;
  18         37  
  18         96  
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 33 my $class = shift;
72              
73             # check parameters
74 13         484 my $self = validate_with(
75             params => \@_,
76             spec => $spec,
77             allow_extra => 0,
78             );
79              
80             # canonical representations (arrayref) for some attributes
81 13         89 for my $attr (qw/isa table_parent parent join_parent/) {
82 52 50       190 ref $self->{$attr} or $self->{$attr} = [$self->{$attr}];
83             }
84              
85             # initial hashrefs for schema members
86 13         65 $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         38 my $isa = delete $self->{isa};
92              
93 13         37 bless $self, $class;
94              
95             # create the Perl class
96             define_class(
97             name => $self->{class},
98 13         106 isa => $isa,
99             metadm => $self,
100             );
101              
102 12         58 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   14623 no strict 'refs';
  18         66  
  18         13242  
111             # retrieve list of meta-objects
112             *{$kind."s"} = sub {
113 8     8   19 my $self = shift;
114 8         14 return values %{$self->{$kind}};
  8         52  
115             };
116              
117             # retrieve single named object
118             *{$kind} = sub {
119 136     136   373 my ($self, $name) = @_;
120             # remove schema prefix, if any
121 136         752 $name =~ s/^$self->{class}:://;
122 136         851 return $self->{$kind}{$name};
123             };
124             }
125              
126              
127             sub db_table {
128 4     4 0 15 my ($self, $db_name) = @_;
129 4     10   30 return firstval {uc($_->db_from) eq uc($db_name)} $self->tables;
  10         27  
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 93 my $self = shift;
140 48         72 my %args;
141              
142             # last member of @_ might be a hashref with named parameters
143 48 100       132 %args = %{pop @_} if ref $_[-1];
  10         42  
144              
145             # parse positional parameters (old syntax)
146 48         200 my ($class_name, $db_name, @primary_key) = @_;
147 48 50 33     251 $db_name && @primary_key
148             or croak "not enough args to \$schema->Table(); "
149             . "did you mean \$schema->table() ?";
150 48   33     278 $args{class} ||= $class_name;
151 48   33     236 $args{db_name} ||= $db_name;
152 48   50     254 $args{primary_key} ||= \@primary_key;
153              
154             # define it
155 48         209 $self->define_table(%args);
156              
157 48         236 return $self->class;
158             }
159              
160             sub View {
161 2     2 1 82 my $self = shift;
162 2         8 my %args;
163              
164             # last member of @_ might be a hashref with named parameters
165 2 50       9 %args = %{pop @_} if ref $_[-1];
  0         0  
166              
167             # parse positional parameters (old syntax)
168 2         12 my ($class_name, $default_columns, $sql, $where, @parents) = @_;
169 2   33     17 $args{class} ||= $class_name;
170 2   33     12 $args{db_name} ||= $sql;
171 2   33     13 $args{where} ||= $where;
172 2   33     24 $args{default_columns} ||= $default_columns;
173 2   50     16 $args{parents} ||= [map {$self->table($_)} @parents];
  4         11  
174              
175             # define it
176 2         34 $self->define_table(%args);
177              
178 2         10 return $self->class;
179             }
180              
181             sub Type {
182 4     4 1 18 my ($self, $type_name, %handlers) = @_;
183              
184 4         20 $self->define_type(
185             name => $type_name,
186             handlers => \%handlers,
187             );
188              
189 4         18 return $self->class;
190             }
191              
192             sub Association {
193 18     18 1 46 my $self = shift;
194              
195 18         90 $self->define_association(
196             kind => 'Association',
197             $self->_parse_association_end(A => shift),
198             $self->_parse_association_end(B => shift),
199             );
200              
201 18         105 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 38 my $self = shift;
211              
212 14         77 $self->define_association(
213             kind => 'Composition',
214             $self->_parse_association_end(A => shift),
215             $self->_parse_association_end(B => shift),
216             );
217              
218 14         80 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   150 no strict 'refs';
  18         40  
  18         4337  
229             *{"define_$kind"} = sub {
230 88     88   182 my $self = shift;
231              
232             # force metaclass to be loaded (it could be a user-defined subclass)
233 88         836 load $self->{$metaclass};
234              
235             # instanciate the metaclass
236 88         4152 unshift @_, schema => $self;
237 88         497 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       481 $self->{$kind}{$meta_obj->{name}} = $meta_obj
242             unless $kind eq 'path';
243              
244 88         217 return $self;
245             };
246             }
247              
248              
249             # defining joins (different from the common pattern above)
250             sub define_join {
251 48     48 0 109 my $self = shift;
252              
253             # parse arguments
254 48         174 my ($joins, $aliased_tables) = $self->_parse_join_path(@_);
255              
256             # build class name
257 45         167 my $subclass = join "", map {($_->{kind}, $_->{name})} @$joins;
  121         359  
258 45         151 my $class_name = "$self->{class}::AutoJoin::$subclass";
259              
260             # do nothing if join class was already loaded
261 18 100   18   139 { no strict 'refs'; return $class_name->metadm if @{$class_name.'::ISA'}; }
  18         49  
  18         22179  
  45         82  
  45         69  
  45         512  
262              
263             # otherwise, build the new class
264              
265             # prepare args for SQL::Abstract::More::join
266 29         108 my @sqla_join_args = ($joins->[0]{db_table});
267 29         127 foreach my $join (@$joins[1 .. $#$joins]) {
268             my $join_spec = {
269             operator => $join->{kind},
270             condition => $join->{condition},
271             using => $join->{using},
272 51         168 };
273 51         139 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         88 parents => [uniq map {$_->{table}} @$joins],
  80         517  
281             sqla_join_args => \@sqla_join_args,
282             aliased_tables => $aliased_tables,
283             );
284 29 100       159 $args{primary_key} = $joins->[0]{primary_key} if $joins->[0]{primary_key};
285 29         244 my $meta_join = DBIx::DataModel::Meta::Source::Join->new(%args);
286              
287             # store into our registry
288 29         154 $self->{join}{$subclass} = $meta_join;
289              
290 29         223 return $meta_join;
291             }
292              
293              
294              
295             #----------------------------------------------------------------------
296             # PRIVATE UTILITY METHODS
297             #----------------------------------------------------------------------
298              
299              
300             sub _parse_association_end {
301 64     64   161 my ($self, $letter, $end_params)= @_;
302              
303 64         176 my ($table, $role, $multiplicity, @cols) = @$end_params;
304              
305             # prepend schema name in table, unless it already contains "::"
306 64 50       517 $table =~ s/^/$self->{class}::/ unless $table =~ /::/;
307              
308             # if role is 0, or 'none', or '---', make it empty
309 64 100 66     484 $role = undef if $role && $role =~ /^(0|""|''|-+|none)$/;
310              
311             # pair of parameters for this association end
312 64         317 my %letter_params = (
313             table => $table->metadm,
314             role => $role,
315             multiplicity => $multiplicity,
316             );
317 64 100       192 $letter_params{join_cols} = \@cols if @cols;
318 64         311 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   148 my ($self, $initial_table, @join_names) = @_;
331 48         93 my %aliased_tables;
332              
333 48 50 33     267 $initial_table && @join_names
334             or croak "join: not enough arguments";
335              
336             # build first member of the @join result
337 48         194 my %first_join = (kind => '', name => $initial_table);
338 48 100       282 $initial_table =~ s/\|(.+)$// and $first_join{alias} = $1;
339 48 100       167 my $table = $self->table($initial_table)
340             or croak "...->join('$initial_table', ...) : this schema has "
341             . "no table named '$initial_table'";
342 47         358 $first_join{table} = $table;
343 47         233 $first_join{primary_key} = [$table->primary_key];
344 47         179 $first_join{db_table} = $table->db_from;
345 47 100       194 if ($first_join{alias}) {
346 12         43 $first_join{db_table} .= "|$first_join{alias}";
347 12         32 $aliased_tables{$first_join{alias}} = $table->name;
348             }
349              
350             # initial infrastructure for looping over path specifications
351 47   66     232 my %source = (($first_join{alias} || $table->name) => \%first_join);
352 47         154 my @joins = (\%first_join);
353 47         106 my $join_kind;
354             my $seen_left_join;
355              
356 47         132 foreach my $join_name (@join_names) {
357              
358             # if it is a connector like '=>' or '<=>' or '<=' (see SQLAM syntax) ...
359 90 100       283 if ($join_name =~ /^[<>]?=[<>=]?$/) {
360 12 50       38 !$join_kind or croak "'$join_kind' can't be followed by '$join_name'";
361 12         26 $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       851 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   530 : lastval {$_->{table}{path}{$path_name}} @joins;
  58         244  
375 78 100 66     491 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     589 || ($seen_left_join && $self->{sql_no_inner_after_left_join}))
      66        
383             ? '=>' : '<=>';
384 76 100       201 $seen_left_join = 1 if $join_kind eq '=>';
385              
386             # if max. multiplicity > 1, the join has no primary key
387 76 100       231 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     252 my $left_table = $source_join->{alias} || $source_join->{db_table};
391 76   66     279 my $right_table = $alias || $path->{to}->db_from;
392 76         123 my %condition; # for joining with a ON clause
393 76         166 my $using = []; # for joining with a USING clause
394 76         128 while (my ($left_col, $right_col) = each %{$path->{on}}) {
  152         631  
395 76 50       189 if ($left_col eq $right_col) {
396             # both cols of equal name ==> can participate in a USING clause
397 76 50       246 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         197 $left_col = "$left_table.$left_col";
407 76         162 $right_col = "$right_table.$right_col";
408 76         294 $condition{$left_col} = { -ident => $right_col };
409             }
410 76         214 my $db_table = $path->{to}->db_from;
411 76 100       188 $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         386 db_table => $db_table,
417             condition => \%condition,
418             using => $using,
419             };
420 76         143 push @joins, $new_join;
421 76   66     253 $source{$alias || $path_name} = $new_join;
422              
423             # remember aliased table
424 76 100       206 $aliased_tables{$alias} = $path->{to}->name if $alias;
425              
426             # reset join kind for next loop
427 76         217 undef $join_kind;
428             }
429             }
430              
431 45         188 return (\@joins, \%aliased_tables);
432             }
433              
434              
435              
436              
437              
438             1;
439              
440             __END__