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