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