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