<?php
/* $Id: db_view.php,v 1.20 2002/07/21 11:20:52 rurban Exp $

  Version: 0.1

  Mysql compatibility DB class for views and triggers in PHP.
  Multi-tables are horrible to manipulate in mysql.
  SELECT can use joined tables, but UPDATE, INSERT, DELETE not.

  http://xarch.tu-graz.ac.at/software/tep/modules/db_view/

  "db_view" provides:
  * An abstraction class and helpers for multi-tabled Mysql data. 
    (init, select, select_row, count, insert, update, delete)

  * Atomic multi-table updates, inserts, deletes
    eg: update items, month set items.price=month.price where items.id=month.id;
    Also primitive non-atomic (=insecure) rollbacks.

  * Functions, Triggers and Defaults per action 
    (select, update, insert, delete; before or after)

  * Rich logging and debugging facilities.

  Todo:
  * Stored procedures: create_aggregate_function to group result sets.

  * Create relations automatically. 

  * Test multi-table functionality.
 
  * Cascading triggers.

  * Maybe integrate into PEAR/DB to support all databases.

  * Better rollback and transaction support. 

  Change the provided custom ur_db_ functions at the end to fit your needs. 
 
  ******************************************************************************

  Copyright (c) 2002 Reini Urban
  Released under the GNU General Public License

  Usage:
   $GLOBALS['db_link'] = mysql_pconnect(DB_SERVER, DB_SERVER_USERNAME, DB_SERVER_PASSWORD);
   define ('DB_DATABASE', 'test');

   class derived_db extends db_view {
     var $private_vars, ...;
     function derived_db ($id = false) {
       $this->db_tables = array('table1','table2');
       $this->db_tables_aliases = array('t1','t2');
       $this->db_select     = 'table1 t1 left join table2 t2 using (id)';
       $this->db_init();
       if ($id) $db_view::init($id);
     }
   }
   $obj1 = new derived_db();     // empty object
   $obj2 = new derived_db($id);  // or to initialize the class vars with values from select_row($id)
*/

class db_view {
  
// required vars:
  
var $db_tables;           // array of all table names
  
var $db_select;                  // SQL string: "t1 JOIN t2 using(id)" or just  "WHERE p1.id=p2.id"

  // todo:
  
var $db_relations;            // array of (src_tbl, src_col, dest_tbl, dest_col, join_type)
                  // join_type may be automatically extracted from the index types
                // if the field names match
  /* array('t1' => array('id' => 't1_id'),
           't2' => array(
             'id'       => 't2_id',
                         'join_t1'  => 't2_id=t1_id',   // ' t1 left join t2 on (t2_id=t1_id) '
                         'where_t1' => 't2_id=t1_id')), // ' t1, t2 where t2_id=t1_id '
  */

  // optional vars:
  
var $db_tables_aliases;      // array of all table aliases
  
var $db_insert;                  // array per table of where clauses to insert.
  
var $db_update;                  // array per table of where clauses to update.
  
var $db_delete;                  // array per table of where clauses to delete.
  
var $db_where;                  // array per table of where clauses to update, insert, delete.

  
var $db_tables_count;       // count(tables)
  
var $db_tables_fields;       // array of all fields per table
  
var $db_tables_fields_primary// array of all primary keys per table. defaults to array(0).
  //  var $db_all_fields;           // flat array of all field names
  
var $db_fields;             // comma delimited string of all fields per tables, first must be the id
  
var $db_id;                 // id of the main db: tables [0];
  
var $db_table_name;              // <=> table[0]
  
var $db_fields_prefix;        // try to strip this prefix from the db fields to get the object vars

  
var $_db_fields_prefix_length;
  var 
$_db_fields_index;        // field hash (name => index) for row extraction and callbacks
  
var $_db_database;               // database name
  
var $_db_link;               // database handle
  
var $_db_cb_defaults;        // defaults on SELECT, INSERT, UPDATE, ... like now() for 'last_modified'
  
var $_db_cb_triggers;        // procedural triggers, to be registered for each exact event, table and field (mostly for side effects)
  
var $_db_cb_functions;    // unary user-defined functions on SELECT, DELETE, INSERT, ...
  
var $_db_query_last;          // last query string, for debugging
  
var $_db_query_before;        // collected trigger sideeffects, for debugging
  
var $_db_query_after;         // collected trigger sideeffects, for debugging
  
var $_db_no_named_arrays;     // don't create named aliased field slots: $db_tables_fields['table1'] besides $db_tables_fields[0]
  
var $_db_debug false;

  
// Todo: update, insert, delete relations per table
  // update t1 ... where id=1
  // update t2 ... where id=1 and t2_f2=$t1_f1
  // update t3 ... where id=1 and t3_f2=$t1_f1

  // array_walk() needs a reference. not yet used.
  
function _stripSlashes (&$value) {
    
stripSlashes($value);
  }

  function 
_table_list () {
    
$s '';
    for (
$i=0$i $this->db_tables_count$i++) {
      
$s .= $this->db_tables[$i] . ' as ' $this->db_tables_aliases[$i] . ', ';
    }
    return 
substr($s,0,-2);
  }

