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