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