  function 
_table_id ($table_id) {
    return 
$this->db_tables_fields[$table_id][0];
  }

  
// case insensitive!
  
function _table_index ($table_name) {
    return 
ur_array_position($table_name,$this->db_tables,true);
  }

  
// case insensitive!
  
function _field_index ($table_index$field_name) {
    return 
ur_array_position($field_name,$this->db_tables_fields[$table_index],true);
  }

  function 
_from () {
    
// $this->db_fields is already preprocessed, for SELECT only
    
return 'SELECT ' $this->db_fields ' FROM ' $this->db_select;
  }

  
// with array of values and scope (global hash), with single string or...
  // _where_select(array('id'), array(1));
  // _where_select(array('products_id','language_id'), 'HTTP_POST_VARS');
  // _where_select("id='1'");
  // _where_select();
  
function _where_select ($fields false$values false) {
    if (! 
stristr($this->db_select,' WHERE ')) {
      return 
$this->_where (' WHERE ' $this->db_select$fields$values);
    } else {
      return 
$this->_where (' ' $this->db_select$fields$values);
    }
  }
  
// for INSERT,UPDATE,DELETE:
  // _where_where('DELETE', 1, array('id'), array(1));
  // _where_where('UPDATE', 0, array('products_id','language_id'), array(1,$language_id));
  
function _where_where ($action$table_index$fields false$values false) {
    if (! 
stristr($this->db_where[$action][$table_index],' WHERE ')) {
      return 
$this->_where (' WHERE ' $this->db_where[$action][$table_index], $fields$values);
    } else {
      return 
$this->_where (' ' $this->db_where[$action][$table_index],  $fields$values);
    }
  }

  function 
_where ($query$fields false$values false$operators = array()) {
    if (! 
$fields ) return $query;
    if ( 
$query and stristr($query,' WHERE ') and (substr($query,-6,6) != 'WHERE '))
      
$query .= " AND";
    if (! 
$values ) {
      if (
is_string($fields)) // explicit simple string
    
return $query ' ' $fields;
      
//else
      //$values = 'GLOBALS';  // default to global hash
    
}
    if ( 
$values ) {
      if (
is_array($values) and is_array($fields)) {   // pairs of field => values
    
for ($i=0$i count($fields); $i++) {
      
// operator defaults to =
          
if (empty($operators[$i])) $operators[$i] = '=';
      
$query .= " " $fields[$i] . $operators[$i] . "'" mysql_escape_string($values[$i]) . "' AND";
    }
    return 
substr($query,0,-3);
      } else {
    if (empty(
$operators[$i])) $operators[$i] = '=';
    return 
" " $fields[$i] . $operators[$i] . "'" mysql_escape_string($v) . "'";
      }
      
/*
      } elseif (is_string($values)) { // scope hash, default: $GLOBALS, Example: where(array('f1'),'HTTP_POST_VARS')
    for ($i=0; $i < count($fields); $i++) {
      $v = $$values[$i];
          if (empty($operators[$i])) $operators[$i] = '=';
      $query .= " " . $fields[$i] . $operators[$i] . "'" . mysql_escape_string($v) . "' AND";
    }
    return substr($query,0,-3);
      }
      */
    
}
  }

// class constructor. normally not used. use db_init() to init from derived classes instead.
// php doesn't call this for virtual classes
  
function db_view ($db_database$db_link$db_tables$db_select$db_where) {
    
$this->_db_database $db_database;
    
$this->_db_link     $db_link;
    
$this->db_tables $db_tables;
    
$this->db_select $db_select;
    
$this->db_where  $db_where;
    
// list($this->db_select, $this->db_where)  = $this->_do_something_clever_with($db_relations);
    
$this->db_init();
    return;
  }

