package Storm::Aeolus; { $Storm::Aeolus::VERSION = '0.18'; } use Moose; use MooseX::SemiAffordanceAccessor; use MooseX::StrictConstructor; use DateTime::Format::MySQL; use Storm::Types qw( MooseAttribute Storm StormEnabledClassName StormMetaRelationship StormObjectTypeConstraint StormSource ); has 'storm' => ( is => 'rw', isa => Storm, required => 1, weak_ref => 1, ); sub backup_class_table { my ( $self, $class, $fh, $opts ) = @_; confess 'You did not supply a filehandle, usage: $aeolus->backup_class_table( $class, $fh)' if ! $fh; $opts->{timestamp} ||= DateTime->now; my $meta = $class->meta; my $table = $meta->storm_table; print $fh qq[# class: $class\n]; print $fh qq[# table: ] . $table->name . qq[\n]; print $fh qq[# timestamp: ] . DateTime::Format::MySQL->format_datetime( $opts->{timestamp} ) . "\n"; $self->_dump_table_to_file( $table->name, $fh ); } sub backup_class { my ( $self, $class, $fh, $opts ) = @_; confess 'You did not supply a filehandle, usage: $aeolus->backup_class_table( $class, $fh)' if ! $fh; $opts->{timestamp} ||= DateTime->now; $self->backup_class_table( $class, $fh, $opts ); $self->backup_junction_tables( $class, $fh, $opts ); } sub backup_junction_tables { my ( $self, $class, $fh, $opts ) = @_; confess 'You did not supply a filehandle, usage: $aeolus->backup_class_table( $class, $fh)' if ! $fh; $opts->{timestamp} ||= DateTime->now; my $meta = $class->meta; my @relationships = map { $meta->get_relationship( $_ ) } $meta->get_relationship_list; my $dbh = $self->storm->source->dbh; for my $r ( @relationships ) { next if $r->isa( 'Storm::Meta::Relationship::OneToMany' ); my $table = $r->junction_table; print $fh qq[# junction_table ]; print $fh qq[# class1: $class\n]; print $fh qq[# class2: ] . $r->foreign_class . qq[\n]; print $fh qq[# table: ] . $table . qq[\n]; print $fh qq[# timestamp: ] . DateTime::Format::MySQL->format_datetime( $opts->{timestamp} ) . "\n"; $self->_dump_table_to_file( $table, $fh ); } } # private method used to dump a database table to a filehandle sub _dump_table_to_file { my ( $self, $table, $fh ) = @_; # dump table to file my $sql = 'SELECT * FROM ' . $table . ';'; my $dbh = $self->storm->dbh; my $sth = $dbh->prepare( $sql ); $sth->execute; my @cols = @{$sth->{NAME}}; print $fh join ( '|', @cols ), "\n"; no warnings; while ( my @data = $sth->fetchrow_array ) { print $fh join ( '|', @data ), "\n"; } } # method: class_table_installed $class # returns true if the $class is installed to the database, returns # false otherwise sub class_table_installed { my ( $self, $class ) = @_; my $table = $class->meta->storm_table->name; my %tables = ( map { $_ => 1 } $self->storm->source->tables ); $tables{$table} ? 1 : 0; } sub column_definition { my ( $self, $attr ) = @_; $self->meta->throw_error( qq[$attr is not a Moose attribute] ) if ! is_MooseAttribute( $attr ); my $type_constraint = $attr->type_constraint; my $definition = $type_constraint ? undef : 'VARCHAR(64)'; $definition = $attr->define if defined $attr->define; my $policy = $self->storm->policy; while ( ! $definition ) { # check to see if there is a definition for the type constraint if ( $policy->has_definition( $type_constraint->name ) ) { $definition = $policy->get_definition( $type_constraint->name ); } # check to see if the type constraint is Storm enabled class elsif ( is_StormObjectTypeConstraint( $type_constraint ) ) { $definition = $self->column_definition( $type_constraint->class->meta->primary_key ); } # if not, check the parent type constraint for definitions else { $type_constraint = $type_constraint->parent; $definition = 'VARCHAR(64)' if ! $type_constraint; } } return $definition; } sub find_foreign_attributes { my ( $self, $class ) = @_; my $meta = $class->meta; $self->meta->throw_error( qq[$class is not a Storm enabled class] ) if ! is_StormEnabledClassName( $class ); # find the foreign attributes my @foreign_attributes; for my $attr ( map { $meta->get_attribute($_) } $meta->get_attribute_list ) { next if ! $attr->column; my $type_constraint = $attr->type_constraint; while ( $type_constraint ) { # we need to account for how maybe types work if ($type_constraint->parent && $type_constraint->parent->name eq 'Maybe') { use Moose::Util::TypeConstraints; $type_constraint = find_type_constraint($type_constraint->{type_parameter}); } if ( is_StormObjectTypeConstraint( $type_constraint ) ) { push @foreign_attributes, [$attr, $type_constraint->class]; last; } else { $type_constraint = $type_constraint->parent; } } } return @foreign_attributes; } sub install_class { my ( $self, $class ) = @_; $self->meta->throw_error( qq[$class is not a Storm enabled class] ) if ! is_StormEnabledClassName( $class ); $self->install_class_table( $class ); $self->install_junction_tables( $class ); return 1; } sub install_class_table { my ( $self, $class ) = @_; $self->meta->throw_error( qq[$class is not a Storm enabled class] ) if ! is_StormEnabledClassName( $class ); my $sql = $self->table_definition( $class ); my $dbh = $self->storm->source->dbh; $dbh->do( $sql ); confess $dbh->errstr if $dbh->err; return 1; } sub install_foreign_keys { my ( $self, $model ) = @_; for my $class ( $model->members ) { $self->install_foreign_keys_to_class_table( $class ); $self->install_foreign_keys_to_junction_tables( $class ); } } sub install_foreign_keys_to_class_table { my ( $self, $class ) = @_; $self->meta->throw_error( qq[$class is not a Storm enabled class] ) if ! is_StormEnabledClassName( $class ); my $meta = $class->meta; # find the foreign attributes my @foreign_attributes = $self->find_foreign_attributes( $class ); my $dbh = $self->storm->source->dbh; my @key_statements; for ( @foreign_attributes ) { my ( $attr, $foreign_class ) = @$_; if ( $attr->does('ForeignKey') ) { my $name1 = $class->meta->storm_table->name . $attr->column->name; $name1 = substr $name1, -30; my $name2 = $foreign_class->meta->storm_table->name . $foreign_class->meta->primary_key->column->name; $name2 = substr $name2, -30; my $cname = 'FK' . $name1 . $name2; my $string = "CONSTRAINT `$cname`\n\t\tFOREIGN KEY (" . $attr->column->name . ")\n"; $string .= "\t\tREFERENCES " . $foreign_class->meta->storm_table->name; $string .= '(' . $foreign_class->meta->primary_key->column->name . ')'; $string .= "\n\t\tON DELETE " . $attr->on_delete; $string .= "\n\t\tON UPDATE " . $attr->on_update; push @key_statements, $string; } } if ( @key_statements ) { for ( @key_statements ) { my $sql = 'ALTER TABLE `' . $class->meta->storm_table->name . "`\n"; $sql .= "\tADD "; $sql .= $_ . ';'; print $sql, "\n"; $dbh->do( $sql ); confess $dbh->errstr if $dbh->err; } } } sub install_foreign_keys_to_junction_tables { my ( $self, $class ) = @_; $self->meta->throw_error( qq[$class is not a Storm enabled class] ) if ! is_StormEnabledClassName( $class ); my $meta = $class->meta; my @relationships = map { $meta->get_relationship( $_ ) } $meta->get_relationship_list; my $dbh = $self->storm->source->dbh; for my $r ( @relationships ) { next if $r->isa( 'Storm::Meta::Relationship::OneToMany' ); my $table = $r->junction_table; my $col1 = $r->local_match; my $col2 = $r->foreign_match; # skip if the table already exists in the database my $infosth = $dbh->table_info( undef, undef, $table, undef ); my @tableinfo = $infosth->fetchrow_array; next if @tableinfo; my $sql .= 'ALTER TABLE `' . $table . "` ADD \n"; $sql .= "\tCONSTRAINT `FK_$table"."$col1` FOREIGN KEY ($col1)\n"; $sql .= "\t\tREFERENCES " . $meta->storm_table->name . "(" . $meta->primary_key->column->name . ")\n"; print $sql, "\n\n"; #$dbh->do( $sql ); #confess $dbh->errstr if $dbh->err; } } sub install_junction_tables { my ( $self, $class ) = @_; $self->meta->throw_error( qq[$class is not a Storm enabled class] ) if ! is_StormEnabledClassName( $class ); my $meta = $class->meta; my @relationships = map { $meta->get_relationship( $_ ) } $meta->get_relationship_list; my $dbh = $self->storm->source->dbh; for my $r ( @relationships ) { next if $r->isa( 'Storm::Meta::Relationship::OneToMany' ); my $table = $r->junction_table; my $col1 = $r->local_match; my $col2 = $r->foreign_match; # skip if the table already exists in the database my $infosth = $dbh->table_info( undef, undef, $table, undef ); my @tableinfo = $infosth->fetchrow_array; next if @tableinfo; my $sql = 'CREATE TABLE ' . $table . ' (' . "\n"; $sql .= "\t" . $col1 . ' ' . $self->column_definition( $meta->primary_key ) . ",\n"; $sql .= "\t" . $col2 . ' ' . $self->column_definition( $r->foreign_class->meta->primary_key ) . "\n"; #$sql .= "\tFOREIGN KEY (" . $col1 . ") REFERENCES "; #$sql .= $r->foreign_class->meta->storm_table->name . '(' . $r->foreign_class->meta->primary_key->column->name . "),\n"; #$sql .= "\tFOREIGN KEY (" . $col2 . ") REFERENCES "; #$sql .= $meta->storm_table->name . '(' . $meta->primary_key->column->name . ")\n"; $sql .= ');'; $dbh->do( $sql ); confess $dbh->errstr if $dbh->err; } } sub start_fresh { my ( $self ) = @_; my $source = $self->storm->source; $source->disable_foreign_key_checks; $source->dbh->do("DROP TABLE $_") for $self->storm->source->tables; $source->enable_foreign_key_checks; } sub table_definition { my ( $self, $class ) = @_; $self->meta->throw_error( qq[$class is not a Storm enabled class] ) if ! is_StormEnabledClassName( $class ); my $meta = $class->meta; my $table = $meta->storm_table; my %defmap; # definition map # get the definition for each attribute for my $attr ( $meta->get_all_attributes ) { # TODO: Change how we identify a sotrm column here next if ! $attr->can('column') || ! $attr->column; $defmap{ $attr->name } = { column => $attr->column, definition => $self->column_definition( $attr ), }; } my $sql = 'CREATE TABLE ' . $table->name . ' (' . "\n"; my (@definitions, @key_statements); # primary key definition if ( $meta->primary_key ) { my $def = delete $defmap{ $meta->primary_key->name }; my $string = "\t" . $def->{column}->name . " "; $string .= $def->{definition}; $string .= ' PRIMARY KEY'; $string .= ' ' . $self->storm->source->auto_increment_token if $meta->primary_key->does('AutoIncrement'); push @definitions, $string; } # remaing attribute definitions for my $attname ( sort keys %defmap ) { my $string = "\t" . $defmap{ $attname }->{column}->name . " "; $string .= $defmap{ $attname }->{definition}; push @definitions, $string; } # foreign key definitions #my @foreign_attributes = $self->find_foreign_attributes( $class ); #for ( @foreign_attributes ) { # my ( $attr, $foreign_class ) = @$_; # # my $string = "\tFOREIGN KEY (" . $attr->column->name . ") "; # $string .= "REFERENCES " . $foreign_class->meta->storm_table->name; # $string .= '(' . $foreign_class->meta->primary_key->column->name . ')'; # push @key_statements, $string; #} $sql .= join ",\n", @definitions; $sql .= ",\n" . join(",\n", @key_statements) if @key_statements; $sql .= "\n);"; return $sql; } sub install_model { my ( $self, $model ) = @_; for my $class ( $model->members ) { $self->install_class( $class ); } $self->install_foreign_keys( $model ); } no Moose; __PACKAGE__->meta->make_immutable; 1; __END__ =pod =head1 NAME Storm::Aeolus - Install classes to the database =head1 SYNOPSIS $storm->aeolus->install_class_table( 'Person' ); $storm->aeolus->install_junction_tables( 'Person' ); $storm->aeolus->install_class( 'Person' ); =head1 DESCRIPTION Aeolus is the Greek god of the winds. C can introspect your object classes and create the appropriate definitions in the database. It is important you setup a policy (see L) for any custom types you have created. =head1 ATTRIBUTES =over 4 =item storm The L storm instance that Aeolus should act on. =back =head1 METHODS =over 4 =item backup_class $class, $filehandle, [\%opts] Backup the data for an entire class and write it to the supplised fielhandle. = item backup_class_table $class, $filehandle, [\%opts] =item install_class $class Installs the all necessary tables for storing the class by calling C and C on the C<$class>. =item install_class_table $class Installs the primary data table for the C<$class>. =item install_junction_tables $class Installs any junction tables necessary to store relationship information between objects. =item install_model $class Calls C for all members of the model; =back =head1 AUTHOR Jeffrey Ray Hallock Ejeffrey.hallock at gmail dot comE =head1 COPYRIGHT Copyright (c) 2010 Jeffrey Ray Hallock. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut