increment the version numbers in Column.pm and Table.pm and the "use" statements...
[DBIx-DBSchema.git] / DBSchema.pm
index 327a0cc..d7d6dc0 100644 (file)
@@ -1,17 +1,20 @@
 package DBIx::DBSchema;
 
 use strict;
 package DBIx::DBSchema;
 
 use strict;
-use vars qw(@ISA $VERSION);
+use vars qw(@ISA $VERSION $DEBUG);
 #use Exporter;
 #use Exporter;
-#use Carp qw(verbose);
-use DBI;
-use FreezeThaw qw(freeze thaw cmpStr);
-use DBIx::DBSchema::Table;
+use Storable;
+use DBIx::DBSchema::_util qw(_load_driver _dbh);
+use DBIx::DBSchema::Table 0.03;
+use DBIx::DBSchema::Column;
+use DBIx::DBSchema::ColGroup::Unique;
+use DBIx::DBSchema::ColGroup::Index;
 
 #@ISA = qw(Exporter);
 @ISA = ();
 
 
 #@ISA = qw(Exporter);
 @ISA = ();
 
-$VERSION = "0.1";
+$VERSION = "0.31";
+$DEBUG = 0;
 
 =head1 NAME
 
 
 =head1 NAME
 
@@ -36,17 +39,30 @@ DBIx::DBSchema - Database-independent schema objects
 
   $DBIx_DBSchema_table_object = $schema->table("table_name");
 
 
   $DBIx_DBSchema_table_object = $schema->table("table_name");
 
-  $sql_string = $schema->sql($dsn);
+  @sql = $schema->sql($dbh);
+  @sql = $schema->sql($dsn, $username, $password);
+  @sql = $schema->sql($dsn); #doesn't connect to database - less reliable
 
   $perl_code = $schema->pretty_print;
   %hash = eval $perl_code;
 
   $perl_code = $schema->pretty_print;
   %hash = eval $perl_code;
-  $schema = pretty_read DBIx::DBSchema \%hash;
+  use DBI qw(:sql_types); $schema = pretty_read DBIx::DBSchema \%hash;
 
 =head1 DESCRIPTION
 
 DBIx::DBSchema objects are collections of DBIx::DBSchema::Table objects and
 represent a database schema.
 
 
 =head1 DESCRIPTION
 
 DBIx::DBSchema objects are collections of DBIx::DBSchema::Table objects and
 represent a database schema.
 
+This module implements an OO-interface to database schemas.  Using this module,
+you can create a database schema with an OO Perl interface.  You can read the
+schema from an existing database.  You can save the schema to disk and restore
+it a different process.  Most importantly, DBIx::DBSchema can write SQL
+CREATE statements statements for different databases from a single source.
+
+Currently supported databases are MySQL and PostgreSQL.  Sybase support is
+partially implemented.  DBIx::DBSchema will attempt to use generic SQL syntax
+for other databases.  Assistance adding support for other databases is
+welcomed.  See L<DBIx::DBSchema::DBD>, "Driver Writer's Guide and Base Class".
+
 =head1 METHODS
 
 =over 4
 =head1 METHODS
 
 =over 4
@@ -70,7 +86,7 @@ sub new {
 
 }
 
 
 }
 
-=item new_odbc DATABASE_HANDLE || DATA_SOURCE USERNAME PASSWORD [ ATTR ]
+=item new_odbc DATABASE_HANDLE | DATA_SOURCE USERNAME PASSWORD [ ATTR ]
 
 Creates a new DBIx::DBSchema object from an existing data source, which can be
 specified by passing an open DBI database handle, or by passing the DBI data
 
 Creates a new DBIx::DBSchema object from an existing data source, which can be
 specified by passing an open DBI database handle, or by passing the DBI data
@@ -80,19 +96,20 @@ closely correspond to any non-portable column types.  Use this to import a
 schema that you wish to use with many different database engines.  Although
 primary key and (unique) index information will only be read from databases
 with DBIx::DBSchema::DBD drivers (currently MySQL and PostgreSQL), import of
 schema that you wish to use with many different database engines.  Although
 primary key and (unique) index information will only be read from databases
 with DBIx::DBSchema::DBD drivers (currently MySQL and PostgreSQL), import of