  function 
db_init ($id false) {
    
/*
      if ((get_parent_class($this) != 'db_view') or (get_class($this) == 'db_view')) {
    die ('->db_init() must be called from a user_class deriving from class db_view');
      }
    */
    
$this->_db_debug = ($GLOBALS['DB_DEBUG'] and headers_sent());
    if (! 
$this->_db_database$this->_db_database DB_DATABASE;
    if (! 
$this->_db_link$this->_db_link $GLOBALS['db_link'];
    if (! 
$this->db_tables or ! $this->_db_database or ! $this->_db_link) {
      die (
'db_view object not correctly initialized.');
    }
    
/*
    if ( ! ($this->db_relations or ($this->db_select and $this->db_where ))) {
      // parse relations automatically:
      $this->db_create_relations($db_name, $db_link) ||
        die ('object $db_relations or ($db_select and $db_where) undefined.');
    }
    */
    
if (! $this->db_where ) {
      if (
$this->db_delete$this->db_where['DELETE'] = $this->db_delete;
      if (
$this->db_update$this->db_where['UPDATE'] = $this->db_update;
      if (
$this->db_insert$this->db_where['INSERT'] = $this->db_insert;
    }
    if (! 
$this->db_table_name$this->db_table_name $this->db_tables[0];
    
$this->db_tables_count count($this->db_tables);
    if (empty(
$this->db_tables_fields)) {  // use all existing fields
        
$this->db_tables_fields = array();
    for (
$i=0$i $this->db_tables_count$i++) {
      
$fields mysql_list_fields($this->_db_database$this->db_tables[$i], $this->_db_link);
      
$columns mysql_num_fields($fields);
      for (
$j=0$j $columns$j++) {
        
$this->db_tables_fields[$i][] = mysql_field_name($fields$j);
      }
    }
    }
    for (
$i=0$i $this->db_tables_count$i++) {
      
$table_name $this->db_tables[$i];
      if (
is_string($this->db_tables_fields[$i])) {
    
$this->db_tables_fields[$i] = explode(',',$this->db_tables_fields[$i]);
      } elseif (!empty(
$this->db_tables_fields[$table_name]) and 
        
is_string($this->db_tables_fields[$table_name])) {
    
$this->db_tables_fields[$i] = explode(',',$this->db_tables_fields[$table_name]);
        
$this->db_tables_fields[$table_name] =& $this->db_tables_fields[$i];
      } elseif (empty(
$this->db_tables_fields[$i]) and empty($this->db_tables_fields[$table_name])) {
    
$fields mysql_list_fields($this->_db_database$this->db_tables[$i], $this->_db_link);
    
$columns mysql_num_fields($fields);
    for (
$j=0$j $columns$j++) {
      
$this->db_tables_fields[$i][] = mysql_field_name($fields$j);
    }
      } else {
    if (empty(
$this->db_tables_fields[$i]))
      
$this->db_tables_fields[$i] =& $this->db_tables_fields[$table_name];
      }
      if (! 
$this->_db_no_named_arrays )
    
$this->db_tables_fields[$table_name] =& $this->db_tables_fields[$i];
    }
    if (empty(
$this->db_tables_aliases)) { // create unique names from the first chars
      
$this->db_tables_aliases = array();
      for (
$i=0$i $this->db_tables_count$i++) {
    
$table_name $this->db_tables[$i];
    
$j=1;  // create unique aliases
    
while (in_array(substr($table_name,0,$j), $this->db_tables_aliases) and
           (
$j <= strlen($table_name))) {
      
$j++;
    }
    
$this->db_tables_aliases[$i] = substr($table_name,0,$j);
    if (! 
$this->_db_no_named_arrays )
      
$this->db_tables_aliases[$table_name] =& $this->db_tables_aliases[$i];
      }
    }
    if (! 
$this->db_id$this->db_id $this->db_tables_aliases[0] . '.' $this->db_tables_fields[0][0];
    
$db_fields = array(); $this->db_fields='';
    for (
$i=0$i $this->db_tables_count$i++) {
      for (
$j=0$j count($this->db_tables_fields[$i]); $j++) {
    if (!
$db_fields or !in_array($this->db_tables_fields[$i][$j],$db_fields)) { // the first occurence only
      
$this->db_fields .= $this->db_tables_aliases[$i] . '.' $this->db_tables_fields[$i][$j] . ',';
      
$db_fields[] = $this->db_tables_fields[$i][$j];
    }
      }
    }
    
//$this->db_all_fields = $db_fields;
    
$this->db_fields substr($this->db_fields,0,-1);
    if (! 
$this->db_tables_fields_primary $this->db_tables_fields_primary = array();
    for (
$i=0$i $this->db_tables_count$i++) {
      
$table_name $this->db_tables[$i];
      if (! 
$this->db_tables_fields_primary[$i] ) {
    
$this->db_tables_fields_primary[$i] = array(0);
    if (! 
$this->_db_no_named_arrays )
      
$this->db_tables_fields_primary[$table_name] =& $this->db_tables_fields_primary[$i];
      }
    }
    
$i 0;
    foreach (
$db_fields as $f) {
      
$this->_db_fields_index[$f] = $i;
      
$i++;
    }
    unset(
$db_fields);

    
// allow abbrevated db_select: "WHERE " ...
    
if (strtoupper(substr($this->db_select,0,6)) == 'WHERE ') {
      
$this->db_select $this->_table_list() . " " $this->db_select;
    }
    if (! 
$this->db_select
      
$this->db_select $this->_table_list() . ' WHERE ';

    
// initialize the child class with values from id
    
if ((get_class($this) != 'db_view') and !($id === false)) {
      
$super get_class($this);
      
//$this->init($id);
      // all these don't work. the php object system sucks.
      // {$super::init($id)};
      // eval ($super . "::init($id)");
      // db_view::init($this);
    
}
    return;
  }

  
// fixme
  
function set_obj_field ($f$value) { // from db value
    
@$this->$f $value;
    if (
$php_error and $this->_db_fields_prefix_length) {
      
$this->debug($this->db_table_name '->_db_fields_prefix_length ' $this->_db_fields_prefix_length ': ' substr($f,$this->_db_fields_prefix_length) . "\n");
      
$s substr($f,$this->_db_fields_prefix_length);
      @
$this->$s $value;  // set all fields of the class
    
}
  }

  
// todo
  
function set_db_field ($fetch_array$values) { // from obj var
    
@$this->$f $values[$f];  // set all fields of the class
    
if ($php_error and $this->_db_fields_prefix_length) {
      
$s substr($f,$this->_db_fields_prefix_length);
      @
$this->$s $values[$f];  // set all fields of the class
    
}
  }

  
// warning: this doesn't work in db_view yet! see above.
  // so use it in your constructor as below:
  //    if ($id) db_view::init($id);
  
function init ($id) {
    if (
get_class($this) != 'db_view') {
      
$values $this->select_row($id);
      
$this->debug("::init($id) => " serialize($values) . "\n");
      if (
$this->db_fields_prefix) { 
    
$this->_db_fields_prefix_length strlen($this->db_fields_prefix);
    
$this->debug($this->db_table_name '->db_fields_prefix "' $this->db_fields_prefix "\"\n");
      }
      
$fields $this->db_all_fields();
      
//$obj_fields = get_object_vars($this);
      
foreach ($fields as $f) {
    if (
$this->db_fields_prefix and ur_str_begins_with($f$this->db_fields_prefix)) {
      
$s substr($f$this->_db_fields_prefix_length);
      @
$this->$s $values[$f];  // set abbrevated fieldname
      
$this->debug($s " => " . @$this->$s " " $f "(" $values[$f] . ")\n");
      
// $this->set_obj_field($f,$values[$f]);
    
}
    @
$this->$f $values[$f];  // set all fields of the class
      
}
      return 
$values;
    } else {
      return die(
'db_view->init() is forbidden. Call it within the parent class as db_view::init()');
    }
  }

  function 
debug ($msg) {
    
//$this->_db_debug = ($GLOBALS['DB_DEBUG'] and headers_sent());
    
if ($this->_db_debug) {
      if (
headers_sent()) echo "DB_DEBUG: $msg\n";
      else 
error_log($msg,DB_LOG,3);
    }
  }

  function 
select ($where ''$order '') {
    
$this->debug($this->db_table_name '->select() ' $where $order "\n");
    
// fixme: merge $where with $this->db_select
    
if (is_array($where))
      
$query_string $this->_from() . $this->_where_select($where[0],$where[1]) . ' ' $order;
    else
      
$query_string $this->_from() . $this->_where_select($where) . ' ' $order;
    
$this->debug($this->db_table_name.'->select() '$query_string);
    
$this->_db_query_last $query_string;
    
$q ur_db_query($query_string);
    return 
$q;
  }

  
// after select row
  // to be called for each selected row to apply defaults and triggers
  // we assume $values to be a numerically indexed array plus hash (fetch_array()).
  
function select_row_cb ($values) {
    if (empty(
$this->_db_cb_functions['SELECT']) and empty($this->_db_cb_triggers['SELECT']) and 
    empty(
$this->_db_cb_defaults['SELECT'])) return $values;
    
//if (substr_count($this->db_fields,',')+1 == count($values)) {
    
$this->debug($this->db_table_name.'->_callbacks()');
    for (
$i=0$i $this->db_tables_count$i++) {
      if (
$this->_db_cb_functions['SELECT'][$i] or $this->_db_cb_triggers['SELECT']['AFTER'][$i] or $this->_db_cb_defaults['SELECT'][$i]) {
    
$j 0;
    foreach(
$this->db_tables_fields[$i] as $f) {
      
$k $this->_db_fields_index[$f];
      if (
$func $this->_db_cb_functions['SELECT'][$i][$j]) {
        
$this->debug($this->db_table_name."->_function() < $f [$i][$j][$k] $func($values[$k])");
        
$values[$k] = $func($values[$k]);
        
$values[$f] =& $values[$k];
        
$this->debug($this->db_table_name.'->_function() => ' $values[$k]);
      }
      if (
$func $this->_db_cb_triggers['SELECT']['AFTER'][$i][$j]) {
        
$this->debug($this->db_table_name.'->_trigger() < ' $values[$k]);
        
$values[$k] = $func($id,$f,$values[$k]);
        
$values[$f] =& $values[$k];
        
$this->debug($this->db_table_name.'->_trigger() => ' $values[$k]);
      } elseif (empty(
$values[$k]) and $this->_db_cb_defaults['SELECT'][$i][$j]) {
        
$this->debug($this->db_table_name.'->_db_cb_defaults() < ' $values[$k]);
        
$values[$k] = $this->_db_cb_defaults['SELECT'][$i][$j];
        
$values[$f] =& $values[$k];
        
$this->debug($this->db_table_name.'->_db_cb_defaults() => ' $values[$k]);
      }
      
$j++;
    }
      }
    }
    return 
$values;
  }

  function 
count ($where '') {
    
$this->_db_debug = ($GLOBALS['DB_DEBUG'] and headers_sent());
    
$this->debug($this->db_table_name.'->count() ' $where $order);
    
$query_string 'SELECT count(*) as total FROM ';
    
$query_string .= $this->db_select ' ';
    if (
is_array($where))
      
$query_string .= $this->_where_select($where[0],$where[1]);
    else
      
$query_string .= $this->_where_select($where);
    
$this->debug($this->db_table_name."->count() $query_string");
    
$this->_db_query_last $query_string;
    
$v = @mysql_fetch_row(@ur_db_query($query_string));
    return @
$v[0];
  }

  
// select single row as mysql_fetch_array() with callbacks
  // unbuffered
  
function select_row ($id) {
    
$this->debug($this->db_table_name."->select_row() $id \n");
    
$query_string 'SELECT ' $this->db_fields ' FROM ' $this->_where_select($this->db_id "='$id'");
    
$this->_db_query_last $query_string;
    
$r mysql_unbuffered_query($query_string);
    
//$r = ur_db_query($query_string);
    
if (STORE_DB_TRANSACTIONS == true) {
      
error_log($query_string "\n"3STORE_DB_LOG);
    }
    if (! 
$r) return;
    
$values mysql_fetch_array($r);
    
//array_walk($values,'stripSlashes');
    //if ($this->_db_debug) var_dump($values);
    
if (STORE_DB_TRANSACTIONS == true) {
      
$result_error mysql_error();
      if (! 
$result_error) {
    
$result_error " => " $id;
      }
      
error_log("  RESULT " $r " " $result_error "\n"3STORE_DB_LOG);
    }
    return 
$this->select_row_cb($values);
  }

  
// return new id of first table
  // values: named hash (not indexed!)
  
function insert ($values false) {
    global 
$db_rollback;
    
//$this->_db_debug = ($GLOBALS['DB_DEBUG'] and headers_sent());
    
$db_rollback = array();
    if (! 
$values$values $this->as_array(); // hash! no indexed array (f not k as index)
    
if (! is_array($values) ) { // insert one 'field_name' only
      
$field_name $values;
      
$values = array();
      
$values[$field_name] = $this->$field_name;
    } else {
      
reset ($values);
      list(
$key$value) = each($values);
      if (! 
is_string($values[$key]) ) { // numbered array if field_names: array(field1,field2,...)
    
reset ($values);
        foreach (
$values as $field_name) {
      
$values[$field_name] = $this->$field_name;
    }
      }
    }
    
//$this->debug($this->db_table_name . "->insert() $values");
    
ur_db_lock($this->db_tables);
    
// ignore relations. insert one by one, ohne primary id's
    
for ($i=0$i $this->db_tables_count$i++) {
      
//if (empty($this->db_insert[$i])) continue;
      
$query_string 'INSERT INTO ' $this->db_tables[$i] . ' SET ';
      
$j 0$okay 0;
      foreach(
$this->db_tables_fields[$i] as $f) {
    if (! 
in_array($j,$this->db_tables_fields_primary[$i])) {
      
//$k = $this->_db_fields_index[$f];
      
if ($func $this->_db_cb_triggers['INSERT']['BEFORE'][$i][$j]) {
        
$values[$f] = $func($id,$f,$values[$f]);
        
//$values[$k] =& $values[$f];
        
$query_before .= $values[$f] . ";";
      }
      if (
$func $this->_db_cb_functions['INSERT'][$i][$j]) {
        
$this->debug($this->db_table_name.'->_function() < ' $values[$f]);
        
$values[$f] = $func($values[$f]);
        
//$values[$k] =& $values[$f];
        
$this->debug($this->db_table_name.'->_function() => ' $values[$f]);
      }
      if (empty(
$values[$f]) and $this->_db_cb_defaults['INSERT'][$i][$j]) {
        
$values[$f] = $this->_db_cb_defaults['INSERT'][$i][$j];
        
//$values[$f] =& $values[$k];
      
}
      
$query_string .= $f "='" mysql_escape_string($values[$f]) . "', ";
      
$okay++;
    }
    
$j++;
      }
      if (! 
$okay) die(get_class($this) . "->insert(): no values matching field_names");
      
$query_string substr($query_string,0,-2);
      
$this->_db_query_last $query_string;
      
$r ur_db_query($query_string);
      
$this->debug($query_before "|" $query_string);
      if (! 
$r ur_db_error($query_stringmysql_errno(), mysql_error());
      
$id mysql_insert_id();
      if (
$i == 0$insert_id $id;
      
$db_rollback[] = 'DELETE FROM ' $this->db_tables[$i] . $this->_where($i,array($this->db_table_fields_primary[$i][0]),array($id));
    }
    if (
$this->_db_cb_triggers['INSERT']['AFTER'][$i]) {
      
$j 0;
      foreach(
$this->db_tables_fields[$i] as $f) {
    if (
$func $this->_db_cb_triggers['INSERT']['AFTER'][$i][$j]) {
      
$q $func($id,$f,$values[$f]); // only for sideeffects
      
$query_after .= $q ";";
    }
      }
    }
    
ur_db_unlock();
    unset (
$db_rollback);
    
$this->debug("|$query_after|$insert_id");
    
$db_id $this->db_tables_fields[0][0];
    @
$this->$db_id $insert_id;
    return 
$insert_id;
  }

  
// sample:
  // ->update($id, array('orders_status' => ORDERS_STATUS_PROCESSING, 
  //                 'debit_to_id' => $debit_to_id, 
  //             'orders_date_checked' => $date_now));
  // todo: remove arg $id. we already have $this->db_id
  
function update($values=false$where '') {
    global 
$db_rollback;
    
$db_id $this->db_tables_fields[0][0];
    
$id $this->$db_id;
    
//$this->_db_debug = ($GLOBALS['DB_DEBUG'] and headers_sent());
    
$this->debug($this->db_table_name."->update() $id $values $where");
    
ur_db_lock($this->db_tables);
    
$db_rollback = array();
    if (! 
$values$values $this->as_array();
    if (! 
is_array($values)) { // update one 'field_name' only
      
$field_name $values;
      
$values = array();
      
$values[$field_name] = $this->$field_name;
    } else { 
// check for flat array
      
reset($values);
      list(
$key$value) = each($values);
      if (! 
is_string($values[$key]) ) { // numbered array of field_names: array(field1,field2,...)
    
reset ($values);
        foreach (
$values as $field_name) {
      
$values[$field_name] = $this->$field_name;
    }
      }
    }
    
// ignore relations. change one by one.
    
for ($i=0$i $this->db_tables_count$i++) {
      
$where $this->_where_where('UPDATE',$i,array($this->_table_id($i)), array($id));
      
// $db_rollback[] = 'UPDATE ' . $this->db_tables[$i] . $where; // oldvalues
      // if (empty($this->db_update[$i])) continue;
      
$query_string 'UPDATE ' $this->db_tables[$i] . ' SET ';
      
$j 0$okay 0;
      foreach(
$this->db_tables_fields[$i] as $f) {
    if (! 
in_array($j,$this->db_tables_fields_primary[$i])) {
      
//$k = $this->_db_fields_index[$f];
      
if (empty($values[$f]) and $this->_db_cb_defaults['UPDATE'][$i][$j]) {
        
$values[$f] = $this->_db_cb_defaults['UPDATE'][$i][$j];
        
//$values[$f] =& $values[$k];
      
}
      if (
$func $this->_db_cb_triggers['UPDATE']['BEFORE'][$i][$j]) { // change value by trigger
        
$q = @$func($id,$f,$values[$f],$where);
        if (! 
$php_errormsg) {
          
$values[$f] = $q;
          
//$values[$f] =& $values[$k];
          
$this->_db_query_before .= ($q ";"); // collect side-effects
        
} else {
          
$this->_db_query_before .= ($php_errormsg ";"); // collect errormsgs
        
}
      }
      if (! empty (
$values[$f]) ) {
        
$query_string .= $f "='" mysql_escape_string($values[$f]) . "', ";
        
$okay++;
      }
    }
    
$j++; // $k++;
      
}
      if (! 
$okay) die(get_class($this) . "->update(): no values matching field_names");
      
// fixme: merge $where with $this->db_where[$i]
      
$query_string substr($query_string,0,-2) . $where;
      
$this->_db_query_last $query_string;
      
$r ur_db_query($query_string);
      
$this->debug("{$this->_db_query_before}|$query_string");
      if (
$this->_db_cb_triggers['UPDATE']['AFTER'][$i]) {
    
$j 0;
    foreach(
$this->db_tables_fields[$i] as $f) {
      if (
$func $this->_db_cb_triggers['UPDATE']['AFTER'][$i][$j]) { // do side-effects
        
$q = @$func($id,$f,$values[$f],$where);
        if (! 
$php_errormsg) {
          
$this->_db_query_after .= ($q ";");            // collect side-effect results
        
} else {
          
$this->_db_query_after .= ($php_errormsg ";"); // collect errormsgs
        
}
      }
      
$j++;
    }
    
$this->debug($this->_db_query_after);
      }
    }
    
ur_db_unlock();
    unset (
$db_rollback);
    if (
$r)
      return 
mysql_affected_rows($r);
    else
      return 
0;
  }

  
// todo: remove arg $id. we already have $this->db_id
  
function delete($id$where '') {
    global 
$db_rollback;
    
//$this->_db_debug = ($GLOBALS['DB_DEBUG'] and headers_sent());
    
ur_db_lock($this->db_tables);
    
$db_id $this->db_tables_fields[0][0];
    
$id $this->$db_id;
    
$db_rollback = array();
    
// ignore relations. delete one by one.
    
for ($i=0$i $this->db_tables_count$i++) {
      
$table_name $this->db_tables[$i];
      
// if (empty($this->db_delete[$i])) continue;
      // fixme: merge $where with $this->db_where[$i]
      
$where $this->_where_where('DELETE',$i,array($this->_table_id($i)), array($id));
      
$query_string 'DELETE FROM ' $table_name $where;
      if (
$funcs $this->_db_cb_triggers['DELETE']['BEFORE'][$i]) {
    foreach (
$funcs as $func) {
      
$q $func($id,$where);
      
$this->_db_query_before .= $q ";"// collect side-effects
    
}
    
$this->debug($this->_db_query_before);
      }
      
$this->_db_query_last $query_string;
      
$r ur_db_query($query_string);
      
$this->debug($query_string);
      if (
$funcs $this->_db_cb_triggers['DELETE']['AFTER'][$i]) {
    foreach (
$funcs as $func) {
      
$q $func($id,$where);
      
$this->_db_query_after .= $q ";";  // collect side-effects
    
}
    
$this->debug($this->_db_query_after);
      }
    }
    
ur_db_unlock();
    unset (
$db_rollback);
    if (
$r)
      return 
mysql_affected_rows($r);
    else
      return 
0;
  }

  
// register_default('update', 'users_info','date_modified','now()');
  // => default value for UPDATE users_info SET date_modified='now()'
  // similar to but, simplier than a sql trigger.
  
function register_default ($action$table_name$field_name$value) {
    
$action strtoupper($action);
    
// get table and field indices before
    
$i $this->_table_index($table_name);
    
$j $this->_field_index($i$field_name);
    if (empty(
$this->_db_cb_defaults[$action])) {
      
$this->_db_cb_defaults[$action] = array();
      
$this->_db_cb_defaults[$action][$i][$j] = $value;
    }
  }

  
// Procedural PHP triggers, similar to SQL triggers
  // UPDATE, INSERT, SELETE trigger columns wise, DELETE row wise.
  // php function:
  //  $this->create_trigger('SELECT',TABLE_MAIL_LOG,'template_vars',create_function('$id,$name,$value','return unserialize($name)'));
  //  $this->create_trigger('INSERT|UPDATE',TABLE_MAIL_LOG,'template_vars',create_function('$id,$name,$value','return serialize($name)'));
  //  $this->create_trigger ('select', 'products', 'products_price', create_function('$id,$name,$value','return tep_currency_format($value);'));
  //    but $this->create_function ('select', 'products', 'products_price', 'tep_currency_format') is much simplier!
  //
  // SQL functions only via php functions, like this:
  //   create_trigger ('update', 'products_description', 'date_modified',
  //                   create_function('$id,$v','ur_db_query("update products_description set date_modified = now()");'));
  //   create_trigger ('delete', 'company_name', 'ignored',
  //                   create_function('$id,$name','ur_db_query("update company set company_name = \'$name\'");'));
  // Note: We don't check for cascading triggers yet.
  //
  // Params: the id as first param, the field_name as 2nd, the field_value as 3rd parameter.
  // * select triggers: when: <after>, args: (id, field_name, field_value) => string
  // * update triggers: when: <before>|after, args: (id, field_name, field_value, the user-defined where parameter)
  // * insert triggers: when: <before>|after, args: (id, field_name, field_value)
  // * delete triggers: when: before|<after>, args: (id), field_name ignored
  //
  // See the simplier create_function for just one parameter functions.
  
function create_trigger ($action$table_name$field_name$php_func$when false) {
    if (
strstr($action,'|')) {
      foreach (
explode('|',$action) as $act) {
    
$this->create_trigger($act$table_name$field_name$php_func$when);
      }
      return;
    } else {
      
$action strtoupper($action);
      
$i $this->_table_index($table_name);
      
$j $this->_field_index($i$field_name);
      if (! 
$this->_db_cb_triggers[$action]) $this->_db_cb_triggers[$action] = array();
      if (! 
$when) {
    if ((
$action == 'SELECT') or ($action == 'DELETE')) {
      
$when 'AFTER';
    } else {
      
$when 'BEFORE';
    }
      }
      if (
$action == 'DELETE') {
    if (! 
$this->_db_cb_triggers[$action][$when][$i]) $this->_db_cb_triggers[$action][$when][$i] = array();
    
$this->_db_cb_triggers[$action][$when][$i][] = $php_func// numeric array for delete
      
} else {
    
$this->_db_cb_triggers[$action][$when][$i][$j] = $php_func// hash
      
}
    }
  }

  
// Unregister this kind of callback.
  // e.g. drop_trigger ('delete', "company", 'company_name');
  
function drop_trigger ($action$table_name$field_name) {
    if (
strstr($action,'|')) {
      foreach (
explode('|',$action) as $act) {
    
$this->drop_trigger($act$table_name$field_name);
      }
      return;
    } else {
      
$i $this->_table_index($table_name);
      
$action strtoupper($action);
      if (
$action == 'DELETE') {
    unset(
$this->_db_cb_triggers[$action]['AFTER'][$i]);
    unset(
$this->_db_cb_triggers[$action]['BEFORE'][$i]);
      } else {
    
$j $this->_field_index($i$field_name);
    unset(
$this->_db_cb_triggers[$action]['AFTER'][$i][$j]);
    unset(
$this->_db_cb_triggers[$action]['BEFORE'][$i][$j]);
      }
    }
  }

  
// user-defined callbacks and functions in PHP
  // for efficiency we must register it with each field
  // create_function ('select', 'products', 'products_price', 'tep_currency_format')
  // simple function with one param: called on the each value in the result set
  
function create_function ($action$table_name$field_name$func) {
    if (
strstr($action,'|')) {
      foreach (
explode('|',$action) as $act) {
    
$this->create_function($act$table_name$field_name$func);
      }
      return;
    } else {
      
$action strtoupper($action);
      
$i $this->_table_index($table_name);
      
$j $this->_field_index($i$field_name);
      if (! 
$this->_db_cb_functions[$action]) $this->_db_cb_functions[$action] = array();
      
$this->_db_cb_functions[$action][$i][$j] = $func// hash
    
}
  }

  
// Unregister this kind of callback.
  
function drop_function ($action$table_name$field_name) {
    if (
strstr($action,'|')) {
      foreach (
explode('|',$action) as $act) {
    
$this->drop_function($act$table_name$field_name);
      }
      return;
    } else {
      
$action strtoupper($action);
      
$i $this->_table_index($table_name);
      
$j $this->_field_index($i$field_name);
      unset(
$this->_db_cb_functions[$action][$i][$j]);
    }
  }

  
// Registers a callback which may group a result set. See docs for better SQL Engines.
  // FIXME: implement the callbacks to this and provide samples
  
function create_aggregate_function ($action$table_name$field_name$funcname$func$return_type) {
    if (
strstr($action,'|')) {
      foreach (
explode('|',$action) as $act) {
    
$this->create_aggregate_function($act$table_name$field_name$funcname$func$return_type);
      }
      return;
    } else {
      
$i $this->_table_index($table_name);
      
$j $this->_field_index($i$field_name);
      
$action strtoupper($action);
      if (! 
$this->_db_aggregate_functions[$action]) $this->_db_aggregate_functions[$action] = array();
      
$this->_db_aggregate_functions[$action][$i][$j] = array($funcname$func$return_type);
    }
  }

  
// Unregister this kind of callback.
  
function drop_aggregate_function ($action$table_name$field_name) {
    if (
strstr($action,'|')) {
      foreach (
explode('|',$action) as $act) {
    
$this->drop_aggregate_function($act$table_name$field_name);
      }
      return;
    } else {
      
$action strtoupper($action);
      
$i $this->_table_index($table_name);
      
$j $this->_field_index($i$field_name);
      if (isset(
$this->_db_aggregate_functions[$action][$i][$j])) {
          unset(
$this->_db_aggregate_functions[$action][$i][$j]);
          return 
true;
      } else {
      return 
false;
      }
    }
  }

  function 
debug_print_all_fields() {
    foreach (
explode(',',$this->fields) as $f) {
      
$s .= "$f: {$this->$f}<br />";
    }
    return 
$s;
  }

  function 
db_all_fields () {           // flat array of all field names
    
$db_fields = array();
    for (
$i=0$i $this->db_tables_count$i++) {
      for (
$j=0$j count($this->db_tables_fields[$i]); $j++) {
    if (!
$db_fields or !in_array($this->db_tables_fields[$i][$j],$db_fields)) { // the first occurence only
      
$db_fields[] = $this->db_tables_fields[$i][$j];
    }
      }
    }
    return 
$db_fields;
  }

  function 
dump () {
    
var_dump($this);
  }
  function 
print_r () {
    
print_r($this);
  }

  function 
all_vars () {
    
$r = array();
    foreach (
get_object_vars($this) as $f => $v) {
      if (
preg_match('/^(DB_|_).*/i',$f)) continue;
      if (
preg_match('/.*_fields$/i',$f)) continue;
      
$r[$f] = $v;
    }
    return 
$r;
  }

  function 
as_array ($exclude false$fields false) {
    
$a = array();
    if (! 
$fields )
      
$fields array_merge($this->db_all_fields(),array_keys($this->all_vars()));
    foreach (
$fields as $f) {
      if (
is_array($exclude)) {
    if (
in_array($f,$exclude)) continue;
      }
      
$a[$f] = $this->{$f};
    }
    return 
$a;
  }

}

////
// parse db for primary indices, other indices, and matching field names 
// create 1:1 and 1:n relations in a relation table:
// src_table, src_column, dest_table, dest_column, many_bool
function db_create_relations ($db_name$db_link) {
  die(
'db_create_relations() not yet ready');
}

if (! 
function_exists('ur_array_position')) {
////
// returns position of found value in indexed array
  
function ur_array_position ($value$array$case_insensitive false) {
    if (!
is_array($array)) return false;
    for (
$i=0$i count($array); $i++) {
      if (
$array[$i] === $value) return $i;
      if (
is_string($value)) {
    if (
$case_insensitive) {
      if (
strcasecmp($array[$i], $value) === 0) return $i;
    } else {
      if (
strcmp($array[$i], $value) === 0) return $i;
    }
      }
    }
    return 
false;
  }
}

if (! 
function_exists('ur_db_query')) {

  
// sample query with logging
  
function ur_db_query($db_query) {
    global 
$db_link;
    if (
STORE_DB_TRANSACTIONS == true) {
       
error_log($db_query "\n"3STORE_DB_LOG);
    }
    
$result mysql_query($db_query$db_link) or ur_db_error($db_querymysql_errno(), mysql_error());
    if (
STORE_DB_TRANSACTIONS == true) {
       
$result_error mysql_error();
       if (
COUNT_DB_TRANSACTION_ROWS and !$result_error) {
     if ( 
preg_match('/^select/i',$db_query))
       
$result_error .= " =>#" . @mysql_num_rows($result);
     elseif (
preg_match('/^insert/i',$db_query))
       
$result_error .= " =>id " mysql_insert_id();
     elseif (
preg_match('/^(delete|update)/i',$db_query))
       
$result_error .= " =>#" mysql_affected_rows();
       }
       
error_log("  RESULT " $result " " $result_error "\n"3STORE_DB_LOG);
    }
    return 
$result;
  }

  function 
ur_db_lock($tables) {
    if (
is_array($tables))
      return 
ur_db_query('lock tables ' implode(' write, '$tables) . ' write');
    else
      return 
false;
  }

  function 
ur_db_unlock() {
    return 
ur_db_query('unlock tables');
  }

  function 
ur_db_error ($query$errno$error) {
    global 
$db_rollback$HTTP_SERVER_VARS;
    if (
$db_rollback and is_array($db_rollback)) {
      
ur_db_unlock();
      foreach (
$db_rollback as $q) {
    
ur_db_query($q);
      }
    }
    if (
domain_test()) {
      if (! 
getenv('REQUEST_URI'))
    die(
$errno ' - ' $error ' "' $query '" [STOP]');
      else
    die(
'<font color="#000000"><b>' $errno ' - ' $error '<br /><br />' $query '<br /><br /><small><font color="#ff0000">[STOP]</font></small><br /><br /></b></font>');
    } else {
      
error_log($HTTP_SERVER_VARS['REQUEST_URI'] . ' ' $HTTP_SERVER_VARS['REMOTE_ADDR'] . " SQL ERR: $errno . ' - ' . $error . ' ' . $query"1ADMIN_EMAIL"Subject: [SQL] $error");
    }
    die(
'<font color="#000000"><b>SQL ERROR ' $errno '<br />The system administrator has been automatically informed of this problem. Sorry.<br /><small><font color="#ff0000">[STOP]</font></small><br /><br /></b></font>');
  }

}

?>