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