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