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