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