Make Schema::Graph only load if "as_graph" is called on a Schema object
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Schema / Field.pm
index c31e075..93d25e2 100644 (file)
@@ -1,9 +1,9 @@
 package SQL::Translator::Schema::Field;
 
 # ----------------------------------------------------------------------
-# $Id: Field.pm,v 1.3 2003-05-05 04:32:39 kycl4rk Exp $
+# $Id: Field.pm,v 1.27 2007-10-24 10:55:44 schiffbruechige Exp $
 # ----------------------------------------------------------------------
-# Copyright (C) 2003 Ken Y. Clark <kclark@cpan.org>
+# Copyright (C) 2002-4 SQLFairy Authors
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -30,8 +30,8 @@ SQL::Translator::Schema::Field - SQL::Translator field object
 
   use SQL::Translator::Schema::Field;
   my $field = SQL::Translator::Schema::Field->new(
-      name => 'foo',
-      sql  => 'select * from foo',
+      name  => 'foo',
+      table => $table,
   );
 
 =head1 DESCRIPTION
@@ -43,16 +43,63 @@ C<SQL::Translator::Schema::Field> is the field object.
 =cut
 
 use strict;
-use Class::Base;
 use SQL::Translator::Schema::Constants;
+use SQL::Translator::Utils 'parse_list_arg';
+
+use base 'SQL::Translator::Schema::Object';
 
-use base 'Class::Base';
 use vars qw($VERSION $TABLE_COUNT $VIEW_COUNT);
 
-$VERSION = 1.00;
+$VERSION = sprintf "%d.%02d", q$Revision: 1.27 $ =~ /(\d+)\.(\d+)/;
+
+# Stringify to our name, being careful not to pass any args through so we don't
+# accidentally set it to undef. We also have to tweak bool so the object is
+# still true when it doesn't have a name (which shouldn't happen!).
+use overload
+    '""'     => sub { shift->name },
+    'bool'   => sub { $_[0]->name || $_[0] },
+    fallback => 1,
+;
+
+use DBI qw(:sql_types);
+
+# Mapping from string to sql contstant
+our %type_mapping = (
+  integer => SQL_INTEGER,
+  int     => SQL_INTEGER,
+
+  smallint => SQL_SMALLINT,
+  bigint => 9999, # DBI doesn't export a constatn for this. Le suck
+
+  double => SQL_DOUBLE,
+
+  decimal => SQL_DECIMAL,
+  numeric => SQL_NUMERIC,
+  dec => SQL_DECIMAL,
 
+  bit => SQL_BIT,
+
+  date => SQL_DATE,
+  datetime => SQL_DATETIME,
+  timestamp => SQL_TIMESTAMP,
+  time => SQL_TIME,
+
+  char => SQL_CHAR,
+  varchar => SQL_VARCHAR,
+  binary => SQL_BINARY,
+  varbinary => SQL_VARBINARY,
+  tinyblob => SQL_BLOB,
+  blob => SQL_BLOB,
+  text => SQL_LONGVARCHAR
+
+);
 # ----------------------------------------------------------------------
