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