bring up to date to use the new schema loader
[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
c5178f25 29Version 1.001010
e5963c1b 30
31=cut
32
c5178f25 33our $VERSION = '1.001010';
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 {
9b7171c7 79 "sets": [
8a1df391 80 {
9b7171c7 81 "class": "Artist",
82 "ids": ["1", "3"]
8a1df391 83 },
84 {
9b7171c7 85 "class": "Producer",
86 "ids": ["5"],
87 "fetch": [
8a1df391 88 {
9b7171c7 89 "rel": "artists",
90 "quantity": "2"
8a1df391 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 {
9b7171c7 116 "sets": [
8a1df391 117 {
9b7171c7 118 "class": "Artist",
119 "ids": ["1", "3"]
8a1df391 120 },
121 {
9b7171c7 122 "class": "Producer",
123 "ids": ["5"],
124 "fetch": [
8a1df391 125 {
9b7171c7 126 "rel": "artists",
127 "quantity": "2"
8a1df391 128 }
129 ]
130 }
131 ],
9b7171c7 132 "rules": {
133 "Artist": {
134 "fetch": [ {
135 "rel": "cds",
136 "quantity": "all"
8a1df391 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 {
9b7171c7 147 "sets": [
a5561f96 148 {
9b7171c7 149 "class": "Artist",
150 "ids": ["1", "3"],
151 "fetch": [ {
152 "rel": "cds",
153 "quantity": "all"
8a1df391 154 } ]
155 },
156 {
9b7171c7 157 "class": "Producer",
158 "ids": ["5"],
159 "fetch": [ {
160 "rel": "artists",
161 "quantity": "2",
162 "fetch": [ {
163 "rel": "cds",
164 "quantity": "all"
8a1df391 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 {
9b7171c7 181 "sets": [ {
182 "class": "Producer",
183 "ids": ["5"]
8a1df391 184 } ],
9b7171c7 185 "includes": [
186 { "file": "base.json" }
8a1df391 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 {
9b7171c7 200 "sets": [ {
201 "class": "RecentItems",
202 "ids": ["9"]
8a1df391 203 } ],
9b7171c7 204 "datetime_relative": "2007-10-30 00:00:00"
8a1df391 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 {
9b7171c7 220 "might_have": { "fetch": 1 },
221 "sets": [
8a1df391 222 {
9b7171c7 223 "class": "Artist",
224 "ids": ["1", "3"]
8a1df391 225 },
226 {
9b7171c7 227 "class": "Producer",
228 "ids": ["5"]
8a1df391 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 {
9b7171c7 263 "sets": [{
264 "class": "Artist",
265 "quantiy": "all",
266 "cond": { "name": "Dave" }
8a1df391 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 {
9b7171c7 280 "sets": [ {
281 "class": "Artist",
282 "quantiy": "all",
283 "cond": { "no1_singles": "\> no1_albums" }
8a1df391 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 {
9b7171c7 295 "sets": [ {
296 "class": "Artist",
297 "quantiy": "all",
298 "cond": { "cds.position": { ">": 4 } },
299 "join": ["cds"]
8a1df391 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 {
9b7171c7 310 "sets": [ {
311 "class": "Artist",
312 "ids": ["1", "3"],
313 "fetch": [ {
314 "rel": "cds",
315 "quantity": "3",
316 "cond": { "position": "2" }
8a1df391 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} } ) {
28acb622 528 if ( my $source = eval { $schema->source($k) } ) {
529 $config->{rules}{$source->source_name} = $v;
06b7a1cc 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");
7f25d8f8 866 $pre_schema->storage->txn_do(sub {
867 $pre_schema->storage->with_deferred_fk_checks(sub {
868 foreach my $table (@tables) {
869 eval {
870 $dbh->do("drop table $table" . ($params->{cascade} ? ' cascade' : '') )
871 };
872 }
873 });
9586eb0c 874 });
384c3f0c 875
876 # import new ddl file to db
877 my $ddl_file = $params->{ddl};
878 $self->msg("- deploying schema using $ddl_file");
f81264b2 879 my $data = _read_sql($ddl_file);
880 foreach (@$data) {
881 eval { $dbh->do($_) or warn "SQL was:\n $_"};
1ac1b0d7 882 if ($@ && !$self->{ignore_sql_errors}) { die "SQL was:\n $_\n$@"; }
384c3f0c 883 }
384c3f0c 884 $self->msg("- finished importing DDL into DB");
885
886 # load schema object from our new DB
b4c67f96 887 $namespace_counter++;
0a54a6e8 888 my $namespace2 = "DBIx::Class::Fixtures::GeneratedSchema_$namespace_counter";
b4c67f96 889 Class::C3::Componentised->inject_base( $namespace2 => $schema_class );
890 my $schema = $namespace2->connect(@{$connection_details});
384c3f0c 891 return $schema;
892}
893
f81264b2 894sub _read_sql {
895 my $ddl_file = shift;
896 my $fh;
897 open $fh, "<$ddl_file" or die ("Can't open DDL file, $ddl_file ($!)");
898 my @data = split(/\n/, join('', <$fh>));
899 @data = grep(!/^--/, @data);
900 @data = split(/;/, join('', @data));
901 close($fh);
902 @data = grep { $_ && $_ !~ /^-- / } @data;
903 return \@data;
904}
a5561f96 905
906=head2 populate
907
908=over 4
909
910=item Arguments: \%$attrs
911
912=item Return Value: 1
913
914=back
915
8a1df391 916 $fixtures->populate( {
917 # directory to look for fixtures in, as specified to dump
918 directory => '/home/me/app/fixtures',
919
920 # DDL to deploy
921 ddl => '/home/me/app/sql/ddl.sql',
922
923 # database to clear, deploy and then populate
924 connection_details => ['dbi:mysql:dbname=app_dev', 'me', 'password'],
925
926 # DDL to deploy after populating records, ie. FK constraints
927 post_ddl => '/home/me/app/sql/post_ddl.sql',
928
929 # use CASCADE option when dropping tables
930 cascade => 1,
931
932 # optional, set to 1 to run ddl but not populate
933 no_populate => 0,
934
935 # Dont try to clean the database, just populate over whats there. Requires
936 # schema option. Use this if you want to handle removing old data yourself
937 # no_deploy => 1
938 # schema => $schema
939 } );
a5561f96 940
9e77162b 941In this case the database app_dev will be cleared of all tables, then the
942specified DDL deployed to it, then finally all fixtures found in
943/home/me/app/fixtures will be added to it. populate will generate its own
944DBIx::Class schema from the DDL rather than being passed one to use. This is
945better as custom insert methods are avoided which can to get in the way. In
946some cases you might not have a DDL, and so this method will eventually allow a
947$schema object to be passed instead.
a5561f96 948
9e77162b 949If needed, you can specify a post_ddl attribute which is a DDL to be applied
950after all the fixtures have been added to the database. A good use of this
951option would be to add foreign key constraints since databases like Postgresql
952cannot disable foreign key checks.
f81264b2 953
9e77162b 954If your tables have foreign key constraints you may want to use the cascade
955attribute which will make the drop table functionality cascade, ie 'DROP TABLE
956$table CASCADE'.
f81264b2 957
9e77162b 958C<directory> is a required attribute.
959
960If you wish for DBIx::Class::Fixtures to clear the database for you pass in
961C<dll> (path to a DDL sql file) and C<connection_details> (array ref of DSN,
962user and pass).
963
964If you wish to deal with cleaning the schema yourself, then pass in a C<schema>
965attribute containing the connected schema you wish to operate on and set the
966C<no_deploy> attribute.
a5561f96 967
968=cut
969
384c3f0c 970sub populate {
971 my $self = shift;
972 my ($params) = @_;
0a54a6e8 973 DBIx::Class::Exception->throw('first arg to populate must be hash ref')
974 unless ref $params eq 'HASH';
975
976 DBIx::Class::Exception->throw('directory param not specified')
977 unless $params->{directory};
384c3f0c 978
9a9a7832 979 my $fixture_dir = dir(delete $params->{directory});
0a54a6e8 980 DBIx::Class::Exception->throw("fixture directory '$fixture_dir' does not exist")
981 unless -d $fixture_dir;
384c3f0c 982
983 my $ddl_file;
9e77162b 984 my $dbh;
985 my $schema;
384c3f0c 986 if ($params->{ddl} && $params->{connection_details}) {
9a9a7832 987 $ddl_file = file(delete $params->{ddl});
384c3f0c 988 unless (-e $ddl_file) {
989 return DBIx::Class::Exception->throw('DDL does not exist at ' . $ddl_file);
990 }
991 unless (ref $params->{connection_details} eq 'ARRAY') {
992 return DBIx::Class::Exception->throw('connection details must be an arrayref');
993 }
8a1df391 994 $schema = $self->_generate_schema({
995 ddl => $ddl_file,
996 connection_details => delete $params->{connection_details},
997 %{$params}
998 });
9e77162b 999 } elsif ($params->{schema} && $params->{no_deploy}) {
1000 $schema = $params->{schema};
384c3f0c 1001 } else {
0a54a6e8 1002 DBIx::Class::Exception->throw('you must set the ddl and connection_details params');
384c3f0c 1003 }
1004
3ad96388 1005
1006 return 1 if $params->{no_populate};
1007
4fb695f4 1008 $self->msg("\nimporting fixtures");
384c3f0c 1009 my $tmp_fixture_dir = dir($fixture_dir, "-~populate~-" . $<);
384c3f0c 1010 my $version_file = file($fixture_dir, '_dumper_version');
0a54a6e8 1011# DBIx::Class::Exception->throw('no version file found');
1012# unless -e $version_file;
384c3f0c 1013
1014 if (-e $tmp_fixture_dir) {
1015 $self->msg("- deleting existing temp directory $tmp_fixture_dir");
4fb695f4 1016 $tmp_fixture_dir->rmtree;
384c3f0c 1017 }
1018 $self->msg("- creating temp dir");
51794e1c 1019 $tmp_fixture_dir->mkpath();
0a54a6e8 1020 for ( map { $schema->source($_)->from } $schema->sources) {
1021 my $from_dir = $fixture_dir->subdir($_);
1022 next unless -e $from_dir;
1023 dircopy($from_dir, $tmp_fixture_dir->subdir($_) );
1024 }
9e77162b 1025
1026 unless (-d $tmp_fixture_dir) {
0a54a6e8 1027 DBIx::Class::Exception->throw("Unable to create temporary fixtures dir: $tmp_fixture_dir: $!");
9e77162b 1028 }
384c3f0c 1029
384c3f0c 1030 my $fixup_visitor;
0a54a6e8 1031 my $formatter = $schema->storage->datetime_parser;
0566a82d 1032 unless ($@ || !$formatter) {
1033 my %callbacks;
1034 if ($params->{datetime_relative_to}) {
1035 $callbacks{'DateTime::Duration'} = sub {
1036 $params->{datetime_relative_to}->clone->add_duration($_);
1037 };
1038 } else {
1039 $callbacks{'DateTime::Duration'} = sub {
1040 $formatter->format_datetime(DateTime->today->add_duration($_))
1041 };
1042 }
1043 $callbacks{object} ||= "visit_ref";
1044 $fixup_visitor = new Data::Visitor::Callback(%callbacks);
384c3f0c 1045 }
1ac1b0d7 1046
7f25d8f8 1047 $schema->storage->txn_do(sub {
1048 $schema->storage->with_deferred_fk_checks(sub {
1049 foreach my $source (sort $schema->sources) {
1050 $self->msg("- adding " . $source);
1051 my $rs = $schema->resultset($source);
1052 my $source_dir = $tmp_fixture_dir->subdir( lc $rs->result_source->from );
1053 next unless (-e $source_dir);
1054 my @rows;
1055 while (my $file = $source_dir->next) {
1056 next unless ($file =~ /\.fix$/);
1057 next if $file->is_dir;
1058 my $contents = $file->slurp;
1059 my $HASH1;
1060 eval($contents);
1061 $HASH1 = $fixup_visitor->visit($HASH1) if $fixup_visitor;
1062 push(@rows, $HASH1);
1063 }
1064 $rs->populate(\@rows) if scalar(@rows);
1ac1b0d7 1065 }
7f25d8f8 1066 });
1ac1b0d7 1067 });
8a1df391 1068 $self->do_post_ddl( {
1069 schema=>$schema,
1070 post_ddl=>$params->{post_ddl}
1071 } ) if $params->{post_ddl};
f81264b2 1072
384c3f0c 1073 $self->msg("- fixtures imported");
1074 $self->msg("- cleaning up");
1075 $tmp_fixture_dir->rmtree;
b099fee9 1076 return 1;
384c3f0c 1077}
1078
6a05e381 1079sub do_post_ddl {
1080 my ($self, $params) = @_;
1081
1082 my $schema = $params->{schema};
1083 my $data = _read_sql($params->{post_ddl});
1084 foreach (@$data) {
1085 eval { $schema->storage->dbh->do($_) or warn "SQL was:\n $_"};
1ac1b0d7 1086 if ($@ && !$self->{ignore_sql_errors}) { die "SQL was:\n $_\n$@"; }
6a05e381 1087 }
1088 $self->msg("- finished importing post-populate DDL into DB");
1089}
1090
0fc424b7 1091sub msg {
1092 my $self = shift;
1093 my $subject = shift || return;
9a9a7832 1094 my $level = shift || 1;
9a9a7832 1095 return unless $self->debug >= $level;
0fc424b7 1096 if (ref $subject) {
1097 print Dumper($subject);
1098 } else {
1099 print $subject . "\n";
1100 }
1101}
a5561f96 1102
1103=head1 AUTHOR
1104
1105 Luke Saunders <luke@shadowcatsystems.co.uk>
1106
3b4f6e76 1107 Initial development sponsored by and (c) Takkle, Inc. 2007
1108
a5561f96 1109=head1 CONTRIBUTORS
1110
1111 Ash Berlin <ash@shadowcatsystems.co.uk>
8a1df391 1112
a5561f96 1113 Matt S. Trout <mst@shadowcatsystems.co.uk>
8a1df391 1114
fc17c598 1115 Drew Taylor <taylor.andrew.j@gmail.com>
a5561f96 1116
9b7171c7 1117 Frank Switalski <fswitalski@gmail.com>
1118
3b4f6e76 1119=head1 LICENSE
1120
1121 This library is free software under the same license as perl itself
1122
a5561f96 1123=cut
1124
e5963c1b 11251;