-column names and attributes *should* work for any database.
+column names and attributes *should* work for any database.  Note that this
+method only uses "ODBC" column types; it does not require or use an ODBC
+driver.
 
 =cut
 
 sub new_odbc {
 
 =cut
 
 sub new_odbc {
-  my($proto, $dbh) = (shift, shift);
-  $dbh = DBI->connect( $dbh, @_ ) or die $DBI::errstr unless ref($dbh);
+  my($proto, $dbh) = ( shift, _dbh(@_) );
   $proto->new(
     map { new_odbc DBIx::DBSchema::Table $dbh, $_ } _tables_from_dbh($dbh)
   );
 }
 
   $proto->new(
     map { new_odbc DBIx::DBSchema::Table $dbh, $_ } _tables_from_dbh($dbh)
   );
 }
 
-=item new_native DATABASE_HANDLE || DATA_SOURCE USERNAME PASSWORD [ ATTR ]
+=item new_native DATABASE_HANDLE | DATA_SOURCE USERNAME PASSWORD [ ATTR ]
 
 Creates a new DBIx::DBSchema object from an existing data source, which can be
 specified by passing an open DBI database handle, or by passing the DBI data
 
 Creates a new DBIx::DBSchema object from an existing data source, which can be
 specified by passing an open DBI database handle, or by passing the DBI data
