Move script-test
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Admin.pm
1 #
2 #===============================================================================
3 #
4 #         FILE:  Admin.pm
5 #
6 #  DESCRIPTION:  Administrative functions for DBIx::Class Schemata
7 #
8 #        FILES:  ---
9 #         BUGS:  ---
10 #        NOTES:  ---
11 #       AUTHOR:  Gordon Irving (), <Gordon.irving@sophos.com>
12 #      VERSION:  1.0
13 #      CREATED:  28/11/09 12:27:15 GMT
14 #     REVISION:  ---
15 #===============================================================================
16
17 package DBIx::Class::Admin;
18
19 use Moose;
20 use MooseX::Types -declare => [qw( DBICConnectInfo )];
21 use MooseX::Types::Moose qw/Int HashRef ArrayRef Str Any/;
22 use MooseX::Types::JSON qw(JSON);
23 use MooseX::Types::Path::Class qw(Dir File);
24 use Try::Tiny;
25
26 use parent 'Class::C3::Componentised';
27
28 use JSON::Any;
29
30 use namespace::autoclean;
31
32 my @_deps = qw(Moose MooseX::Types MooseX::Types::JSON MooseX::Types::Path::Class Try::Tiny parent JSON::Any Class::C3::Componentised namespace::autoclean);
33
34 coerce ArrayRef,
35   from JSON,
36   via { _json_to_data ($_) };
37
38 coerce HashRef,
39   from JSON,
40   via { _json_to_data($_) };
41
42 subtype DBICConnectInfo,
43   as ArrayRef;
44
45 coerce DBICConnectInfo,
46   from JSON,
47    via { return _json_to_data($_) } ;
48
49 coerce DBICConnectInfo,
50   from Str,
51     via { return _json_to_data($_) };
52
53 coerce DBICConnectInfo,
54   from HashRef,
55    via { [ $_->{dsn}, $_->{user}, $_->{password} ]  };
56
57
58 =head1 NAME
59
60 DBIx::Class::Admin - Administration object for schemas
61
62 =head1 SYNOPSIS
63
64   use DBIx::Class::Admin;
65
66   # ddl manipulation
67   my $admin = DBIx::Class::Admin->new(
68     schema_class=> 'MY::Schema',
69     sql_dir=> $sql_dir,
70     connect_info => { dsn => $dsn, user => $user, password => $pass },
71   );
72
73   # create SQLite sql
74   $admin->create('SQLite');
75
76   # create SQL diff for an upgrade
77   $admin->create('SQLite', {} , "1.0");
78
79   # upgrade a database
80   $admin->upgrade();
81
82   # install a version for an unversioned schema
83   $admin->install("3.0");
84
85 =head1 Attributes
86
87 =head2 lib
88
89 add a library search path
90
91 =cut
92
93 has lib => (
94   is    => 'ro',
95   isa    => Dir,
96   coerce  => 1,
97   trigger => \&_set_inc,
98 );
99
100 sub _set_inc {
101   my ($self, $lib) = @_;
102   push @INC, $lib->stringify;
103 }
104
105
106 =head2 schema_class
107
108 the class of the schema to load
109
110 =cut
111
112 has 'schema_class' => (
113   is    => 'ro',
114   isa    => 'Str',
115   coerce  => 1,
116 );
117
118
119 =head2 schema
120
121 A pre-connected schema object can be provided for manipulation
122
123 =cut
124
125 has 'schema' => (
126   is      => 'ro',
127   isa      => 'DBIx::Class::Schema',
128   lazy_build  => 1,
129 );
130
131 sub _build_schema {
132   my ($self)  = @_;
133   $self->ensure_class_loaded($self->schema_class);
134
135   $self->connect_info->[3]->{ignore_version} =1;
136   return $self->schema_class->connect(@{$self->connect_info()} ); # ,  $self->connect_info->[3], { ignore_version => 1} );
137 }
138
139
140 =head2 resultset
141
142 a resultset from the schema to operate on
143
144 =cut
145
146 has 'resultset' => (
147   is      => 'rw',
148   isa      => Str,
149 );
150
151
152 =head2 where
153
154 a hash ref or json string to be used for identifying data to manipulate
155
156 =cut
157
158 has 'where' => (
159   is      => 'rw',
160   isa      => HashRef,
161   coerce    => 1,
162 );
163
164
165 =head2 set
166
167 a hash ref or json string to be used for inserting or updating data
168
169 =cut
170
171 has 'set' => (
172   is      => 'rw',
173   isa      => HashRef,
174   coerce    => 1,
175 );
176
177
178 =head2 attrs
179
180 a hash ref or json string to be used for passing additonal info to the ->search call
181
182 =cut
183
184 has 'attrs' => (
185   is       => 'rw',
186   isa      => HashRef,
187   coerce    => 1,
188 );
189
190
191 =head2 connect_info
192
193 connect_info the arguments to provide to the connect call of the schema_class
194
195 =cut
196
197 has 'connect_info' => (
198   is      => 'ro',
199   isa      => DBICConnectInfo,
200   lazy_build  => 1,
201   coerce    => 1,
202 );
203
204 sub _build_connect_info {
205   my ($self) = @_;
206   return $self->_find_stanza($self->config, $self->config_stanza);
207 }
208
209
210 =head2 config_file
211
212 config_file provide a config_file to read connect_info from, if this is provided
213 config_stanze should also be provided to locate where the connect_info is in the config
214 The config file should be in a format readable by Config::General
215
216 =cut
217
218 has config_file => (
219   is      => 'ro',
220   isa      => File,
221   coerce    => 1,
222 );
223
224
225 =head2 config_stanza
226
227 config_stanza for use with config_file should be a '::' deliminated 'path' to the connection information
228 designed for use with catalyst config files
229
230 =cut
231
232 has 'config_stanza' => (
233   is      => 'ro',
234   isa      => 'Str',
235 );
236
237
238 =head2 config
239
240 Instead of loading from a file the configuration can be provided directly as a hash ref.  Please note 
241 config_stanza will still be required.
242
243 =cut
244
245 has config => (
246   is      => 'ro',
247   isa      => HashRef,
248   lazy_build  => 1,
249 );
250
251 sub _build_config {
252   my ($self) = @_;
253   try { require Config::Any } catch { die "Config::Any is required to parse the config file"; };
254
255   my $cfg = Config::Any->load_files ( {files => [$self->config_file], use_ext =>1, flatten_to_hash=>1});
256
257   # just grab the config from the config file
258   $cfg = $cfg->{$self->config_file};
259   return $cfg;
260 }
261
262
263 =head2 sql_dir
264
265 The location where sql ddl files should be created or found for an upgrade.
266
267 =cut
268
269 has 'sql_dir' => (
270   is      => 'ro',
271   isa      => Dir,
272   coerce    => 1,
273 );
274
275
276 =head2 version
277
278 Used for install, the version which will be 'installed' in the schema
279
280 =cut
281
282 has version => (
283   is      => 'rw',
284   isa      => 'Str',
285 );
286
287
288 =head2 preversion
289
290 Previouse version of the schema to create an upgrade diff for, the full sql for that version of the sql must be in the sql_dir
291
292 =cut
293
294 has preversion => (
295   is      => 'rw',
296   isa      => 'Str',
297 );
298
299
300 =head2 force
301
302 Try and force certain operations.
303
304 =cut
305
306 has force => (
307   is      => 'rw',
308   isa      => 'Bool',
309 );
310
311
312 =head2 quiet
313
314 Be less verbose about actions
315
316 =cut
317
318 has quiet => (
319   is      => 'rw',
320   isa      => 'Bool',
321 );
322
323 has '_confirm' => (
324   is    => 'bare',
325   isa    => 'Bool',
326 );
327
328
329 =head1 METHODS
330
331 =head2 create
332
333 =over 4
334
335 =item Arguments: $sqlt_type, \%sqlt_args, $preversion
336
337 =back
338
339 L<create> will generate sql for the supplied schema_class in sql_dir.  The flavour of sql to 
340 generate can be controlled by suppling a sqlt_type which should be a L<SQL::Translator> name.  
341
342 Arguments for L<SQL::Translator> can be supplied in the sqlt_args hashref.
343
344 Optional preversion can be supplied to generate a diff to be used by upgrade.
345
346 =cut
347
348 sub create {
349   my ($self, $sqlt_type, $sqlt_args, $preversion) = @_;
350
351   $preversion ||= $self->preversion();
352
353   my $schema = $self->schema();
354   # create the dir if does not exist
355   $self->sql_dir->mkpath() if ( ! -d $self->sql_dir);
356
357   $schema->create_ddl_dir( $sqlt_type, (defined $schema->schema_version ? $schema->schema_version : ""), $self->sql_dir->stringify, $preversion, $sqlt_args );
358 }
359
360
361 =head2 upgrade
362
363 =over 4
364
365 =item Arguments: <none>
366
367 =back
368
369 upgrade will attempt to upgrade the connected database to the same version as the schema_class.
370 B<MAKE SURE YOU BACKUP YOUR DB FIRST>
371
372 =cut
373
374 sub upgrade {
375   my ($self) = @_;
376   my $schema = $self->schema();
377   if (!$schema->get_db_version()) {
378     # schema is unversioned
379     die "could not determin current schema version, please either install or deploy";
380   } else {
381     my $ret = $schema->upgrade();
382     return $ret;
383   }
384 }
385
386
387 =head2 install
388
389 =over 4
390
391 =item Arguments: $version
392
393 =back
394
395 install is here to help when you want to move to L<DBIx::Class::Schema::Versioned> and have an existing 
396 database.  install will take a version and add the version tracking tables and 'install' the version.  No 
397 further ddl modification takes place.  Setting the force attribute to a true value will allow overriding of 
398 already versioned databases.
399
400 =cut
401
402 sub install {
403   my ($self, $version) = @_;
404
405   my $schema = $self->schema();
406   $version ||= $self->version();
407   if (!$schema->get_db_version() ) {
408     # schema is unversioned
409     print "Going to install schema version\n";
410     my $ret = $schema->install($version);
411     print "retun is $ret\n";
412   }
413   elsif ($schema->get_db_version() and $self->force ) {
414     warn "Forcing install may not be a good idea";
415     if($self->_confirm() ) {
416       # FIXME private api
417       $self->schema->_set_db_version({ version => $version});
418     }
419   }
420   else {
421     die "schema already has a version not installing, try upgrade instead";
422   }
423
424 }
425
426
427 =head2 deploy
428
429 =over 4
430
431 =item Arguments: $args
432
433 =back
434
435 deploy will create the schema at the connected database.  C<$args> are passed straight to 
436 L<DBIx::Class::Schema/deploy>.
437
438 =cut
439
440 sub deploy {
441   my ($self, $args) = @_;
442   my $schema = $self->schema();
443   if (!$schema->get_db_version() ) {
444     # schema is unversioned
445     $schema->deploy( $args, $self->sql_dir)
446       or die "could not deploy schema";
447   } else {
448     die "there already is a database with a version here, try upgrade instead";
449   }
450 }
451
452
453 # FIXME ensure option spec compatability
454 #die('Do not use the where option with the insert op') if ($where);
455 #die('Do not use the attrs option with the insert op') if ($attrs);
456
457
458 =head2 insert
459
460 =over 4
461
462 =item Arguments: $rs, $set
463
464 =back
465
466 insert takes the name of a resultset from the schema_class and a hashref of data to insert
467 into that resultset
468
469 =cut
470
471 sub insert {
472   my ($self, $rs, $set) = @_;
473
474   $rs ||= $self->resultset();
475   $set ||= $self->set();
476   my $resultset = $self->schema->resultset($rs);
477   my $obj = $resultset->create( $set );
478   print ''.ref($resultset).' ID: '.join(',',$obj->id())."\n" if (!$self->quiet);
479 }
480
481
482 =head2 update
483
484 =over 4
485
486 =item Arguments: $rs, $set, $where
487
488 =back
489
490 update takes the name of a resultset from the schema_class, a hashref of data to update and
491 a where hash used to form the search for the rows to update.
492
493 =cut
494
495 sub update {
496   my ($self, $rs, $set, $where) = @_;
497
498   $rs ||= $self->resultset();
499   $where ||= $self->where();
500   $set ||= $self->set();
501   my $resultset = $self->schema->resultset($rs);
502   $resultset = $resultset->search( ($where||{}) );
503
504   my $count = $resultset->count();
505   print "This action will modify $count ".ref($resultset)." records.\n" if (!$self->quiet);
506
507   if ( $self->force || $self->_confirm() ) {
508     $resultset->update_all( $set );
509   }
510 }
511
512 # FIXME
513 #die('Do not use the set option with the delete op') if ($set);
514
515
516 =head2 delete
517
518 =over 4
519
520 =item Arguments: $rs, $where, $attrs
521
522 =back
523
524 delete takes the name of a resultset from the schema_class, a where hashref and a attrs to pass to ->search.
525 The found data is deleted and cannot be recovered.
526
527 =cut
528
529 sub delete {
530   my ($self, $rs, $where, $attrs) = @_;
531
532   $rs ||= $self->resultset();
533   $where ||= $self->where();
534   $attrs ||= $self->attrs();
535   my $resultset = $self->schema->resultset($rs);
536   $resultset = $resultset->search( ($where||{}), ($attrs||()) );
537
538   my $count = $resultset->count();
539   print "This action will delete $count ".ref($resultset)." records.\n" if (!$self->quiet);
540
541   if ( $self->force || $self->_confirm() ) {
542     $resultset->delete_all();
543   }
544 }
545
546
547 =head2 select
548
549 =over 4
550
551 =item Arguments: $rs, $where, $attrs
552
553 =back
554
555 select takes the name of a resultset from the schema_class, a where hashref and a attrs to pass to ->search. 
556 The found data is returned in a array ref where the first row will be the columns list.
557
558 =cut
559
560 sub select {
561   my ($self, $rs, $where, $attrs) = @_;
562
563   $rs ||= $self->resultset();
564   $where ||= $self->where();
565   $attrs ||= $self->attrs();
566   my $resultset = $self->schema->resultset($rs);
567   $resultset = $resultset->search( ($where||{}), ($attrs||()) );
568
569   my @data;
570   my @columns = $resultset->result_source->columns();
571   push @data, [@columns];# 
572
573   while (my $row = $resultset->next()) {
574     my @fields;
575     foreach my $column (@columns) {
576       push( @fields, $row->get_column($column) );
577     }
578     push @data, [@fields];
579   }
580
581   return \@data;
582 }
583
584 sub _confirm {
585   my ($self) = @_;
586   print "Are you sure you want to do this? (type YES to confirm) \n";
587   # mainly here for testing
588   return 1 if ($self->meta->get_attribute('_confirm')->get_value($self));
589   my $response = <STDIN>;
590   return 1 if ($response=~/^YES/);
591   return;
592 }
593
594 sub _find_stanza {
595   my ($self, $cfg, $stanza) = @_;
596   my @path = split /::/, $stanza;
597   while (my $path = shift @path) {
598     if (exists $cfg->{$path}) {
599       $cfg = $cfg->{$path};
600     }
601     else {
602       die "could not find $stanza in config, $path did not seem to exist";
603     }
604   }
605   return $cfg;
606 }
607
608 sub _json_to_data {
609   my ($json_str) = @_;
610   my $json = JSON::Any->new(allow_barekey => 1, allow_singlequote => 1, relaxed=>1);
611   my $ret = $json->jsonToObj($json_str);
612   return $ret;
613 }
614
615
616 {  # deps check
617
618 my @_missing_deps;
619 foreach my $dep (@_deps) {
620   eval "require $dep";
621   if ($@) {
622     push @_missing_deps, $dep;
623   }
624 }
625
626 if (@_missing_deps > 0) {
627   die "The following dependecies are missing " . join ",", @_missing_deps;
628 }
629
630
631 }
632
633
634 =head1 AUTHORS
635
636 See L<DBIx::Class/CONTRIBUTORS>.
637
638 =head1 LICENSE
639
640 You may distribute this code under the same terms as Perl itself
641
642 =cut
643
644 1;