Fix fixture filenames to be compatible with ntfs systems
[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;
917e1e25 9use Path::Class qw(dir file tempdir);
c040a9b0 10use File::Spec::Functions 'catfile', 'catdir';
0fc424b7 11use Config::Any::JSON;
12use Data::Dump::Streamer;
4fb695f4 13use Data::Visitor::Callback;
0fc424b7 14use File::Path;
4fb695f4 15use File::Copy::Recursive qw/dircopy/;
6116de11 16use File::Copy qw/move/;
0fc424b7 17use Hash::Merge qw( merge );
e5963c1b 18use Data::Dumper;
aa9f3cc7 19use Class::C3::Componentised;
c040a9b0 20use MIME::Base64;
e5963c1b 21
b099fee9 22use base qw(Class::Accessor::Grouped);
e5963c1b 23
aa9f3cc7 24our $namespace_counter = 0;
0566a82d 25
8a1df391 26__PACKAGE__->mk_group_accessors( 'simple' => qw/config_dir
c040a9b0 27 _inherited_attributes debug schema_class dumped_objects config_attrs/);
e5963c1b 28
01a10324 29our $VERSION = '1.001026';
e0f67e5b 30
31$VERSION = eval $VERSION;
e5963c1b 32
33=head1 NAME
34
d9b65413 35DBIx::Class::Fixtures - Dump data and repopulate a database using rules
9f96b203 36
e5963c1b 37=head1 SYNOPSIS
38
8a1df391 39 use DBIx::Class::Fixtures;
e5963c1b 40
8a1df391 41 ...
e5963c1b 42
8a1df391 43 my $fixtures = DBIx::Class::Fixtures->new({
44 config_dir => '/home/me/app/fixture_configs'
45 });
e5963c1b 46
8a1df391 47 $fixtures->dump({
48 config => 'set_config.json',
49 schema => $source_dbic_schema,
50 directory => '/home/me/app/fixtures'
51 });
e5963c1b 52
8a1df391 53 $fixtures->populate({
54 directory => '/home/me/app/fixtures',
55 ddl => '/home/me/app/sql/ddl.sql',
56 connection_details => ['dbi:mysql:dbname=app_dev', 'me', 'password'],
57 post_ddl => '/home/me/app/sql/post_ddl.sql',
58 });
e5963c1b 59
60=head1 DESCRIPTION
61
8a1df391 62Dump fixtures from source database to filesystem then import to another
63database (with same schema) at any time. Use as a constant dataset for running
64tests against or for populating development databases when impractical to use
65production clones. Describe fixture set using relations and conditions based on
66your DBIx::Class schema.
a5561f96 67
68=head1 DEFINE YOUR FIXTURE SET
69
8a1df391 70Fixture sets are currently defined in .json files which must reside in your
71config_dir (e.g. /home/me/app/fixture_configs/a_fixture_set.json). They
72describe which data to pull and dump from the source database.
a5561f96 73
74For example:
75
8a1df391 76 {
9b7171c7 77 "sets": [
8a1df391 78 {
9b7171c7 79 "class": "Artist",
80 "ids": ["1", "3"]
8a1df391 81 },
82 {
9b7171c7 83 "class": "Producer",
84 "ids": ["5"],
85 "fetch": [
8a1df391 86 {
9b7171c7 87 "rel": "artists",
88 "quantity": "2"
8a1df391 89 }
90 ]
91 }
92 ]
93 }
94
95This will fetch artists with primary keys 1 and 3, the producer with primary
96key 5 and two of producer 5's artists where 'artists' is a has_many DBIx::Class
97rel from Producer to Artist.
a5561f96 98
95566320 99The top level attributes are as follows:
100
a5561f96 101=head2 sets
102
8a1df391 103Sets must be an array of hashes, as in the example given above. Each set
104defines a set of objects to be included in the fixtures. For details on valid
105set attributes see L</SET ATTRIBUTES> below.
a5561f96 106
107=head2 rules
108
8a1df391 109Rules place general conditions on classes. For example if whenever an artist
110was dumped you also wanted all of their cds dumped too, then you could use a
111rule to specify this. For example:
112
113 {
9b7171c7 114 "sets": [
8a1df391 115 {
9b7171c7 116 "class": "Artist",
117 "ids": ["1", "3"]
8a1df391 118 },
119 {
9b7171c7 120 "class": "Producer",
121 "ids": ["5"],
122 "fetch": [
8a1df391 123 {
9b7171c7 124 "rel": "artists",
125 "quantity": "2"
8a1df391 126 }
127 ]
128 }
129 ],
9b7171c7 130 "rules": {
131 "Artist": {
132 "fetch": [ {
133 "rel": "cds",
134 "quantity": "all"
8a1df391 135 } ]
136 }
137 }
138 }
139
140In this case all the cds of artists 1, 3 and all producer 5's artists will be
141dumped as well. Note that 'cds' is a has_many DBIx::Class relation from Artist
142to CD. This is eqivalent to:
143
144 {
9b7171c7 145 "sets": [
a5561f96 146 {
9b7171c7 147 "class": "Artist",
148 "ids": ["1", "3"],
149 "fetch": [ {
150 "rel": "cds",
151 "quantity": "all"
8a1df391 152 } ]
153 },
154 {
9b7171c7 155 "class": "Producer",
156 "ids": ["5"],
157 "fetch": [ {
158 "rel": "artists",
159 "quantity": "2",
160 "fetch": [ {
161 "rel": "cds",
162 "quantity": "all"
8a1df391 163 } ]
164 } ]
165 }
166 ]
167 }
a5561f96 168
169rules must be a hash keyed by class name.
170
95566320 171L</RULE ATTRIBUTES>
172
96f2cd20 173=head2 includes
174
8a1df391 175To prevent repetition between configs you can include other configs. For
176example:
96f2cd20 177
8a1df391 178 {
9b7171c7 179 "sets": [ {
180 "class": "Producer",
181 "ids": ["5"]
8a1df391 182 } ],
9b7171c7 183 "includes": [
184 { "file": "base.json" }
8a1df391 185 ]
186 }
96f2cd20 187
8a1df391 188Includes must be an arrayref of hashrefs where the hashrefs have key 'file'
189which is the name of another config file in the same directory. The original
190config is merged with its includes using L<Hash::Merge>.
96f2cd20 191
95566320 192=head2 datetime_relative
193
8a1df391 194Only available for MySQL and PostgreSQL at the moment, must be a value that
195DateTime::Format::* can parse. For example:
95566320 196
8a1df391 197 {
9b7171c7 198 "sets": [ {
199 "class": "RecentItems",
200 "ids": ["9"]
8a1df391 201 } ],
9b7171c7 202 "datetime_relative": "2007-10-30 00:00:00"
8a1df391 203 }
95566320 204
8a1df391 205This will work when dumping from a MySQL database and will cause any datetime
206fields (where datatype => 'datetime' in the column def of the schema class) to
207be dumped as a DateTime::Duration object relative to the date specified in the
208datetime_relative value. For example if the RecentItem object had a date field
209set to 2007-10-25, then when the fixture is imported the field will be set to 5
210days in the past relative to the current time.
95566320 211
a5561f96 212=head2 might_have
213
8a1df391 214Specifies whether to automatically dump might_have relationships. Should be a
215hash with one attribute - fetch. Set fetch to 1 or 0.
216
217 {
9b7171c7 218 "might_have": { "fetch": 1 },
219 "sets": [
8a1df391 220 {
9b7171c7 221 "class": "Artist",
222 "ids": ["1", "3"]
8a1df391 223 },
224 {
9b7171c7 225 "class": "Producer",
226 "ids": ["5"]
8a1df391 227 }
228 ]
229 }
230
231Note: belongs_to rels are automatically dumped whether you like it or not, this
232is to avoid FKs to nowhere when importing. General rules on has_many rels are
233not accepted at this top level, but you can turn them on for individual sets -
234see L</SET ATTRIBUTES>.
a5561f96 235
236=head1 SET ATTRIBUTES
237
238=head2 class
239
240Required attribute. Specifies the DBIx::Class object class you wish to dump.
241
242=head2 ids
243
8a1df391 244Array of primary key ids to fetch, basically causing an $rs->find($_) for each.
245If the id is not in the source db then it just won't get dumped, no warnings or
246death.
a5561f96 247
248=head2 quantity
249
8a1df391 250Must be either an integer or the string 'all'. Specifying an integer will
251effectively set the 'rows' attribute on the resultset clause, specifying 'all'
252will cause the rows attribute to be left off and for all matching rows to be
253dumped. There's no randomising here, it's just the first x rows.
a5561f96 254
255=head2 cond
256
8a1df391 257A hash specifying the conditions dumped objects must match. Essentially this is
258a JSON representation of a DBIx::Class search clause. For example:
a5561f96 259
8a1df391 260 {
9b7171c7 261 "sets": [{
262 "class": "Artist",
263 "quantiy": "all",
264 "cond": { "name": "Dave" }
8a1df391 265 }]
266 }
a5561f96 267
8a1df391 268This will dump all artists whose name is 'dave'. Essentially
269$artist_rs->search({ name => 'Dave' })->all.
a5561f96 270
271Sometimes in a search clause it's useful to use scalar refs to do things like:
272
8a1df391 273 $artist_rs->search({ no1_singles => \'> no1_albums' })
a5561f96 274
275This could be specified in the cond hash like so:
276
8a1df391 277 {
9b7171c7 278 "sets": [ {
279 "class": "Artist",
280 "quantiy": "all",
281 "cond": { "no1_singles": "\> no1_albums" }
8a1df391 282 } ]
283 }
a5561f96 284
8a1df391 285So if the value starts with a backslash the value is made a scalar ref before
286being passed to search.
a5561f96 287
288=head2 join
289
290An array of relationships to be used in the cond clause.
291
8a1df391 292 {
9b7171c7 293 "sets": [ {
294 "class": "Artist",
295 "quantiy": "all",
296 "cond": { "cds.position": { ">": 4 } },
297 "join": ["cds"]
8a1df391 298 } ]
299 }
a5561f96 300
301Fetch all artists who have cds with position greater than 4.
302
303=head2 fetch
304
305Must be an array of hashes. Specifies which rels to also dump. For example:
306
8a1df391 307 {
9b7171c7 308 "sets": [ {
309 "class": "Artist",
310 "ids": ["1", "3"],
311 "fetch": [ {
312 "rel": "cds",
313 "quantity": "3",
314 "cond": { "position": "2" }
8a1df391 315 } ]
316 } ]
317 }
a5561f96 318
319Will cause the cds of artists 1 and 3 to be dumped where the cd position is 2.
320
8a1df391 321Valid attributes are: 'rel', 'quantity', 'cond', 'has_many', 'might_have' and
322'join'. rel is the name of the DBIx::Class rel to follow, the rest are the same
323as in the set attributes. quantity is necessary for has_many relationships, but
324not if using for belongs_to or might_have relationships.
a5561f96 325
326=head2 has_many
327
8a1df391 328Specifies whether to fetch has_many rels for this set. Must be a hash
329containing keys fetch and quantity.
a5561f96 330
8a1df391 331Set fetch to 1 if you want to fetch them, and quantity to either 'all' or an
332integer.
a5561f96 333
95566320 334Be careful here, dumping has_many rels can lead to a lot of data being dumped.
335
a5561f96 336=head2 might_have
337
8a1df391 338As with has_many but for might_have relationships. Quantity doesn't do anything
339in this case.
a5561f96 340
8a1df391 341This value will be inherited by all fetches in this set. This is not true for
342the has_many attribute.
a5561f96 343
c040a9b0 344=head2 external
345
346In some cases your database information might be keys to values in some sort of
347external storage. The classic example is you are using L<DBIx::Class::InflateColumn::FS>
348to store blob information on the filesystem. In this case you may wish the ability
349to backup your external storage in the same way your database data. The L</external>
350attribute lets you specify a handler for this type of issue. For example:
351
352 {
353 "sets": [{
354 "class": "Photo",
355 "quantity": "all",
356 "external": {
357 "file": {
358 "class": "File",
359 "args": {"path":"__ATTR(photo_dir)__"}
360 }
361 }
362 }]
363 }
364
365This would use L<DBIx::Class::Fixtures::External::File> to read from a directory
366where the path to a file is specified by the C<file> field of the C<Photo> source.
367We use the uninflated value of the field so you need to completely handle backup
368and restore. For the common case we provide L<DBIx::Class::Fixtures::External::File>
369and you can create your own custom handlers by placing a '+' in the namespace:
370
371 "class": "+MyApp::Schema::SomeExternalStorage",
372
373Although if possible I'd love to get patches to add some of the other common
374types (I imagine storage in MogileFS, Redis, etc or even Amazon might be popular.)
375
376See L<DBIx::Class::Fixtures::External::File> for the external handler interface.
377
a5561f96 378=head1 RULE ATTRIBUTES
379
380=head2 cond
381
382Same as with L</SET ATTRIBUTES>
383
384=head2 fetch
385
386Same as with L</SET ATTRIBUTES>
387
388=head2 join
389
390Same as with L</SET ATTRIBUTES>
391
392=head2 has_many
393
394Same as with L</SET ATTRIBUTES>
395
396=head2 might_have
397
398Same as with L</SET ATTRIBUTES>
e5963c1b 399
c040a9b0 400=head1 RULE SUBSTITUTIONS
401
402You can provide the following substitution patterns for your rule values. An
403example of this might be:
404
405 {
406 "sets": [{
407 "class": "Photo",
408 "quantity": "__ENV(NUMBER_PHOTOS_DUMPED)__",
409 }]
410 }
411
412=head2 ENV
413
414Provide a value from %ENV
415
416=head2 ATTR
417
418Provide a value from L</config_attrs>
419
420=head2 catfile
421
422Create the path to a file from a list
423
72c8a36d 424=head2 catdir
c040a9b0 425
426Create the path to a directory from a list
427
0fc424b7 428=head1 METHODS
429
430=head2 new
e5963c1b 431
a5561f96 432=over 4
433
434=item Arguments: \%$attrs
435
436=item Return Value: $fixture_object
437
438=back
439
8a1df391 440Returns a new DBIx::Class::Fixture object. %attrs can have the following
441parameters:
442
443=over
444
445=item config_dir:
446
447required. must contain a valid path to the directory in which your .json
448configs reside.
449
450=item debug:
451
452determines whether to be verbose
453
454=item ignore_sql_errors:
1ac1b0d7 455
8a1df391 456ignore errors on import of DDL etc
1ac1b0d7 457
c040a9b0 458=item config_attrs
459
460A hash of information you can use to do replacements inside your configuration
461sets. For example, if your set looks like:
462
463 {
464 "sets": [ {
465 "class": "Artist",
466 "ids": ["1", "3"],
467 "fetch": [ {
468 "rel": "cds",
469 "quantity": "__ATTR(quantity)__",
470 } ]
471 } ]
472 }
473
474 my $fixtures = DBIx::Class::Fixtures->new( {
475 config_dir => '/home/me/app/fixture_configs'
476 config_attrs => {
477 quantity => 100,
478 },
479 });
480
481You may wish to do this if you want to let whoever runs the dumps have a bit
482more control
483
8a1df391 484=back
a5561f96 485
8a1df391 486 my $fixtures = DBIx::Class::Fixtures->new( {
487 config_dir => '/home/me/app/fixture_configs'
488 } );
a5561f96 489
0fc424b7 490=cut
e5963c1b 491
492sub new {
493 my $class = shift;
494
495 my ($params) = @_;
496 unless (ref $params eq 'HASH') {
497 return DBIx::Class::Exception->throw('first arg to DBIx::Class::Fixtures->new() must be hash ref');
498 }
499
500 unless ($params->{config_dir}) {
501 return DBIx::Class::Exception->throw('config_dir param not specified');
502 }
503
504 my $config_dir = dir($params->{config_dir});
505 unless (-e $params->{config_dir}) {
506 return DBIx::Class::Exception->throw('config_dir directory doesn\'t exist');
507 }
508
509 my $self = {
0fc424b7 510 config_dir => $config_dir,
14e0a204 511 _inherited_attributes => [qw/datetime_relative might_have rules belongs_to/],
da25ed7c 512 debug => $params->{debug} || 0,
0a54a6e8 513 ignore_sql_errors => $params->{ignore_sql_errors},
65a80d4e 514 dumped_objects => {},
c040a9b0 515 use_create => $params->{use_create} || 0,
30421f98 516 use_find_or_create => $params->{use_find_or_create} || 0,
c040a9b0 517 config_attrs => $params->{config_attrs} || {},
e5963c1b 518 };
519
520 bless $self, $class;
521
522 return $self;
523}
524
d9b65413 525=head2 available_config_sets
526
527Returns a list of all the config sets found in the L</config_dir>. These will
528be a list of the json based files containing dump rules.
529
530=cut
531
532my @config_sets;
533sub available_config_sets {
534 @config_sets = scalar(@config_sets) ? @config_sets : map {
535 $_->basename;
536 } grep {
537 -f $_ && $_=~/json$/;
538 } dir((shift)->config_dir)->children;
539}
540
0fc424b7 541=head2 dump
542
a5561f96 543=over 4
544
545=item Arguments: \%$attrs
546
547=item Return Value: 1
548
549=back
550
8a1df391 551 $fixtures->dump({
552 config => 'set_config.json', # config file to use. must be in the config
553 # directory specified in the constructor
554 schema => $source_dbic_schema,
555 directory => '/home/me/app/fixtures' # output directory
556 });
a5561f96 557
8a1df391 558or
2ef30e95 559
8a1df391 560 $fixtures->dump({
561 all => 1, # just dump everything that's in the schema
562 schema => $source_dbic_schema,
563 directory => '/home/me/app/fixtures' # output directory
564 });
2ef30e95 565
8a1df391 566In this case objects will be dumped to subdirectories in the specified
567directory. For example:
a5561f96 568
8a1df391 569 /home/me/app/fixtures/artist/1.fix
570 /home/me/app/fixtures/artist/3.fix
571 /home/me/app/fixtures/producer/5.fix
a5561f96 572
13ff7633 573schema and directory are required attributes. also, one of config or all must
574be specified.
575
576Lastly, the C<config> parameter can be a Perl HashRef instead of a file name.
577If this form is used your HashRef should conform to the structure rules defined
578for the JSON representations.
a5561f96 579
0fc424b7 580=cut
581
582sub dump {
583 my $self = shift;
584
585 my ($params) = @_;
586 unless (ref $params eq 'HASH') {
587 return DBIx::Class::Exception->throw('first arg to dump must be hash ref');
588 }
589
2ef30e95 590 foreach my $param (qw/schema directory/) {
0fc424b7 591 unless ($params->{$param}) {
592 return DBIx::Class::Exception->throw($param . ' param not specified');
593 }
594 }
595
47a8ceb9 596 if($params->{excludes} && !$params->{all}) {
597 return DBIx::Class::Exception->throw("'excludes' param only works when using the 'all' param");
598 }
599
2ef30e95 600 my $schema = $params->{schema};
2ef30e95 601 my $config;
602 if ($params->{config}) {
f4fe4f1f 603 $config = ref $params->{config} eq 'HASH' ?
13ff7633 604 $params->{config} :
605 do {
606 #read config
607 my $config_file = $self->config_dir->file($params->{config});
608 $self->load_config_file($config_file);
609 };
2ef30e95 610 } elsif ($params->{all}) {
47a8ceb9 611 my %excludes = map {$_=>1} @{$params->{excludes}||[]};
8a1df391 612 $config = {
613 might_have => { fetch => 0 },
614 has_many => { fetch => 0 },
615 belongs_to => { fetch => 0 },
47a8ceb9 616 sets => [
617 map {
618 { class => $_, quantity => 'all' };
619 } grep {
620 !$excludes{$_}
621 } $schema->sources],
8a1df391 622 };
2ef30e95 623 } else {
8a1df391 624 DBIx::Class::Exception->throw('must pass config or set all');
0fc424b7 625 }
626
627 my $output_dir = dir($params->{directory});
628 unless (-e $output_dir) {
d85d888e 629 $output_dir->mkpath ||
8a1df391 630 DBIx::Class::Exception->throw("output directory does not exist at $output_dir");
0fc424b7 631 }
632
9f96b203 633 $self->msg("generating fixtures");
917e1e25 634 my $tmp_output_dir = tempdir();
0fc424b7 635
6116de11 636 if (-e $tmp_output_dir) {
0fc424b7 637 $self->msg("- clearing existing $tmp_output_dir");
6116de11 638 $tmp_output_dir->rmtree;
0fc424b7 639 }
6116de11 640 $self->msg("- creating $tmp_output_dir");
641 $tmp_output_dir->mkpath;
0fc424b7 642
643 # write version file (for the potential benefit of populate)
8a1df391 644 $tmp_output_dir->file('_dumper_version')
645 ->openw
646 ->print($VERSION);
0fc424b7 647
c040a9b0 648 # write our current config set
649 $tmp_output_dir->file('_config_set')
650 ->openw
651 ->print( Dumper $config );
652
0fc424b7 653 $config->{rules} ||= {};
654 my @sources = sort { $a->{class} cmp $b->{class} } @{delete $config->{sets}};
8a1df391 655
06b7a1cc 656 while ( my ($k,$v) = each %{ $config->{rules} } ) {
28acb622 657 if ( my $source = eval { $schema->source($k) } ) {
658 $config->{rules}{$source->source_name} = $v;
06b7a1cc 659 }
660 }
661
0fc424b7 662 foreach my $source (@sources) {
663 # apply rule to set if specified
664 my $rule = $config->{rules}->{$source->{class}};
665 $source = merge( $source, $rule ) if ($rule);
666
667 # fetch objects
2ef30e95 668 my $rs = $schema->resultset($source->{class});
c40935c5 669
670 if ($source->{cond} and ref $source->{cond} eq 'HASH') {
0a54a6e8 671 # if value starts with \ assume it's meant to be passed as a scalar ref
672 # to dbic. ideally this would substitute deeply
673 $source->{cond} = {
674 map {
675 $_ => ($source->{cond}->{$_} =~ s/^\\//) ? \$source->{cond}->{$_}
676 : $source->{cond}->{$_}
677 } keys %{$source->{cond}}
678 };
c40935c5 679 }
680
0a54a6e8 681 $rs = $rs->search($source->{cond}, { join => $source->{join} })
682 if $source->{cond};
683
0fc424b7 684 $self->msg("- dumping $source->{class}");
0a54a6e8 685
0fc424b7 686 my %source_options = ( set => { %{$config}, %{$source} } );
687 if ($source->{quantity}) {
0a54a6e8 688 $rs = $rs->search({}, { order_by => $source->{order_by} })
689 if $source->{order_by};
690
8a1df391 691 if ($source->{quantity} =~ /^\d+$/) {
692 $rs = $rs->search({}, { rows => $source->{quantity} });
693 } elsif ($source->{quantity} ne 'all') {
0a54a6e8 694 DBIx::Class::Exception->throw("invalid value for quantity - $source->{quantity}");
0fc424b7 695 }
696 }
8a1df391 697 elsif ($source->{ids} && @{$source->{ids}}) {
0fc424b7 698 my @ids = @{$source->{ids}};
8a1df391 699 my (@pks) = $rs->result_source->primary_columns;
700 die "Can't dump multiple col-pks using 'id' option" if @pks > 1;
701 $rs = $rs->search_rs( { $pks[0] => { -in => \@ids } } );
0fc424b7 702 }
8a1df391 703 else {
0fc424b7 704 DBIx::Class::Exception->throw('must specify either quantity or ids');
705 }
706
8a1df391 707 $source_options{set_dir} = $tmp_output_dir;
708 $self->dump_rs($rs, \%source_options );
d3ef0865 709 }
710
da25ed7c 711 # clear existing output dir
712 foreach my $child ($output_dir->children) {
713 if ($child->is_dir) {
714 next if ($child eq $tmp_output_dir);
715 if (grep { $_ =~ /\.fix/ } $child->children) {
716 $child->rmtree;
717 }
718 } elsif ($child =~ /_dumper_version$/) {
719 $child->remove;
720 }
0fc424b7 721 }
722
723 $self->msg("- moving temp dir to $output_dir");
a0bae4fd 724 dircopy($tmp_output_dir, $output_dir);
8a1df391 725
0fc424b7 726 if (-e $output_dir) {
727 $self->msg("- clearing tmp dir $tmp_output_dir");
728 # delete existing fixture set
729 $tmp_output_dir->remove;
730 }
731
732 $self->msg("done");
733
734 return 1;
735}
736
8a1df391 737sub load_config_file {
738 my ($self, $config_file) = @_;
739 DBIx::Class::Exception->throw("config does not exist at $config_file")
740 unless -e $config_file;
741
742 my $config = Config::Any::JSON->load($config_file);
743
744 #process includes
745 if (my $incs = $config->{includes}) {
746 $self->msg($incs);
747 DBIx::Class::Exception->throw(
748 'includes params of config must be an array ref of hashrefs'
749 ) unless ref $incs eq 'ARRAY';
750
751 foreach my $include_config (@$incs) {
752 DBIx::Class::Exception->throw(
753 'includes params of config must be an array ref of hashrefs'
754 ) unless (ref $include_config eq 'HASH') && $include_config->{file};
755
756 my $include_file = $self->config_dir->file($include_config->{file});
757
758 DBIx::Class::Exception->throw("config does not exist at $include_file")
759 unless -e $include_file;
760
761 my $include = Config::Any::JSON->load($include_file);
762 $self->msg($include);
763 $config = merge( $config, $include );
764 }
765 delete $config->{includes};
766 }
767
768 # validate config
769 return DBIx::Class::Exception->throw('config has no sets')
770 unless $config && $config->{sets} &&
771 ref $config->{sets} eq 'ARRAY' && scalar @{$config->{sets}};
772
773 $config->{might_have} = { fetch => 0 } unless exists $config->{might_have};
774 $config->{has_many} = { fetch => 0 } unless exists $config->{has_many};
775 $config->{belongs_to} = { fetch => 1 } unless exists $config->{belongs_to};
776
777 return $config;
778}
779
780sub dump_rs {
781 my ($self, $rs, $params) = @_;
782
783 while (my $row = $rs->next) {
784 $self->dump_object($row, $params);
785 }
786}
787
0fc424b7 788sub dump_object {
8a1df391 789 my ($self, $object, $params) = @_;
0fc424b7 790 my $set = $params->{set};
c040a9b0 791
792 my $v = Data::Visitor::Callback->new(
793 plain_value => sub {
794 my ($visitor, $data) = @_;
795 my $subs = {
796 ENV => sub {
797 my ( $self, $v ) = @_;
798 if (! defined($ENV{$v})) {
799 return "";
800 } else {
801 return $ENV{ $v };
802 }
803 },
804 ATTR => sub {
805 my ($self, $v) = @_;
806 if(my $attr = $self->config_attrs->{$v}) {
807 return $attr;
808 } else {
809 return "";
810 }
811 },
812 catfile => sub {
813 my ($self, @args) = @_;
814 catfile(@args);
815 },
816 catdir => sub {
817 my ($self, @args) = @_;
818 catdir(@args);
819 },
820 };
821
822 my $subsre = join( '|', keys %$subs );
823 $_ =~ s{__($subsre)(?:\((.+?)\))?__}{ $subs->{ $1 }->( $self, $2 ? split( /,/, $2 ) : () ) }eg;
824
825 return $_;
826 }
827 );
828
829 $v->visit( $set );
830
0fc424b7 831 die 'no dir passed to dump_object' unless $params->{set_dir};
832 die 'no object passed to dump_object' unless $object;
833
834 my @inherited_attrs = @{$self->_inherited_attributes};
835
8a1df391 836 my @pk_vals = map {
837 $object->get_column($_)
838 } $object->primary_columns;
839
840 my $key = join("\0", @pk_vals);
841
842 my $src = $object->result_source;
843 my $exists = $self->dumped_objects->{$src->name}{$key}++;
844
845
0fc424b7 846 # write dir and gen filename
dcdf675f 847 my $source_dir = $params->{set_dir}->subdir($self->_name_for_source($src));
8a1df391 848 $source_dir->mkpath(0, 0777);
5f3da1e0 849
7a8790e2 850 # Convert characters not allowed on windows
851 my $file = io->catfile("$source_dir",
852 join('-', map { s|[/\\:\*\|\?"<>]|_|g; $_; } @pk_vals) . '.fix'
0a54a6e8 853 );
8a1df391 854
0fc424b7 855 # write file
0fc424b7 856 unless ($exists) {
857 $self->msg('-- dumping ' . $file->stringify, 2);
858 my %ds = $object->get_columns;
859
c040a9b0 860 if($set->{external}) {
861 foreach my $field (keys %{$set->{external}}) {
862 my $key = $ds{$field};
863 my ($plus, $class) = ( $set->{external}->{$field}->{class}=~/^(\+)*(.+)$/);
864 my $args = $set->{external}->{$field}->{args};
865
866 $class = "DBIx::Class::Fixtures::External::$class" unless $plus;
867 eval "use $class";
868
869 $ds{external}->{$field} =
870 encode_base64( $class
b3e8abba 871 ->backup($key => $args),'');
c040a9b0 872 }
873 }
874
0fc424b7 875 # mess with dates if specified
0566a82d 876 if ($set->{datetime_relative}) {
8a1df391 877 my $formatter= $object->result_source->schema->storage->datetime_parser;
0566a82d 878 unless ($@ || !$formatter) {
879 my $dt;
880 if ($set->{datetime_relative} eq 'today') {
881 $dt = DateTime->today;
882 } else {
883 $dt = $formatter->parse_datetime($set->{datetime_relative}) unless ($@);
884 }
0fc424b7 885
0566a82d 886 while (my ($col, $value) = each %ds) {
887 my $col_info = $object->result_source->column_info($col);
0fc424b7 888
0566a82d 889 next unless $value
890 && $col_info->{_inflate_info}
017d2ab4 891 && (
892 (uc($col_info->{data_type}) eq 'DATETIME')
893 or (uc($col_info->{data_type}) eq 'DATE')
894 or (uc($col_info->{data_type}) eq 'TIME')
895 or (uc($col_info->{data_type}) eq 'TIMESTAMP')
896 or (uc($col_info->{data_type}) eq 'INTERVAL')
897 );
0fc424b7 898
0566a82d 899 $ds{$col} = $object->get_inflated_column($col)->subtract_datetime($dt);
900 }
901 } else {
b099fee9 902 warn "datetime_relative not supported for this db driver at the moment";
0fc424b7 903 }
904 }
905
906 # do the actual dumping
907 my $serialized = Dump(\%ds)->Out();
8a1df391 908 $file->openw->print($serialized);
0fc424b7 909 }
910
2ef30e95 911 # don't bother looking at rels unless we are actually planning to dump at least one type
0a54a6e8 912 my ($might_have, $belongs_to, $has_many) = map {
06b7a1cc 913 $set->{$_}{fetch} || $set->{rules}{$src->source_name}{$_}{fetch}
0a54a6e8 914 } qw/might_have belongs_to has_many/;
915
916 return unless $might_have
917 || $belongs_to
918 || $has_many
8a1df391 919 || $set->{fetch};
2ef30e95 920
0fc424b7 921 # dump rels of object
0fc424b7 922 unless ($exists) {
8a1df391 923 foreach my $name (sort $src->relationships) {
924 my $info = $src->relationship_info($name);
925 my $r_source = $src->related_source($name);
0a54a6e8 926 # if belongs_to or might_have with might_have param set or has_many with
927 # has_many param set then
8a1df391 928 if (
0a54a6e8 929 ( $info->{attrs}{accessor} eq 'single' &&
930 (!$info->{attrs}{join_type} || $might_have)
931 )
932 || $info->{attrs}{accessor} eq 'filter'
933 ||
934 ($info->{attrs}{accessor} eq 'multi' && $has_many)
8a1df391 935 ) {
0fc424b7 936 my $related_rs = $object->related_resultset($name);
937 my $rule = $set->{rules}->{$related_rs->result_source->source_name};
938 # these parts of the rule only apply to has_many rels
939 if ($rule && $info->{attrs}{accessor} eq 'multi') {
0a54a6e8 940 $related_rs = $related_rs->search(
941 $rule->{cond},
942 { join => $rule->{join} }
943 ) if ($rule->{cond});
944
945 $related_rs = $related_rs->search(
946 {},
947 { rows => $rule->{quantity} }
948 ) if ($rule->{quantity} && $rule->{quantity} ne 'all');
949
950 $related_rs = $related_rs->search(
951 {},
952 { order_by => $rule->{order_by} }
953 ) if ($rule->{order_by});
954
0fc424b7 955 }
0a54a6e8 956 if ($set->{has_many}{quantity} &&
957 $set->{has_many}{quantity} =~ /^\d+$/) {
958 $related_rs = $related_rs->search(
959 {},
960 { rows => $set->{has_many}->{quantity} }
961 );
0fc424b7 962 }
0a54a6e8 963
0fc424b7 964 my %c_params = %{$params};
965 # inherit date param
0a54a6e8 966 my %mock_set = map {
967 $_ => $set->{$_}
968 } grep { $set->{$_} } @inherited_attrs;
969
0fc424b7 970 $c_params{set} = \%mock_set;
0a54a6e8 971 $c_params{set} = merge( $c_params{set}, $rule)
972 if $rule && $rule->{fetch};
973
8a1df391 974 $self->dump_rs($related_rs, \%c_params);
0fc424b7 975 }
976 }
977 }
978
979 return unless $set && $set->{fetch};
980 foreach my $fetch (@{$set->{fetch}}) {
981 # inherit date param
0a54a6e8 982 $fetch->{$_} = $set->{$_} foreach
983 grep { !$fetch->{$_} && $set->{$_} } @inherited_attrs;
0fc424b7 984 my $related_rs = $object->related_resultset($fetch->{rel});
985 my $rule = $set->{rules}->{$related_rs->result_source->source_name};
8a1df391 986
0fc424b7 987 if ($rule) {
988 my $info = $object->result_source->relationship_info($fetch->{rel});
989 if ($info->{attrs}{accessor} eq 'multi') {
990 $fetch = merge( $fetch, $rule );
991 } elsif ($rule->{fetch}) {
992 $fetch = merge( $fetch, { fetch => $rule->{fetch} } );
993 }
994 }
8a1df391 995
0a54a6e8 996 die "relationship $fetch->{rel} does not exist for " . $src->source_name
8a1df391 997 unless ($related_rs);
998
0fc424b7 999 if ($fetch->{cond} and ref $fetch->{cond} eq 'HASH') {
0a54a6e8 1000 # if value starts with \ assume it's meant to be passed as a scalar ref
1001 # to dbic. ideally this would substitute deeply
8a1df391 1002 $fetch->{cond} = { map {
1003 $_ => ($fetch->{cond}->{$_} =~ s/^\\//) ? \$fetch->{cond}->{$_}
1004 : $fetch->{cond}->{$_}
1005 } keys %{$fetch->{cond}} };
0fc424b7 1006 }
8a1df391 1007
0a54a6e8 1008 $related_rs = $related_rs->search(
1009 $fetch->{cond},
1010 { join => $fetch->{join} }
1011 ) if $fetch->{cond};
1012
1013 $related_rs = $related_rs->search(
1014 {},
1015 { rows => $fetch->{quantity} }
1016 ) if $fetch->{quantity} && $fetch->{quantity} ne 'all';
1017 $related_rs = $related_rs->search(
1018 {},
1019 { order_by => $fetch->{order_by} }
1020 ) if $fetch->{order_by};
8a1df391 1021
1022 $self->dump_rs($related_rs, { %{$params}, set => $fetch });
0fc424b7 1023 }
1024}
1025
384c3f0c 1026sub _generate_schema {
1027 my $self = shift;
1028 my $params = shift || {};
384c3f0c 1029 require DBI;
1030 $self->msg("\ncreating schema");
384c3f0c 1031
c06f7b96 1032 my $schema_class = $self->schema_class || "DBIx::Class::Fixtures::Schema";
9a9a7832 1033 eval "require $schema_class";
1034 die $@ if $@;
1035
4fb695f4 1036 my $pre_schema;
1037 my $connection_details = $params->{connection_details};
8a1df391 1038
aa9f3cc7 1039 $namespace_counter++;
8a1df391 1040
1041 my $namespace = "DBIx::Class::Fixtures::GeneratedSchema_$namespace_counter";
aa9f3cc7 1042 Class::C3::Componentised->inject_base( $namespace => $schema_class );
8a1df391 1043
aa9f3cc7 1044 $pre_schema = $namespace->connect(@{$connection_details});
1045 unless( $pre_schema ) {
384c3f0c 1046 return DBIx::Class::Exception->throw('connection details not valid');
1047 }
dcdf675f 1048 my @tables = map { $self->_name_for_source($pre_schema->source($_)) } $pre_schema->sources;
f81264b2 1049 $self->msg("Tables to drop: [". join(', ', sort @tables) . "]");
4fb695f4 1050 my $dbh = $pre_schema->storage->dbh;
384c3f0c 1051
1052 # clear existing db
1053 $self->msg("- clearing DB of existing tables");
7f25d8f8 1054 $pre_schema->storage->txn_do(sub {
1055 $pre_schema->storage->with_deferred_fk_checks(sub {
1056 foreach my $table (@tables) {
1057 eval {
1058 $dbh->do("drop table $table" . ($params->{cascade} ? ' cascade' : '') )
1059 };
1060 }
1061 });
9586eb0c 1062 });
384c3f0c 1063
1064 # import new ddl file to db
1065 my $ddl_file = $params->{ddl};
1066 $self->msg("- deploying schema using $ddl_file");
f81264b2 1067 my $data = _read_sql($ddl_file);
1068 foreach (@$data) {
1069 eval { $dbh->do($_) or warn "SQL was:\n $_"};
1ac1b0d7 1070 if ($@ && !$self->{ignore_sql_errors}) { die "SQL was:\n $_\n$@"; }
384c3f0c 1071 }
384c3f0c 1072 $self->msg("- finished importing DDL into DB");
1073
1074 # load schema object from our new DB
b4c67f96 1075 $namespace_counter++;
0a54a6e8 1076 my $namespace2 = "DBIx::Class::Fixtures::GeneratedSchema_$namespace_counter";
b4c67f96 1077 Class::C3::Componentised->inject_base( $namespace2 => $schema_class );
1078 my $schema = $namespace2->connect(@{$connection_details});
384c3f0c 1079 return $schema;
1080}
1081
f81264b2 1082sub _read_sql {
1083 my $ddl_file = shift;
1084 my $fh;
1085 open $fh, "<$ddl_file" or die ("Can't open DDL file, $ddl_file ($!)");
1086 my @data = split(/\n/, join('', <$fh>));
1087 @data = grep(!/^--/, @data);
1088 @data = split(/;/, join('', @data));
1089 close($fh);
1090 @data = grep { $_ && $_ !~ /^-- / } @data;
1091 return \@data;
1092}
a5561f96 1093
5cc47846 1094=head2 dump_config_sets
d9b65413 1095
1096Works just like L</dump> but instead of specifying a single json config set
5cc47846 1097located in L</config_dir> we dump each set named in the C<configs> parameter.
d9b65413 1098
1099The parameters are the same as for L</dump> except instead of a C<directory>
1100parameter we have a C<directory_template> which is a coderef expected to return
1101a scalar that is a root directory where we will do the actual dumping. This
1102coderef get three arguments: C<$self>, C<$params> and C<$set_name>. For
1103example:
1104
1105 $fixture->dump_all_config_sets({
1106 schema => $schema,
5cc47846 1107 configs => [qw/one.json other.json/],
d9b65413 1108 directory_template => sub {
1109 my ($fixture, $params, $set) = @_;
1110 return File::Spec->catdir('var', 'fixtures', $params->{schema}->version, $set);
1111 },
1112 });
1113
1114=cut
1115
5cc47846 1116sub dump_config_sets {
d9b65413 1117 my ($self, $params) = @_;
5cc47846 1118 my $available_config_sets = delete $params->{configs};
d9b65413 1119 my $directory_template = delete $params->{directory_template} ||
1120 DBIx::Class::Exception->throw("'directory_template is required parameter");
1121
5cc47846 1122 for my $set (@$available_config_sets) {
5cc47846 1123 my $localparams = $params;
1124 $localparams->{directory} = $directory_template->($self, $localparams, $set);
1125 $localparams->{config} = $set;
1126 $self->dump($localparams);
745efc60 1127 $self->dumped_objects({}); ## Clear dumped for next go, if there is one!
d9b65413 1128 }
1129}
1130
5cc47846 1131=head2 dump_all_config_sets
1132
745efc60 1133 my %local_params = %$params;
1134 my $local_self = bless { %$self }, ref($self);
1135 $local_params{directory} = $directory_template->($self, \%local_params, $set);
1136 $local_params{config} = $set;
1137 $self->dump(\%local_params);
1138
1139
5cc47846 1140Works just like L</dump> but instead of specifying a single json config set
1141located in L</config_dir> we dump each set in turn to the specified directory.
1142
1143The parameters are the same as for L</dump> except instead of a C<directory>
1144parameter we have a C<directory_template> which is a coderef expected to return
1145a scalar that is a root directory where we will do the actual dumping. This
1146coderef get three arguments: C<$self>, C<$params> and C<$set_name>. For
1147example:
1148
1149 $fixture->dump_all_config_sets({
1150 schema => $schema,
1151 directory_template => sub {
1152 my ($fixture, $params, $set) = @_;
1153 return File::Spec->catdir('var', 'fixtures', $params->{schema}->version, $set);
1154 },
1155 });
1156
1157=cut
1158
1159sub dump_all_config_sets {
1160 my ($self, $params) = @_;
1161 $self->dump_config_sets({
1162 %$params,
1163 configs=>[$self->available_config_sets],
1164 });
1165}
1166
a5561f96 1167=head2 populate
1168
1169=over 4
1170
1171=item Arguments: \%$attrs
1172
1173=item Return Value: 1
1174
1175=back
1176
8a1df391 1177 $fixtures->populate( {
1178 # directory to look for fixtures in, as specified to dump
1179 directory => '/home/me/app/fixtures',
1180
1181 # DDL to deploy
1182 ddl => '/home/me/app/sql/ddl.sql',
1183
1184 # database to clear, deploy and then populate
1185 connection_details => ['dbi:mysql:dbname=app_dev', 'me', 'password'],
1186
1187 # DDL to deploy after populating records, ie. FK constraints
1188 post_ddl => '/home/me/app/sql/post_ddl.sql',
1189
1190 # use CASCADE option when dropping tables
1191 cascade => 1,
1192
1193 # optional, set to 1 to run ddl but not populate
1194 no_populate => 0,
1195
30421f98 1196 # optional, set to 1 to run each fixture through ->create rather than have
65a80d4e 1197 # each $rs populated using $rs->populate. Useful if you have overridden new() logic
30421f98 1198 # that effects the value of column(s).
1199 use_create => 0,
1200
1201 # optional, same as use_create except with find_or_create.
1202 # Useful if you are populating a persistent data store.
1203 use_find_or_create => 0,
65a80d4e 1204
8a1df391 1205 # Dont try to clean the database, just populate over whats there. Requires
1206 # schema option. Use this if you want to handle removing old data yourself
1207 # no_deploy => 1
1208 # schema => $schema
1209 } );
a5561f96 1210
9e77162b 1211In this case the database app_dev will be cleared of all tables, then the
1212specified DDL deployed to it, then finally all fixtures found in
1213/home/me/app/fixtures will be added to it. populate will generate its own
1214DBIx::Class schema from the DDL rather than being passed one to use. This is
1215better as custom insert methods are avoided which can to get in the way. In
1216some cases you might not have a DDL, and so this method will eventually allow a
1217$schema object to be passed instead.
a5561f96 1218
9e77162b 1219If needed, you can specify a post_ddl attribute which is a DDL to be applied
1220after all the fixtures have been added to the database. A good use of this
1221option would be to add foreign key constraints since databases like Postgresql
1222cannot disable foreign key checks.
f81264b2 1223
9e77162b 1224If your tables have foreign key constraints you may want to use the cascade
1225attribute which will make the drop table functionality cascade, ie 'DROP TABLE
1226$table CASCADE'.
f81264b2 1227
9e77162b 1228C<directory> is a required attribute.
1229
1230If you wish for DBIx::Class::Fixtures to clear the database for you pass in
1231C<dll> (path to a DDL sql file) and C<connection_details> (array ref of DSN,
1232user and pass).
1233
1234If you wish to deal with cleaning the schema yourself, then pass in a C<schema>
1235attribute containing the connected schema you wish to operate on and set the
1236C<no_deploy> attribute.
a5561f96 1237
1238=cut
1239
384c3f0c 1240sub populate {
1241 my $self = shift;
1242 my ($params) = @_;
0a54a6e8 1243 DBIx::Class::Exception->throw('first arg to populate must be hash ref')
1244 unless ref $params eq 'HASH';
1245
1246 DBIx::Class::Exception->throw('directory param not specified')
1247 unless $params->{directory};
384c3f0c 1248
9a9a7832 1249 my $fixture_dir = dir(delete $params->{directory});
0a54a6e8 1250 DBIx::Class::Exception->throw("fixture directory '$fixture_dir' does not exist")
1251 unless -d $fixture_dir;
384c3f0c 1252
1253 my $ddl_file;
9e77162b 1254 my $dbh;
1255 my $schema;
384c3f0c 1256 if ($params->{ddl} && $params->{connection_details}) {
9a9a7832 1257 $ddl_file = file(delete $params->{ddl});
384c3f0c 1258 unless (-e $ddl_file) {
1259 return DBIx::Class::Exception->throw('DDL does not exist at ' . $ddl_file);
1260 }
1261 unless (ref $params->{connection_details} eq 'ARRAY') {
1262 return DBIx::Class::Exception->throw('connection details must be an arrayref');
1263 }
8a1df391 1264 $schema = $self->_generate_schema({
1265 ddl => $ddl_file,
1266 connection_details => delete $params->{connection_details},
1267 %{$params}
1268 });
9e77162b 1269 } elsif ($params->{schema} && $params->{no_deploy}) {
1270 $schema = $params->{schema};
384c3f0c 1271 } else {
0a54a6e8 1272 DBIx::Class::Exception->throw('you must set the ddl and connection_details params');
384c3f0c 1273 }
1274
3ad96388 1275
1276 return 1 if $params->{no_populate};
1277
4fb695f4 1278 $self->msg("\nimporting fixtures");
917e1e25 1279 my $tmp_fixture_dir = tempdir();
384c3f0c 1280 my $version_file = file($fixture_dir, '_dumper_version');
c040a9b0 1281 my $config_set_path = file($fixture_dir, '_config_set');
1282 my $config_set = -e $config_set_path ? do { my $VAR1; eval($config_set_path->slurp); $VAR1 } : '';
1283
1284 my $v = Data::Visitor::Callback->new(
1285 plain_value => sub {
1286 my ($visitor, $data) = @_;
1287 my $subs = {
1288 ENV => sub {
1289 my ( $self, $v ) = @_;
1290 if (! defined($ENV{$v})) {
1291 return "";
1292 } else {
1293 return $ENV{ $v };
1294 }
1295 },
1296 ATTR => sub {
1297 my ($self, $v) = @_;
1298 if(my $attr = $self->config_attrs->{$v}) {
1299 return $attr;
1300 } else {
1301 return "";
1302 }
1303 },
1304 catfile => sub {
1305 my ($self, @args) = @_;
1306 catfile(@args);
1307 },
1308 catdir => sub {
1309 my ($self, @args) = @_;
1310 catdir(@args);
1311 },
1312 };
1313
1314 my $subsre = join( '|', keys %$subs );
1315 $_ =~ s{__($subsre)(?:\((.+?)\))?__}{ $subs->{ $1 }->( $self, $2 ? split( /,/, $2 ) : () ) }eg;
1316
1317 return $_;
1318 }
1319 );
1320
1321 $v->visit( $config_set );
1322
1323
1324 my %sets_by_src;
1325 if($config_set) {
1326 %sets_by_src = map { delete($_->{class}) => $_ }
1327 @{$config_set->{sets}}
1328 }
1329
0a54a6e8 1330# DBIx::Class::Exception->throw('no version file found');
1331# unless -e $version_file;
384c3f0c 1332
1333 if (-e $tmp_fixture_dir) {
1334 $self->msg("- deleting existing temp directory $tmp_fixture_dir");
4fb695f4 1335 $tmp_fixture_dir->rmtree;
384c3f0c 1336 }
1337 $self->msg("- creating temp dir");
51794e1c 1338 $tmp_fixture_dir->mkpath();
dcdf675f 1339 for ( map { $self->_name_for_source($schema->source($_)) } $schema->sources) {
0a54a6e8 1340 my $from_dir = $fixture_dir->subdir($_);
1341 next unless -e $from_dir;
1342 dircopy($from_dir, $tmp_fixture_dir->subdir($_) );
1343 }
9e77162b 1344
1345 unless (-d $tmp_fixture_dir) {
0a54a6e8 1346 DBIx::Class::Exception->throw("Unable to create temporary fixtures dir: $tmp_fixture_dir: $!");
9e77162b 1347 }
384c3f0c 1348
384c3f0c 1349 my $fixup_visitor;
0a54a6e8 1350 my $formatter = $schema->storage->datetime_parser;
0566a82d 1351 unless ($@ || !$formatter) {
1352 my %callbacks;
1353 if ($params->{datetime_relative_to}) {
1354 $callbacks{'DateTime::Duration'} = sub {
1355 $params->{datetime_relative_to}->clone->add_duration($_);
1356 };
1357 } else {
1358 $callbacks{'DateTime::Duration'} = sub {
1359 $formatter->format_datetime(DateTime->today->add_duration($_))
1360 };
1361 }
1362 $callbacks{object} ||= "visit_ref";
1363 $fixup_visitor = new Data::Visitor::Callback(%callbacks);
384c3f0c 1364 }
1ac1b0d7 1365
7f25d8f8 1366 $schema->storage->txn_do(sub {
1367 $schema->storage->with_deferred_fk_checks(sub {
1368 foreach my $source (sort $schema->sources) {
1369 $self->msg("- adding " . $source);
1370 my $rs = $schema->resultset($source);
dcdf675f 1371 my $source_dir = $tmp_fixture_dir->subdir( $self->_name_for_source($rs->result_source) );
7f25d8f8 1372 next unless (-e $source_dir);
1373 my @rows;
1374 while (my $file = $source_dir->next) {
1375 next unless ($file =~ /\.fix$/);
1376 next if $file->is_dir;
1377 my $contents = $file->slurp;
1378 my $HASH1;
1379 eval($contents);
1380 $HASH1 = $fixup_visitor->visit($HASH1) if $fixup_visitor;
c040a9b0 1381 if(my $external = delete $HASH1->{external}) {
1382 my @fields = keys %{$sets_by_src{$source}->{external}};
1383 foreach my $field(@fields) {
1384 my $key = $HASH1->{$field};
1385 my $content = decode_base64 ($external->{$field});
1386 my $args = $sets_by_src{$source}->{external}->{$field}->{args};
1387 my ($plus, $class) = ( $sets_by_src{$source}->{external}->{$field}->{class}=~/^(\+)*(.+)$/);
1388 $class = "DBIx::Class::Fixtures::External::$class" unless $plus;
1389 eval "use $class";
1390 $class->restore($key, $content, $args);
1391 }
1392 }
65a80d4e 1393 if ( $params->{use_create} ) {
1394 $rs->create( $HASH1 );
30421f98 1395 } elsif( $params->{use_find_or_create} ) {
1396 $rs->find_or_create( $HASH1 );
65a80d4e 1397 } else {
1398 push(@rows, $HASH1);
1399 }
7f25d8f8 1400 }
1401 $rs->populate(\@rows) if scalar(@rows);
75d9325a 1402
1403 ## Now we need to do some db specific cleanup
1404 ## this probably belongs in a more isolated space. Right now this is
1405 ## to just handle postgresql SERIAL types that use Sequences
1406
1407 my $table = $rs->result_source->name;
1408 for my $column(my @columns = $rs->result_source->columns) {
1409 my $info = $rs->result_source->column_info($column);
1410 if(my $sequence = $info->{sequence}) {
1411 $self->msg("- updating sequence $sequence");
1412 $rs->result_source->storage->dbh_do(sub {
1413 my ($storage, $dbh, @cols) = @_;
1414 $self->msg(my $sql = "SELECT setval('${sequence}', (SELECT max($column) FROM ${table}));");
1415 my $sth = $dbh->prepare($sql);
1416 my $rv = $sth->execute or die $sth->errstr;
1417 $self->msg("- $sql");
1418 });
1419 }
1420 }
1421
1ac1b0d7 1422 }
7f25d8f8 1423 });
1ac1b0d7 1424 });
8a1df391 1425 $self->do_post_ddl( {
75d9325a 1426 schema=>$schema,
8a1df391 1427 post_ddl=>$params->{post_ddl}
1428 } ) if $params->{post_ddl};
f81264b2 1429
384c3f0c 1430 $self->msg("- fixtures imported");
1431 $self->msg("- cleaning up");
1432 $tmp_fixture_dir->rmtree;
b099fee9 1433 return 1;
384c3f0c 1434}
1435
6a05e381 1436sub do_post_ddl {
1437 my ($self, $params) = @_;
1438
1439 my $schema = $params->{schema};
1440 my $data = _read_sql($params->{post_ddl});
1441 foreach (@$data) {
1442 eval { $schema->storage->dbh->do($_) or warn "SQL was:\n $_"};
1ac1b0d7 1443 if ($@ && !$self->{ignore_sql_errors}) { die "SQL was:\n $_\n$@"; }
6a05e381 1444 }
1445 $self->msg("- finished importing post-populate DDL into DB");
1446}
1447
0fc424b7 1448sub msg {
1449 my $self = shift;
1450 my $subject = shift || return;
9a9a7832 1451 my $level = shift || 1;
9a9a7832 1452 return unless $self->debug >= $level;
0fc424b7 1453 if (ref $subject) {
1454 print Dumper($subject);
1455 } else {
1456 print $subject . "\n";
1457 }
1458}
a5561f96 1459
dcdf675f 1460# Helper method for ensuring that the name used for a given source
1461# is always the same (This is used to name the fixture directories
1462# for example)
1463
1464sub _name_for_source {
1465 my ($self, $source) = @_;
1466
1467 return ref $source->name ? $source->source_name : $source->name;
1468}
1469
a5561f96 1470=head1 AUTHOR
1471
1472 Luke Saunders <luke@shadowcatsystems.co.uk>
1473
3b4f6e76 1474 Initial development sponsored by and (c) Takkle, Inc. 2007
1475
a5561f96 1476=head1 CONTRIBUTORS
1477
1478 Ash Berlin <ash@shadowcatsystems.co.uk>
8a1df391 1479
a5561f96 1480 Matt S. Trout <mst@shadowcatsystems.co.uk>
8a1df391 1481
fc17c598 1482 Drew Taylor <taylor.andrew.j@gmail.com>
a5561f96 1483
9b7171c7 1484 Frank Switalski <fswitalski@gmail.com>
1485
bb6d61a7 1486 Chris Akins <chris.hexx@gmail.com>
1487
3b4f6e76 1488=head1 LICENSE
1489
1490 This library is free software under the same license as perl itself
1491
a5561f96 1492=cut
1493
e5963c1b 14941;