add an option to exclude some sources from the all option
[dbsrgits/DBIx-Class-Fixtures.git] / lib / DBIx / Class / Fixtures.pm
1 package DBIx::Class::Fixtures;
2
3 use strict;
4 use warnings;
5
6 use DBIx::Class 0.08100;
7 use DBIx::Class::Exception;
8 use Class::Accessor::Grouped;
9 use Path::Class qw(dir file);
10 use Config::Any::JSON;
11 use Data::Dump::Streamer;
12 use Data::Visitor::Callback;
13 use File::Path;
14 use File::Copy::Recursive qw/dircopy/;
15 use File::Copy qw/move/;
16 use Hash::Merge qw( merge );
17 use Data::Dumper;
18 use Class::C3::Componentised;
19
20 use base qw(Class::Accessor::Grouped);
21
22 our $namespace_counter = 0;
23
24 __PACKAGE__->mk_group_accessors( 'simple' => qw/config_dir
25     _inherited_attributes debug schema_class dumped_objects/);
26
27 =head1 VERSION
28
29 Version 1.001010
30
31 =cut
32
33 our $VERSION = '1.001010';
34
35 =head1 NAME
36
37 DBIx::Class::Fixtures
38
39 =head1 SYNOPSIS
40
41  use DBIx::Class::Fixtures;
42
43  ...
44
45  my $fixtures = DBIx::Class::Fixtures->new({ 
46      config_dir => '/home/me/app/fixture_configs' 
47  });
48
49  $fixtures->dump({
50    config => 'set_config.json',
51    schema => $source_dbic_schema,
52    directory => '/home/me/app/fixtures'
53  });
54
55  $fixtures->populate({
56    directory => '/home/me/app/fixtures',
57    ddl => '/home/me/app/sql/ddl.sql',
58    connection_details => ['dbi:mysql:dbname=app_dev', 'me', 'password'],
59    post_ddl => '/home/me/app/sql/post_ddl.sql',
60  });
61
62 =head1 DESCRIPTION
63
64 Dump fixtures from source database to filesystem then import to another
65 database (with same schema) at any time. Use as a constant dataset for running
66 tests against or for populating development databases when impractical to use
67 production clones. Describe fixture set using relations and conditions based on
68 your DBIx::Class schema.
69
70 =head1 DEFINE YOUR FIXTURE SET
71
72 Fixture sets are currently defined in .json files which must reside in your
73 config_dir (e.g. /home/me/app/fixture_configs/a_fixture_set.json). They
74 describe which data to pull and dump from the source database.
75
76 For example:
77
78  {
79    "sets": [
80      {
81        "class": "Artist",
82        "ids": ["1", "3"]
83      },
84      {
85        "class": "Producer",
86        "ids": ["5"],
87        "fetch": [
88          {
89            "rel": "artists",
90            "quantity": "2"
91          }
92        ]
93      }
94    ] 
95  }
96
97 This will fetch artists with primary keys 1 and 3, the producer with primary
98 key 5 and two of producer 5's artists where 'artists' is a has_many DBIx::Class
99 rel from Producer to Artist.
100
101 The top level attributes are as follows:
102
103 =head2 sets
104
105 Sets must be an array of hashes, as in the example given above. Each set
106 defines a set of objects to be included in the fixtures. For details on valid
107 set attributes see L</SET ATTRIBUTES> below.
108
109 =head2 rules
110
111 Rules place general conditions on classes. For example if whenever an artist
112 was dumped you also wanted all of their cds dumped too, then you could use a
113 rule to specify this. For example:
114
115  {
116    "sets": [
117      {
118        "class": "Artist",
119        "ids": ["1", "3"]
120      }, 
121      {
122        "class": "Producer",
123        "ids": ["5"],
124        "fetch": [
125          { 
126            "rel": "artists",
127            "quantity": "2"
128          }
129        ]
130      }
131    ],
132    "rules": {
133      "Artist": {
134        "fetch": [ {
135          "rel": "cds",
136          "quantity": "all"
137        } ]
138      }
139    }
140  }
141
142 In this case all the cds of artists 1, 3 and all producer 5's artists will be
143 dumped as well. Note that 'cds' is a has_many DBIx::Class relation from Artist
144 to CD. This is eqivalent to:
145
146  {
147    "sets": [
148     {
149        "class": "Artist",
150        "ids": ["1", "3"],
151        "fetch": [ {
152          "rel": "cds",
153          "quantity": "all"
154        } ]
155      }, 
156      {
157        "class": "Producer",
158        "ids": ["5"],
159        "fetch": [ { 
160          "rel": "artists",
161          "quantity": "2",
162          "fetch": [ {
163            "rel": "cds",
164            "quantity": "all"
165          } ]
166        } ]
167      }
168    ]
169  }
170
171 rules must be a hash keyed by class name.
172
173 L</RULE ATTRIBUTES>
174
175 =head2 includes
176
177 To prevent repetition between configs you can include other configs. For
178 example:
179
180  {
181    "sets": [ {
182      "class": "Producer",
183      "ids": ["5"]
184    } ],
185    "includes": [
186      { "file": "base.json" }
187    ]
188  }
189
190 Includes must be an arrayref of hashrefs where the hashrefs have key 'file'
191 which is the name of another config file in the same directory. The original
192 config is merged with its includes using L<Hash::Merge>.
193
194 =head2 datetime_relative
195
196 Only available for MySQL and PostgreSQL at the moment, must be a value that
197 DateTime::Format::* can parse. For example:
198
199  {
200    "sets": [ {
201      "class": "RecentItems",
202      "ids": ["9"]
203    } ],
204    "datetime_relative": "2007-10-30 00:00:00"
205  }
206
207 This will work when dumping from a MySQL database and will cause any datetime
208 fields (where datatype => 'datetime' in the column def of the schema class) to
209 be dumped as a DateTime::Duration object relative to the date specified in the
210 datetime_relative value. For example if the RecentItem object had a date field
211 set to 2007-10-25, then when the fixture is imported the field will be set to 5
212 days in the past relative to the current time.
213
214 =head2 might_have
215
216 Specifies whether to automatically dump might_have relationships. Should be a
217 hash with one attribute - fetch. Set fetch to 1 or 0.
218
219  {
220    "might_have": { "fetch": 1 },
221    "sets": [
222      {
223        "class": "Artist",
224        "ids": ["1", "3"]
225      },
226      {
227        "class": "Producer",
228        "ids": ["5"]
229      }
230    ]
231  }
232
233 Note: belongs_to rels are automatically dumped whether you like it or not, this
234 is to avoid FKs to nowhere when importing.  General rules on has_many rels are
235 not accepted at this top level, but you can turn them on for individual sets -
236 see L</SET ATTRIBUTES>.
237
238 =head1 SET ATTRIBUTES
239
240 =head2 class
241
242 Required attribute. Specifies the DBIx::Class object class you wish to dump.
243
244 =head2 ids
245
246 Array of primary key ids to fetch, basically causing an $rs->find($_) for each.
247 If the id is not in the source db then it just won't get dumped, no warnings or
248 death.
249
250 =head2 quantity
251
252 Must be either an integer or the string 'all'. Specifying an integer will
253 effectively set the 'rows' attribute on the resultset clause, specifying 'all'
254 will cause the rows attribute to be left off and for all matching rows to be
255 dumped. There's no randomising here, it's just the first x rows.
256
257 =head2 cond
258
259 A hash specifying the conditions dumped objects must match. Essentially this is
260 a JSON representation of a DBIx::Class search clause. For example:
261
262  {
263    "sets": [{
264      "class": "Artist",
265      "quantiy": "all",
266      "cond": { "name": "Dave" }
267    }]
268  }
269
270 This will dump all artists whose name is 'dave'. Essentially
271 $artist_rs->search({ name => 'Dave' })->all.
272
273 Sometimes in a search clause it's useful to use scalar refs to do things like:
274
275  $artist_rs->search({ no1_singles => \'> no1_albums' })
276
277 This could be specified in the cond hash like so:
278
279  {
280    "sets": [ {
281      "class": "Artist",
282      "quantiy": "all",
283      "cond": { "no1_singles": "\> no1_albums" }
284    } ]
285  }
286
287 So if the value starts with a backslash the value is made a scalar ref before
288 being passed to search.
289
290 =head2 join
291
292 An array of relationships to be used in the cond clause.
293
294  {
295    "sets": [ {
296      "class": "Artist",
297      "quantiy": "all",
298      "cond": { "cds.position": { ">": 4 } },
299      "join": ["cds"]
300    } ]
301  }
302
303 Fetch all artists who have cds with position greater than 4.
304
305 =head2 fetch
306
307 Must be an array of hashes. Specifies which rels to also dump. For example:
308
309  {
310    "sets": [ {
311      "class": "Artist",
312      "ids": ["1", "3"],
313      "fetch": [ {
314        "rel": "cds",
315        "quantity": "3",
316        "cond": { "position": "2" }
317      } ]
318    } ]
319  }
320
321 Will cause the cds of artists 1 and 3 to be dumped where the cd position is 2.
322
323 Valid attributes are: 'rel', 'quantity', 'cond', 'has_many', 'might_have' and
324 'join'. rel is the name of the DBIx::Class rel to follow, the rest are the same
325 as in the set attributes. quantity is necessary for has_many relationships, but
326 not if using for belongs_to or might_have relationships.
327
328 =head2 has_many
329
330 Specifies whether to fetch has_many rels for this set. Must be a hash
331 containing keys fetch and quantity. 
332
333 Set fetch to 1 if you want to fetch them, and quantity to either 'all' or an
334 integer.
335
336 Be careful here, dumping has_many rels can lead to a lot of data being dumped.
337
338 =head2 might_have
339
340 As with has_many but for might_have relationships. Quantity doesn't do anything
341 in this case.
342
343 This value will be inherited by all fetches in this set. This is not true for
344 the has_many attribute.
345
346 =head1 RULE ATTRIBUTES
347
348 =head2 cond
349
350 Same as with L</SET ATTRIBUTES>
351
352 =head2 fetch
353
354 Same as with L</SET ATTRIBUTES>
355
356 =head2 join
357
358 Same as with L</SET ATTRIBUTES>
359
360 =head2 has_many
361
362 Same as with L</SET ATTRIBUTES>
363
364 =head2 might_have
365
366 Same as with L</SET ATTRIBUTES>
367
368 =head1 METHODS
369
370 =head2 new
371
372 =over 4
373
374 =item Arguments: \%$attrs
375
376 =item Return Value: $fixture_object
377
378 =back
379
380 Returns a new DBIx::Class::Fixture object. %attrs can have the following
381 parameters:
382
383 =over
384
385 =item config_dir: 
386
387 required. must contain a valid path to the directory in which your .json
388 configs reside.
389
390 =item debug: 
391
392 determines whether to be verbose
393
394 =item ignore_sql_errors: 
395
396 ignore errors on import of DDL etc
397
398 =back
399
400  my $fixtures = DBIx::Class::Fixtures->new( {
401    config_dir => '/home/me/app/fixture_configs'
402  } );
403
404 =cut
405
406 sub new {
407   my $class = shift;
408
409   my ($params) = @_;
410   unless (ref $params eq 'HASH') {
411     return DBIx::Class::Exception->throw('first arg to DBIx::Class::Fixtures->new() must be hash ref');
412   }
413
414   unless ($params->{config_dir}) {
415     return DBIx::Class::Exception->throw('config_dir param not specified');
416   }
417
418   my $config_dir = dir($params->{config_dir});
419   unless (-e $params->{config_dir}) {
420     return DBIx::Class::Exception->throw('config_dir directory doesn\'t exist');
421   }
422
423   my $self = {
424               config_dir => $config_dir,
425               _inherited_attributes => [qw/datetime_relative might_have rules belongs_to/],
426               debug => $params->{debug} || 0,
427               ignore_sql_errors => $params->{ignore_sql_errors},
428               dumped_objects => {},
429               use_create => $params->{use_create} || 0
430   };
431
432   bless $self, $class;
433
434   return $self;
435 }
436
437 =head2 dump
438
439 =over 4
440
441 =item Arguments: \%$attrs
442
443 =item Return Value: 1
444
445 =back
446
447  $fixtures->dump({
448    config => 'set_config.json', # config file to use. must be in the config
449                                 # directory specified in the constructor
450    schema => $source_dbic_schema,
451    directory => '/home/me/app/fixtures' # output directory
452  });
453
454 or
455
456  $fixtures->dump({
457    all => 1, # just dump everything that's in the schema
458    schema => $source_dbic_schema,
459    directory => '/home/me/app/fixtures' # output directory
460  });
461
462 In this case objects will be dumped to subdirectories in the specified
463 directory. For example:
464
465  /home/me/app/fixtures/artist/1.fix
466  /home/me/app/fixtures/artist/3.fix
467  /home/me/app/fixtures/producer/5.fix
468
469 schema and directory are required attributes. also, one of config or all must be specified.
470
471 =cut
472
473 sub dump {
474   my $self = shift;
475
476   my ($params) = @_;
477   unless (ref $params eq 'HASH') {
478     return DBIx::Class::Exception->throw('first arg to dump must be hash ref');
479   }
480
481   foreach my $param (qw/schema directory/) {
482     unless ($params->{$param}) {
483       return DBIx::Class::Exception->throw($param . ' param not specified');
484     }
485   }
486
487   if($params->{excludes} && !$params->{all}) {
488     return DBIx::Class::Exception->throw("'excludes' param only works when using the 'all' param");
489   }
490
491   my $schema = $params->{schema};
492   my $config;
493   if ($params->{config}) {
494     #read config
495     my $config_file = $self->config_dir->file($params->{config});
496     $config = $self->load_config_file($config_file);
497   } elsif ($params->{all}) {
498     my %excludes = map {$_=>1} @{$params->{excludes}||[]};
499     $config = { 
500       might_have => { fetch => 0 },
501       has_many => { fetch => 0 },
502       belongs_to => { fetch => 0 },
503       sets => [
504         map {
505           { class => $_, quantity => 'all' };
506         } grep {
507           !$excludes{$_}
508         } $schema->sources],
509     };
510   } else {
511     DBIx::Class::Exception->throw('must pass config or set all');
512   }
513
514   my $output_dir = dir($params->{directory});
515   unless (-e $output_dir) {
516     $output_dir->mkpath ||
517     DBIx::Class::Exception->throw("output directory does not exist at $output_dir");
518   }
519
520   $self->msg("generating  fixtures");
521   my $tmp_output_dir = dir($output_dir, '-~dump~-' . $<);
522
523   if (-e $tmp_output_dir) {
524     $self->msg("- clearing existing $tmp_output_dir");
525     $tmp_output_dir->rmtree;
526   }
527   $self->msg("- creating $tmp_output_dir");
528   $tmp_output_dir->mkpath;
529
530   # write version file (for the potential benefit of populate)
531   $tmp_output_dir->file('_dumper_version')
532                  ->openw
533                  ->print($VERSION);
534
535   $config->{rules} ||= {};
536   my @sources = sort { $a->{class} cmp $b->{class} } @{delete $config->{sets}};
537
538   while ( my ($k,$v) = each %{ $config->{rules} } ) {
539     if ( my $source = eval { $schema->source($k) } ) {
540       $config->{rules}{$source->source_name} = $v;
541     }
542   }
543
544   foreach my $source (@sources) {
545     # apply rule to set if specified
546     my $rule = $config->{rules}->{$source->{class}};
547     $source = merge( $source, $rule ) if ($rule);
548
549     # fetch objects
550     my $rs = $schema->resultset($source->{class});
551
552     if ($source->{cond} and ref $source->{cond} eq 'HASH') {
553       # if value starts with \ assume it's meant to be passed as a scalar ref
554       # to dbic. ideally this would substitute deeply
555       $source->{cond} = { 
556         map { 
557           $_ => ($source->{cond}->{$_} =~ s/^\\//) ? \$source->{cond}->{$_} 
558                                                    : $source->{cond}->{$_} 
559         } keys %{$source->{cond}} 
560       };
561     }
562
563     $rs = $rs->search($source->{cond}, { join => $source->{join} }) 
564       if $source->{cond};
565
566     $self->msg("- dumping $source->{class}");
567
568     my %source_options = ( set => { %{$config}, %{$source} } );
569     if ($source->{quantity}) {
570       $rs = $rs->search({}, { order_by => $source->{order_by} }) 
571         if $source->{order_by};
572
573       if ($source->{quantity} =~ /^\d+$/) {
574         $rs = $rs->search({}, { rows => $source->{quantity} });
575       } elsif ($source->{quantity} ne 'all') {
576         DBIx::Class::Exception->throw("invalid value for quantity - $source->{quantity}");
577       }
578     }
579     elsif ($source->{ids} && @{$source->{ids}}) {
580       my @ids = @{$source->{ids}};
581       my (@pks) = $rs->result_source->primary_columns;
582       die "Can't dump multiple col-pks using 'id' option" if @pks > 1;
583       $rs = $rs->search_rs( { $pks[0] => { -in => \@ids } } );
584     }
585     else {
586       DBIx::Class::Exception->throw('must specify either quantity or ids');
587     }
588
589     $source_options{set_dir} = $tmp_output_dir;
590     $self->dump_rs($rs, \%source_options );
591   }
592
593   # clear existing output dir
594   foreach my $child ($output_dir->children) {
595     if ($child->is_dir) {
596       next if ($child eq $tmp_output_dir);
597       if (grep { $_ =~ /\.fix/ } $child->children) {
598         $child->rmtree;
599       }
600     } elsif ($child =~ /_dumper_version$/) {
601       $child->remove;
602     }
603   }
604
605   $self->msg("- moving temp dir to $output_dir");
606   move($_, dir($output_dir, $_->relative($_->parent)->stringify)) 
607     for $tmp_output_dir->children;
608
609   if (-e $output_dir) {
610     $self->msg("- clearing tmp dir $tmp_output_dir");
611     # delete existing fixture set
612     $tmp_output_dir->remove;
613   }
614
615   $self->msg("done");
616
617   return 1;
618 }
619
620 sub load_config_file {
621   my ($self, $config_file) = @_;
622   DBIx::Class::Exception->throw("config does not exist at $config_file")
623     unless -e $config_file;
624
625   my $config = Config::Any::JSON->load($config_file);
626
627   #process includes
628   if (my $incs = $config->{includes}) {
629     $self->msg($incs);
630     DBIx::Class::Exception->throw(
631       'includes params of config must be an array ref of hashrefs'
632     ) unless ref $incs eq 'ARRAY';
633     
634     foreach my $include_config (@$incs) {
635       DBIx::Class::Exception->throw(
636         'includes params of config must be an array ref of hashrefs'
637       ) unless (ref $include_config eq 'HASH') && $include_config->{file};
638       
639       my $include_file = $self->config_dir->file($include_config->{file});
640
641       DBIx::Class::Exception->throw("config does not exist at $include_file")
642         unless -e $include_file;
643       
644       my $include = Config::Any::JSON->load($include_file);
645       $self->msg($include);
646       $config = merge( $config, $include );
647     }
648     delete $config->{includes};
649   }
650   
651   # validate config
652   return DBIx::Class::Exception->throw('config has no sets')
653     unless $config && $config->{sets} && 
654            ref $config->{sets} eq 'ARRAY' && scalar @{$config->{sets}};
655
656   $config->{might_have} = { fetch => 0 } unless exists $config->{might_have};
657   $config->{has_many} = { fetch => 0 }   unless exists $config->{has_many};
658   $config->{belongs_to} = { fetch => 1 } unless exists $config->{belongs_to};
659
660   return $config;
661 }
662
663 sub dump_rs {
664     my ($self, $rs, $params) = @_;
665
666     while (my $row = $rs->next) {
667         $self->dump_object($row, $params);
668     }
669 }
670  
671 sub dump_object {
672   my ($self, $object, $params) = @_;  
673   my $set = $params->{set};
674   die 'no dir passed to dump_object' unless $params->{set_dir};
675   die 'no object passed to dump_object' unless $object;
676
677   my @inherited_attrs = @{$self->_inherited_attributes};
678
679   my @pk_vals = map {
680     $object->get_column($_) 
681   } $object->primary_columns;
682
683   my $key = join("\0", @pk_vals);
684
685   my $src = $object->result_source;
686   my $exists = $self->dumped_objects->{$src->name}{$key}++;
687
688
689   # write dir and gen filename
690   my $source_dir = $params->{set_dir}->subdir(lc $src->from);
691   $source_dir->mkpath(0, 0777);
692
693   # strip dir separators from file name
694   my $file = $source_dir->file(
695       join('-', map { s|[/\\]|_|g; $_; } @pk_vals) . '.fix'
696   );
697
698
699   # write file
700   unless ($exists) {
701     $self->msg('-- dumping ' . $file->stringify, 2);
702     my %ds = $object->get_columns;
703
704     # mess with dates if specified
705     if ($set->{datetime_relative}) {
706       my $formatter= $object->result_source->schema->storage->datetime_parser;
707       unless ($@ || !$formatter) {
708         my $dt;
709         if ($set->{datetime_relative} eq 'today') {
710           $dt = DateTime->today;
711         } else {
712           $dt = $formatter->parse_datetime($set->{datetime_relative}) unless ($@);
713         }
714
715         while (my ($col, $value) = each %ds) {
716           my $col_info = $object->result_source->column_info($col);
717
718           next unless $value
719             && $col_info->{_inflate_info}
720               && uc($col_info->{data_type}) eq 'DATETIME';
721
722           $ds{$col} = $object->get_inflated_column($col)->subtract_datetime($dt);
723         }
724       } else {
725         warn "datetime_relative not supported for this db driver at the moment";
726       }
727     }
728
729     # do the actual dumping
730     my $serialized = Dump(\%ds)->Out();
731     $file->openw->print($serialized);
732   }
733
734   # don't bother looking at rels unless we are actually planning to dump at least one type
735   my ($might_have, $belongs_to, $has_many) = map {
736     $set->{$_}{fetch} || $set->{rules}{$src->source_name}{$_}{fetch}
737   } qw/might_have belongs_to has_many/;
738
739   return unless $might_have
740              || $belongs_to
741              || $has_many
742              || $set->{fetch};
743
744   # dump rels of object
745   unless ($exists) {
746     foreach my $name (sort $src->relationships) {
747       my $info = $src->relationship_info($name);
748       my $r_source = $src->related_source($name);
749       # if belongs_to or might_have with might_have param set or has_many with
750       # has_many param set then
751       if (
752             ( $info->{attrs}{accessor} eq 'single' && 
753               (!$info->{attrs}{join_type} || $might_have) 
754             )
755          || $info->{attrs}{accessor} eq 'filter' 
756          || 
757             ($info->{attrs}{accessor} eq 'multi' && $has_many)
758       ) {
759         my $related_rs = $object->related_resultset($name);       
760         my $rule = $set->{rules}->{$related_rs->result_source->source_name};
761         # these parts of the rule only apply to has_many rels
762         if ($rule && $info->{attrs}{accessor} eq 'multi') {               
763           $related_rs = $related_rs->search(
764             $rule->{cond}, 
765             { join => $rule->{join} }
766           ) if ($rule->{cond});
767
768           $related_rs = $related_rs->search(
769             {},
770             { rows => $rule->{quantity} }
771           ) if ($rule->{quantity} && $rule->{quantity} ne 'all');
772
773           $related_rs = $related_rs->search(
774             {}, 
775             { order_by => $rule->{order_by} }
776           ) if ($rule->{order_by});               
777
778         }
779         if ($set->{has_many}{quantity} && 
780             $set->{has_many}{quantity} =~ /^\d+$/) {
781           $related_rs = $related_rs->search(
782             {}, 
783             { rows => $set->{has_many}->{quantity} }
784           );
785         }
786
787         my %c_params = %{$params};
788         # inherit date param
789         my %mock_set = map { 
790           $_ => $set->{$_} 
791         } grep { $set->{$_} } @inherited_attrs;
792
793         $c_params{set} = \%mock_set;
794         $c_params{set} = merge( $c_params{set}, $rule)
795           if $rule && $rule->{fetch};
796
797         $self->dump_rs($related_rs, \%c_params);
798       } 
799     }
800   }
801   
802   return unless $set && $set->{fetch};
803   foreach my $fetch (@{$set->{fetch}}) {
804     # inherit date param
805     $fetch->{$_} = $set->{$_} foreach 
806       grep { !$fetch->{$_} && $set->{$_} } @inherited_attrs;
807     my $related_rs = $object->related_resultset($fetch->{rel});
808     my $rule = $set->{rules}->{$related_rs->result_source->source_name};
809
810     if ($rule) {
811       my $info = $object->result_source->relationship_info($fetch->{rel});
812       if ($info->{attrs}{accessor} eq 'multi') {
813         $fetch = merge( $fetch, $rule );
814       } elsif ($rule->{fetch}) {
815         $fetch = merge( $fetch, { fetch => $rule->{fetch} } );
816       }
817     } 
818
819     die "relationship $fetch->{rel} does not exist for " . $src->source_name 
820       unless ($related_rs);
821
822     if ($fetch->{cond} and ref $fetch->{cond} eq 'HASH') {
823       # if value starts with \ assume it's meant to be passed as a scalar ref
824       # to dbic.  ideally this would substitute deeply
825       $fetch->{cond} = { map { 
826           $_ => ($fetch->{cond}->{$_} =~ s/^\\//) ? \$fetch->{cond}->{$_} 
827                                                   : $fetch->{cond}->{$_} 
828       } keys %{$fetch->{cond}} };
829     }
830
831     $related_rs = $related_rs->search(
832       $fetch->{cond}, 
833       { join => $fetch->{join} }
834     ) if $fetch->{cond};
835
836     $related_rs = $related_rs->search(
837       {},
838       { rows => $fetch->{quantity} }
839     ) if $fetch->{quantity} && $fetch->{quantity} ne 'all';
840     $related_rs = $related_rs->search(
841       {}, 
842       { order_by => $fetch->{order_by} }
843     ) if $fetch->{order_by};
844
845     $self->dump_rs($related_rs, { %{$params}, set => $fetch });
846   }
847 }
848
849 sub _generate_schema {
850   my $self = shift;
851   my $params = shift || {};
852   require DBI;
853   $self->msg("\ncreating schema");
854
855   my $schema_class = $self->schema_class || "DBIx::Class::Fixtures::Schema";
856   eval "require $schema_class";
857   die $@ if $@;
858
859   my $pre_schema;
860   my $connection_details = $params->{connection_details};
861
862   $namespace_counter++;
863
864   my $namespace = "DBIx::Class::Fixtures::GeneratedSchema_$namespace_counter";
865   Class::C3::Componentised->inject_base( $namespace => $schema_class );
866
867   $pre_schema = $namespace->connect(@{$connection_details});
868   unless( $pre_schema ) {
869     return DBIx::Class::Exception->throw('connection details not valid');
870   }
871   my @tables = map { $pre_schema->source($_)->from } $pre_schema->sources;
872   $self->msg("Tables to drop: [". join(', ', sort @tables) . "]");
873   my $dbh = $pre_schema->storage->dbh;
874
875   # clear existing db
876   $self->msg("- clearing DB of existing tables");
877   $pre_schema->storage->txn_do(sub {
878     $pre_schema->storage->with_deferred_fk_checks(sub {
879       foreach my $table (@tables) {
880         eval { 
881           $dbh->do("drop table $table" . ($params->{cascade} ? ' cascade' : '') ) 
882         };
883       }
884     });
885   });
886
887   # import new ddl file to db
888   my $ddl_file = $params->{ddl};
889   $self->msg("- deploying schema using $ddl_file");
890   my $data = _read_sql($ddl_file);
891   foreach (@$data) {
892     eval { $dbh->do($_) or warn "SQL was:\n $_"};
893           if ($@ && !$self->{ignore_sql_errors}) { die "SQL was:\n $_\n$@"; }
894   }
895   $self->msg("- finished importing DDL into DB");
896
897   # load schema object from our new DB
898   $namespace_counter++;
899   my $namespace2 = "DBIx::Class::Fixtures::GeneratedSchema_$namespace_counter";
900   Class::C3::Componentised->inject_base( $namespace2 => $schema_class );
901   my $schema = $namespace2->connect(@{$connection_details});
902   return $schema;
903 }
904
905 sub _read_sql {
906   my $ddl_file = shift;
907   my $fh;
908   open $fh, "<$ddl_file" or die ("Can't open DDL file, $ddl_file ($!)");
909   my @data = split(/\n/, join('', <$fh>));
910   @data = grep(!/^--/, @data);
911   @data = split(/;/, join('', @data));
912   close($fh);
913   @data = grep { $_ && $_ !~ /^-- / } @data;
914   return \@data;
915 }
916
917 =head2 populate
918
919 =over 4
920
921 =item Arguments: \%$attrs
922
923 =item Return Value: 1
924
925 =back
926
927  $fixtures->populate( {
928    # directory to look for fixtures in, as specified to dump
929    directory => '/home/me/app/fixtures', 
930
931    # DDL to deploy
932    ddl => '/home/me/app/sql/ddl.sql', 
933
934    # database to clear, deploy and then populate
935    connection_details => ['dbi:mysql:dbname=app_dev', 'me', 'password'], 
936
937    # DDL to deploy after populating records, ie. FK constraints
938    post_ddl => '/home/me/app/sql/post_ddl.sql',
939
940    # use CASCADE option when dropping tables
941    cascade => 1,
942
943    # optional, set to 1 to run ddl but not populate 
944    no_populate => 0,
945
946         # optional, set to 1 to run each fixture through ->create rather than have
947    # each $rs populated using $rs->populate. Useful if you have overridden new() logic
948         # that effects the value of column(s).
949         use_create => 0,
950
951    # Dont try to clean the database, just populate over whats there. Requires
952    # schema option. Use this if you want to handle removing old data yourself
953    # no_deploy => 1
954    # schema => $schema
955  } );
956
957 In this case the database app_dev will be cleared of all tables, then the
958 specified DDL deployed to it, then finally all fixtures found in
959 /home/me/app/fixtures will be added to it. populate will generate its own
960 DBIx::Class schema from the DDL rather than being passed one to use. This is
961 better as custom insert methods are avoided which can to get in the way. In
962 some cases you might not have a DDL, and so this method will eventually allow a
963 $schema object to be passed instead.
964
965 If needed, you can specify a post_ddl attribute which is a DDL to be applied
966 after all the fixtures have been added to the database. A good use of this
967 option would be to add foreign key constraints since databases like Postgresql
968 cannot disable foreign key checks.
969
970 If your tables have foreign key constraints you may want to use the cascade
971 attribute which will make the drop table functionality cascade, ie 'DROP TABLE
972 $table CASCADE'.
973
974 C<directory> is a required attribute. 
975
976 If you wish for DBIx::Class::Fixtures to clear the database for you pass in
977 C<dll> (path to a DDL sql file) and C<connection_details> (array ref  of DSN,
978 user and pass).
979
980 If you wish to deal with cleaning the schema yourself, then pass in a C<schema>
981 attribute containing the connected schema you wish to operate on and set the
982 C<no_deploy> attribute.
983
984 =cut
985
986 sub populate {
987   my $self = shift;
988   my ($params) = @_;
989   DBIx::Class::Exception->throw('first arg to populate must be hash ref')
990     unless ref $params eq 'HASH';
991
992   DBIx::Class::Exception->throw('directory param not specified')
993     unless $params->{directory};
994
995   my $fixture_dir = dir(delete $params->{directory});
996   DBIx::Class::Exception->throw("fixture directory '$fixture_dir' does not exist")
997     unless -d $fixture_dir;
998
999   my $ddl_file;
1000   my $dbh;
1001   my $schema;
1002   if ($params->{ddl} && $params->{connection_details}) {
1003     $ddl_file = file(delete $params->{ddl});
1004     unless (-e $ddl_file) {
1005       return DBIx::Class::Exception->throw('DDL does not exist at ' . $ddl_file);
1006     }
1007     unless (ref $params->{connection_details} eq 'ARRAY') {
1008       return DBIx::Class::Exception->throw('connection details must be an arrayref');
1009     }
1010     $schema = $self->_generate_schema({ 
1011       ddl => $ddl_file, 
1012       connection_details => delete $params->{connection_details},
1013       %{$params}
1014     });
1015   } elsif ($params->{schema} && $params->{no_deploy}) {
1016     $schema = $params->{schema};
1017   } else {
1018     DBIx::Class::Exception->throw('you must set the ddl and connection_details params');
1019   }
1020
1021
1022   return 1 if $params->{no_populate}; 
1023   
1024   $self->msg("\nimporting fixtures");
1025   my $tmp_fixture_dir = dir($fixture_dir, "-~populate~-" . $<);
1026   my $version_file = file($fixture_dir, '_dumper_version');
1027 #  DBIx::Class::Exception->throw('no version file found');
1028 #    unless -e $version_file;
1029
1030   if (-e $tmp_fixture_dir) {
1031     $self->msg("- deleting existing temp directory $tmp_fixture_dir");
1032     $tmp_fixture_dir->rmtree;
1033   }
1034   $self->msg("- creating temp dir");
1035   $tmp_fixture_dir->mkpath();
1036   for ( map { $schema->source($_)->from } $schema->sources) {
1037     my $from_dir = $fixture_dir->subdir($_);
1038     next unless -e $from_dir;
1039     dircopy($from_dir, $tmp_fixture_dir->subdir($_) );
1040   }
1041
1042   unless (-d $tmp_fixture_dir) {
1043     DBIx::Class::Exception->throw("Unable to create temporary fixtures dir: $tmp_fixture_dir: $!");
1044   }
1045
1046   my $fixup_visitor;
1047   my $formatter = $schema->storage->datetime_parser;
1048   unless ($@ || !$formatter) {
1049     my %callbacks;
1050     if ($params->{datetime_relative_to}) {
1051       $callbacks{'DateTime::Duration'} = sub {
1052         $params->{datetime_relative_to}->clone->add_duration($_);
1053       };
1054     } else {
1055       $callbacks{'DateTime::Duration'} = sub {
1056         $formatter->format_datetime(DateTime->today->add_duration($_))
1057       };
1058     }
1059     $callbacks{object} ||= "visit_ref"; 
1060     $fixup_visitor = new Data::Visitor::Callback(%callbacks);
1061   }
1062
1063   $schema->storage->txn_do(sub {
1064     $schema->storage->with_deferred_fk_checks(sub {
1065       foreach my $source (sort $schema->sources) {
1066         $self->msg("- adding " . $source);
1067         my $rs = $schema->resultset($source);
1068         my $source_dir = $tmp_fixture_dir->subdir( lc $rs->result_source->from );
1069         next unless (-e $source_dir);
1070         my @rows;
1071         while (my $file = $source_dir->next) {
1072           next unless ($file =~ /\.fix$/);
1073           next if $file->is_dir;
1074           my $contents = $file->slurp;
1075           my $HASH1;
1076           eval($contents);
1077           $HASH1 = $fixup_visitor->visit($HASH1) if $fixup_visitor;
1078           if ( $params->{use_create} ) {
1079             $rs->create( $HASH1 );
1080           } else {
1081             push(@rows, $HASH1);
1082           }
1083         }
1084         $rs->populate(\@rows) if scalar(@rows);
1085       }
1086     });
1087   });
1088   $self->do_post_ddl( {
1089     schema=>$schema, 
1090     post_ddl=>$params->{post_ddl}
1091   } ) if $params->{post_ddl};
1092
1093   $self->msg("- fixtures imported");
1094   $self->msg("- cleaning up");
1095   $tmp_fixture_dir->rmtree;
1096   return 1;
1097 }
1098
1099 sub do_post_ddl {
1100   my ($self, $params) = @_;
1101
1102   my $schema = $params->{schema};
1103   my $data = _read_sql($params->{post_ddl});
1104   foreach (@$data) {
1105     eval { $schema->storage->dbh->do($_) or warn "SQL was:\n $_"};
1106           if ($@ && !$self->{ignore_sql_errors}) { die "SQL was:\n $_\n$@"; }
1107   }
1108   $self->msg("- finished importing post-populate DDL into DB");
1109 }
1110
1111 sub msg {
1112   my $self = shift;
1113   my $subject = shift || return;
1114   my $level = shift || 1;
1115   return unless $self->debug >= $level;
1116   if (ref $subject) {
1117         print Dumper($subject);
1118   } else {
1119         print $subject . "\n";
1120   }
1121 }
1122
1123 =head1 AUTHOR
1124
1125   Luke Saunders <luke@shadowcatsystems.co.uk>
1126
1127   Initial development sponsored by and (c) Takkle, Inc. 2007
1128
1129 =head1 CONTRIBUTORS
1130
1131   Ash Berlin <ash@shadowcatsystems.co.uk>
1132
1133   Matt S. Trout <mst@shadowcatsystems.co.uk>
1134
1135   Drew Taylor <taylor.andrew.j@gmail.com>
1136
1137   Frank Switalski <fswitalski@gmail.com>
1138
1139 =head1 LICENSE
1140
1141   This library is free software under the same license as perl itself
1142
1143 =cut
1144
1145 1;