upped version ready for release
[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   };
430
431   bless $self, $class;
432
433   return $self;
434 }
435
436 =head2 dump
437
438 =over 4
439
440 =item Arguments: \%$attrs
441
442 =item Return Value: 1
443
444 =back
445
446  $fixtures->dump({
447    config => 'set_config.json', # config file to use. must be in the config
448                                 # directory specified in the constructor
449    schema => $source_dbic_schema,
450    directory => '/home/me/app/fixtures' # output directory
451  });
452
453 or
454
455  $fixtures->dump({
456    all => 1, # just dump everything that's in the schema
457    schema => $source_dbic_schema,
458    directory => '/home/me/app/fixtures' # output directory
459  });
460
461 In this case objects will be dumped to subdirectories in the specified
462 directory. For example:
463
464  /home/me/app/fixtures/artist/1.fix
465  /home/me/app/fixtures/artist/3.fix
466  /home/me/app/fixtures/producer/5.fix
467
468 schema and directory are required attributes. also, one of config or all must be specified.
469
470 =cut
471
472 sub dump {
473   my $self = shift;
474
475   my ($params) = @_;
476   unless (ref $params eq 'HASH') {
477     return DBIx::Class::Exception->throw('first arg to dump must be hash ref');
478   }
479
480   foreach my $param (qw/schema directory/) {
481     unless ($params->{$param}) {
482       return DBIx::Class::Exception->throw($param . ' param not specified');
483     }
484   }
485
486   my $schema = $params->{schema};
487   my $config;
488   if ($params->{config}) {
489     #read config
490     my $config_file = $self->config_dir->file($params->{config});
491     $config = $self->load_config_file($config_file);
492   } elsif ($params->{all}) {
493     $config = { 
494       might_have => { fetch => 0 },
495       has_many => { fetch => 0 },
496       belongs_to => { fetch => 0 },
497       sets => [map {{ class => $_, quantity => 'all' }} $schema->sources] 
498     };
499   } else {
500     DBIx::Class::Exception->throw('must pass config or set all');
501   }
502
503   my $output_dir = dir($params->{directory});
504   unless (-e $output_dir) {
505     $output_dir->mkpath ||
506     DBIx::Class::Exception->throw("output directory does not exist at $output_dir");
507   }
508
509   $self->msg("generating  fixtures");
510   my $tmp_output_dir = dir($output_dir, '-~dump~-' . $<);
511
512   if (-e $tmp_output_dir) {
513     $self->msg("- clearing existing $tmp_output_dir");
514     $tmp_output_dir->rmtree;
515   }
516   $self->msg("- creating $tmp_output_dir");
517   $tmp_output_dir->mkpath;
518
519   # write version file (for the potential benefit of populate)
520   $tmp_output_dir->file('_dumper_version')
521                  ->openw
522                  ->print($VERSION);
523
524   $config->{rules} ||= {};
525   my @sources = sort { $a->{class} cmp $b->{class} } @{delete $config->{sets}};
526
527   while ( my ($k,$v) = each %{ $config->{rules} } ) {
528     if ( my $source = eval { $schema->source($k) } ) {
529       $config->{rules}{$source->source_name} = $v;
530     }
531   }
532
533   foreach my $source (@sources) {
534     # apply rule to set if specified
535     my $rule = $config->{rules}->{$source->{class}};
536     $source = merge( $source, $rule ) if ($rule);
537
538     # fetch objects
539     my $rs = $schema->resultset($source->{class});
540
541     if ($source->{cond} and ref $source->{cond} eq 'HASH') {
542       # if value starts with \ assume it's meant to be passed as a scalar ref
543       # to dbic. ideally this would substitute deeply
544       $source->{cond} = { 
545         map { 
546           $_ => ($source->{cond}->{$_} =~ s/^\\//) ? \$source->{cond}->{$_} 
547                                                    : $source->{cond}->{$_} 
548         } keys %{$source->{cond}} 
549       };
550     }
551
552     $rs = $rs->search($source->{cond}, { join => $source->{join} }) 
553       if $source->{cond};
554
555     $self->msg("- dumping $source->{class}");
556
557     my %source_options = ( set => { %{$config}, %{$source} } );
558     if ($source->{quantity}) {
559       $rs = $rs->search({}, { order_by => $source->{order_by} }) 
560         if $source->{order_by};
561
562       if ($source->{quantity} =~ /^\d+$/) {
563         $rs = $rs->search({}, { rows => $source->{quantity} });
564       } elsif ($source->{quantity} ne 'all') {
565         DBIx::Class::Exception->throw("invalid value for quantity - $source->{quantity}");
566       }
567     }
568     elsif ($source->{ids} && @{$source->{ids}}) {
569       my @ids = @{$source->{ids}};
570       my (@pks) = $rs->result_source->primary_columns;
571       die "Can't dump multiple col-pks using 'id' option" if @pks > 1;
572       $rs = $rs->search_rs( { $pks[0] => { -in => \@ids } } );
573     }
574     else {
575       DBIx::Class::Exception->throw('must specify either quantity or ids');
576     }
577
578     $source_options{set_dir} = $tmp_output_dir;
579     $self->dump_rs($rs, \%source_options );
580   }
581
582   # clear existing output dir
583   foreach my $child ($output_dir->children) {
584     if ($child->is_dir) {
585       next if ($child eq $tmp_output_dir);
586       if (grep { $_ =~ /\.fix/ } $child->children) {
587         $child->rmtree;
588       }
589     } elsif ($child =~ /_dumper_version$/) {
590       $child->remove;
591     }
592   }
593
594   $self->msg("- moving temp dir to $output_dir");
595   move($_, dir($output_dir, $_->relative($_->parent)->stringify)) 
596     for $tmp_output_dir->children;
597
598   if (-e $output_dir) {
599     $self->msg("- clearing tmp dir $tmp_output_dir");
600     # delete existing fixture set
601     $tmp_output_dir->remove;
602   }
603
604   $self->msg("done");
605
606   return 1;
607 }
608
609 sub load_config_file {
610   my ($self, $config_file) = @_;
611   DBIx::Class::Exception->throw("config does not exist at $config_file")
612     unless -e $config_file;
613
614   my $config = Config::Any::JSON->load($config_file);
615
616   #process includes
617   if (my $incs = $config->{includes}) {
618     $self->msg($incs);
619     DBIx::Class::Exception->throw(
620       'includes params of config must be an array ref of hashrefs'
621     ) unless ref $incs eq 'ARRAY';
622     
623     foreach my $include_config (@$incs) {
624       DBIx::Class::Exception->throw(
625         'includes params of config must be an array ref of hashrefs'
626       ) unless (ref $include_config eq 'HASH') && $include_config->{file};
627       
628       my $include_file = $self->config_dir->file($include_config->{file});
629
630       DBIx::Class::Exception->throw("config does not exist at $include_file")
631         unless -e $include_file;
632       
633       my $include = Config::Any::JSON->load($include_file);
634       $self->msg($include);
635       $config = merge( $config, $include );
636     }
637     delete $config->{includes};
638   }
639   
640   # validate config
641   return DBIx::Class::Exception->throw('config has no sets')
642     unless $config && $config->{sets} && 
643            ref $config->{sets} eq 'ARRAY' && scalar @{$config->{sets}};
644
645   $config->{might_have} = { fetch => 0 } unless exists $config->{might_have};
646   $config->{has_many} = { fetch => 0 }   unless exists $config->{has_many};
647   $config->{belongs_to} = { fetch => 1 } unless exists $config->{belongs_to};
648
649   return $config;
650 }
651
652 sub dump_rs {
653     my ($self, $rs, $params) = @_;
654
655     while (my $row = $rs->next) {
656         $self->dump_object($row, $params);
657     }
658 }
659  
660 sub dump_object {
661   my ($self, $object, $params) = @_;  
662   my $set = $params->{set};
663   die 'no dir passed to dump_object' unless $params->{set_dir};
664   die 'no object passed to dump_object' unless $object;
665
666   my @inherited_attrs = @{$self->_inherited_attributes};
667
668   my @pk_vals = map {
669     $object->get_column($_) 
670   } $object->primary_columns;
671
672   my $key = join("\0", @pk_vals);
673
674   my $src = $object->result_source;
675   my $exists = $self->dumped_objects->{$src->name}{$key}++;
676
677
678   # write dir and gen filename
679   my $source_dir = $params->{set_dir}->subdir(lc $src->from);
680   $source_dir->mkpath(0, 0777);
681
682   # strip dir separators from file name
683   my $file = $source_dir->file(
684       join('-', map { s|[/\\]|_|g; $_; } @pk_vals) . '.fix'
685   );
686
687
688   # write file
689   unless ($exists) {
690     $self->msg('-- dumping ' . $file->stringify, 2);
691     my %ds = $object->get_columns;
692
693     # mess with dates if specified
694     if ($set->{datetime_relative}) {
695       my $formatter= $object->result_source->schema->storage->datetime_parser;
696       unless ($@ || !$formatter) {
697         my $dt;
698         if ($set->{datetime_relative} eq 'today') {
699           $dt = DateTime->today;
700         } else {
701           $dt = $formatter->parse_datetime($set->{datetime_relative}) unless ($@);
702         }
703
704         while (my ($col, $value) = each %ds) {
705           my $col_info = $object->result_source->column_info($col);
706
707           next unless $value
708             && $col_info->{_inflate_info}
709               && uc($col_info->{data_type}) eq 'DATETIME';
710
711           $ds{$col} = $object->get_inflated_column($col)->subtract_datetime($dt);
712         }
713       } else {
714         warn "datetime_relative not supported for this db driver at the moment";
715       }
716     }
717
718     # do the actual dumping
719     my $serialized = Dump(\%ds)->Out();
720     $file->openw->print($serialized);
721   }
722
723   # don't bother looking at rels unless we are actually planning to dump at least one type
724   my ($might_have, $belongs_to, $has_many) = map {
725     $set->{$_}{fetch} || $set->{rules}{$src->source_name}{$_}{fetch}
726   } qw/might_have belongs_to has_many/;
727
728   return unless $might_have
729              || $belongs_to
730              || $has_many
731              || $set->{fetch};
732
733   # dump rels of object
734   unless ($exists) {
735     foreach my $name (sort $src->relationships) {
736       my $info = $src->relationship_info($name);
737       my $r_source = $src->related_source($name);
738       # if belongs_to or might_have with might_have param set or has_many with
739       # has_many param set then
740       if (
741             ( $info->{attrs}{accessor} eq 'single' && 
742               (!$info->{attrs}{join_type} || $might_have) 
743             )
744          || $info->{attrs}{accessor} eq 'filter' 
745          || 
746             ($info->{attrs}{accessor} eq 'multi' && $has_many)
747       ) {
748         my $related_rs = $object->related_resultset($name);       
749         my $rule = $set->{rules}->{$related_rs->result_source->source_name};
750         # these parts of the rule only apply to has_many rels
751         if ($rule && $info->{attrs}{accessor} eq 'multi') {               
752           $related_rs = $related_rs->search(
753             $rule->{cond}, 
754             { join => $rule->{join} }
755           ) if ($rule->{cond});
756
757           $related_rs = $related_rs->search(
758             {},
759             { rows => $rule->{quantity} }
760           ) if ($rule->{quantity} && $rule->{quantity} ne 'all');
761
762           $related_rs = $related_rs->search(
763             {}, 
764             { order_by => $rule->{order_by} }
765           ) if ($rule->{order_by});               
766
767         }
768         if ($set->{has_many}{quantity} && 
769             $set->{has_many}{quantity} =~ /^\d+$/) {
770           $related_rs = $related_rs->search(
771             {}, 
772             { rows => $set->{has_many}->{quantity} }
773           );
774         }
775
776         my %c_params = %{$params};
777         # inherit date param
778         my %mock_set = map { 
779           $_ => $set->{$_} 
780         } grep { $set->{$_} } @inherited_attrs;
781
782         $c_params{set} = \%mock_set;
783         $c_params{set} = merge( $c_params{set}, $rule)
784           if $rule && $rule->{fetch};
785
786         $self->dump_rs($related_rs, \%c_params);
787       } 
788     }
789   }
790   
791   return unless $set && $set->{fetch};
792   foreach my $fetch (@{$set->{fetch}}) {
793     # inherit date param
794     $fetch->{$_} = $set->{$_} foreach 
795       grep { !$fetch->{$_} && $set->{$_} } @inherited_attrs;
796     my $related_rs = $object->related_resultset($fetch->{rel});
797     my $rule = $set->{rules}->{$related_rs->result_source->source_name};
798
799     if ($rule) {
800       my $info = $object->result_source->relationship_info($fetch->{rel});
801       if ($info->{attrs}{accessor} eq 'multi') {
802         $fetch = merge( $fetch, $rule );
803       } elsif ($rule->{fetch}) {
804         $fetch = merge( $fetch, { fetch => $rule->{fetch} } );
805       }
806     } 
807
808     die "relationship $fetch->{rel} does not exist for " . $src->source_name 
809       unless ($related_rs);
810
811     if ($fetch->{cond} and ref $fetch->{cond} eq 'HASH') {
812       # if value starts with \ assume it's meant to be passed as a scalar ref
813       # to dbic.  ideally this would substitute deeply
814       $fetch->{cond} = { map { 
815           $_ => ($fetch->{cond}->{$_} =~ s/^\\//) ? \$fetch->{cond}->{$_} 
816                                                   : $fetch->{cond}->{$_} 
817       } keys %{$fetch->{cond}} };
818     }
819
820     $related_rs = $related_rs->search(
821       $fetch->{cond}, 
822       { join => $fetch->{join} }
823     ) if $fetch->{cond};
824
825     $related_rs = $related_rs->search(
826       {},
827       { rows => $fetch->{quantity} }
828     ) if $fetch->{quantity} && $fetch->{quantity} ne 'all';
829     $related_rs = $related_rs->search(
830       {}, 
831       { order_by => $fetch->{order_by} }
832     ) if $fetch->{order_by};
833
834     $self->dump_rs($related_rs, { %{$params}, set => $fetch });
835   }
836 }
837
838 sub _generate_schema {
839   my $self = shift;
840   my $params = shift || {};
841   require DBI;
842   $self->msg("\ncreating schema");
843
844   my $schema_class = $self->schema_class || "DBIx::Class::Fixtures::Schema";
845   eval "require $schema_class";
846   die $@ if $@;
847
848   my $pre_schema;
849   my $connection_details = $params->{connection_details};
850
851   $namespace_counter++;
852
853   my $namespace = "DBIx::Class::Fixtures::GeneratedSchema_$namespace_counter";
854   Class::C3::Componentised->inject_base( $namespace => $schema_class );
855
856   $pre_schema = $namespace->connect(@{$connection_details});
857   unless( $pre_schema ) {
858     return DBIx::Class::Exception->throw('connection details not valid');
859   }
860   my @tables = map { $pre_schema->source($_)->from } $pre_schema->sources;
861   $self->msg("Tables to drop: [". join(', ', sort @tables) . "]");
862   my $dbh = $pre_schema->storage->dbh;
863
864   # clear existing db
865   $self->msg("- clearing DB of existing tables");
866   $pre_schema->storage->txn_do(sub {
867     $pre_schema->storage->with_deferred_fk_checks(sub {
868       foreach my $table (@tables) {
869         eval { 
870           $dbh->do("drop table $table" . ($params->{cascade} ? ' cascade' : '') ) 
871         };
872       }
873     });
874   });
875
876   # import new ddl file to db
877   my $ddl_file = $params->{ddl};
878   $self->msg("- deploying schema using $ddl_file");
879   my $data = _read_sql($ddl_file);
880   foreach (@$data) {
881     eval { $dbh->do($_) or warn "SQL was:\n $_"};
882           if ($@ && !$self->{ignore_sql_errors}) { die "SQL was:\n $_\n$@"; }
883   }
884   $self->msg("- finished importing DDL into DB");
885
886   # load schema object from our new DB
887   $namespace_counter++;
888   my $namespace2 = "DBIx::Class::Fixtures::GeneratedSchema_$namespace_counter";
889   Class::C3::Componentised->inject_base( $namespace2 => $schema_class );
890   my $schema = $namespace2->connect(@{$connection_details});
891   return $schema;
892 }
893
894 sub _read_sql {
895   my $ddl_file = shift;
896   my $fh;
897   open $fh, "<$ddl_file" or die ("Can't open DDL file, $ddl_file ($!)");
898   my @data = split(/\n/, join('', <$fh>));
899   @data = grep(!/^--/, @data);
900   @data = split(/;/, join('', @data));
901   close($fh);
902   @data = grep { $_ && $_ !~ /^-- / } @data;
903   return \@data;
904 }
905
906 =head2 populate
907
908 =over 4
909
910 =item Arguments: \%$attrs
911
912 =item Return Value: 1
913
914 =back
915
916  $fixtures->populate( {
917    # directory to look for fixtures in, as specified to dump
918    directory => '/home/me/app/fixtures', 
919
920    # DDL to deploy
921    ddl => '/home/me/app/sql/ddl.sql', 
922
923    # database to clear, deploy and then populate
924    connection_details => ['dbi:mysql:dbname=app_dev', 'me', 'password'], 
925
926    # DDL to deploy after populating records, ie. FK constraints
927    post_ddl => '/home/me/app/sql/post_ddl.sql',
928
929    # use CASCADE option when dropping tables
930    cascade => 1,
931
932    # optional, set to 1 to run ddl but not populate 
933    no_populate => 0,
934
935    # Dont try to clean the database, just populate over whats there. Requires
936    # schema option. Use this if you want to handle removing old data yourself
937    # no_deploy => 1
938    # schema => $schema
939  } );
940
941 In this case the database app_dev will be cleared of all tables, then the
942 specified DDL deployed to it, then finally all fixtures found in
943 /home/me/app/fixtures will be added to it. populate will generate its own
944 DBIx::Class schema from the DDL rather than being passed one to use. This is
945 better as custom insert methods are avoided which can to get in the way. In
946 some cases you might not have a DDL, and so this method will eventually allow a
947 $schema object to be passed instead.
948
949 If needed, you can specify a post_ddl attribute which is a DDL to be applied
950 after all the fixtures have been added to the database. A good use of this
951 option would be to add foreign key constraints since databases like Postgresql
952 cannot disable foreign key checks.
953
954 If your tables have foreign key constraints you may want to use the cascade
955 attribute which will make the drop table functionality cascade, ie 'DROP TABLE
956 $table CASCADE'.
957
958 C<directory> is a required attribute. 
959
960 If you wish for DBIx::Class::Fixtures to clear the database for you pass in
961 C<dll> (path to a DDL sql file) and C<connection_details> (array ref  of DSN,
962 user and pass).
963
964 If you wish to deal with cleaning the schema yourself, then pass in a C<schema>
965 attribute containing the connected schema you wish to operate on and set the
966 C<no_deploy> attribute.
967
968 =cut
969
970 sub populate {
971   my $self = shift;
972   my ($params) = @_;
973   DBIx::Class::Exception->throw('first arg to populate must be hash ref')
974     unless ref $params eq 'HASH';
975
976   DBIx::Class::Exception->throw('directory param not specified')
977     unless $params->{directory};
978
979   my $fixture_dir = dir(delete $params->{directory});
980   DBIx::Class::Exception->throw("fixture directory '$fixture_dir' does not exist")
981     unless -d $fixture_dir;
982
983   my $ddl_file;
984   my $dbh;
985   my $schema;
986   if ($params->{ddl} && $params->{connection_details}) {
987     $ddl_file = file(delete $params->{ddl});
988     unless (-e $ddl_file) {
989       return DBIx::Class::Exception->throw('DDL does not exist at ' . $ddl_file);
990     }
991     unless (ref $params->{connection_details} eq 'ARRAY') {
992       return DBIx::Class::Exception->throw('connection details must be an arrayref');
993     }
994     $schema = $self->_generate_schema({ 
995       ddl => $ddl_file, 
996       connection_details => delete $params->{connection_details},
997       %{$params}
998     });
999   } elsif ($params->{schema} && $params->{no_deploy}) {
1000     $schema = $params->{schema};
1001   } else {
1002     DBIx::Class::Exception->throw('you must set the ddl and connection_details params');
1003   }
1004
1005
1006   return 1 if $params->{no_populate}; 
1007   
1008   $self->msg("\nimporting fixtures");
1009   my $tmp_fixture_dir = dir($fixture_dir, "-~populate~-" . $<);
1010   my $version_file = file($fixture_dir, '_dumper_version');
1011 #  DBIx::Class::Exception->throw('no version file found');
1012 #    unless -e $version_file;
1013
1014   if (-e $tmp_fixture_dir) {
1015     $self->msg("- deleting existing temp directory $tmp_fixture_dir");
1016     $tmp_fixture_dir->rmtree;
1017   }
1018   $self->msg("- creating temp dir");
1019   $tmp_fixture_dir->mkpath();
1020   for ( map { $schema->source($_)->from } $schema->sources) {
1021     my $from_dir = $fixture_dir->subdir($_);
1022     next unless -e $from_dir;
1023     dircopy($from_dir, $tmp_fixture_dir->subdir($_) );
1024   }
1025
1026   unless (-d $tmp_fixture_dir) {
1027     DBIx::Class::Exception->throw("Unable to create temporary fixtures dir: $tmp_fixture_dir: $!");
1028   }
1029
1030   my $fixup_visitor;
1031   my $formatter = $schema->storage->datetime_parser;
1032   unless ($@ || !$formatter) {
1033     my %callbacks;
1034     if ($params->{datetime_relative_to}) {
1035       $callbacks{'DateTime::Duration'} = sub {
1036         $params->{datetime_relative_to}->clone->add_duration($_);
1037       };
1038     } else {
1039       $callbacks{'DateTime::Duration'} = sub {
1040         $formatter->format_datetime(DateTime->today->add_duration($_))
1041       };
1042     }
1043     $callbacks{object} ||= "visit_ref"; 
1044     $fixup_visitor = new Data::Visitor::Callback(%callbacks);
1045   }
1046
1047   $schema->storage->txn_do(sub {
1048     $schema->storage->with_deferred_fk_checks(sub {
1049       foreach my $source (sort $schema->sources) {
1050         $self->msg("- adding " . $source);
1051         my $rs = $schema->resultset($source);
1052         my $source_dir = $tmp_fixture_dir->subdir( lc $rs->result_source->from );
1053         next unless (-e $source_dir);
1054         my @rows;
1055         while (my $file = $source_dir->next) {
1056           next unless ($file =~ /\.fix$/);
1057           next if $file->is_dir;
1058           my $contents = $file->slurp;
1059           my $HASH1;
1060           eval($contents);
1061           $HASH1 = $fixup_visitor->visit($HASH1) if $fixup_visitor;
1062           push(@rows, $HASH1);
1063         }
1064         $rs->populate(\@rows) if scalar(@rows);
1065       }
1066     });
1067   });
1068   $self->do_post_ddl( {
1069     schema=>$schema, 
1070     post_ddl=>$params->{post_ddl}
1071   } ) if $params->{post_ddl};
1072
1073   $self->msg("- fixtures imported");
1074   $self->msg("- cleaning up");
1075   $tmp_fixture_dir->rmtree;
1076   return 1;
1077 }
1078
1079 sub do_post_ddl {
1080   my ($self, $params) = @_;
1081
1082   my $schema = $params->{schema};
1083   my $data = _read_sql($params->{post_ddl});
1084   foreach (@$data) {
1085     eval { $schema->storage->dbh->do($_) or warn "SQL was:\n $_"};
1086           if ($@ && !$self->{ignore_sql_errors}) { die "SQL was:\n $_\n$@"; }
1087   }
1088   $self->msg("- finished importing post-populate DDL into DB");
1089 }
1090
1091 sub msg {
1092   my $self = shift;
1093   my $subject = shift || return;
1094   my $level = shift || 1;
1095   return unless $self->debug >= $level;
1096   if (ref $subject) {
1097         print Dumper($subject);
1098   } else {
1099         print $subject . "\n";
1100   }
1101 }
1102
1103 =head1 AUTHOR
1104
1105   Luke Saunders <luke@shadowcatsystems.co.uk>
1106
1107   Initial development sponsored by and (c) Takkle, Inc. 2007
1108
1109 =head1 CONTRIBUTORS
1110
1111   Ash Berlin <ash@shadowcatsystems.co.uk>
1112
1113   Matt S. Trout <mst@shadowcatsystems.co.uk>
1114
1115   Drew Taylor <taylor.andrew.j@gmail.com>
1116
1117   Frank Switalski <fswitalski@gmail.com>
1118
1119 =head1 LICENSE
1120
1121   This library is free software under the same license as perl itself
1122
1123 =cut
1124
1125 1;