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