@@ -103,8 +120,7 @@ only available if there is a DBIx::DBSchema::DBD for the corresponding database
 =cut
 
 sub new_native {
 =cut
 
 sub new_native {
-  my($proto, $dbh) = (shift, shift);
-  $dbh = DBI->connect( $dbh, @_ ) or die $DBI::errstr unless ref($dbh);
+  my($proto, $dbh) = (shift, _dbh(@_) );
   $proto->new(
     map { new_native DBIx::DBSchema::Table ( $dbh, $_ ) } _tables_from_dbh($dbh)
   );
   $proto->new(
     map { new_native DBIx::DBSchema::Table ( $dbh, $_ ) } _tables_from_dbh($dbh)
   );
@@ -118,12 +134,23 @@ Loads a DBIx::DBSchema object from a file.
 
 sub load {
   my($proto,$file)=@_; #use $proto ?
 
 sub load {
   my($proto,$file)=@_; #use $proto ?
-  open(FILE,"<$file") or die "Can't open $file: $!";
-  my($string)=join('',<FILE>); #can $string have newlines?  pry not?
-  close FILE or die "Can't close $file: $!";
-  my($self)=thaw $string;
-  #no bless needed?
+
+  my $self;
+
+  #first try Storable
+  eval { $self = Storable::retrieve($file); };
+
+  if ( $@ && $@ =~ /not.*storable/i ) { #then try FreezeThaw
+    eval "use FreezeThaw;";
+    die $@ if $@;
+    open(FILE,"<$file") or die "Can't open $file: $!";
+    my $string = join('',<FILE>);
+    close FILE or die "Can't close $file: $!";
+    ($self) = FreezeThaw::thaw($string);
+  }
+
   $self;
   $self;
+
 }
 
 =item save FILENAME
 }
 
 =item save FILENAME
@@ -133,14 +160,8 @@ Saves a DBIx::DBSchema object to a file.
 =cut
 
 sub save {
 =cut
 
 sub save {
-  my($self,$file)=@_;
-  my($string)=freeze $self;
-  open(FILE,">$file") or die "Can't open $file: $!";
-  print FILE $string;
-  close FILE or die "Can't close file: $!";
-  my($check_self)=thaw $string;
-  die "Verify error: Can't freeze and thaw dbdef $self"
-    if (cmpStr($self,$check_self));
+  #my($self, $file) = @_;
+  Storable::nstore(@_);
 }
 
 =item addtable TABLE_OBJECT
 }
 
 =item addtable TABLE_OBJECT
@@ -176,22 +197,108 @@ sub table {
   $self->{'tables'}->{$table};
 }
 
   $self->{'tables'}->{$table};
 }
 
-=item sql_string [ DATASRC ]
+=item sql [ DATABASE_HANDLE | DATA_SOURCE [ USERNAME PASSWORD [ ATTR ] ] ]
 
 Returns a list of SQL `CREATE' statements for this schema.
 
 
 Returns a list of SQL `CREATE' statements for this schema.
 
-If passed a DBI data source such as `DBI:mysql:database' or
+The data source can be specified by passing an open DBI database handle, or by
+passing the DBI data source name, username and password.  
+
+Although the username and password are optional, it is best to call this method
+with a database handle or data source including a valid username and password -
+a DBI connection will be opened and the quoting and type mapping will be more
+reliable.
+
+If passed a DBI data source (or handle) such as `DBI:mysql:database' or
 `DBI:Pg:dbname=database', will use syntax specific to that database engine.
 Currently supported databases are MySQL and PostgreSQL.
 
 `DBI:Pg:dbname=database', will use syntax specific to that database engine.
 Currently supported databases are MySQL and PostgreSQL.
 
-If not passed a data source, or if there is no driver for the specified
-database, will attempt to use generic SQL syntax.
+If not passed a data source (or handle), or if there is no driver for the
+specified database, will attempt to use generic SQL syntax.
 
 =cut
 
 
 =cut
 
-sub sql_string {
-  my($self, $datasrc) = @_;
-  map { $self->table($_)->sql_create_table($datasrc); } $self->tables;
+sub sql {
+  my($self, $dbh) = ( shift, _dbh(@_) );
+  map { $self->table($_)->sql_create_table($dbh); } $self->tables;
+}
+
+=item sql_update_schema PROTOTYPE_SCHEMA [ DATABASE_HANDLE | DATA_SOURCE [ USERNAME PASSWORD [ ATTR ] ] ]
+
+Returns a list of SQL statements to update this schema so that it is idential
+to the provided prototype schema, also a DBIx::DBSchema object.
+
+ #Optionally, the data source can be specified by passing an open DBI database
+ #handle, or by passing the DBI data source name, username and password.  
+ #
+ #If passed a DBI data source (or handle) such as `DBI:mysql:database' or
+ #`DBI:Pg:dbname=database', will use syntax specific to that database engine.
+ #Currently supported databases are MySQL and PostgreSQL.
+ #
+ #If not passed a data source (or handle), or if there is no driver for the
+ #specified database, will attempt to use generic SQL syntax.
+
+Right now this method knows how to add new tables and alter existing tables.
+It doesn't know how to drop tables yet.
+
+See L<DBIx::DBSchema::Table/sql_alter_table>,
+L<DBIx::DBSchema::Column/sql_add_coumn> and
+L<DBIx::DBSchema::Column/sql_alter_column> for additional specifics and
+limitations.
+
+=cut
+
+#gosh, false laziness w/DBSchema::Table::sql_alter_schema
+
+sub sql_update_schema {
+  my($self, $new, $dbh) = ( shift, shift, _dbh(@_) );
+
+  my @r = ();
+
+  foreach my $table ( $new->tables ) {
+  
+    if ( $self->table($table) ) {
+  
+      warn "$table exists\n" if $DEBUG > 1;
+
+      push @r,
+        $self->table($table)->sql_alter_table( $new->table($table), $dbh );
+
+    } else {
+  
+      warn "table $table does not exist.\n" if $DEBUG;
+
+      push @r, 
+        $new->table($table)->sql_create_table( $dbh );
+  
+    }
+  
+  }
+
+  # should eventually drop tables not in $new
+
+  warn join("\n", @r). "\n"
+    if $DEBUG;
+
+  @r;
+  
+}
+
+=item update_schema PROTOTYPE_SCHEMA, DATABASE_HANDLE | DATA_SOURCE [ USERNAME PASSWORD [ ATTR ] ]
+
+Same as sql_update_schema, except actually runs the SQL commands to update
+the schema.  Throws a fatal error if any statement fails.
+
+=cut
+
+sub update_schema {
+  my($self, $new, $dbh) = ( shift, shift, _dbh(@_) );
+
+  foreach my $statement ( $self->sql_update_schema( $new, $dbh ) ) {
+    $dbh->do( $statement )
+      or die "Error: ". $dbh->errstr. "\n executing: $statement";
+  }
+
 }
 
 =item pretty_print
 }
 
 =item pretty_print
@@ -209,10 +316,18 @@ sub pretty_print {
       "'$table' => {\n".
         "  'columns' => [\n".
           join("", map { 
       "'$table' => {\n".
         "  'columns' => [\n".
           join("", map { 
+                         #cant because -w complains about , in qw()
+                         # (also biiiig problems with empty lengths)
+                         #"    qw( $_ ".
+                         #$self->table($table)->column($_)->type. " ".
+                         #( $self->table($table)->column($_)->null ? 'NULL' : 0 ). " ".
+                         #$self->table($table)->column($_)->length. " ),\n"
                          "    '$_', ".
                          "'". $self->table($table)->column($_)->type. "', ".
                          "    '$_', ".
                          "'". $self->table($table)->column($_)->type. "', ".
-                         "'". $self->table($table)->column($_)->null. "', ".
-                         "'". $self->table($table)->column($_)->length. "'\n"
+                         "'". $self->table($table)->column($_)->null. "', ". 
+                         "'". $self->table($table)->column($_)->length. "', ".
+                         "'". $self->table($table)->column($_)->default. "', ".
+                         "'". $self->table($table)->column($_)->local. "',\n"
                        } $self->table($table)->columns
           ).
         "  ],\n".
                        } $self->table($table)->columns
           ).
         "  ],\n".
@@ -227,7 +342,7 @@ sub pretty_print {
           ). " ],\n"
         #"  'index' => [ ".    " ],\n"
     } $self->tables
           ). " ],\n"
         #"  'index' => [ ".    " ],\n"
     } $self->tables