-sub init {
+
+__PACKAGE__->_attributes( qw/
+    table name data_type size is_primary_key is_nullable
+    is_auto_increment default_value comments is_foreign_key
+    is_unique order sql_data_type
+/);
 
 =pod
 
@@ -60,19 +107,49 @@ sub init {
 
 Object constructor.
 
-  my $schema = SQL::Translator::Schema::Field->new;
+  my $field = SQL::Translator::Schema::Field->new(
+      name  => 'foo',
+      table => $table,
+  );
 
 =cut
 
-    my ( $self, $config ) = @_;
+# ----------------------------------------------------------------------
+sub comments {
 
-    for my $arg ( qw[ name data_type size is_primary_key nullable table ] ) {
-        next unless defined $config->{ $arg };
-        $self->$arg( $config->{ $arg } ) or return;
+=pod
+
+=head2 comments
+
+Get or set the comments on a field.  May be called several times to 
+set and it will accumulate the comments.  Called in an array context,
+returns each comment individually; called in a scalar context, returns
+all the comments joined on newlines.
+
+  $field->comments('foo');
+  $field->comments('bar');
+  print join( ', ', $field->comments ); # prints "foo, bar"
+
+=cut
+
+    my $self = shift;
+
+    for my $arg ( @_ ) {
+        $arg = $arg->[0] if ref $arg;
+        push @{ $self->{'comments'} }, $arg if $arg;
+    }
+
+    if ( @{ $self->{'comments'} || [] } ) {
+        return wantarray 
+            ? @{ $self->{'comments'} || [] }
+            : join( "\n", @{ $self->{'comments'} || [] } );
+    }
+    else {
+        return wantarray ? () : '';
     }
-    return $self;
 }
 
+
 # ----------------------------------------------------------------------
 sub data_type {
 
@@ -87,10 +164,28 @@ Get or set the field's data type.
 =cut
 
     my $self = shift;
-    $self->{'data_type'} = shift if @_;
+    if (@_) {
+      $self->{'data_type'} = $_[0];
+      $self->{'sql_data_type'} = $type_mapping{lc $_[0]} || SQL_UNKNOWN_TYPE unless exists $self->{sql_data_type};
+    }
     return $self->{'data_type'} || '';
 }
 
+sub sql_data_type {
+
+=head2 sql_data_type
+
+Constant from DBI package representing this data type. See L<DBI/DBI Constants>
+for more details.
+
+=cut
+
+    my $self = shift;
+    $self->{sql_data_type} = shift if @_;
+    return $self->{sql_data_type} || 0;
+
+}
+
 # ----------------------------------------------------------------------
 sub default_value {
 
@@ -112,6 +207,54 @@ assume an error like other methods.
 }
 
 # ----------------------------------------------------------------------
+=pod
+
+=head2 extra
+
+Get or set the field's "extra" attibutes (e.g., "ZEROFILL" for MySQL).
+Accepts a hash(ref) of name/value pairs to store;  returns a hash.
+
+  $field->extra( qualifier => 'ZEROFILL' );
+  my %extra = $field->extra;
+
+=cut
+
+
+# ----------------------------------------------------------------------
+sub foreign_key_reference {
+
+=pod
+
+=head2 foreign_key_reference
+
+Get or set the field's foreign key reference;
+
+  my $constraint = $field->foreign_key_reference( $constraint );
+
+=cut
+
+    my $self = shift;
+
+    if ( my $arg = shift ) {
+        my $class = 'SQL::Translator::Schema::Constraint';
+        if ( UNIVERSAL::isa( $arg, $class ) ) {
+            return $self->error(
+                'Foreign key reference for ', $self->name, 'already defined'
+            ) if $self->{'foreign_key_reference'};
+
+            $self->{'foreign_key_reference'} = $arg;
+        }
+        else {
+            return $self->error(
+                "Argument to foreign_key_reference is not an $class object"
+            );
+        }
+    }
+
+    return $self->{'foreign_key_reference'};
+}
+
+# ----------------------------------------------------------------------
 sub is_auto_increment {
 
 =pod
@@ -120,7 +263,7 @@ sub is_auto_increment {
 
 Get or set the field's C<is_auto_increment> attribute.
 
-  my $is_pk = $field->is_auto_increment(1);
+  my $is_auto = $field->is_auto_increment(1);
 
 =cut
 
@@ -147,13 +290,86 @@ Get or set the field's C<is_auto_increment> attribute.
 }
 
 # ----------------------------------------------------------------------
+sub is_foreign_key {
+
+=pod
+
+=head2 is_foreign_key
+
+Returns whether or not the field is a foreign key.
+
+  my $is_fk = $field->is_foreign_key;
+
+=cut
+
+    my ( $self, $arg ) = @_;
+
+    unless ( defined $self->{'is_foreign_key'} ) {
+        if ( my $table = $self->table ) {
+            for my $c ( $table->get_constraints ) {
+                if ( $c->type eq FOREIGN_KEY ) {
+                    my %fields = map { $_, 1 } $c->fields;
+                    if ( $fields{ $self->name } ) {
+                        $self->{'is_foreign_key'} = 1;
+                        $self->foreign_key_reference( $c );
+                        last;
+                    }
+                }
+            }
+        }
+    }
+
+    return $self->{'is_foreign_key'} || 0;
+}
+
+# ----------------------------------------------------------------------
+sub is_nullable {
+
+=pod
+
+=head2 is_nullable
+
+Get or set whether the field can be null.  If not defined, then 
+returns "1" (assumes the field can be null).  The argument is evaluated
+by Perl for True or False, so the following are eqivalent:
+
+  $is_nullable = $field->is_nullable(0);
+  $is_nullable = $field->is_nullable('');
+  $is_nullable = $field->is_nullable('0');
+
+While this is technically a field constraint, it's probably easier to
+represent this as an attribute of the field.  In order keep things
+consistent, any other constraint on the field (unique, primary, and
+foreign keys; checks) are represented as table constraints.
+
+=cut
+
+    my ( $self, $arg ) = @_;
+
+    if ( defined $arg ) {
+        $self->{'is_nullable'} = $arg ? 1 : 0;
+    }
+
+    if ( 
+        defined $self->{'is_nullable'} && 
+        $self->{'is_nullable'} == 1    &&
+        $self->is_primary_key
+    ) {
+        $self->{'is_nullable'} = 0;
+    }
+
+    return defined $self->{'is_nullable'} ? $self->{'is_nullable'} : 1;
+}
+
+# ----------------------------------------------------------------------
 sub is_primary_key {
 
 =pod
 
 =head2 is_primary_key
 
-Get or set the field's C<is_primary_key> attribute.
+Get or set the field's C<is_primary_key> attribute.  Does not create
+a table constraint (should it?).
 
   my $is_pk = $field->is_primary_key(1);
 
@@ -181,6 +397,58 @@ Get or set the field's C<is_primary_key> attribute.
 }
 
 # ----------------------------------------------------------------------
+sub is_unique {
+
+=pod
+
+=head2 is_unique
+
+Determine whether the field has a UNIQUE constraint or not.
+
+  my $is_unique = $field->is_unique;
+
+=cut
+
+    my $self = shift;
+    
+    unless ( defined $self->{'is_unique'} ) {
+        if ( my $table = $self->table ) {
+            for my $c ( $table->get_constraints ) {
+                if ( $c->type eq UNIQUE ) {
+                    my %fields = map { $_, 1 } $c->fields;
+                    if ( $fields{ $self->name } ) {
+                        $self->{'is_unique'} = 1;
+                        last;
+                    }
+                }
+            }
+        }
+    }
+
+    return $self->{'is_unique'} || 0;
+}
+
+# ----------------------------------------------------------------------
+sub is_valid {
+
+=pod
+
+=head2 is_valid
+
+Determine whether the field is valid or not.
+
+  my $ok = $field->is_valid;
+
+=cut
+
+    my $self = shift;
+    return $self->error('No name')         unless $self->name;
+    return $self->error('No data type')    unless $self->data_type;
+    return $self->error('No table object') unless $self->table;
+    return 1;
+}
+
+# ----------------------------------------------------------------------
 sub name {
 
 =pod
@@ -189,15 +457,22 @@ sub name {
 
 Get or set the field's name.
 
-  my $name = $field->name('foo');
+ my $name = $field->name('foo');
+
+The field object will also stringify to its name.
+
+ my $setter_name = "set_$field";
+
+Errors ("No field name") if you try to set a blank name.
 
 =cut
 
     my $self = shift;
 
-    if ( my $arg = shift ) {
+    if ( @_ ) {
+        my $arg = shift || return $self->error( "No field name" );
         if ( my $table = $self->table ) {
-            return $self->error( qq[Can't use field name "$arg": table exists] )
+            return $self->error( qq[Can't use field name "$arg": field exists] )
                 if $table->get_field( $arg );
         }
 
@@ -207,30 +482,56 @@ Get or set the field's name.
     return $self->{'name'} || '';
 }
 
+sub full_name {
+
+=head2 full_name
+
+Read only method to return the fields name with its table name pre-pended.
+e.g. "person.foo".
+
+=cut
+
+    my $self = shift;
+    return $self->table.".".$self->name;
+}
+
 # ----------------------------------------------------------------------
-sub nullable {
+sub order {
 
 =pod
 
-=head2 nullable
+=head2 order
 
-Get or set the whether the field can be null.  If not defined, then 
-returns "1" (assumes the field can be null).  The argument is evaluated
-by Perl for True or False, so the following are eqivalent:
+Get or set the field's order.
 
-  $nullable = $field->nullable(0);
-  $nullable = $field->nullable('');
-  $nullable = $field->nullable('0');
+  my $order = $field->order(3);
 
 =cut
 
     my ( $self, $arg ) = @_;
 
-    if ( defined $arg ) {
-        $self->{'nullable'} = $arg ? 1 : 0;
+    if ( defined $arg && $arg =~ /^\d+$/ ) {
+        $self->{'order'} = $arg;
     }
 
-    return defined $self->{'nullable'} ? $self->{'nullable'} : 1;
+    return $self->{'order'} || 0;
+}
+
+# ----------------------------------------------------------------------
+sub schema {
+
+=head2 schema 
+
+Shortcut to get the fields schema ($field->table->schema) or undef if it
+doesn't have one.
+
+  my $schema = $field->schema;
+
+=cut
+
+    my $self = shift;
+    if ( my $table = $self->table ) { return $table->schema || undef; }
+    return undef;
 }
 
 # ----------------------------------------------------------------------
@@ -254,8 +555,7 @@ numbers and returns a string.
 =cut
 
     my $self    = shift;
-    my $numbers = UNIVERSAL::isa( $_[0], 'ARRAY' ) 
-        ? shift : [ map { split /,/ } @_ ];
+    my $numbers = parse_list_arg( @_ );
 
     if ( @$numbers ) {
         my @new;
@@ -267,50 +567,103 @@ numbers and returns a string.
         $self->{'size'} = \@new if @new; # only set if all OK
     }
 
-    return join( ',', @{ $self->{'size'} || [0] } );
+    return wantarray 
+        ? @{ $self->{'size'} || [0] }
+        : join( ',', @{ $self->{'size'} || [0] } )
+    ;
 }
 
 # ----------------------------------------------------------------------
-sub is_valid {
+sub table {
 
 =pod
 
-=head2 is_valid
+=head2 table
 
-Determine whether the field is valid or not.
+Get or set the field's table object. As the table object stringifies this can
+also be used to get the table name.
 
-  my $ok = $field->is_valid;
+  my $table = $field->table;
+  print "Table name: $table";
 
 =cut
 
     my $self = shift;
-    return $self->error('No name')         unless $self->name;
-    return $self->error('No data type')    unless $self->data_type;
-    return $self->error('No table object') unless $self->table;
-    return 1;
+    if ( my $arg = shift ) {
+        return $self->error('Not a table object') unless
+            UNIVERSAL::isa( $arg, 'SQL::Translator::Schema::Table' );
+        $self->{'table'} = $arg;
+    }
+
+    return $self->{'table'};
+}
+
+sub parsed_field {
+
+=head2 
+
+Returns the field exactly as the parser found it
+
+=cut
+
+    my $self = shift;
+
+    if (@_) {
+      my $value = shift;
+      $self->{parsed_field} = $value;
+      return $value || $self;
+    }
+    return $self->{parsed_field} || $self;
 }
 
 # ----------------------------------------------------------------------
-sub table {
+sub equals {
 
 =pod
 
-=head2 table
+=head2 equals
 
-Get or set the field's table object.
+Determines if this field is the same as another
 
-  my $table = $field->table;
+  my $isIdentical = $field1->equals( $field2 );
 
 =cut
 
     my $self = shift;
-    if ( my $arg = shift ) {
-        return $self->error('Not a table object') unless
-            UNIVERSAL::isa( $arg, 'SQL::Translator::Schema::Table' );
-        $self->{'table'} = $arg;
+    my $other = shift;
+    my $case_insensitive = shift;
+    
+    return 0 unless $self->SUPER::equals($other);
+    return 0 unless $case_insensitive ? uc($self->name) eq uc($other->name) : $self->name eq $other->name;
+
+    # Comparing types: use sql_data_type if both are not 0. Else use string data_type
+    if ($self->sql_data_type && $other->sql_data_type) {
+        return 0 unless $self->sql_data_type == $other->sql_data_type
+    } else {
+        return 0 unless lc($self->data_type) eq lc($other->data_type)
     }
 
-    return $self->{'table'};
+    return 0 unless $self->size eq $other->size;
+    return 0 unless (!defined $self->default_value || $self->default_value eq 'NULL') eq (!defined $other->default_value || $other->default_value eq 'NULL');
+    return 0 if defined $self->default_value && $self->default_value ne $other->default_value;
+    return 0 unless $self->is_nullable eq $other->is_nullable;
+#    return 0 unless $self->is_unique eq $other->is_unique;
+    return 0 unless $self->is_primary_key eq $other->is_primary_key;
+#    return 0 unless $self->is_foreign_key eq $other->is_foreign_key;
+    return 0 unless $self->is_auto_increment eq $other->is_auto_increment;
+#    return 0 unless $self->comments eq $other->comments;
+    return 0 unless $self->_compare_objects(scalar $self->extra, scalar $other->extra);
+    return 1;
+}
+
+# ----------------------------------------------------------------------
+sub DESTROY {
+#
+# Destroy cyclical references.
+#
+    my $self = shift;
+    undef $self->{'table'};
+    undef $self->{'foreign_key_reference'};
 }
 
 1;
@@ -321,6 +674,6 @@ Get or set the field's table object.
 
 =head1 AUTHOR
 
-Ken Y. Clark E<lt>kclark@cpan.orgE<gt>
+Ken Y. Clark E<lt>kclark@cpan.orgE<gt>.
 
 =cut