# ================================================================== # Gossamer Threads Module Library - http://gossamer-threads.com/ # # GT::SQL::Driver # CVS Info : 087,071,086,086,085 # $Id: Driver.pm,v 2.6 2005/11/03 01:38:30 jagerman Exp $ # # Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved. # ================================================================== # # Description: # Overview: This implements a driver class. # package GT::SQL::Driver; # =============================================================== use strict; use GT::SQL::Table; use GT::AutoLoader; use GT::SQL::Driver::Types; use GT::SQL::Driver::debug; use Exporter(); require GT::SQL::Driver::sth; use vars qw/%CONN @ISA $DEBUG $VERSION $ERROR_MESSAGE $ATTRIBS %QUERY_MAP/; use constant PROTOCOL => 2; $ATTRIBS = { name => '', schema => '', dbh => '', connect => {} }; $ERROR_MESSAGE = 'GT::SQL'; $VERSION = sprintf "%d.%03d", q$Revision: 2.6 $ =~ /(\d+)\.(\d+)/; $DEBUG = 0; @ISA = qw/GT::SQL::Driver::debug/; %QUERY_MAP = ( # QUERY => METHOD (will be prefixed with '_prepare_' or '_execute_') CREATE => 'create', INSERT => 'insert', ALTER => 'alter', SELECT => 'select', UPDATE => 'update', DROP => 'drop', DELETE => 'delete', DESCRIBE => 'describe', 'SHOW TABLES' => 'show_tables', 'SHOW INDEX' => 'show_index' ); $DBI::errstr if 0; sub load_driver { # ----------------------------------------------------------------------------- # Loads a sub-driver (i.e. GT::SQL::Driver::MYSQL, GT::SQL::Driver::PG, etc.), # and creates and returns a new driver object. The first argument should be # the name of the driver (e.g. 'PG'), and the remaining arguments are passed to # new() - which could well be handled by the driver. # my ($class, $driver, @opts) = @_; # Old GT::SQL versions had an 'ODBC' driver that wasn't an ODBC driver, but an # MSSQL driver that used ODBC. $driver = 'MSSQL' if $driver eq 'ODBC'; my $pkg = "GT::SQL::Driver::$driver"; my $lib_path = $INC{'GT/SQL/Driver.pm'}; $lib_path =~ s|GT/SQL/Driver\.pm$||; { # Ensure that the driver is loaded from the same location as GT/SQL/Driver.pm local @INC = ($lib_path, @INC); require "GT/SQL/Driver/$driver.pm"; } my $protocol = $pkg->protocol_version; return $class->fatal(DRIVERPROTOCOL => PROTOCOL, $protocol) if $protocol != PROTOCOL; return $pkg->new(@opts); } sub new { # ----------------------------------------------------------------------------- # Generic new() method for drivers to inherit; load_driver() should be used # instead to get a driver object. # my $this = shift; my $class = ref $this || $this; my $self = bless {}, $class; my $opts = $self->common_param(@_) or return $self->fatal(BADARGS => "$class->new(HASH REF or HASH); invalid parameter: '@_'"); # Otherwise we need to make sure we have a schema. $opts->{schema} and ref $opts->{schema} or return $self->fatal(BADARGS => "$class->new(HASH REF or HASH); must specify schema and name"); $self->{name} = $opts->{name}; $self->{schema} = $opts->{schema}; $self->{connect} = $opts->{connect}; $self->{_debug} = $opts->{debug} || $DEBUG; $self->{_err_pkg} = $opts->{_err_pkg} || 'GT::SQL'; $self->{dbh} = undef; $self->{hints} = { $self->hints }; $self->debug("New driver object loaded from table: $self->{name}.") if ($self->{_debug} > 2); return $self; } # This method is designed to be subclassed to provide "hints" for simple, small # differences between drivers, which simplifies the code over using a subclass. # It returns a hash of hints, with values of "1" unless otherwise indicated. # Currently supported hints are: # case_map # Corrects ->fetchrow_hashref column case when the database doesn't # prefix_indexes # Indexes will be prefixed with the table name (including the table's prefix) # fix_index_dbprefix # Look for erroneous (db_prefix)(index) when dropping indexes # now # Specifies an SQL value to use instead of NOW() (for 'time_check' columns, among other things) # bind # An array ref of: [\%BIND_HASH, ('COLUMNTYPE' => $bind_type, 'TYPE2' => $bind_type2, ...)] for drivers that need special placeholder binding for certain column types # ai # Contains a string to use for an AI column; or a code reference that is passed ($table, $column) and returns the string, or an array reference of queries to run to create the ai sequence after the column/table has been created # drop_pk_constraint # use ALTER TABLE ... DROP CONSTRAINT pkeyname to drop a primary key sub hints { () } # Removing the () breaks under 5.00404, as it will return @_ in list context $COMPILE{protocol_version} = __LINE__ . <<'END_OF_SUB'; sub protocol_version { # ----------------------------------------------------------------------------- # This checks the GT::SQL::Driver protocol, and dies if the versions aren't # equal. The protocol version only changes for major driver changes such as # the v2.000 version of this module, which had the drivers do their own queries # (as opposed to the previous hack of having drivers trying to return alternate # versions of MySQL's queries). All protocol v2 and above drivers are required # to override this - any driver that does not is, by definition, a protocol v1 # driver. # # The current protocol version is defined by the PROTOCOL constant - but # drivers that haven't overridden protocol_version() are, by definition, v1. # 1; } END_OF_SUB sub available_drivers { # ----------------------------------------------------------------------------- # Returns a list of available GT::SQL::Driver::* drivers # my $driver_path = $INC{'GT/SQL/Driver.pm'}; $driver_path =~ s/\.pm$//; my $dh = \do { local *DH; *DH }; my @drivers; opendir $dh, $driver_path or return (); while (defined(my $driver = readdir $dh)) { # By convention, only all-uppercase modules are accepted as GT::SQL drivers next unless $driver =~ /^([A-Z_][A-Z0-9_]*)\.pm$/; push @drivers, $1; } @drivers; } sub connect { # ------------------------------------------------------------------- # Returns the current database handle. # my $self = shift; $self->{dbh} and return $self->{dbh}; eval { require DBI }; if ($@) { return $self->warn(CANTCONNECT => "DBI module not installed. You must install the perl database module DBI from: http://www.perl.com/CPAN/modules/by-module/DBI"); } # Make sure we have a database, otherwise probably an error. exists $self->{connect}->{database} or return $self->fatal(CANTCONNECT => "No connection string passed to tbl->connect, make sure your table object got a connection hash."); keys %{$self->{schema}} or return $self->fatal(CANTCONNECT => "Unable to connect to database without a valid schema."); my $dsn = $self->dsn($self->{connect}); my $conn_key = "$dsn\0$self->{connect}->{login}\0$self->{connect}->{password}"; if (defined $CONN{$conn_key}) { $self->{dbh} = $CONN{$conn_key}; $self->debug("Using stored connection: $dsn") if ($self->{_debug} > 1); return $CONN{$conn_key}; } # Connect to the database. $self->debug("Connecting to database with: '$dsn', '$self->{connect}->{login}', '******'") if ($self->{_debug} > 1); my $res = eval { $CONN{$conn_key} = DBI->connect($dsn, $self->{connect}->{login}, $self->{connect}->{password}, { RaiseError => $self->{connect}->{RaiseError}, PrintError => $self->{connect}->{PrintError}, AutoCommit => 1 }) or die "$DBI::errstr\n"; 1; }; $res or return $self->warn(CANTCONNECT => "$@"); $self->{dbh} = $CONN{$conn_key}; $self->debug("Connected successfully to database.") if $self->{_debug} > 1; return $self->{dbh}; } $COMPILE{dsn} = __LINE__ . <<'END_OF_SUB'; sub dsn { # ------------------------------------------------------------------- # Creates the data source name used by DBI to connect to the database. # Since this is database-dependant, this is just a stub. # require Carp; Carp::croak("Driver has no dsn()"); } END_OF_SUB $COMPILE{prepare_raw} = __LINE__ . <<'END_OF_SUB'; sub prepare_raw { # --------------------------------------------------------------- # Returns a raw sth object. # WARNING: MAKE SURE YOUR SQL IS PORTABLE AS NO ALTERATIONS WILL # BE MADE! ALSO YOU MUST MANUALLY CALL ->finish ON THESE! # my ($self, $query) = @_; $self->debug("Preparing RAW query: $query") if $self->{_debug} > 1; my $sth = $self->{dbh}->prepare($query) or return $self->warn(CANTPREPARE => $query); $self->debug("RAW STH is prepared: $query") if $self->{_debug} > 1; return $sth; } END_OF_SUB $COMPILE{prepare} = __LINE__ . <<'END_OF_SUB'; sub prepare { # --------------------------------------------------------------- # We can override whatever type of queries we need to alter by replacing # the _prepare_* functions. # my ($self, $query) = @_; if (! defined $query) { return $self->warn(CANTPREPARE => "", "Empty Query"); } # For any drivers that need hacked-in limit support (currently MS SQL and Oracle): delete @$self{qw/_limit _lim_offset _lim_rows/}; if (my $now = $self->{hints}->{now}) { $query =~ s/\bNOW\(\)/$now/g; } if ($query =~ /^\s*SHOW\s+TABLES\s*(?:;\s*)?$/i) { $self->{do} = 'SHOW TABLES'; } elsif ($query =~ /^\s*SHOW\s+INDEX\s+FROM\s+\w+\s*(?:;\s*)?$/i) { # See 'Driver-specific notes' below $self->{do} = 'SHOW INDEX'; } else { $self->{do} = uc +($query =~ /(\w+)/)[0]; } if (my $meth = $QUERY_MAP{$self->{do}}) { $meth = "_prepare_$meth"; $query = $self->$meth($query) or return; } $self->{query} = $query; $self->debug("Preparing query: $query") if $self->{_debug} > 1; $self->{sth} = $self->{dbh}->prepare($query) or return $self->warn(CANTPREPARE => $query, $DBI::errstr); my $pkg = ref($self) . '::sth'; $self->debug("CREATING $pkg OBJECT") if $self->{_debug} > 2; return $pkg->new($self); } END_OF_SUB # Define one generic prepare, and alias all the specific _prepare_* functions to it sub _generic_prepare { $_[1] } for (*_prepare_create, *_prepare_insert, *_prepare_alter, *_prepare_select, *_prepare_update, *_prepare_drop, *_prepare_delete, *_prepare_describe) { $_ = \&_generic_prepare; } # Driver-specific notes: # 'SHOW TABLES' # The driver should return single-column rows of non-system tables in the # database. The name of the column is not important, and users of SHOW TABLE # should not depend on it (i.e. do not use ->fetchrow_hashref) *_prepare_show_tables = \&_generic_prepare; # 'SHOW INDEX FROM table' # Drivers should return one row per column per index, having at least the keys: # - index_name: the name of the index # - index_column: the name of the column # - index_unique: 1 if the index is unique, 0 otherwise # - index_primary: 1 if the column is a primary key, 0 otherwise # # The rows must be grouped by index, and ordered by the position of the column # within said groupings. # # So, for a unique index named 'unique1' on columns 'col1', 'col2', 'col3', and # a normal index named 'index1' on 'col3', 'col4', and a primary key on # 'colpk', you should get (at a minimum; extra columns are permitted): # +------------+--------------+--------------+---------------+ # | index_name | index_column | index_unique | index_primary | # +------------+--------------+--------------+---------------+ # | unique1 | col1 | 1 | 0 | # | unique1 | col2 | 1 | 0 | # | unique1 | col3 | 1 | 0 | # | index1 | col3 | 0 | 0 | # | index1 | col4 | 0 | 0 | # | PRIMARY | colpk | 1 | 1 | # +------------+--------------+--------------+---------------+ # 'PRIMARY' above should be changed by drivers whose databases have named # primary keys, otherwise using 'PRIMARY' for the primary key is recommended. # # Any other information may be returned; users of this query mapping should # always use ->fetchrow_hashref, and access the above four keys for # portability. # # Note that index_primary results may overlap other indexes for some databases # - Oracle, in particular, will bind a primary key onto an existing index if # possible. In such a case, you'll get the index indicated normally, but some # of the columns may make up the primary key. For example, the following # result would indicate that there is one index on col1, col2, col3, and that # there is a primary key made up of (col1, col2): # # +------------+--------------+--------------+---------------+ # | index_name | index_column | index_unique | index_primary | # +------------+--------------+--------------+---------------+ # | index1 | col1 | 0 | 1 | # | index1 | col2 | 0 | 1 | # | index1 | col3 | 0 | 0 | # +------------+--------------+--------------+---------------+ # # Currently, results such as the above are known to occur in Oracle databases # where a primary key was added to an already-indexed column after creating the # table - other databases give primary keys an independant index. # # Although _prepare_show_index is defined here, no drivers actually satisfy the # above without some query result remapping, and as such all currently override # either this or _execute_show_index. *_prepare_show_index = \&_generic_prepare; $COMPILE{extract_index_name} = __LINE__ . <<'END_OF_SUB'; sub extract_index_name { # ----------------------------------------------------------------------------- # Takes an table name and database index name (which could be prefixed, if the # database uses prefixes) and returns the GT::SQL index name (i.e. without # prefix). my ($self, $table, $index) = @_; if ($self->{hints}->{prefix_indexes}) { $index =~ s/^\Q$table\E(?=.)//i; } $index; } END_OF_SUB sub disconnect { # ------------------------------------------------------------------- # Disconnect from the database. # my $self = shift; $self->{dbh} and $self->{dbh}->disconnect; } sub reset_env { # ------------------------------------------------------------------- # Remove all database connections that aren't still alive # @GT::SQL::Driver::debug::QUERY_STACK = (); for my $dsn (keys %CONN) { next if ($CONN{$dsn} and $CONN{$dsn}->ping); $CONN{$dsn}->disconnect if ($CONN{$dsn}); delete $CONN{$dsn}; } } sub do { # ------------------------------------------------------------------- # Do a query. # my $self = shift; ($self->prepare(@_) or return)->execute; } $COMPILE{do_raw_transaction} = __LINE__ . <<'END_OF_SUB'; sub do_raw_transaction { # ----------------------------------------------------------------------------- # Do a series of queries as a single transaction - note that this is only # supported under DBI >= 1.20; older versions of DBI result in the queries # being performed without a transaction. # This subroutine should be passed a list of queries; the queries will be run # in order. Each query may optionally be an array reference where the first # element is the query, and remaining elements are placeholders to use when # executing the query. Furthermore, you may pass a reference to the string # or array reference to specify a non-critical query. # # For example: # $self->do_raw_transaction( # "QUERY1", # \["QUERY2 ?", $value], # \"QUERY3", # ["QUERY4 ?, ?", $value1, $value2] # ); # # This will attempt to run the 4 queries, and will abort if query 1 or 4 do not # succeed. # # Also note that this is ONLY meant to be used by individual drivers as it # assumes the queries passed in are ready to run without any rewriting. As # such, any use outside of individual drivers should be considered an error. # # Returns '1' on success, undef on failure of any query (excepting non-critical # queries, see above). # my ($self, @queries) = @_; my $transaction = $DBI::VERSION >= 1.20; $self->{dbh}->begin_work if $transaction; $self->debug("Begin query transaction") if $self->{_debug}; $self->debug("Transaction not possible; DBI version < 1.20") if $self->{_debug} and not $transaction; my $time; $time = Time::HiRes::time() if $self->{_debug} and exists $INC{"Time/HiRes.pm"}; for (@queries) { my $critical = not(ref eq 'SCALAR' or ref eq 'REF'); my $q = $critical ? $_ : $$_; my ($query, @ph) = ref $q ? @$q : $q; if ($self->{_debug}) { my $debugquery = GT::SQL::Driver::debug->replace_placeholders($query, @ph); $self->debug("Executing query $debugquery"); } my $did = $self->{dbh}->do($query, undef, @ph); if (!$did and $critical) { $self->warn(CANTEXECUTE => $query => $DBI::errstr); $self->debug("Critical query failed, transaction aborted; performing transaction rollback") if $self->{_debug} and $transaction; $self->{dbh}->rollback if $transaction; return undef; } } $self->debug("Transaction complete; committing") if $self->{_debug}; $self->{dbh}->commit if $transaction; if ($self->{_debug} and exists $INC{"Time/HiRes.pm"}) { my $elapsed = Time::HiRes::time() - $time; $self->debug(sprintf("Transaction execution took: %.6fs", $elapsed)); } 1; } END_OF_SUB sub quote { # ----------------------------------------------------------- # This subroutines quotes (or not) a value. # my $val = pop; return 'NULL' if not defined $val; return $$val if ref $val eq 'SCALAR' or ref $val eq 'LVALUE'; (values %CONN)[0]->quote($val); } $COMPILE{create_table} = __LINE__ . <<'END_OF_SUB'; sub create_table { # ------------------------------------------------------------------- # Creates a table. # my $self = shift; $self->connect or return; my $table = $self->{name}; # Figure out the order of the create, and then build the create statement. my %pos = map { $_ => $self->{schema}->{cols}->{$_}->{pos} } keys %{$self->{schema}->{cols}}; my (@field_defs, $ai_queries); for my $field (sort { $pos{$a} <=> $pos{$b} } keys %{$self->{schema}->{cols}}) { my %field_def = map { defined $self->{schema}->{cols}->{$field}->{$_} ? ($_ => $self->{schema}->{cols}->{$field}->{$_}) : () } keys %{$self->{schema}->{cols}->{$field}}; my $is_ai = $self->{schema}->{ai} && $field eq $self->{schema}->{ai}; delete $field_def{default} if $is_ai; my $def = $field . ' ' . ($self->column_sql(\%field_def) or return); if ($is_ai) { my $ai = $self->{hints}->{ai} || 'AUTO_INCREMENT'; $ai = $ai->($table, $field) if ref $ai eq 'CODE'; if (ref $ai eq 'ARRAY') { $ai_queries = $ai; } else { $def .= " $ai"; } } push @field_defs, $def; } # Add the primary key. if (@{$self->{schema}->{pk}}) { push @field_defs, "PRIMARY KEY (" . join(",", @{$self->{schema}->{pk}}) . ")"; } # Create the table my $create_query = "\n\tCREATE TABLE $table (\n\t\t"; $create_query .= join ",\n\t\t", @field_defs; $create_query .= "\n\t)"; $self->do($create_query) or return; # If the database needs separate queries to set up the auto-increment, run them if ($ai_queries) { for (@$ai_queries) { $self->do($_); } } # Create the table's indexes for my $type (qw/index unique/) { my $create_index = "create_$type"; while (my ($index_name, $index) = each %{$self->{schema}->{$type}}) { $self->$create_index($table => $index_name => @$index) if @$index; } } 1; } END_OF_SUB $COMPILE{column_sql} = __LINE__ . <<'END_OF_SUB'; sub column_sql { # ----------------------------------------------------------------------------- # Converts a column definition into an SQL string used in the create table # statement, and (for some drivers) when adding a new column to a table. # my ($self, $opts) = @_; ref $opts eq 'HASH' or return $self->fatal(BADARGS => '$obj->column_sql (HASH_REF)'); $opts->{type} or return $self->fatal(BADARGS => 'Column definition does not have a SQL type defined'); my $pkg = ref($self) . '::Types'; my $type = uc $opts->{type}; if ($pkg->can($type)) { $self->debug("Using driver specific SQL for type $opts->{type}") if $self->{_debug} > 1; } elsif (GT::SQL::Driver::Types->can($type)) { $pkg = 'GT::SQL::Driver::Types'; } else { return $self->fatal(BADTYPE => $opts->{type}); } $pkg->$type({%$opts}); } END_OF_SUB $COMPILE{insert} = __LINE__ . <<'END_OF_SUB'; sub insert { # ----------------------------------------------------------------------------- # This subroutine, using a couple driver hints, handles insertions for every # driver currently supported. # my ($self, $input) = @_; my (@names, @values, @placeholders, @binds); my %got; my $ai = $self->{schema}->{ai}; my $bind = $self->{hints}->{bind}; my $cols = $self->{schema}->{cols}; while (my ($col, $val) = each %$input) { ++$got{$col}; next if $ai and $col eq $ai and !$val; push @names, $col; my $def = $cols->{$col}; if ($def->{time_check} and (not defined $val or $val eq '' or $val eq 'NOW()')) { push @values, $self->{hints}->{now} || 'NOW()'; } elsif ($def->{type} =~ /DATE/ and (not defined $val or $val eq '')) { push @values, 'NULL'; } elsif (ref $val eq 'SCALAR' or ref $val eq 'LVALUE') { push @values, $$val; } else { push @placeholders, $val; push @values, '?'; if ($bind and defined $val) { for (my $i = 1; $i < @$bind; $i += 2) { if ($def->{type} =~ /$bind->[$i]/) { push @binds, [scalar @placeholders, $col, $bind->[$i+1]]; last; } } } } } # Update any timestamp columns to current time. for my $col (keys %$cols) { next unless not $got{$col} and $cols->{$col}->{time_check}; push @names, $col; push @values, $self->{hints}->{now} || 'NOW()'; $got{$col} = 1; } # Add an auto increment field if required if ($ai and not $input->{$ai}) { my @ai_insert = $self->ai_insert($ai); if (@ai_insert) { push @names, $ai_insert[0]; push @values, $ai_insert[1]; } } # Fill in any missing defaults for my $col (keys %$cols) { next if $ai and $col eq $ai or $got{$col} or not exists $cols->{$col}->{default}; my $val = $cols->{$col}->{default}; push @names, $col; push @values, '?'; # If the column is numeric, make sure a '' becomes a null, due to # problems where old libraries or the table editor could have set the # default to '': if (defined $val and $val eq '' and $cols->{$col}->{type} =~ /^(?:INTEGER|REAL|FLOAT|DOUBLE|DECIMAL)$|INT$/) { $val = undef; } push @placeholders, $val; $got{$col} = 1; if ($bind and defined $val) { my $def = $cols->{$col}; for (my $i = 1; $i < @$bind; $i += 2) { if ($def->{type} =~ /$bind->[$i]/) { push @binds, [scalar @placeholders, $col, $bind->[$i+1]]; last; } } } } # Create the SQL and statement handle. my $query = "INSERT INTO $self->{name} ("; $query .= join ',', @names; $query .= ") VALUES ("; $query .= join ',', @values; $query .= ")"; $bind->[0]->{$query} = \@binds if $bind; my $sth = $self->prepare($query) or return; $sth->execute(@placeholders) or return; $sth; } END_OF_SUB sub ai_insert { # ----------------------------------------------------------------------------- # Returns a column name and value to use for the AI column when inserting a # row. If this returns an empty list, no value will be inserted. This will # only be called when the table has an auto-increment column, so checking is # not necessary. The sole argument passed in is the name of the column. # my ($self, $ai) = @_; return $ai, 'NULL'; } $COMPILE{insert_multiple} = __LINE__ . <<'END_OF_SUB'; sub insert_multiple { # ----------------------------------------------------------------------------- # Performs a multiple-insertion. By default, this is simply done as multiple # executes on a single insertion, and as a single transaction if under # DBI >= 1.20. # my ($self, $cols, $args) = @_; $self->{dbh}->begin_work if $DBI::VERSION >= 1.20; my $count; for my $val (@$args) { my %set; for my $i (0 .. $#$cols) { $set{$cols->[$i]} = $val->[$i]; } ++$count if $self->insert(\%set); } $self->{dbh}->commit if $DBI::VERSION >= 1.20; $count; } END_OF_SUB sub update { # ------------------------------------------------------------------- my ($self, $set, $where) = @_; my $c = $self->{schema}->{cols}; my %set; for my $cond (@{$set->{cond}}) { if (ref $cond eq 'ARRAY') { $set{$cond->[0]}++ if exists $c->{$cond->[0]} and $c->{$cond->[0]}->{time_check}; } } for my $col (keys %$c) { next unless not $set{$col} and $c->{$col}->{time_check}; $set->add($col, '=', \($self->{hints}->{now} || 'NOW()')); } my ($sql_set, $set_vals, $set_cols) = $set->sql(1); my ($sql_where, $where_vals, $where_cols) = $where->sql(1); my $i = 1; # Set up binds, if necessary my @binds; my $bind = $self->{hints}->{bind}; if ($bind) { for my $col (@$set_cols) { next unless exists $c->{$col}; for (my $j = 1; $j < @$bind; $j += 2) { if ($c->{$col}->{type} =~ /$bind->[$j]/) { push @binds, [scalar $i, $col, $bind->[$j+1]]; last; } } $i++; } } my $query = "UPDATE $self->{name} SET $sql_set"; $query .= " WHERE $sql_where" if $sql_where; $bind->[0]->{$query} = \@binds if $bind; my $sth = $self->prepare($query) or return; $sth->execute(@$set_vals, @$where_vals) or return; $sth; } sub delete { # ------------------------------------------------------------------- my ($self, $where) = @_; my ($sql_where, $where_vals) = $where ? $where->sql(1) : (); my $sql = "DELETE FROM $self->{name}"; $sql .= " WHERE $sql_where" if $sql_where; my $sth = $self->prepare($sql) or return; $sth->execute(@$where_vals) or return; $sth; } sub select { # ------------------------------------------------------------------- my ($self, $field_arr, $where, $opts) = @_; my ($fields, $opt_clause) = ('', ''); if (ref $field_arr and @$field_arr) { $fields = join ",", @$field_arr; } else { $fields = '*'; } my ($sql_where, $where_vals) = $where->sql(1); $sql_where and ($sql_where = " WHERE $sql_where"); if ($opts) { for my $opt (@$opts) { next if (! defined $opt); $opt_clause .= " $opt"; } } my $sql = "SELECT $fields FROM " . $self->{name}; $sql .= $sql_where if $sql_where; $sql .= $opt_clause if $opt_clause; my $sth = $self->prepare($sql) or return; $sth->execute(@$where_vals) or return; $sth; } $COMPILE{drop_table} = __LINE__ . <<'END_OF_SUB'; sub drop_table { # ------------------------------------------------------------------- # Drops the table passed in. # my ($self, $table) = @_; $self->do("DROP TABLE $table"); } END_OF_SUB $COMPILE{column_exists} = __LINE__ . <<'END_OF_SUB'; sub column_exists { # ----------------------------------------------------------------------------- # Returns true or false value depending on whether the column exists in the # table. This defaults to a DESCRIBE of the table, then looks for the column # in the DESCRIBE results - but many databases probably have a much more # efficient alternative. # my ($self, $table, $column) = @_; my $sth = $self->prepare("DESCRIBE $table") or return; $sth->execute or return; my $found; while (my ($col) = $sth->fetchrow) { $found = 1, last if $col eq $column; } $found; } END_OF_SUB $COMPILE{add_column} = __LINE__ . <<'END_OF_SUB'; sub add_column { # ------------------------------------------------------------------- # Adds a column to a table. # my ($self, $table, $column, $def) = @_; $self->do("ALTER TABLE $table ADD $column $def"); } END_OF_SUB $COMPILE{drop_column} = __LINE__ . <<'END_OF_SUB'; sub drop_column { # ------------------------------------------------------------------- # Drops a column from a table. # my ($self, $table, $column) = @_; $self->do("ALTER TABLE $table DROP $column"); } END_OF_SUB $COMPILE{alter_column} = __LINE__ . <<'END_OF_SUB'; sub alter_column { # ----------------------------------------------------------------------------- # Changes a column. Takes table name, column name, definition for the new # column (string), and the old column definition (hash ref). The new column # definition should already be set in the table object # ($self->{table}->{schema}->{cols}->{$column_name}). # my ($self, $table, $column, $new_def, $old_col) = @_; $self->do("ALTER TABLE $table CHANGE $column $column $new_def"); } END_OF_SUB $COMPILE{create_index} = __LINE__ . <<'END_OF_SUB'; sub create_index { # ----------------------------------------------------------------------------- # Adds an index - checks driver hints for whether or not to prefix the index # with the prefixed table name. # my ($self, $table, $index_name, @index_cols) = @_; $index_name = $table . $index_name if $self->{hints}->{prefix_indexes}; $self->do("CREATE INDEX $index_name ON $table (" . join(",", @index_cols) . ")"); } END_OF_SUB $COMPILE{create_unique} = __LINE__ . <<'END_OF_SUB'; sub create_unique { # ----------------------------------------------------------------------------- # Adds a unique index to a table, using the prefixed table name as a prefix. # my ($self, $table, $unique_name, @unique_cols) = @_; $unique_name = $table . $unique_name if $self->{hints}->{prefix_indexes}; $self->do("CREATE UNIQUE INDEX $unique_name ON $table (" . join(",", @unique_cols) . ")"); } END_OF_SUB $COMPILE{drop_index} = __LINE__ . <<'END_OF_SUB'; sub drop_index { # ----------------------------------------------------------------------------- # Drops an index. # my ($self, $table, $index_name) = @_; $index_name = $table . $index_name if $self->{hints}->{prefix_indexes}; my $dropped = $self->do("DROP INDEX $index_name"); $dropped ||= $self->do("DROP INDEX $self->{connect}->{PREFIX}$index_name") if $self->{hints}->{fix_index_dbprefix}; $dropped; } END_OF_SUB $COMPILE{create_pk} = __LINE__ . <<'END_OF_SUB'; sub create_pk { # ------------------------------------------------------------------- # Adds a primary key to a table. # my ($self, $table, @cols) = @_; $self->do("ALTER TABLE $table ADD PRIMARY KEY (" . join(",", @cols) . ")"); } END_OF_SUB $COMPILE{drop_pk} = __LINE__ . <<'END_OF_SUB'; sub drop_pk { # ------------------------------------------------------------------- # Drop a primary key. # my ($self, $table) = @_; my $do; if ($self->{hints}->{drop_pk_constraint}) { # To drop a primary key in ODBC or Pg, you drop the primary key # constraint, which implicitly drops the index implicitly created by a # primary key. my $sth = $self->prepare("SHOW INDEX FROM $table") or return; $sth->execute or return; my $pk_constraint; while (my $index = $sth->fetchrow_hashref) { if ($index->{index_primary}) { $pk_constraint = $index->{index_name}; last; } } $pk_constraint or return $self->warn(CANTEXECUTE => "ALTER TABLE $table DROP PRIMARY KEY" => "No primary key found for $table"); $do = "ALTER TABLE $table DROP CONSTRAINT $pk_constraint"; } else { $do = "ALTER TABLE $table DROP PRIMARY KEY"; } $self->do($do); } END_OF_SUB 1;