1 package DBIx::Class::Fixtures;
6 use DBIx::Class::Exception;
8 use Path::Class qw(dir file);
10 use Config::Any::JSON;
11 use Data::Dump::Streamer;
12 use Data::Visitor::Callback;
14 use File::Copy::Recursive qw/dircopy/;
15 use File::Copy qw/move/;
16 use Hash::Merge qw( merge );
19 use base qw(Class::Accessor);
22 'mysql' => 'DateTime::Format::MySQL',
23 'pg' => 'DateTime::Format::Pg',
26 __PACKAGE__->mk_accessors(qw(config_dir _inherited_attributes debug schema_class ));
34 our $VERSION = '1.000';
42 use DBIx::Class::Fixtures;
46 my $fixtures = DBIx::Class::Fixtures->new({ config_dir => '/home/me/app/fixture_configs' });
49 config => 'set_config.json',
50 schema => $source_dbic_schema,
51 directory => '/home/me/app/fixtures'
55 directory => '/home/me/app/fixtures',
56 ddl => '/home/me/app/sql/ddl.sql',
57 connection_details => ['dbi:mysql:dbname=app_dev', 'me', 'password']
64 Luke Saunders <luke@shadowcatsystems.co.uk>
78 unless (ref $params eq 'HASH') {
79 return DBIx::Class::Exception->throw('first arg to DBIx::Class::Fixtures->new() must be hash ref');
82 unless ($params->{config_dir}) {
83 return DBIx::Class::Exception->throw('config_dir param not specified');
86 my $config_dir = dir($params->{config_dir});
87 unless (-e $params->{config_dir}) {
88 return DBIx::Class::Exception->throw('config_dir directory doesn\'t exist');
92 config_dir => $config_dir,
93 _inherited_attributes => [qw/datetime_relative might_have rules/],
94 debug => $params->{debug}
110 unless (ref $params eq 'HASH') {
111 return DBIx::Class::Exception->throw('first arg to dump must be hash ref');
114 foreach my $param (qw/config schema directory/) {
115 unless ($params->{$param}) {
116 return DBIx::Class::Exception->throw($param . ' param not specified');
120 my $config_file = file($self->config_dir, $params->{config});
121 unless (-e $config_file) {
122 return DBIx::Class::Exception->throw('config does not exist at ' . $config_file);
125 my $config = Config::Any::JSON->load($config_file);
126 unless ($config && $config->{sets} && ref $config->{sets} eq 'ARRAY' && scalar(@{$config->{sets}})) {
127 return DBIx::Class::Exception->throw('config has no sets');
130 my $output_dir = dir($params->{directory});
131 unless (-e $output_dir) {
132 return DBIx::Class::Exception->throw('output directory does not exist at ' . $output_dir);
135 my $schema = $params->{schema};
137 $self->msg("generating fixtures");
138 my $tmp_output_dir = dir($output_dir, '-~dump~-');
140 if (-e $tmp_output_dir) {
141 $self->msg("- clearing existing $tmp_output_dir");
142 $tmp_output_dir->rmtree;
144 $self->msg("- creating $tmp_output_dir");
145 $tmp_output_dir->mkpath;
147 # write version file (for the potential benefit of populate)
148 my $version_file = file($tmp_output_dir, '_dumper_version');
149 write_file($version_file->stringify, $VERSION);
151 $config->{rules} ||= {};
152 my @sources = sort { $a->{class} cmp $b->{class} } @{delete $config->{sets}};
153 my %options = ( is_root => 1 );
154 foreach my $source (@sources) {
155 # apply rule to set if specified
156 my $rule = $config->{rules}->{$source->{class}};
157 $source = merge( $source, $rule ) if ($rule);
160 my $rs = $schema->resultset($source->{class});
161 $rs = $rs->search($source->{cond}, { join => $source->{join} }) if ($source->{cond});
162 $self->msg("- dumping $source->{class}");
164 my %source_options = ( set => { %{$config}, %{$source} } );
165 if ($source->{quantity}) {
166 $rs = $rs->search({}, { order_by => $source->{order_by} }) if ($source->{order_by});
167 if ($source->{quantity} eq 'all') {
168 push (@objects, $rs->all);
169 } elsif ($source->{quantity} =~ /^\d+$/) {
170 push (@objects, $rs->search({}, { rows => $source->{quantity} }));
172 DBIx::Class::Exception->throw('invalid value for quantity - ' . $source->{quantity});
175 if ($source->{ids}) {
176 my @ids = @{$source->{ids}};
177 my @id_objects = grep { $_ } map { $rs->find($_) } @ids;
178 push (@objects, @id_objects);
180 unless ($source->{quantity} || $source->{ids}) {
181 DBIx::Class::Exception->throw('must specify either quantity or ids');
185 foreach my $object (@objects) {
186 $source_options{set_dir} = $tmp_output_dir;
187 $self->dump_object($object, { %options, %source_options } );
192 foreach my $dir ($output_dir->children) {
193 next if ($dir eq $tmp_output_dir);
194 $dir->remove || $dir->rmtree;
197 $self->msg("- moving temp dir to $output_dir");
198 move($_, dir($output_dir, $_->relative($_->parent)->stringify)) for $tmp_output_dir->children;
199 if (-e $output_dir) {
200 $self->msg("- clearing tmp dir $tmp_output_dir");
201 # delete existing fixture set
202 $tmp_output_dir->remove;
211 my ($self, $object, $params, $rr_info) = @_;
212 my $set = $params->{set};
213 die 'no dir passed to dump_object' unless $params->{set_dir};
214 die 'no object passed to dump_object' unless $object;
216 my @inherited_attrs = @{$self->_inherited_attributes};
218 # write dir and gen filename
219 my $source_dir = dir($params->{set_dir}, lc($object->result_source->from));
220 mkdir($source_dir->stringify, 0777);
221 my $file = file($source_dir, join('-', map { $object->get_column($_) } sort $object->primary_columns) . '.fix');
224 my $exists = (-e $file->stringify) ? 1 : 0;
226 $self->msg('-- dumping ' . $file->stringify, 2);
227 my %ds = $object->get_columns;
229 my $driver = $object->result_source->schema->storage->dbh->{Driver}->{Name};
230 my $formatter= $db_to_parser{$driver};
231 eval "require $formatter" if ($formatter);
233 # mess with dates if specified
234 if ($set->{datetime_relative}) {
235 unless ($@ || !$formatter) {
237 if ($set->{datetime_relative} eq 'today') {
238 $dt = DateTime->today;
240 $dt = $formatter->parse_datetime($set->{datetime_relative}) unless ($@);
243 while (my ($col, $value) = each %ds) {
244 my $col_info = $object->result_source->column_info($col);
247 && $col_info->{_inflate_info}
248 && uc($col_info->{data_type}) eq 'DATETIME';
250 $ds{$col} = $object->get_inflated_column($col)->subtract_datetime($dt);
253 warn "datetime_relative not supported for $driver at the moment";
257 # do the actual dumping
258 my $serialized = Dump(\%ds)->Out();
259 write_file($file->stringify, $serialized);
260 my $mode = 0777; chmod $mode, $file->stringify;
263 # dump rels of object
264 my $s = $object->result_source;
266 foreach my $name (sort $s->relationships) {
267 my $info = $s->relationship_info($name);
268 my $r_source = $s->related_source($name);
269 # if belongs_to or might_have with might_have param set or has_many with has_many param set then
270 if (($info->{attrs}{accessor} eq 'single' && (!$info->{attrs}{join_type} || ($set->{might_have} && $set->{might_have}->{fetch}))) || $info->{attrs}{accessor} eq 'filter' || ($info->{attrs}{accessor} eq 'multi' && ($set->{has_many} && $set->{has_many}->{fetch}))) {
271 my $related_rs = $object->related_resultset($name);
272 my $rule = $set->{rules}->{$related_rs->result_source->source_name};
273 # these parts of the rule only apply to has_many rels
274 if ($rule && $info->{attrs}{accessor} eq 'multi') {
275 $related_rs = $related_rs->search($rule->{cond}, { join => $rule->{join} }) if ($rule->{cond});
276 $related_rs = $related_rs->search({}, { rows => $rule->{quantity} }) if ($rule->{quantity} && $rule->{quantity} ne 'all');
277 $related_rs = $related_rs->search({}, { order_by => $rule->{order_by} }) if ($rule->{order_by});
279 if ($set->{has_many}->{quantity} && $set->{has_many}->{quantity} =~ /^\d+$/) {
280 $related_rs = $related_rs->search({}, { rows => $set->{has_many}->{quantity} });
282 my %c_params = %{$params};
284 my %mock_set = map { $_ => $set->{$_} } grep { $set->{$_} } @inherited_attrs;
285 $c_params{set} = \%mock_set;
286 # use Data::Dumper; print ' -- ' . Dumper($c_params{set}, $rule->{fetch}) if ($rule && $rule->{fetch});
287 $c_params{set} = merge( $c_params{set}, $rule) if ($rule && $rule->{fetch});
288 # use Data::Dumper; print ' -- ' . Dumper(\%c_params) if ($rule && $rule->{fetch});
289 $self->dump_object($_, \%c_params) foreach $related_rs->all;
294 return unless $set && $set->{fetch};
295 foreach my $fetch (@{$set->{fetch}}) {
297 $fetch->{$_} = $set->{$_} foreach grep { !$fetch->{$_} && $set->{$_} } @inherited_attrs;
298 my $related_rs = $object->related_resultset($fetch->{rel});
299 my $rule = $set->{rules}->{$related_rs->result_source->source_name};
301 my $info = $object->result_source->relationship_info($fetch->{rel});
302 if ($info->{attrs}{accessor} eq 'multi') {
303 $fetch = merge( $fetch, $rule );
304 } elsif ($rule->{fetch}) {
305 $fetch = merge( $fetch, { fetch => $rule->{fetch} } );
308 die "relationship " . $fetch->{rel} . " does not exist for " . $s->source_name unless ($related_rs);
309 if ($fetch->{cond} and ref $fetch->{cond} eq 'HASH') {
310 # if value starts with / assume it's meant to be passed as a scalar ref to dbic
311 # ideally this would substitute deeply
312 $fetch->{cond} = { map { $_ => ($fetch->{cond}->{$_} =~ s/^\\//) ? \$fetch->{cond}->{$_} : $fetch->{cond}->{$_} } keys %{$fetch->{cond}} };
314 $related_rs = $related_rs->search($fetch->{cond}, { join => $fetch->{join} }) if ($fetch->{cond});
315 $related_rs = $related_rs->search({}, { rows => $fetch->{quantity} }) if ($fetch->{quantity} && $fetch->{quantity} ne 'all');
316 $related_rs = $related_rs->search({}, { order_by => $fetch->{order_by} }) if ($fetch->{order_by});
317 $self->dump_object($_, { %{$params}, set => $fetch }) foreach $related_rs->all;
321 sub _generate_schema {
323 my $params = shift || {};
325 $self->msg("\ncreating schema");
326 # die 'must pass version param to generate_schema_from_ddl' unless $params->{version};
328 my $schema_class = $self->schema_class || "DBIx::Class::Fixtures::Schema";
329 eval "require $schema_class";
333 my $connection_details = $params->{connection_details};
334 unless( $pre_schema = $schema_class->connect(@{$connection_details}) ) {
335 return DBIx::Class::Exception->throw('connection details not valid');
337 my @tables = map { $pre_schema->source($_)->from }$pre_schema->sources;
338 my $dbh = $pre_schema->storage->dbh;
341 $self->msg("- clearing DB of existing tables");
342 eval { $dbh->do('SET foreign_key_checks=0') };
343 $dbh->do('drop table ' . $_) for (@tables);
345 # import new ddl file to db
346 my $ddl_file = $params->{ddl};
347 $self->msg("- deploying schema using $ddl_file");
349 open $fh, "<$ddl_file" or die ("Can't open DDL file, $ddl_file ($!)");
350 my @data = split(/\n/, join('', <$fh>));
351 @data = grep(!/^--/, @data);
352 @data = split(/;/, join('', @data));
354 @data = grep { $_ && $_ !~ /^-- / } @data;
356 eval { $dbh->do($_) or warn "SQL was:\n $_"};
357 if ($@) { die "SQL was:\n $_\n$@"; }
359 $self->msg("- finished importing DDL into DB");
361 # load schema object from our new DB
362 $self->msg("- loading fresh DBIC object from DB");
363 my $schema = $schema_class->connect(@{$connection_details});
370 unless (ref $params eq 'HASH') {
371 return DBIx::Class::Exception->throw('first arg to populate must be hash ref');
374 foreach my $param (qw/directory/) {
375 unless ($params->{$param}) {
376 return DBIx::Class::Exception->throw($param . ' param not specified');
379 my $fixture_dir = dir(delete $params->{directory});
380 unless (-e $fixture_dir) {
381 return DBIx::Class::Exception->throw('fixture directory does not exist at ' . $fixture_dir);
386 if ($params->{ddl} && $params->{connection_details}) {
387 $ddl_file = file(delete $params->{ddl});
388 unless (-e $ddl_file) {
389 return DBIx::Class::Exception->throw('DDL does not exist at ' . $ddl_file);
391 unless (ref $params->{connection_details} eq 'ARRAY') {
392 return DBIx::Class::Exception->throw('connection details must be an arrayref');
394 } elsif ($params->{schema}) {
395 return DBIx::Class::Exception->throw('passing a schema is not supported at the moment');
397 return DBIx::Class::Exception->throw('you must set the ddl and connection_details params');
400 my $schema = $self->_generate_schema({ ddl => $ddl_file, connection_details => delete $params->{connection_details}, %{$params} });
401 $self->msg("\nimporting fixtures");
402 my $tmp_fixture_dir = dir($fixture_dir, "-~populate~-" . $<);
404 my $version_file = file($fixture_dir, '_dumper_version');
405 unless (-e $version_file) {
406 # return DBIx::Class::Exception->throw('no version file found');
409 if (-e $tmp_fixture_dir) {
410 $self->msg("- deleting existing temp directory $tmp_fixture_dir");
411 $tmp_fixture_dir->rmtree;
413 $self->msg("- creating temp dir");
414 dircopy(dir($fixture_dir, $schema->source($_)->from), dir($tmp_fixture_dir, $schema->source($_)->from)) for $schema->sources;
416 eval { $schema->storage->dbh->do('SET foreign_key_checks=0') };
419 my $driver = $schema->storage->dbh->{Driver}->{Name};
420 my $formatter= $db_to_parser{$driver};
421 eval "require $formatter" if ($formatter);
422 unless ($@ || !$formatter) {
424 if ($params->{datetime_relative_to}) {
425 $callbacks{'DateTime::Duration'} = sub {
426 $params->{datetime_relative_to}->clone->add_duration($_);
429 $callbacks{'DateTime::Duration'} = sub {
430 $formatter->format_datetime(DateTime->today->add_duration($_))
433 $callbacks{object} ||= "visit_ref";
434 $fixup_visitor = new Data::Visitor::Callback(%callbacks);
436 foreach my $source (sort $schema->sources) {
437 $self->msg("- adding " . $source);
438 my $rs = $schema->resultset($source);
439 my $source_dir = dir($tmp_fixture_dir, lc($rs->result_source->from));
440 next unless (-e $source_dir);
441 while (my $file = $source_dir->next) {
442 next unless ($file =~ /\.fix$/);
443 next if $file->is_dir;
444 my $contents = $file->slurp;
447 $HASH1 = $fixup_visitor->visit($HASH1) if $fixup_visitor;
452 $self->msg("- fixtures imported");
453 $self->msg("- cleaning up");
454 $tmp_fixture_dir->rmtree;
455 eval { $schema->storage->dbh->do('SET foreign_key_checks=1') };
460 my $subject = shift || return;
461 my $level = shift || 1;
463 return unless $self->debug >= $level;
465 print Dumper($subject);
467 print $subject . "\n";