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