# ================================================================== # Gossamer Threads Module Library - http://gossamer-threads.com/ # # GT::Base # Author : Scott Beck # CVS Info : 087,071,086,086,085 # $Id: Creator.pm,v 1.74 2004/09/22 02:43:29 jagerman Exp $ # # Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved. # ================================================================== # # Description: # Class used to make changes to tables and create tables. # package GT::SQL::Creator; # =============================================================== use GT::SQL; use GT::Base; use GT::AutoLoader; use strict; use vars qw/@ISA $DEBUG $VERSION $error $ERROR_MESSAGE/; $VERSION = sprintf "%d.%03d", q$Revision: 1.74 $ =~ /(\d+)\.(\d+)/; $ERROR_MESSAGE = 'GT::SQL'; @ISA = qw/GT::Base/; $DEBUG = 0; sub new { # ------------------------------------------------------------------- # Setup a new creator object. # my $this = shift; my $class = ref $this || $this; my $self = bless {}, $class; # Get the arguments my $opts = {}; if (@_ == 0) { return $self->fatal(BADARGS => "$class->new(HASH_REF or HASH). No arguments") } elsif (@_ == 1 and ref $_[0] eq 'HASH') { $opts = shift } elsif (not @_ % 2) { $opts = {@_} } else { return $self->fatal(BADARGS => "$class->new(HASH_REF or HASH). Wrong arguments") } ref $opts->{table} or return $self->fatal(BADARGS => "$class->new(HASH_REF or HASH); No table passed in to creator."); $self->{table} = $opts->{table}; $self->{connect} = $opts->{connect}; $self->{_debug} = $opts->{debug} || $DEBUG; $self->{_err_pkg} = $opts->{_err_pkg} || __PACKAGE__; $self->debug("OBJECT CREATED") if $self->{_debug} > 2; return $self; } ## # $obj->create; # ------------------- # Checks to see that the table is not there. # Returns undef if it is. If the table is not # there creates the table. # # $obj->create("force"); # ----------------------------- # This will check to see if the table is there. # If it is create_table will drop the table # then create the current one. ## sub create { my $self = shift; my $force = shift || 'check'; my $opts = shift || {}; $self->{table}->connect() or return; # Error checking $self->{table}->check_schema or return; keys %{$self->{table}->cols} or return $self->fatal('NOTABLEDEFS'); if ($self->_uses_weights) { $self->_get_indexer()->pre_create_table() or return } my $table_name = $self->{table}->name(); # Force the creation if force is specified if ($force eq 'force') { $self->debug("Forcing the table creation") if $self->{_debug} > 1; my $ret; { local ($SIG{__DIE__}, $@); eval { $ret = $self->{table}->do_query("SELECT COUNT(*) FROM $table_name") }; $GT::SQL::error = ''; } if (defined $ret) { $self->debug("Table $table_name exists. Dropping table") if ($self->{_debug} > 1); $self->drop_table; } else { $self->debug("Not dropping table $table_name because it does not exist") if $self->{_debug} > 1; } } elsif ($force eq 'check' or $force eq 'upgrade' ) { my $ret; { local ($SIG{__DIE__}, $@); eval { $ret = $self->{table}->do_query("SELECT COUNT(*) FROM $table_name") }; $GT::SQL::error = ''; } if (defined $ret) { if ( $force eq 'upgrade' ) { return $self->_consolidate( $opts ); } else { return $self->warn(TBLEXISTS => $table_name); } } } $self->{table}->{driver}->create_table($force) or return; # Set up some defaults $self->set_defaults; $self->{table}->save_state or return; # now that the table has been made, if the user has requested weighted-indexing of tables, handle that if ($self->_uses_weights) { $self->_get_indexer()->post_create_table() or return } # then handle anything related to file databases $self->_file_create_tables(); return 1; } sub _uses_weights { #------------------------------------------------------------------------------- return keys %{$_[0]->{table}->weight()} } $COMPILE{_get_indexer} = __LINE__ . <<'END_OF_SUB'; sub _get_indexer { #------------------------------------------------------------------------------- my $self = shift; $self->debug("CREATING GT::SQL::Indexer OBJECT") if $self->{_debug} > 2; require GT::SQL::Search; my $indexer = GT::SQL::Search->load_indexer( table => $self->{table}, debug => $self->{_debug} ); return $indexer; } END_OF_SUB $COMPILE{_file_create_tables} = __LINE__ . <<'END_OF_SUB'; sub _file_create_tables { # creates file upload tables if required my $self = shift; if ( $self->{table}->_file_cols() ) { # ... create the table because we have file columns require GT::SQL::File; my $ftable = GT::SQL::File->new( table => $self->{table}, connect => $self->{connect} ); $ftable->debug_level($self->{_debug}); $ftable->install({ parent_tablename => $self->{table}->name() }); }; $self->{table}->_file_cols(1); } END_OF_SUB sub set_defaults { my $self = shift; my %cols = ref $_[0] ? %{shift()} : $self->{table}->cols(); my %file_defs = (form_type => 'FILE', form_size => '20', file_save_in => '.', file_save_scheme => 'HASHED'); for my $col (keys %cols) { my $attrib = $cols{$col}; if ($attrib->{type} =~ /char/i) { $self->{table}->{schema}->{cols}->{$col}->{form_type} ||= 'TEXT'; $self->{table}->{schema}->{cols}->{$col}->{form_size} ||= 20; if ($attrib->{form_type} and $attrib->{form_type} =~ /file/i) { my $col_info = $self->{table}->{schema}->{cols}->{$col}; for (qw(form_type form_size file_save_in file_save_scheme)) { $col_info->{$_} ||= $file_defs{$_} unless $col_info->{$_}; } $col_info->{file_log_path} ||= $col_info->{file_save_in}; } } elsif ($attrib->{type} =~ /text|blob/i) { $self->{table}->{schema}->{cols}->{$col}->{form_type} ||= 'TEXTAREA'; $self->{table}->{schema}->{cols}->{$col}->{form_size} ||= 30; } elsif ($attrib->{type} =~ /int|double|float/i) { $self->{table}->{schema}->{cols}->{$col}->{form_type} ||= 'TEXT'; $self->{table}->{schema}->{cols}->{$col}->{form_size} ||= 10; } elsif ($attrib->{type} =~ /enum/i) { $self->{table}->{schema}->{cols}->{$col}->{form_type} ||= 'SELECT'; } elsif ($attrib->{type} =~ /date|timestamp/i) { $self->{table}->{schema}->{cols}->{$col}->{form_type} ||= 'DATE'; $self->{table}->{schema}->{cols}->{$col}->{form_size} ||= 20; } } } ## # $obj->load_table; # ----------------- # Creates a schema based on an existing sql # table and saves it. ## $COMPILE{load_table} = __LINE__ . <<'END_OF_SUB'; sub load_table { my $self = shift; $self->{table}->connect() or return; $self->_load_table(@_) or return; $self->{table}->save_state() or return; } END_OF_SUB $COMPILE{_load_table} = __LINE__ . <<'END_OF_SUB'; sub _load_table { my $self = shift; $self->debug("DESCRIBE $self->{table}->{name}") if $self->{_debug}; my $sth = $self->{table}->{driver}->prepare("DESCRIBE $self->{table}->{name}") or return; $sth->execute() or return; my ($pos, %index, %unique, %cols, @pk, %other) = (1); # Default to the current ai value, if any, because some databases don't # associate an increment to a value (such a postgres, where sequences are # completely separate from tables and columns) my $ai = $self->{table}->ai; my $table_name = $self->{table}->name; my %col_case_map = map { lc $_ => $_ } keys %{$self->{table}->cols}; my %index_case_map = map { lc $_ => $_ } keys %{$self->{table}->index}; my %unique_case_map = map { lc $_ => $_ } keys %{$self->{table}->unique}; # Get the column defintions. while (my $col = $sth->fetchrow_hashref) { my $name = $col_case_map{lc $col->{Field}} || $col->{Field}; my $type = $col->{Type}; my $not_null = $col->{Null} ? 0 : 1; my $default = ($col->{Default} and $col->{Default} ne 'NULL') ? $col->{Default} : undef; $ai = $name if $col->{Extra} and $col->{Extra} =~ /AUTO/i; $_ = $type; if (/^((?:var)?char)\((\d+)/i) { %other = (type => uc $1, size => $2); $other{binary} = 1 if /binary/i; } elsif (/^(var)?binary\((\d+)/i) { %other = (type => "\U${1}char", size => $2); $other{binary} = 1; } elsif (/^((?:tiny|small|medium|big)?int)/i) { %other = (type => uc $1); $other{zerofill} = 1 if /zerofill/i; $other{unsigned} = 1 if /unsigned/i; } # decimal(10,5) elsif (/^(?:decimal)\((\d+),\s*(\d+)\)/i) { %other = (type => 'DECIMAL', precision => $1, scale => $2); $other{zerofill} = 1 if /zerofill/i; } elsif (/^(?:double|float8)/i) { %other = (type => 'DOUBLE'); $other{zerofill} = 1 if /zerofill/i; } elsif (/^(?:float|real)/i) { %other = (type => 'REAL'); $other{zerofill} = 1 if /zerofill/i; } elsif (/^(datetime|date|timestamp|time|year|(?:tiny|medium|long)?(?:text|blob))/i) { %other = (type => uc $1); } elsif (/^enum\('([^\)]+)'\)/i) { %other = ( type => 'ENUM', values => [split /'\s*,\s*'/, $1] ); } else { return $self->fatal(BADTYPE => $type); } my %col = ( pos => $pos, %other ); $col{default} = $default if defined $default; $col{not_null} = 1 if $not_null; $cols{$name} = \%col; $pos++; } # Retrieve index information $sth = $self->{table}->{driver}->prepare("SHOW INDEX FROM $self->{table}->{name}") or return; $sth->execute() or return; my ($pk_index_name, @pk_index_cols); while (my $index = $sth->fetchrow_hashref) { my $name = lc $self->{table}->{driver}->extract_index_name($self->{table}->{name}, $index->{index_name}); $name = ($index->{index_unique} ? $unique_case_map{$name} : $index_case_map{$name}) || $name; my $field = $col_case_map{lc $index->{index_column}} || $index->{index_column}; if ($index->{index_primary}) { push @pk, $field if $index->{index_primary}; # Ignore primary indexes that we don't know about because pk's CAN # overlap regular indexes in some databases next unless exists $unique_case_map{$name} or exists $index_case_map{$name}; } if ($index->{index_unique}) { push @{$unique{$name}}, $field; } else { push @{$index{$name}}, $field; } } my $old_cols = $self->{table}->cols; for my $col (keys %cols) { for my $val (keys %{$old_cols->{$col}}) { $cols{$col}->{$val} = $old_cols->{$col}->{$val} unless exists $cols{$col}->{$val}; } } $self->{table}->cols(\%cols); $self->{table}->pk(@pk); $self->{table}->ai($ai || ''); $self->{table}->index(\%index); $self->{table}->unique(\%unique); return 1; } END_OF_SUB ## # $obj->drop_table; # ----------------- # Drops the current table. ## $COMPILE{drop_table} = __LINE__ . <<'END_OF_SUB'; sub drop_table { my $self = shift; require GT::SQL::Editor; # Were ->{fk} there, Editor would wipe the current table from all fk_tables my $fk = delete $self->{table}->{schema}->{fk}; my $ret = GT::SQL::Editor->new( debug => $self->{_debug}, table => $self->{table}, connect => $self->{table}->{connect} )->drop_table(@_); $self->{table}->{schema}->{fk} = $fk; $ret; } END_OF_SUB ## # $obj->clear_schema # ------------------ # Resets the schema to an empty schema. ## sub clear_schema { my $self = shift; %{$self->{table}->{schema}} = ( index => {}, unique => {}, cols => {}, pk => [], fk => {}, subclass => {}, ai => '', fk_tables => [] ); $self->{table}->{search_driver} = 'NONINDEXED'; } ## # $obj->cols($hash_ref); # --------------------------- # Sets the relations columns as specified by $hash_ref. # the hash should look like { $col_name => { type => 'int' } }. # # $obj->cols( # $col1 => { # type => 'int', # not_null => 1 # }, # $col2 => { ... } # ); # -------------------------- # Sets the relations columns as specified via method # params. ## sub cols { my $self = shift; return $self->{table}->cols(@_); } ## # $obj->pk($array_ref); # -------------------------- # Sets relation primary key, $array_ref is the # reference to an array which looks like # ["FIELD1", ..., "FIELDN"] # # $obj->pk($field1, $field2, ...); # ------------------------------------- # Sets relation primary key given the fields # which are in parameter. ## sub pk { my $self = shift; $self->{table}->pk(@_) or return; return 1; } ## # $obj->ai($column); # ----------------------- # Sets the AUTO INCRIMENT column. ## sub ai { my $self = shift; $self->{table}->ai(@_) or return; return 1; } ## # $obj->name($table_name); # ----------------------------- # Sets the name for the table to create. ## sub name { my $self = shift; $self->{table}->name(@_) or return; return 1; } ## # $obj->form_display($nice_name); # ------------------------ # Sets the name of the table as it is displayed # using the Display module. ## sub form_display { my $self = shift; $self->{table}->form_display(@_) or return; return 1; } ## # $obj->index($index_name, $col1, ..., $coln); # ------------------------------------------------- # Sets an index called $index_name handling $col1, # ..., $coln. # # $obj->index( # { # $index1 => [field1, field2], # $index2 => [field3, field4] # } # ); # ---------------------------------------------- # Sets indexes for this table specified by the key # with the values as the fields. ## sub index { my $self = shift; $self->{table}->index(@_) or return; return 1; } ## # $obj->search_driver( $searching_driver ); # -------------------------------------------------- ## sub search_driver { my $self = shift; $self->{table}->search_driver(@_) or return; return 1; } ## # $obj->unique($index_name, $col1, ..., $coln); # -------------------------------------------------- # Sets an unique index called $index_name handling $col1, # ..., $coln. # # $obj->unique({ # $index1 => [field1, field2], # $index2 => [field3, field4] # }); # -------------------------------- # Sets uniques for this table specified by the key # with the values as the fields. ## sub unique { my $self = shift; $self->{table}->unique(@_) or return; return 1; } ## # $obj->fk({ # RELATION_NAME => { # SOURCE_FIELD_1 => TARGET_FIELD_1, # ... # SOURCE_FIELD_n => TARGET_FIELD_n # } # }); # ----------------------------------------- # You can set all the relations for the tables this way. # sets the source and target schemas for the given relation # name. Source and target schemas shall have the same type ! # # $obj->fk(RELATION_NAME => { SOURCE_FIELD_1 => TARGET_FIELD }); # -------------------------------------------------------------- # Sets the foreign key relations for one relation. # # this structure introduces a limitations: a table cannot # refer two schemas in the same target table, which should # really not be a problem. ## sub fk { my $self = shift; $self->{table}->fk(@_) or return; return 1; } sub subclass { return shift->{table}->subclass(@_) } ## # $obj->save_schema # Saves the schema (.def) file. Useful when loading tables # that already exist, but you don't want to overwrite. ## sub save_schema { return unless ($_[0]->{table}); return $_[0]->{table}->save_state(); } $COMPILE{_consolidate} = __LINE__ . <<'END_OF_SUB'; sub _consolidate { #------------------------------------------------------------------------------- my $self = shift; my $opts = shift; my $long_name = $self->{table}->{name}; my $table_name = $long_name; my $prefix = $self->{connect}->{PREFIX}; $table_name =~ s,^$prefix,,; my $file = "$self->{connect}->{def_path}/$long_name.def"; # $self->clear_schema(); my $table = $self->{table}->table( $table_name ) or die $GT::SQL::error; $table->connect(); my $source = $table->{schema}; my $destination = $self->{table}->{schema}; # HANDLE COLUMNS my $s_cols = $source->{cols}; my $d_cols = $destination->{cols}; # special vars my ( %POSITION, %CHANGED, %REMOVED, %ADDED ); # compare the table columns from source to destination my ( $cols, %col_order ); %col_order = map { $_ => $s_cols->{$_}->{'pos'} } keys %$s_cols; for my $col_name ( keys %col_order ) { if ( $d_cols->{$col_name} ) { if ( _is_different( $d_cols->{$col_name}, $s_cols->{$col_name} ) ) { for my $option ( %{$d_cols->{$col_name}} ) { my $d_opts = $d_cols->{$col_name}; my $s_opts = $s_cols->{$col_name}; if ( $option eq 'pos' ) { if ( $d_opts->{pos} != $s_opts->{pos} ) { $POSITION{$col_name} = $d_opts; }; } elsif ( ref $d_opts->{$option} eq 'ARRAY' ) { my $d_ar = $d_opts->{$option}; my $s_ar = $s_opts->{$option}; if ( @$d_ar != @$s_ar ) { $CHANGED{$col_name} = $d_cols->{$col_name}; } else { for my $index ( 0..( scalar(@$d_ar)-1 ) ) { if ( $d_ar->[$index] != $s_ar->[$index] ) { $CHANGED{$col_name} = $d_cols->{$col_name}; } } } } else { ( $d_opts->{$option} ne $s_opts->{$option} ) and $CHANGED{$col_name} = $d_cols->{$col_name}; } } } } else { $REMOVED{$col_name} = $s_cols->{$col_name}; }; } # compare the table columns from destination to source %col_order = map { $_ => $d_cols->{$_}->{'pos'} } keys %$d_cols; for my $col_name ( keys %col_order ) { if ( !$s_cols->{$col_name} ) { $ADDED{$col_name} = $d_cols->{$col_name}; } } # HANDLE INDEXES my $d_idx = $destination->{index}; my $s_idx = $source->{index}; my %index_order = map { $_ => 1 } ( keys %$d_idx, keys %$s_idx ); my %INDEXES = (); for my $idx_name ( keys %index_order ) { if ( $d_idx->{$idx_name} and $d_idx->{$idx_name} ) { my $s_cols = join "|", sort @{$d_idx->{$idx_name} || []}; my $d_cols = join "|", sort @{$s_idx->{$idx_name} || []}; if ( $s_cols ne $d_cols ) { $INDEXES{$idx_name} = $d_idx->{$idx_name}; } else { $INDEXES{$idx_name} = 'EQ'; } } elsif ( !$d_idx->{$idx_name} and $s_idx->{$idx_name} ) { $INDEXES{$idx_name} = 'REMOVED'; } elsif ( !$s_idx->{$idx_name} and $d_idx->{$idx_name} ) { $INDEXES{$idx_name} = 'ADDED'; } } # HANDLE AUTOINCREMENT my $AI = undef; if ( $destination->{ai} eq $source->{ai} ) { $AI = 'EQ'; } else { $AI = $destination->{ai}; } # HANDLE PK my $PK = undef; $d_cols = join "|", sort @{$destination->{pk} || []}; $s_cols = join "|", sort @{$source->{pk} || []}; if ( $d_cols eq $s_cols ) { $PK = 'EQ'; } else { $PK = $destination->{pk}; } # HANDLE FK my %FK = (); my $d_fk = $destination->{fk}; my $s_fk = $source->{fk}; %index_order = map { $_ => 1 } ( keys %$d_fk, keys %$s_fk ); for my $col_name ( keys %$d_fk ) { if ( _is_different( $d_fk->{ $col_name }, $s_fk->{ $col_name } ) ) { $FK{$col_name} = $s_fk->{ $col_name }; } else { $FK{$col_name} = 'EQ'; } } # HANDLE SUBCLASS my %SUBCLASS = (); my $d_sc = $destination->{subclass}; my $s_sc = $source->{subclass}; %index_order = map { $_ => 1 } ( keys %$d_sc, keys %$s_sc ); for my $key ( keys %index_order ) { if ( _is_different( $d_fk->{ $key }, $s_fk->{ $key } ) ) { $SUBCLASS{ $key } = $d_fk->{ $key } ; } else { $SUBCLASS{ $key } = 'EQ'; } } # HANDLE UNIQUE my $d_uni = $destination->{unique}; my $s_uni = $source->{unique}; my %unique_order = map { $_ => 1 } ( keys %$d_uni, keys %$s_uni ); my %UNIQUE = (); for my $idx_name ( keys %unique_order ) { if ( $d_uni->{$idx_name} and $d_uni->{$idx_name} ) { my $s_cols = join "|", sort @{$d_uni->{$idx_name}}; my $d_cols = join "|", sort @{$s_uni->{$idx_name}}; if ( $s_cols ne $d_cols ) { $UNIQUE{$idx_name} = $d_uni->{$idx_name}; } else { $UNIQUE{$idx_name} = 'EQ'; } } elsif ( !$d_uni->{$idx_name} and $s_uni->{$idx_name} ) { $UNIQUE{$idx_name} = 'REMOVED'; } elsif ( !$s_uni->{$idx_name} and $d_uni->{$idx_name} ) { $UNIQUE{$idx_name} = 'ADDED'; } }; # Summon callback if required $opts->{callback} and ( &{$opts->{callback}}( $self, $table, \%POSITION, \%CHANGED, \%REMOVED, \%ADDED, \%INDEXES, $AI, $PK, \%SUBCLASS, \%UNIQUE ) or return ); # if position movements are required we must read all the data into a temp # table first my $DO_POSITION = 0; $DO_POSITION = $self->_create_temp_table( $table ); # ... change columns drop the columns my $sth = $table->do_query(qq!DROP TABLE $long_name!) or die $GT::SQL::error; # change the columns that have to be changed. $self->create( 'force' ) or die $GT::SQL::error; # ... add the columns that have been removed in the past if ( %REMOVED and $self->{carry_over_columns} ) { my $editor = $self->{table}->editor($table_name); my $pos = scalar( keys %{$destination->{cols}} ); for my $col_name ( sort { $REMOVED{$a}->{pos} <=> $REMOVED{$b}->{pos} } keys %REMOVED ) { $REMOVED{$col_name}->{pos} = ++$pos; $editor->add_col( $col_name, $REMOVED{$col_name} ) or die $GT::SQL::error; } } # ... now copy the data over $cols = $source->{cols}; my $copy_cols = join ",", sort { $cols->{$a}->{pos} <=> $cols->{$b}->{pos} } grep { $self->{carry_over_columns} ? 1 : not $REMOVED{$_} } keys %$cols; $table->do_query(qq! INSERT INTO $long_name ($copy_cols) SELECT $copy_cols FROM $DO_POSITION !) or die $GT::SQL::error; if ( %CHANGED ) { my $editor = $self->{table}->editor($table_name); for my $col_name ( keys %CHANGED ) { $editor->alter_col( $col_name, $CHANGED{$col_name} ); } } return 1; } END_OF_SUB $COMPILE{_create_temp_table} = __LINE__ . <<'END_OF_SUB'; sub _create_temp_table { #------------------------------------------------------------------------------- # my $self = shift; my $table = shift; my $source = $table->{schema}; my $def_path = $self->{connect}->{def_path}; use GT::MD5; my $table_name = ''; while ( -e ( $def_path . ( $table_name = GT::MD5::md5_hex( time() * rand() * 10000 ) ) ) ) {}; my $c = $table->creator( $table_name ); my $struct = _copy_struct( $source ); $struct->{fk_tables} = {}; $struct->{fk} = {}; $struct->{subclass} = {}; for ( values %{$struct->{cols}} ) { delete $_->{weight}; } $c->cols( %{$struct->{cols}} ); %{$c->{table}->{schema}} = %$struct; $c->create( "force" ) or die $GT::SQL::error; my $tbl = $table->table( $table_name ); my $s_name = $table->name(); my $d_name = $tbl->name(); $tbl->connect(); $tbl->do_query(qq|INSERT INTO $d_name SELECT * FROM $s_name|) or die $GT::SQL::error; return $table_name; } END_OF_SUB $COMPILE{_copy_struct} = __LINE__ . <<'END_OF_SUB'; sub _copy_struct { #------------------------------------------------------------------------------- # my $source = shift; my $copied_struct = undef; if ( ref $source eq 'HASH' ) { $copied_struct = {}; for my $key ( keys %$source ) { $copied_struct->{ $key } = _copy_struct( $source->{$key} ); } } elsif ( ref $source eq 'ARRAY' ) { $copied_struct = []; for my $element ( @$source ) { push @$copied_struct, _copy_struct( $element ); } } else { $copied_struct = $source; } return $copied_struct; } END_OF_SUB $COMPILE{_is_different} = __LINE__ . <<'END_OF_SUB'; sub _is_different { #------------------------------------------------------------------------------- my ( $source, $destination ) = @_; if ( ref $source ne ref $destination ) { return 1 } if ( ref $source eq 'HASH' ) { my %keys = map { $_ => 1 } ( keys %$source, keys %$destination ); for my $key ( keys %keys ) { _is_different( $source->{$key}, $destination->{$key} ) and return 1; } } elsif ( ref $source eq 'ARRAY' ) { my $ca = scalar(@$source); my $cb = scalar(@$destination); my $count = ( $ca > $cb ) ? $ca : $cb; for my $index ( 0 .. ( $count - 1 ) ) { _is_different( $source->[$index], $destination->[$index] ) and return 1; } } else { ( $source ne $destination ) and return 1; } return; } END_OF_SUB $COMPILE{_compare} = __LINE__ . <<'END_OF_SUB'; sub _compare { #------------------------------------------------------------------------------- # takes a hashref or arrayref and compares the two # my ( $source, $destination ) = @_; if ( ref $source ne ref $destination ) { return [ 'NE_TYPES', ref $source, ref $destination ]; } if ( ref $source eq 'HASH' ) { return _comp_hash( $source, $destination ); } elsif ( ref $source eq 'ARRAY' ) { return _comp_array( $source, $destination ); } else { return; } } END_OF_SUB $COMPILE{_comp_hash} = __LINE__ . <<'END_OF_SUB'; sub _comp_hash { #------------------------------------------------------------------------------- my ( $source, $destination ) = @_; my %errs; my %keys = map { $_ => 1 } ( keys %$source, keys %$destination ); for my $key ( keys %keys ) { my $src = $source->{$key}; my $dst = $destination->{$key}; if ( ref $src or ref $dst ) { $errs{$key} = _compare( $src, $dst ); } elsif ( $src eq $dst ) { $errs{$key} = 'EQ'; } else { $errs{$key} = [ 'NE', $src, $dst ]; } } return \%errs; } END_OF_SUB $COMPILE{_comp_array} = __LINE__ . <<'END_OF_SUB'; sub _comp_array { #------------------------------------------------------------------------------- my ( $source, $destination ) = @_; my @errs; my $ca = scalar(@$source); my $cb = scalar(@$destination); my $count = ( $ca > $cb ) ? $ca : $cb; for my $index ( 0 .. ( $count - 1 ) ) { my $src = $source->[$index]; my $dst = $destination->[$index]; if ( ref $src or ref $dst ) { push @errs, _compare( $src, $dst ); } elsif ( $src eq $dst ) { push @errs, 'EQ'; } else { push @errs, [ 'NE', $src, $dst ]; } } return \@errs; } END_OF_SUB 1; __END__ =head1 NAME GT::SQL::Creator - an object to create SQL tables. =head1 SYNOPSIS my $creator = $DB->creator('Newtable'); $creator->cols( col1 => { pos => 1 type => 'CHAR', size => 50 }, col2 => { pos => 2, type => 'INT', not_null => 1 } ); $creator->pk('col2'); $creator->ai('col2'); $creator->create or die "Unable to create: $GT::SQL::error"; =head1 DESCRIPTION A creator object is used to build new SQL tables. To get a new creator object, you need to call creator() from an existing GT::SQL object. The object that is returned has methods to set up your table. You will need to call this method for each table you want to create. $creator = $obj->creator($table); You must pass in the name of the table you want to create. This means if you have a table named C you must call C<-Ecreator> with C<'MyTable'> as the argument. $creator = $obj->creator('MyTable'); From this point you can call create methods on your creator object to define and create your table. =head2 cols I is used to define the columns that will be in the new table by setting properties such as the type, whether it allows null values, unsigned etc. For detailed information on the types and options accepted, please see L. The following describes the options accepted that do not directly affect the underlying database: =over 4 =item values This specifies the values for the I column type. If you are using an I this must be set. The value for this should be an array reference of the possible values for the I column. The values in the array that is passed in will be quoted by DBI's quote method. =item regex This is a regex that the value must pass before being inserted into the database. =item form_display This is a "pretty name" that will be used by the HTML module for creating attractive forms automatically. =item form_size This is the form field length to be used by the HTML module. =item form_type This is the type of form to use by the HTML module: select, checkbox radio, text, textarea or hidden. =item form_names This is for multi select or checkboxes and is an array ref of names that get displayed. =item form_values This is for multi select or checkboxes and is an array ref of the actual values that will be stored in the database. =item time_check This is only useful for TIMESTAMP fields. If set to 1, the module will not allow you to update a record which has an older timestamp then what is in the database. This is very helpful for protecting against multiple updates. =item weight By giving an item a weight, GT::SQL will maintain a search index table, and use that search index table when called using query. This is only useful for indexing large text fields and should not be used normally. The higher the weight, the more influence that column will have on the result. So if a Title was set to weight 3 and a Description to weight 1, then when doing a search, a match in the title would make the result appear before a match in the description. =back So an example would look like: $creator->cols( $col1 => { type => 'ENUM', values => ['val1', 'val2' ... ], not_null => 1 }, $col2 => { ... } ); Sets the relations columns as specified via method parameters. The only required key for the has is type. However some column types require other values be set such as I requires you specify the values. =head2 pk C lets you specify the primary keys for the current table. This method can be called with an array of primary key columns in which case all the specified column names in the array will make up the primary keys. If you call it with a single scalar value this is assumed to be the primary key for the table. $creator->pk($field1, $field2, ...); =head2 ai This specifies the auto increment column for the current table. There can be only one auto increment column per table, it must be a numeric type, it must be not null and it must be the primary key. This limitation is checked when you call create. If it is not a numeric column type you will get a fatal error when you call create. If any of the other limitations fail the creator class will correct. =head2 index C allows you to specify the name and the columns for you table indexes. There are two ways to call this method. You can set up all your indexes at once by calling it with hash reference like this: $creator->index({ $index1 => [field1, field2], $index2 => [field3, field4] }); The keys to this hash reference are the index names and the values are an array reference containing the columns that are part of the named index. The order for these columns are maintained during the create. You can also pass in one index at a time like this; $creator->index($index_name, $col1, ..., $coln); The first argument is the name of the index and all the rest are treated as columns that are part of this index. Again the order of the columns are maintained. =head2 unique The C method allows you to specify the unique indexes for the current table. This method takes the same arguments as the C method. =head2 fk C allows you to specify foreign key relations for your tables. You CAN NOT specify foreign keys for tables that have not been created yet. There are two ways to pass in arguments to C. The first way is passing in a hash reference. $creator->fk({ $FOREIGN_TABLE_NAME => { $LOCAL_TABLE_COL_1 => $FOREIGN_TABLE_COL_1, ... $LOCAL_TABLE_COL_n => $FOREIGN_TABLE_COL_n } }); The keys to the hash are the names of the tables you are relating to. The values are a hash reference that contain the name of the current tables columns as the keys and the name of the foreign tables columns that we are relating to as the values. You cannot relate fields to your self. You also need to be careful not to create circular references. This is checked when you call this method. If there is a circular reference detected you will receive a fatal error. Foreign keys currently effect selects only. =head2 search_driver This affects how the weighted records are indexed. By default the system will attempt to use best driver for the DBMS. However, if you'd like to force the indexing system to an alternative type, such as for MYSQL you can use this. * note: though the MYSQL driver is faster, the internal indexing system has better support for phrase searching and keyword searching. To set the driver, call C with the appropriate driver name. The following example will force the system into using the internally implemented indexing scheme. $creator->search_driver('INTERNAL'); Currently, the only other valid option is "MYSQL". -note- The MYSQL driver occasionally behaves oddly with a small number of records. In that case, set the search scheme to "INTERNAL". =head2 create This is the method you call to create your table after you have specified all your table definitions. Several checks are made when this method is called to ensure the table is created correctly. One of the things that is done is checking to see that the table you are trying to create does not exist. If the table does exist I will return undefined and set the error in $GT::SQL::error. You can specify to have C drop the table by passing in "force". $creator->create('force'); -or- $creator->create; C returns true on success and undef on failure. =head1 COPYRIGHT Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved. http://www.gossamer-threads.com/ =head1 VERSION Revision: $Id: Creator.pm,v 1.74 2004/09/22 02:43:29 jagerman Exp $ =cut