-  ), "}\n";
+  ). "}\n";
 }
 
 =cut
 }
 
 =cut
@@ -240,23 +355,35 @@ B<pretty_print> method.
 =cut
 
 sub pretty_read {
 =cut
 
 sub pretty_read {
-  die "unimplemented (pull from fs-setup)";
-  my($proto) = @_;
+  my($proto, $href) = @_;
+  my $schema = $proto->new( map {  
+    my(@columns);
+    while ( @{$href->{$_}{'columns'}} ) {
+      push @columns, DBIx::DBSchema::Column->new(
+        splice @{$href->{$_}{'columns'}}, 0, 6
+      );
+    }
+    DBIx::DBSchema::Table->new(
+      $_,
+      $href->{$_}{'primary_key'},
+      DBIx::DBSchema::ColGroup::Unique->new($href->{$_}{'unique'}),
+      DBIx::DBSchema::ColGroup::Index->new($href->{$_}{'index'}),
+      @columns,
+    );
+  } (keys %{$href}) );
 }
 
 # private subroutines
 
 }
 
 # private subroutines
 
-sub _load_driver {
-  my($dbh) = @_;
-  my $driver = $dbh->{Driver}->{Name};
-  #require "DBIx/DBSchema/DBD/$driver.pm";
-  #$driver;
-  eval 'require "DBIx/DBSchema/DBD/$driver.pm"' and $driver;
-}
-
 sub _tables_from_dbh {
   my($dbh) = @_;
 sub _tables_from_dbh {
   my($dbh) = @_;
-  my $sth = $dbh->table_info or die $dbh->errstr;
+  my $driver = _load_driver($dbh);
+  my $db_catalog =
+    scalar(eval "DBIx::DBSchema::DBD::$driver->default_db_catalog");
+  my $db_schema  =
+    scalar(eval "DBIx::DBSchema::DBD::$driver->default_db_schema");
+  my $sth = $dbh->table_info($db_catalog, $db_schema, '', 'TABLE')
+    or die $dbh->errstr;
   #map { $_->{TABLE_NAME} } grep { $_->{TABLE_TYPE} eq 'TABLE' }
   #  @{ $sth->fetchall_arrayref({ TABLE_NAME=>1, TABLE_TYPE=>1}) };
   map { $_->[0] } grep { $_->[1] =~ /^TABLE$/i }
   #map { $_->{TABLE_NAME} } grep { $_->{TABLE_TYPE} eq 'TABLE' }
   #  @{ $sth->fetchall_arrayref({ TABLE_NAME=>1, TABLE_TYPE=>1}) };
   map { $_->[0] } grep { $_->[1] =~ /^TABLE$/i }
@@ -265,13 +392,25 @@ sub _tables_from_dbh {
 
 =back
 
 
 =back
 
-=head1 AUTHOR
+=head1 AUTHORS
 
 Ivan Kohler <ivan-dbix-dbschema@420.am>
 
 
 Ivan Kohler <ivan-dbix-dbschema@420.am>
 
+Charles Shapiro <charles.shapiro@numethods.com> and Mitchell Friedman
+<mitchell.friedman@numethods.com> contributed the start of a Sybase driver.
+
+Daniel Hanks <hanksdc@about-inc.com> contributed the Oracle driver.
+
+Jesse Vincent contributed the SQLite driver.
+
+=head1 CONTRIBUTIONS
+
+Contributions are welcome!  I'm especially keen on any interest in the first
+three items/projects below under BUGS.
+
 =head1 COPYRIGHT
 
 =head1 COPYRIGHT
 
-Copyright (c) 2000 Ivan Kohler
+Copyright (c) 2000-2006 Ivan Kohler
 Copyright (c) 2000 Mail Abuse Prevention System LLC
 All rights reserved.
 This program is free software; you can redistribute it and/or modify it under
 Copyright (c) 2000 Mail Abuse Prevention System LLC
 All rights reserved.
 This program is free software; you can redistribute it and/or modify it under
@@ -279,17 +418,39 @@ the same terms as Perl itself.
 
 =head1 BUGS
 
 
 =head1 BUGS
 
+Indices are not stored by name.  Index representation could use an overhaul.
+
+Multiple primary keys are not yet supported.
+
+Foreign keys and other constraints are not yet supported.
+
+Eventually it would be nice to have additional transformations (deleted,
+modified columns, added/modified/indices (probably need em named first),
+added/deleted tables
+
+Need to port and test with additional databases
+
 Each DBIx::DBSchema object should have a name which corresponds to its name
 within the SQL database engine (DBI data source).
 
 Each DBIx::DBSchema object should have a name which corresponds to its name
 within the SQL database engine (DBI data source).
 
-pretty_print is atrocious.
+pretty_print is actually pretty ugly.
+
+Perhaps pretty_read should eval column types so that we can use DBI
+qw(:sql_types) here instead of externally.
+
+sql CREATE TABLE output should convert integers
+(i.e. use DBI qw(:sql_types);) to local types using DBI->type_info plus a hash
+to fudge things
+
+sql_update_schema doesn't drop tables yet.
 
 =head1 SEE ALSO
 
 L<DBIx::DBSchema::Table>, L<DBIx::DBSchema::ColGroup>,
 L<DBIx::DBSchema::ColGroup::Unique>, L<DBIx::DBSchema::ColGroup::Index>,
 
 =head1 SEE ALSO
 
 L<DBIx::DBSchema::Table>, L<DBIx::DBSchema::ColGroup>,
 L<DBIx::DBSchema::ColGroup::Unique>, L<DBIx::DBSchema::ColGroup::Index>,
-L<DBIx::DBSchema::Column>, L<DBIx::DBSchema::DBD>, L<DBIx::DBSchema::mysql>,
-L<DBIx::DBSchema::Pg>, L<FS::Record>, L<DBI>
+L<DBIx::DBSchema::Column>, L<DBIx::DBSchema::DBD>,
+L<DBIx::DBSchema::DBD::mysql>, L<DBIx::DBSchema::DBD::Pg>, L<FS::Record>,
+L<DBI>
 
 =cut
 
 
 =cut