summaryrefslogtreecommitdiff
path: root/dbi.class
diff options
context:
space:
mode:
Diffstat (limited to 'dbi.class')
-rw-r--r--dbi.class450
1 files changed, 450 insertions, 0 deletions
diff --git a/dbi.class b/dbi.class
new file mode 100644
index 0000000..c6469fe
--- /dev/null
+++ b/dbi.class
@@ -0,0 +1,450 @@
+<?php
+/*
+** $Id$
+**
+** ITools - the Internet Tools Library
+**
+** Copyright (C) 1995-2003 by the ITools Authors.
+** This program is free software; you can redistribute it and/or
+** modify it under the terms of either the GNU General Public License
+** or the GNU Lesser General Public License, as published by the Free
+** Software Foundation. See http://www.gnu.org/licenses/ for details.
+**
+** dbi.class - UltraFlexible Database Interface 3000
+*/
+
+class it_dbi
+{
+ # Default configuration of dbi class
+ var $_defaultconfig = array
+ (
+ 'db' => null,
+ 'server' => "localhost",
+ 'user' => "itools",
+ 'pw' => "24%^jXC~",
+ 'safety' => 1, /* 0= never die, 1=die if query invalid, 2=die also if no results */
+ 'keyfield' => "ID",
+ 'createclasses' => false,
+ 'classprefix' => "",
+ 'autoincrement' => true,
+ 'randomid' => false,
+ 'sqllog' => false,
+ 'persistent' => false,
+ );
+
+ # Key of currently loaded record or null (public readonly)
+ var $_key;
+
+ # Array of name => length of table fields
+ var $_fields;
+
+
+/**
+ * Constructor: Initialize the DBI interface
+ * @param $config array(key => value) of configuration data
+ * @param $query Optional initial query to run
+ */
+function it_dbi($config, $query = null)
+{
+ # Shortcut: String config means use this table with default values
+ if (!is_array($config))
+ $config = array('table' => $config);
+
+ # Create current settings
+ foreach ($config + $this->_defaultconfig as $key => $value)
+ {
+ $var = "_$key";
+ $this->$var = $value;
+ }
+ unset($this->_defaultconfig); /* to shorten print_r() output */
+
+ if (!isset($this->_db))
+ $this->_db = $GLOBALS['ULTRADB'];
+
+ $dbid = "$this->_db/$this->_server/$this->_user";
+
+ if (!($this->_link = $GLOBALS['it_dbi_link'][$dbid]))
+ {
+ # Use persistent connections but prevent reuse if only DB differs
+ if (!$this->_persistent || $GLOBALS['it_dbi_connected']["$this->_server/$this->_user"]++)
+ $this->_link = @mysql_connect($this->_server, $this->_user, $this->_pw, true);
+ else
+ $this->_link = @mysql_pconnect($this->_server, $this->_user, $this->_pw);
+
+ if (!$this->_link)
+ {
+ # One retry after a short delay (always non-persistent)
+ it::log('sqllog', "it_dbi(): retrying DB link (mysql_connect $this->_server, $this->_db): " . mysql_error());
+ sleep(1);
+ $this->_link = @mysql_connect($this->_server, $this->_user, $this->_pw, true);
+ }
+
+ if (!$this->_link)
+ $this->_fatal("it_dbi(): can't create DB link (mysql_connect $this->_server, $this->_db): " . mysql_error());
+
+ if (!(@mysql_select_db($this->_db, $this->_link)))
+ $this->_fatal("it_dbi(): can't select database \"$this->_db\": " . mysql_error($this->_link));
+
+ $GLOBALS['it_dbi_link'][$dbid] = $this->_link;
+ }
+
+ if ($this->_createclasses)
+ {
+ if (!($tables = mysql_list_tables($this->_db, $this->_link)))
+ $this->_fatal("it_dbi(): can't list on tables \"$this->_db\": " . mysql_error($this->_link));
+
+ $defaults = '';
+ foreach($config as $key => $value)
+ if ($key != 'createclasses')
+ $defaults .= "'$key' => '".strtr($value, array('\'' => '\\\'', '$' => '\\$'))."', ";
+
+ for ($i = 0; $i < mysql_num_rows($tables); $i++)
+ {
+ $table = mysql_tablename($tables, $i);
+ $classname = "$this->_classprefix$table";
+
+ $result = mysql_list_fields($this->_db, $table, $this->_link);
+
+ for ($fields=array(), $j=0; $j < mysql_num_fields($result); $j++)
+ {
+ $fields[mysql_field_name($result, $j)] = mysql_field_len($result, $j);
+ $flags = mysql_field_flags($result, $j);
+
+ if (strstr($flags, "primary_key"))
+ {
+ $keyfield = mysql_field_name($result, $j);
+ $autoincrement = (bool)strstr($flags, "auto_increment");
+ $randomid = (mysql_field_type($result, $j) == "string");
+ }
+ }
+
+ $fields = var_export($fields, true);
+
+ if ($this->_createclasses && (substr($classname, 0, 4) != 'PMA_') && !class_exists($classname))
+ {
+ debug("it_dbi: creating class $classname", 5);
+ eval("
+ class $classname extends it_dbi
+ {
+ function $classname(\$query = null, \$config = array())
+ {
+ \$config += array($defaults'table' => '$table', 'keyfield' => '$keyfield', 'autoincrement' => '$autoincrement', 'randomid' => '$randomid');
+ \$this->_fields = $fields;
+ \$this->it_dbi(\$config, \$query);
+ }
+ }
+ ");
+ }
+ }
+ }
+
+ if (isset($query))
+ if (is_array($query))
+ $this->select($query);
+ else
+ $this->read($query);
+}
+
+
+/**
+ * Internal: construct SQL SET clause of changed values from member vars and tags array.
+ * Merge current values into $tags. Modifies caller's array (callers rely on it)!
+ */
+function _set(&$tags)
+{
+ # Add member vars to tags, considering unquoted fields
+ foreach (get_object_vars($this) as $field => $value)
+ # Don't use isset($tags[$field]) (would not handle null values correctly)
+ if (isset($this->_fields[$field]) && !array_key_exists('-'.$field, $tags) && !array_key_exists($field, $tags))
+ $tags[$field] = $value;
+
+ # Create SQL
+ $r = array();
+ foreach((array)$tags as $key => $value)
+ {
+ if (substr($key, 0, 1) == '-') # Unquoted value (always added)
+ $r[] = substr($key, 1)."=$value";
+ elseif (!isset($this->_data) || ($value !== $this->_data[$key])) # Add to SQL if value has changed
+ $r[] = "$key=".(isset($value) ? "'".mysql_real_escape_string($value, $this->_link)."'" : 'NULL');
+ }
+
+ return $r ? 'SET '.implode(', ', $r) : '';
+}
+
+
+/* Intermal: construct SQL WHERE clause */
+function _where($where)
+{
+ return isset($where) ? ' ' . it_db_table::construct_sql_clause($where) : '';
+}
+
+
+/**
+ * Internal: Output class name::error message and terminate execution.
+ */
+function _fatal($text)
+{
+ it::fatal(get_class($this).'::'.$text);
+ /* NOT REACHED */
+}
+
+
+/**
+ * Post-process data after reading a record.
+ * This is a stub-function that can be overloaded.
+ */
+function _read_post_process()
+{
+}
+
+
+/**
+ * Pre-process data before writing a record.
+ * This is a stub-function that can be overloaded.
+ * @param $tags Reference to update/create tags, can be modified as needed
+ */
+function _write_pre_process(&$tags)
+{
+}
+
+
+/**
+ * Clear record
+ */
+function clear($pp = true)
+{
+ foreach ((array)$this->_fields as $key => $value)
+ unset($this->$key);
+ unset($this->_data);
+ unset($this->_key);
+ $pp && $this->_read_post_process();
+}
+
+
+/**
+ * Semi-internal: send a raw SQL query and return mysql result value
+ * @param $query complete SQL query string
+ * @return raw MySQL result. May die if query fails and safety is big enough
+ */
+function query($query)
+{
+ debug(get_class($this)."::query(\"$query\")", 4);
+
+ $start = gettimeofday();
+
+ if (!($result = mysql_query($query, $this->_link)) && $this->_safety)
+ {
+ if (($this->_safety < 2) && (mysql_errno($this->_link) == 1062)) /* Duplicate entry */
+ return false;
+
+ $error = mysql_errno($this->_link).' ('.mysql_error($this->_link).')';
+ $trace = debug_backtrace();
+ $res = mysql_fetch_row(mysql_query('select database()', $this->_link));
+ $this->_fatal("query(\"$query\") on {$res[0]} failed: $error\nfile {$trace[1]['file']} line {$trace[1]['line']}");
+ }
+
+
+ if ($this->_sqllog || EDC('sqllog'))
+ {
+ $end = gettimeofday();
+ $msec = round(($end['sec'] - $start['sec']) * 1000 + ($end['usec'] - $start['usec']) / 1000);
+ it::log('sqllog', "$msec\t$query");
+ }
+
+ return $result;
+}
+
+
+/**
+ * Read a single record by primary key, not destroying old query result $this->_result
+ * @param $id primary key. If null, record is cleared
+ * @return True if record could be read, false if not.
+ */
+function read($id=null)
+{
+ $mres = $this->_result;
+ $result = $this->select(array($this->_keyfield => $id));
+ $this->_result = $mres;
+ return $result;
+}
+
+
+/**
+ * Select a set of records from table and fetch the first one
+ * @param $query Optional array of (field => value) or (int => sqlstring) pairs. Defaults to null (select all records)
+ * Can contain magic field 'SELECT' for things like 'COUNT(*)' or 'DISTINCT foo', defaults to '*'
+ * Can contain magic field 'JOIN' for things like 'tableA LEFT JOIN tableB ON a=b', defaults to table name
+ * If magic field 'CALC_FOUND_ROWS' is true, sets member var _found_rows to number of matching rows without LIMIT
+ * If magic field 'NOFETCH' is true, then the first row is not prefetched (e.g. when directly using _result)
+ * @return Number of matching rows, use iterate() to fetch the next record
+ * @see it_dbi::iterate(), it_db_table::construct_sql_clause()
+ */
+function select($query = null)
+{
+ $result = 0;
+ $calc_found_rows = false;
+
+ $what = '*';
+ if (isset($query['SELECT']))
+ {
+ $what = $query['SELECT'];
+ unset($query['SELECT']);
+ }
+
+ $join = $this->_table;
+ if (isset($query['JOIN']))
+ {
+ $join = $query['JOIN'];
+ unset($query['JOIN']);
+ }
+
+ unset($this->_found_rows);
+ if (isset($query['CALC_FOUND_ROWS']) && $query['CALC_FOUND_ROWS'])
+ {
+ $calc_found_rows = true;
+ $what = 'SQL_CALC_FOUND_ROWS '.$what;
+ unset($query['CALC_FOUND_ROWS']);
+ }
+
+ $nofetch = $this->_nofetch = isset($query['NOFETCH']) ? $query['NOFETCH'] : false;
+ unset($query['NOFETCH']);
+
+ if ($this->_result = $this->query($sql = "SELECT $what FROM $join " . $this->_where($query)))
+ $result = mysql_num_rows($this->_result);
+
+ if ($calc_found_rows)
+ {
+ list($count) = mysql_fetch_row($this->query('SELECT FOUND_ROWS()'));
+ $this->_found_rows = intval($count);
+ }
+
+ if (!$this->iterate() && ($this->_safety >= 2))
+ $this->_fatal("select(): query produced no results: \"$sql\"");
+
+ $this->_nofetch = !$nofetch;
+
+ return $result;
+}
+
+
+/**
+ * Iterate through select()ed records
+ * @return true if another record was fetched, false if no more records
+ * @see it_dbi::select()
+ */
+function iterate()
+{
+ if (!$this->_nofetch)
+ {
+ $this->clear(false);
+ if ($this->_data = mysql_fetch_assoc($this->_result))
+ {
+ if (!empty($this->_keyfield))
+ $this->_key = $this->_data[$this->_keyfield];
+
+ foreach ($this->_data as $key => $value)
+ $this->$key = $value;
+ }
+
+ $this->_read_post_process();
+ }
+ else
+ $this->_nofetch = false;
+
+ return (bool)$this->_data;
+}
+
+
+/**
+ * Insert a record into table. Values are taken from member vars and $tags.
+ * After inserting, all values are valid (record is read back).
+ * Does not destroy internal state of last select() call
+ * @param $tags Additional key => value pairs (these have priority over member vars)
+ */
+function insert($tags = array(), $command = "INSERT")
+{
+ /* Pre-processing, $tags is passed by reference and may be modified here */
+ $this->_write_pre_process($tags);
+
+ unset($this->_data); # All new value set
+ $set = $this->_set($tags); # Update $tags (!) and generate SQL
+
+ if ($this->_randomid && !isset($tags[$this->_keyfield]))
+ {
+ $tags[$this->_keyfield] = md5(uniqid(mt_rand()));
+ $set = $this->_set($tags); # Generate new SQL containing ID
+ }
+
+ if ($result = $this->query("$command INTO $this->_table " . $set))
+ {
+ $id = ($this->_autoincrement && !isset($tags[$this->_keyfield])) ? mysql_insert_id($this->_link) : $tags[$this->_keyfield];
+ if (!$this->read($id) && $this->_safety)
+ $this->_fatal("insert(): can't read record back (key truncated?), id=\"$id\"");
+ }
+
+ return $result;
+}
+
+
+/**
+ * Replace a record in a table
+ * @param $tags Additional key => value pairs (these have priority over member vars)
+ * Does not destroy internal state of last select() call
+ * @see it_dbi::insert()
+ */
+function replace($tags = array())
+{
+ return $this->insert($tags, "REPLACE");
+}
+
+
+/**
+ * Update a record in a table
+ * @param $tags Additional key => value pairs (these have priority over member vars)
+ * Does not destroy internal state of last select() call
+ */
+function update($tags = array(), $query = null)
+{
+ $result = true;
+
+ /* Pre-processing, $tags is passed by reference and may be modified here */
+ $this->_write_pre_process($tags);
+
+ if (!$query && !empty($this->_keyfield))
+ $query = array($this->_keyfield => $this->_data[$this->_keyfield]);
+
+ if ($set = $this->_set($tags))
+ if ($result = $this->query("UPDATE $this->_table " . $set . $this->_where($query)))
+ if (isset($this->_key) && $this->read($this->_key))
+ $this->_nofetch = false; # So we can do while(iterate()) update();
+
+ return $result;
+}
+
+
+/**
+ * Delete a record
+ * Does not destroy query result $this->_result
+ */
+function delete($query = null)
+{
+ $result = 0;
+
+ if (!$query && $this->_keyfield)
+ {
+ $query = array($this->_keyfield => $this->_key);
+ $this->clear();
+ }
+
+ if ($query)
+ {
+ if ($sres = $this->query(($sql = "DELETE FROM $this->_table " . $this->_where($query))))
+ $result = mysql_affected_rows($this->_link);
+ }
+
+ return $result;
+}
+
+} /* End class it_dbi */
+
+?>