Ferreteria/archive/data.php: Difference between revisions

From Woozle Writes Code
Jump to navigation Jump to search
(latest version from Rizzo (hypertwiki, htyp, vbzcart))
m (21 revisions imported: moving this project here)
 
(6 intermediate revisions by one other user not shown)
Line 1: Line 1:
==About==
==About==
Database abstraction classes; used by [[VbzCart]], [[SpamFerret]], [[AudioFerret]], [[WorkFerret]]
Database abstraction classes; used by [[VbzCart]], [[SpamFerret]], [[AudioFerret]], [[WorkFerret]]
==History==
* '''2013-01-25''' Working version from HostGator 1: seems to have added the data-engine-handling classes
* '''2013-01-27''' Working version from Rizzo: minor changes to handle indirect access to database engine better
==Code==
==Code==
<php><?php
<syntaxhighlight lang=php><?php
/* ===========================
/* ===========================
  *** DATA UTILITY CLASSES ***
  *** DATA UTILITY CLASSES ***
   AUTHOR: Woozle (Nick) Staddon
   AUTHOR: Woozle Staddon
   HISTORY:
   HISTORY:
     2007-05-20 (wzl) These classes have been designed to be db-engine agnostic, but I wasn't able
     2007-05-20 (wzl) These classes have been designed to be db-engine agnostic, but I wasn't able
Line 50: Line 53:
     2011-10-17 (wzl) ValueNz() rewritten (now goes directly to Row array instead of calling Value())
     2011-10-17 (wzl) ValueNz() rewritten (now goes directly to Row array instead of calling Value())
     2012-01-22 (wzl) clsDatabase_abstract
     2012-01-22 (wzl) clsDatabase_abstract
    2012-01-28 (wzl) clsDataEngine classes
    2012-12-31 (wzl) improved error handling in clsDataEngine_MySQL.db_open()
    2013-01-24 (wzl) clsDatabase_abstract:: SelfClass() and Spawn()
   FUTURE:
   FUTURE:
     API FIXES:
     API FIXES:
Line 56: Line 62:
// Select which DB library to use --
// Select which DB library to use --
// exactly one of the following must be true:
// exactly one of the following must be true:
/*
These have been replaced by KS_DEFAULT_ENGINE
define('KF_USE_MYSQL',TRUE); // in progress
define('KF_USE_MYSQL',TRUE); // in progress
define('KF_USE_MYSQLI',FALSE); // complete & tested
define('KF_USE_MYSQLI',FALSE); // complete & tested
define('KF_USE_DBX',false); // not completely written; stalled
define('KF_USE_DBX',false); // not completely written; stalled
*/
define('KS_DEFAULT_ENGINE','clsDataEngine_MySQL'); // classname of default database engine


if (!defined('KDO_DEBUG')) { define('KDO_DEBUG',FALSE); }
if (!defined('KDO_DEBUG')) { define('KDO_DEBUG',FALSE); }
Line 67: Line 80:


abstract class clsDatabase_abstract {
abstract class clsDatabase_abstract {
    protected $objEng; // engine object
    protected $objRes; // result object
    protected $cntOpen;
    public function InitBase() {
$this->cntOpen = 0;
    }
    protected function DefaultEngine() {
$cls = KS_DEFAULT_ENGINE;
return new $cls;
    }
    public function Engine(clsDataEngine $iEngine=NULL) {
if (!is_null($iEngine)) {
    $this->objEng = $iEngine;
} else {
    if (!isset($this->objEng)) {
$this->objEng = $this->DefaultEngine();
    }
}
return $this->objEng;
    }
    public function Open() {
if ($this->cntOpen == 0) {
    $this->Engine()->db_open();
}
if (!$this->isOk()) {
    $this->Engine()->db_get_error();
}
$this->cntOpen++;
    }
    public function Shut() {
$this->cntOpen--;
if ($this->cntOpen == 0) {
    $this->Engine()->db_shut();
}
    }
     /*-----
     /*-----
       PURPOSE: generic table-creation function
       PURPOSE: generic table-creation function
Line 76: Line 127:
     public function Make($iName,$iID=NULL) {
     public function Make($iName,$iID=NULL) {
if (!isset($this->$iName)) {
if (!isset($this->$iName)) {
    $this->$iName = new $iName($this);
    if (class_exists($iName)) {
$this->$iName = new $iName($this);
    } else {
throw new exception('Unknown class "'.$iName.'" requested.');
    }
}
}
if (!is_null($iID)) {
if (!is_null($iID)) {
Line 86: Line 141:
     abstract public function Exec($iSQL);
     abstract public function Exec($iSQL);
     abstract public function DataSet($iSQL=NULL,$iClass=NULL);
     abstract public function DataSet($iSQL=NULL,$iClass=NULL);
}


/*====
    // ENGINE WRAPPER FUNCTIONS
  TODO: this is actually specific to a particular library for MySQL, so it should probably be renamed
    public function engine_db_query($iSQL) {
     to reflect that.
//echo '#2: ENGINE CLASS=['.get_class($this->Engine()).']<br>'; // comes up as clsDataEngine_MySQL
return $this->Engine()->db_query($iSQL);
    }
    public function engine_db_query_ok() {
return $this->objRes->is_okay();
    }
    public function engine_db_get_new_id() {
return $this->Engine()->db_get_new_id();
    }
    public function engine_db_rows_affected() {
return $this->Engine()->db_get_qty_rows_chgd();
    }
/*
    public function engine_row_rewind() {
return $this->objRes->do_rewind();
    }
    public function engine_row_get_next() { throw new exception('how did we get here?');
return $this->objRes->get_next();
    }
    public function engine_row_get_count() {
return $this->objRes->get_count();
    }
    public function engine_row_was_filled() {
return $this->objRes->is_filled();
     }
*/
*/
class clsDatabase extends clsDatabase_abstract {
}
    private $cntOpen; // count of requests to keep db open
/*%%%%
     private $strType; // type of db (MySQL etc.)
  HISTORY:
    private $strUser; // database user
     2013-01-25 InitSpec() only makes sense for _CliSrv, which is the first descendant that needs connection credentials.
    private $strPass; // password
*/
    private $strHost; // host (database server domain-name or IP address)
abstract class clsDataEngine {
     private $strName; // database (schema) name
     private $arSQL;


     private $Conn; // connection object
     //abstract public function InitSpec($iSpec);
  // status
     abstract public function db_open();
    private $strErr; // latest error message
     abstract public function db_shut();
     public $sql; // last SQL executed (or attempted)
    public $arSQL; // array of all SQL statements attempted
     public $doAllowWrite; // FALSE = don't execute UPDATE or INSERT commands, just log them


     public function __construct($iConn) {
    /*----
      $this->Init($iConn);
      RETURNS: clsDataResult descendant
    */
     public function db_query($iSQL) {
$this->LogSQL($iSQL);
return $this->db_do_query($iSQL);
     }
     }
     /*=====
     /*----
       INPUT:  
       RETURNS: clsDataResult descendant
$iConn: type:user:pass@server/dbname
     */
     */
     public function Init($iConn) {
     abstract protected function db_do_query($iSQL);
      $this->doAllowWrite = TRUE; // default
    //abstract public function db_get_error();
      $this->cntOpen = 0;
    abstract public function db_get_new_id();
//     list($part1,$part2) = split('@',$iConn);
    abstract public function db_safe_param($iVal);
      $ar = preg_split('/@/',$iConn);
    //abstract public function db_query_ok(array $iBox);
      if (array_key_exists(1,$ar)) {
    abstract public function db_get_error();
  list($part1,$part2) = preg_split('/@/',$iConn);
    abstract public function db_get_qty_rows_chgd();
      } else {
 
  throw new exception('Connection string not formatted right: ['.$iConn.']');
    // LOGGING -- eventually split this off into handler class
      }
    protected function LogSQL($iSQL) {
//     list($this->strType,$this->strUser,$this->strPass) = split(':',$part1);
$this->sql = $iSQL;
      list($this->strType,$this->strUser,$this->strPass) = preg_split('/:/',$part1);
$this->arSQL[] = $iSQL;
      list($this->strHost,$this->strName) = explode('/',$part2);
      $this->strType = strtolower($this->strType); // make sure it is lowercased, for comparison
      $this->strErr = NULL;
     }
     }
     public function Open() {
     public function ListSQL($iPfx=NULL) {
      CallEnter($this,__LINE__,'clsDatabase.Open()');
$out = '';
      if ($this->cntOpen == 0) {
foreach ($this->arSQL as $sql) {
  // then actually open the db
    $out .= $iPfx.$sql;
      if (KF_USE_MYSQL) {
}
      $this->Conn = mysql_connect( $this->strHost, $this->strUser, $this->strPass, false );
return $out;
      assert('is_resource($this->Conn)');
      $ok = mysql_select_db($this->strName, $this->Conn);
      if (!$ok) {
      $this->getError();
      }
      }
      if (KF_USE_MYSQLI) {
      $this->Conn = new mysqli($this->strHost,$this->strUser,$this->strPass,$this->strName);
      }
      if (KF_USE_DBX) {
      $this->Conn = dbx_connect($this->strType,$this->strHost,$this->strName,$this->strUser,$this->strPass);
      }
      }
      if (!$this->isOk()) {
$this->getError();
      }
      $this->cntOpen++;
      CallExit('clsDatabase.Open() - '.$this->cntOpen.' lock'.Pluralize($this->cntOpen));
     }
     }
    public function Shut() {
}
      CallEnter($this,__LINE__,'clsDatabase.Shut()');
/*%%%%
      $this->cntOpen--;
  PURPOSE: encapsulates the results of a query
      if ($this->cntOpen == 0) {
*/
      if (KF_USE_MYSQL) {
abstract class clsDataResult {
    mysql_close($this->Conn);
    protected $box;
      }
 
      if (KF_USE_MYSQLI) {
    public function __construct(array $iBox=NULL) {
    $this->Conn->close();
$this->box = $iBox;
      }
      if (KF_USE_DBX) {
    dbx_close($this->Conn);
      }
      }
      CallExit('clsDatabase.Shut() - '.$this->cntOpen.' lock'.Pluralize($this->cntOpen));
     }
     }
     public function GetHost() {
    /*----
return $this->strHost;
      PURPOSE: The "Box" is an array containing information which this class needs but which
the calling class has to be responsible for. The caller doesn't need to know what's
in the box, it just needs to keep it safe.
    */
     public function Box(array $iBox=NULL) {
if (!is_null($iBox)) {
    $this->box = $iBox;
}
return $this->box;
     }
     }
     public function GetUser() {
     public function Row(array $iRow=NULL) {
return $this->strUser;
if (!is_null($iRow)) {
    $this->box['row'] = $iRow;
    return $iRow;
}
if ($this->HasRow()) {
    return $this->box['row'];
} else {
    return NULL;
}
     }
     }
     /*=====
     /*----
       PURPOSE: For debugging, mainly
       USAGE: used internally when row retrieval comes back FALSE
      RETURNS: TRUE if database connection is supposed to be open
     */
     */
     public function isOpened() {
    protected function RowClear() {
return ($this->cntOpen > 0);
$this->box['row'] = NULL;
    }
     public function Val($iKey,$iVal=NULL) {
if (!is_null($iVal)) {
    $this->box['row'][$iKey] = $iVal;
    return $iVal;
} else {
    if (!array_key_exists('row',$this->box)) {
throw new exception('Row data not loaded yet.');
    }
    return $this->box['row'][$iKey];
}
     }
     }
    /*=====
     public function HasRow() {
      PURPOSE: Checking status of a db operation
if (array_key_exists('row',$this->box)) {
      RETURNS: TRUE if last operation was successful
    return (!is_null($this->box['row']));
    */
     public function isOk() {
if (empty($this->strErr)) {
    return TRUE;
} elseif ($this->Conn == FALSE) {
    return FALSE;
} else {
} else {
    return FALSE;
    return FALSE;
}
}
     }
     }
     public function getError() {
/* might be useful, but not actually needed now
      if (is_null($this->strErr)) {
     public function HasVal($iKey) {
      // avoid having an ok status overwrite an actual error
$row = $this->Row();
  if (KF_USE_MYSQL) {
return array_key_exists($iKey,$this->box['row']);
      $this->strErr = mysql_error();
    }
  }
*/
  if (KF_USE_MYSQLI) {
    abstract public function is_okay();
      $this->strErr = $this->Conn->error;
    /*----
  }
      ACTION: set the record pointer so the first row in the set will be read next
      }
    */
      return $this->strErr;
    abstract public function do_rewind();
    /*----
      ACTION: Fetch the first/next row of data from a result set
    */
    abstract public function get_next();
    /*----
      ACTION: Return the number of rows in the result set
    */
    abstract public function get_count();
    /*----
      ACTION: Return whether row currently has data.
    */
    abstract public function is_filled();
}
 
/*%%%%%
  PURPOSE: clsDataEngine that is specific to client-server databases
    This type will always need host and schema names, username, and password.
*/
abstract class clsDataEngine_CliSrv extends clsDataEngine {
    protected $strType, $strHost, $strUser, $strPass;
 
    public function InitSpec($iSpec) {
$ar = preg_split('/@/',$iSpec);
if (array_key_exists(1,$ar)) {
    list($part1,$part2) = preg_split('/@/',$iSpec);
} else {
    throw new exception('Connection string not formatted right: ['.$iSpec.']');
}
list($this->strType,$this->strUser,$this->strPass) = preg_split('/:/',$part1);
list($this->strHost,$this->strName) = explode('/',$part2);
$this->strType = strtolower($this->strType); // make sure it is lowercased, for comparison
$this->strErr = NULL;
     }
     }
     public function ClearError() {
     public function Host() {
$this->strErr = NULL;
return $this->strHost;
     }
     }
     protected function LogSQL($iSQL) {
     public function User() {
$this->sql = $iSQL;
return $this->strUser;
$this->arSQL[] = $iSQL;
     }
     }
     public function ListSQL($iPfx=NULL) {
}
$out = '';
 
foreach ($this->arSQL as $sql) {
class clsDataResult_MySQL extends clsDataResult {
    $out .= $iPfx.$sql;
 
    /*----
      HISTORY:
2012-09-06 This needs to be public so descendant helper classes can transfer the resource.
    */
     public function Resource($iRes=NULL) {
if (!is_null($iRes)) {
    $this->box['res'] = $iRes;
}
}
return $out;
return $this->box['res'];
     }
     }
     /*----
     /*----
      NOTES:
* For queries returning a resultset, mysql_query() returns a resource on success, or FALSE on error.
* For other SQL statements, INSERT, UPDATE, DELETE, DROP, etc, mysql_query() returns TRUE on success or FALSE on error.
       HISTORY:
       HISTORY:
2011-03-04 added DELETE to list of write commands; rewrote to be more robust
2012-02-04 revised to use box['ok']
     */
     */
     protected function OkToExecSQL($iSQL) {
     public function do_query($iConn,$iSQL) {
if ($this->doAllowWrite) {
//$ok = mysql_select_db('igov_app');
    return TRUE;
//echo 'OK=['.$ok.']<br>';
$res = mysql_query($iSQL,$iConn);
if (is_resource($res)) {
//echo 'GOT TO LINE '.__LINE__.'<br>';
    $this->Resource($res);
    $this->box['ok'] = TRUE;
} else {
} else {
    // this is a bit of a kluge... need to strip out comments and whitespace
//echo 'GOT TO LINE '.__LINE__.' - SQL='.$iSQL.'<br>';
    // but basically, if the SQL starts with UPDATE, INSERT, or DELETE, then it's a write command so forbid it
    $this->Resource(NULL);
    $sql = strtoupper(trim($iSQL));
    $this->box['ok'] = $res; // TRUE if successful, false otherwise
    $cmd = preg_split (' ',$sql,1); // get just the first word
    switch ($cmd) {
      case 'UPDATE':
      case 'INSERT':
      case 'DELETE':
return FALSE;
      default:
return TRUE;
    }
}
}
     }
     }
     /*=====
     /*----
       FUTURE: Apparently _api_query() has very similar code. Eventually _api_query() should be a method in
       USAGE: call after do_query()
an SQL engine class.
      FUTURE: should probably reflect status of other operations besides do_query()
OLD NOTE: Exec() and _api_query() perform almost identical functions. Do we really need them both?
When we rewrite these as a single function, perhaps include a $iIsWrite flag parameter so we can
eliminate OkToExecSQL().
       HISTORY:
       HISTORY:
2011-02-24 Now passing $this->Conn to mysql_query() because somehow the connection was getting set
2012-02-04 revised to use box['ok']
  to the wiki database instead of the original.
     */
     */
     public function Exec($iSQL) {
     public function is_okay() {
CallEnter($this,__LINE__,__CLASS__.'.'.__METHOD__.'('.$iSQL.')');
return $this->box['ok'];
$this->LogSQL($iSQL);
    }
if ($this->OkToExecSQL($iSQL)) {
    public function do_rewind() {
    if (KF_USE_MYSQL) {
$res = $this->Resource();
$ok = mysql_query($iSQL,$this->Conn);
mysql_data_seek($res, 0);
if (is_resource($ok)) { // this should never happen here
    }
    $ok = TRUE;
    public function get_next() {
}
$res = $this->Resource();
    }
if (is_resource($res)) {
    if (KF_USE_MYSQLI) {
    $row = mysql_fetch_assoc($res);
$objQry = $this->Conn->prepare($iSQL);
    if ($row === FALSE) {
if (is_object($objQry)) {
$this->RowClear();
    $ok = $objQry->execute();
    } else {
} else {
$this->Row($row);
    $ok = false;
    //echo '<br>SQL error: '.$iSQL.'<br>';
}
    }
 
    if (!$ok) {
$this->getError();
    }
 
    if (KF_USE_MYSQL) {
    // no need to do anything; no resource allocated as long as query SQL was non-data-fetching
    }
    if (KF_USE_MYSQLI) {
$objQry->close();
    }
    }
    return $row;
} else {
} else {
    $ok = TRUE;
    return NULL;
}
}
CallExit(__CLASS__.'.'.__METHOD__.'()');
return $ok;
     }
     }
     public function RowsAffected() {
    /*=====
if (KF_USE_MYSQL) {
      ACTION: Return the number of rows in the result set
    return mysql_affected_rows($this->Conn);
    */
     public function get_count() {
$res = $this->Resource();
if ($res === FALSE) {
    return NULL;
} else {
    if (is_resource($res)) {
$arRow = mysql_num_rows($res);
return $arRow;
    } else {
return NULL;
    }
}
}
     }
     }
     public function NewID($iDbg=NULL) {
     public function is_filled() {
if (KF_USE_MYSQL) {
return $this->HasRow();
    $id = mysql_insert_id($this->Conn);
    }
}
 
class clsDataEngine_MySQL extends clsDataEngine_CliSrv {
    private $objConn; // connection object
 
    public function db_open() {
$this->objConn = @mysql_connect( $this->strHost, $this->strUser, $this->strPass, false );
if ($this->objConn === FALSE) {
    $arErr = error_get_last();
    throw new exception('MySQL could not connect: '.$arErr['message']);
} else {
    $ok = mysql_select_db($this->strName, $this->objConn);
    if (!$ok) {
throw new exception('MySQL could not select database "'.$this->strName.'": '.mysql_error());
    }
}
}
if (KF_USE_MYSQLI) {
    }
    $id = $this->Conn->insert_id;
    public function db_shut() {
}
mysql_close($this->objConn);
if ($this->doAllowWrite) {
    }
    assert('$id!=0 /*'.$iDbg.'// SQL was: [ '.$this->sql.' ] */');
    protected function Spawn_ResultObject() {
return new clsDataResult_MySQL();
    }
    /*----
      RETURNS: clsDataResult descendant
    */
    protected function db_do_query($iSQL) {
if (is_resource($this->objConn)) {
    $obj = $this->Spawn_ResultObject();
    $obj->do_query($this->objConn,$iSQL);
    return $obj;
} else {
    throw new Exception('Database Connection object is a '.gettype($this->objConn).', not a resource');
}
}
    }
    public function db_get_new_id() {
$id = mysql_insert_id($this->objConn);
return $id;
return $id;
     }
     }
     public function SafeParam($iString) {
/*
if (is_object($iString)) {
     public function db_query_ok(array $iBox) {
    echo '<b>Internal error</b>: argument is an object of class '.get_class($iString).', not a string.<br>';
$obj = new clsDataQuery_MySQL($iBox);
    throw new exception('Unexpected argument type.');
return $obj->QueryOkay();
    }
*/
    public function db_get_error() {
return mysql_error();
    }
    public function db_safe_param($iVal) {
if (is_resource($this->objConn)) {
    $out = mysql_real_escape_string($iVal,$this->objConn);
} else {
    throw new exception(get_class($this).'.SafeParam("'.$iString.'") has no connection.');
}
}
CallEnter($this,__LINE__,__CLASS__.'.SafeParam("'.$iString.'")');
if (KF_USE_MYSQL) {
    if (is_resource($this->Conn)) {
$out = mysql_real_escape_string($iString,$this->Conn);
    } else {
$out = '<br>'.get_class($this).'.SafeParam("'.$iString.'") has no connection.';
    }
}
if (KF_USE_MYSQLI) {
    $out = $this->Conn->escape_string($iString);
}
CallExit('SafeParam("'.$iString.'")');
return $out;
return $out;
     }
     }
     public function ErrorText() {
     public function db_get_qty_rows_chgd() {
if ($this->strErr == '') {
return mysql_affected_rows($this->objConn);
    $this->_api_getError();
    }
}
}
return $this->strErr;
    }


/******
/*
SECTION: API WRAPPER FUNCTIONS
   These interfaces marked "abstract" have not been completed or tested.
   FUTURE: Create cls_db_api, which should encapsulate the different APIs of the different db libraries.
     They're mainly here as a place to stick the partial code I wrote for them
     On initialization, the clsDatabase can detect which one to use. This will eliminate the need for
     back when I first started writing the data.php library.
     "if-then" statements based on pre-set constants.
*/
*/
     public function _api_query($iSQL) {
abstract class clsDataEngine_MySQLi extends clsDataEngine_CliSrv {
$this->LogSQL($iSQL);
    private $objConn; // connection object
if ($this->OkToExecSQL($iSQL)) {
 
    if (KF_USE_MYSQL) {
     public function db_open() {
if (is_resource($this->Conn)) {
$this->objConn = new mysqli($this->strHost,$this->strUser,$this->strPass,$this->strName);
    return mysql_query($iSQL,$this->Conn);
    }
} else {
    public function db_shut() {
    throw new Exception('Database Connection object is not a resource');
$this->objConn->close();
}
    }
    }
    public function db_get_error() {
    if (KF_USE_MYSQLI) {
return $this->objConn->error;
$this->Conn->real_query($iSQL);
    }
return $this->Conn->store_result();
    public function db_safe_param($iVal) {
    }
return $this->objConn->escape_string($iVal);
    if (KF_USE_DBX) {
return dbx_query($this->Conn,$iSQL,DBX_RESULT_ASSOC);
    }
}
     }
     }
     public function _api_rows_rewind($iRes) {
     protected function db_do_query($iSQL) {
if (KF_USE_MYSQL) {
$this->objConn->real_query($iSQL);
    mysql_data_seek($iRes, 0);
return $this->objConn->store_result();
}
     }
     }
     public function _api_fetch_row($iRes) {
     public function db_get_new_id() {
    // ACTION: Fetch the first/next row of data from a result set
$id = $this->objConn->insert_id;
if (KF_USE_MYSQL) {
return $id;
    if (is_resource($iRes)) {
return mysql_fetch_assoc($iRes);
    } else {
return NULL;
    }
}
if (KF_USE_MYSQLI) {
    return $iRes->fetch_assoc();
}
     }
     }
     /*=====
     public function row_do_rewind(array $iBox) {
      ACTION: Return the number of rows in the result set
    }
     */
    public function row_get_next(array $iBox) {
     public function _api_count_rows($iRes) {
return $iRes->fetch_assoc();
if (KF_USE_MYSQL) {
     }
    if ($iRes === FALSE) {
     public function row_get_count(array $iBox) {
return NULL;
return $iRes->num_rows;
    } else {
    }
if (is_resource($iRes)) {
    public function row_was_filled(array $iBox) {
    return mysql_num_rows($iRes);
return ($this->objData !== FALSE) ;
} else {
    }
    return NULL;
}
}
abstract class clsDataEngine_DBX extends clsDataEngine_CliSrv {
    }
    private $objConn; // connection object
}
 
if (KF_USE_MYSQLI) {
    public function db_open() {
    return $iRes->num_rows;
$this->objConn = dbx_connect($this->strType,$this->strHost,$this->strName,$this->strUser,$this->strPass);
}
    }
    public function db_shut() {
dbx_close($this->Conn);
    }
 
    protected function db_do_query($iSQL) {
return dbx_query($this->objConn,$iSQL,DBX_RESULT_ASSOC);
    }
    public function db_get_new_id() {
    }
    public function row_do_rewind(array $iBox) {
    }
    public function row_get_next(array $iBox) {
    }
    public function row_get_count(array $iBox) {
     }
     }
     public function _api_row_filled($iRow) {
     public function row_was_filled(array $iBox) {
if (KF_USE_MYSQL) {
    return ($iRow !== FALSE) ;
}
     }
     }
}


/******
/*====
SECTION: OBJECT FACTORY
  TODO: this is actually specific to a particular library for MySQL, so it should probably be renamed
    to reflect that.
*/
*/
     public function DataSet($iSQL=NULL,$iClass=NULL) {
class clsDatabase extends clsDatabase_abstract {
if (is_string($iClass)) {
     private $strType; // type of db (MySQL etc.)
    $objData = new $iClass($this);
    private $strUser; // database user
    assert('is_object($objData)');
    private $strPass; // password
    if (!($objData instanceof clsDataSet)) {
    private $strHost; // host (database server domain-name or IP address)
LogError($iClass.' is not a clsDataSet subclass.');
    private $strName; // database (schema) name
    }
 
} else {
    private $Conn; // connection object
    $objData = new clsDataSet($this);
  // status
    assert('is_object($objData)');
    private $strErr; // latest error message
}
    public $sql; // last SQL executed (or attempted)
assert('is_object($objData->Engine())');
    public $arSQL; // array of all SQL statements attempted
if (!is_null($iSQL)) {
    public $doAllowWrite; // FALSE = don't execute UPDATE or INSERT commands, just log them
    if (is_object($objData)) {
 
$objData->Query($iSQL);
    public function __construct($iConn) {
    }
$this->Init($iConn);
}
$this->doAllowWrite = TRUE; // default
return $objData;
    }
    /*=====
      INPUT:
$iConn: type:user:pass@server/dbname
      TO DO:
Init() -> InitSpec()
InitBase() -> Init()
    */
    public function Init($iConn) {
$this->InitBase();
$this->Engine()->InitSpec($iConn);
     }
     }
}
/*=============
  NAME: clsTable_abstract
  PURPOSE: objects for operating on particular tables
    Does not attempt to deal with keys.
*/
abstract class clsTable_abstract {
    protected $objDB;
    protected $vTblName;
    protected $vSngClass; // name of singular class
    public $sqlExec; // last SQL executed on this table


     public function __construct(clsDatabase $iDB) {
    /*=====
$this->objDB = $iDB;
      PURPOSE: For debugging, mainly
      RETURNS: TRUE if database connection is supposed to be open
    */
     public function isOpened() {
return ($this->cntOpen > 0);
     }
     }
     public function DB() { // DEPRECATED - use Engine()
     /*=====
return $this->objDB;
      PURPOSE: Checking status of a db operation
     }
      RETURNS: TRUE if last operation was successful
     public function Engine() {
     */
return $this->objDB;
     public function isOk() {
    }
if (empty($this->strErr)) {
    public function Name($iName=NULL) {
    return TRUE;
if (!is_null($iName)) {
} elseif ($this->Conn == FALSE) {
    $this->vTblName = $iName;
    return FALSE;
} else {
    return FALSE;
}
}
return $this->vTblName;
     }
     }
     public function NameSQL() {
     public function getError() {
assert('is_string($this->vTblName); /* '.print_r($this->vTblName,TRUE).' */');
      if (is_null($this->strErr)) {
return '`'.$this->vTblName.'`';
      // avoid having an ok status overwrite an actual error
  $this->strErr = $this->Engine()->db_get_error();
      }
      return $this->strErr;
     }
     }
     public function ClassSng($iName=NULL) {
     public function ClearError() {
if (!is_null($iName)) {
$this->strErr = NULL;
    $this->vSngClass = $iName;
}
return $this->vSngClass;
     }
     }
    /*----
     protected function LogSQL($iSQL) {
      ACTION: Make sure the item is ready to be released in the wild
$this->sql = $iSQL;
    */
$this->arSQL[] = $iSQL;
     protected function ReleaseItem(clsRecs_abstract $iItem) {
    }
$iItem->Table = $this;
    public function ListSQL($iPfx=NULL) {
$iItem->objDB = $this->objDB;
$out = '';
foreach ($this->arSQL as $sql) {
    $out .= $iPfx.$sql;
}
return $out;
     }
     }
     /*----
     /*----
       ACTION: creates a new uninitialized singular object but sets the Table pointer back to self
       HISTORY:
      RETURNS: created object
2011-03-04 added DELETE to list of write commands; rewrote to be more robust
      FUTURE: maybe this should be renamed GetNew()?
     */
     */
     public function SpawnItem($iClass=NULL) {
     protected function OkToExecSQL($iSQL) {
if (is_null($iClass)) {
if ($this->doAllowWrite) {
    $strCls = $this->ClassSng();
    return TRUE;
} else {
} else {
    $strCls = $iClass;
    // this is a bit of a kluge... need to strip out comments and whitespace
}
    // but basically, if the SQL starts with UPDATE, INSERT, or DELETE, then it's a write command so forbid it
assert('!empty($strCls);');
    $sql = strtoupper(trim($iSQL));
$objItem = new $strCls;
    $cmd = preg_split (' ',$sql,1); // get just the first word
$this->ReleaseItem($objItem);
    switch ($cmd) {
return $objItem;
      case 'UPDATE':
      case 'INSERT':
      case 'DELETE':
return FALSE;
      default:
return TRUE;
    }
}
     }
     }
    /*=====
      HISTORY:
2011-02-24 Now passing $this->Conn to mysql_query() because somehow the connection was getting set
  to the wiki database instead of the original.
    */
    public function Exec($iSQL) {
CallEnter($this,__LINE__,__CLASS__.'.'.__METHOD__.'('.$iSQL.')');
$this->LogSQL($iSQL);
$ok = TRUE;
if ($this->OkToExecSQL($iSQL)) {
     /*----
     /*----
       RETURNS: dataset defined by the given SQL, wrapped in an object of the current class
       RETURNS: clsDataResult descendant
      USAGE: primarily for joins where you want only records where there is no matching record
in the joined table. (If other examples come up, maybe a DataNoJoin() method would
be appropriate.)
     */
     */
    public function DataSQL($iSQL) {
    $res = $this->Engine()->db_query($iSQL);
$strCls = $this->ClassSng();
    if (!$res->is_okay()) {
$obj = $this->Engine()->DataSet($iSQL,$strCls);
$this->getError();
$this->ReleaseItem($obj);
$ok = FALSE;
$this->sqlExec = $iSQL;
    }
return $obj;
}
CallExit(__CLASS__.'.'.__METHOD__.'()');
return $ok;
     }
     }
    /*----
      RETURNS: dataset containing all fields from the current table,
with additional options (everything after the table name) being
defined by $iSQL, wrapped in the current object class.
    */
    public function DataSet($iSQL=NULL,$iClass=NULL) {
global $sql; // for debugging


$sql = 'SELECT * FROM '.$this->NameSQL();
    public function RowsAffected() {
if (!is_null($iSQL)) {
return $this->Engine()->db_get_qty_rows_chgd();
    $sql .= ' '.$iSQL;
    }
    public function NewID($iDbg=NULL) {
return $this->engine_db_get_new_id();
    }
    public function SafeParam($iVal) {
if (is_object($iVal)) {
    echo '<b>Internal error</b>: argument is an object of class '.get_class($iVal).', not a string.<br>';
    throw new exception('Unexpected argument type.');
}
}
return $this->DataSQL($sql);
$out = $this->Engine()->db_safe_param($iVal);
/*
return $out;
$strCls = $this->vSngClass;
    }
$obj = $this->objDB->DataSet($sql,$strCls);
    public function ErrorText() {
$obj->Table = $this;
if ($this->strErr == '') {
return $obj;
    $this->_api_getError();
*/
}
return $this->strErr;
     }
     }
    public function GetData($iWhere=NULL,$iClass=NULL,$iSort=NULL) {
global $sql; // for debugging


$sql = 'SELECT * FROM '.$this->NameSQL();
/******
if (!is_null($iWhere)) {
SECTION: OBJECT FACTORY
    $sql .= ' WHERE '.$iWhere;
}
if (!is_null($iSort)) {
    $sql .= ' ORDER BY '.$iSort;
}
/*
if (is_null($iClass)) {
    $strCls = $this->vSngClass;
} else {
    $strCls = $iClass;
}
*/
*/
 
    public function DataSet($iSQL=NULL,$iClass=NULL) {
//$obj = $this->objDB->DataSet($sql,$strCls);
if (is_string($iClass)) {
//$res = $this->DB()->Exec($sql);
    $objData = new $iClass($this);
$obj = $this->SpawnItem($iClass);
    assert('is_object($objData)');
assert('is_object($obj->Table);');
    if (!($objData instanceof clsDataSet)) {
$obj->Query($sql);
LogError($iClass.' is not a clsDataSet subclass.');
 
    }
$this->sqlExec = $sql;
} else {
if (!is_null($obj)) {
    $objData = new clsDataSet($this);
//     $obj->Table = $this; // 2011-01-20 this should be redundant now
    assert('is_object($objData)');
    $obj->sqlMake = $sql;
}
assert('is_object($objData->Engine())');
if (!is_null($iSQL)) {
    if (is_object($objData)) {
$objData->Query($iSQL);
    }
}
}
return $obj;
return $objData;
     }
     }
    /*----
}
      RETURNS: SQL for creating a new record for the given data
/*=============
      HISTORY:
  NAME: clsTable_abstract
2010-11-20 Created.
  PURPOSE: objects for operating on particular tables
    */
    Does not attempt to deal with keys.
     public function SQL_forInsert(array $iData) {
*/
$sqlNames = '';
abstract class clsTable_abstract {
$sqlVals = '';
     protected $objDB;
foreach($iData as $key=>$val) {
    protected $vTblName;
    if ($sqlNames != '') {
    protected $vSngClass; // name of singular class
$sqlNames .= ',';
    public $sqlExec; // last SQL executed on this table
$sqlVals .= ',';
 
    }
    public function __construct(clsDatabase_abstract $iDB) {
    $sqlNames .= $key;
$this->objDB = $iDB;
    $sqlVals .= $val;
    }
}
    public function DB() { // DEPRECATED - use Engine()
return 'INSERT INTO `'.$this->Name().'` ('.$sqlNames.') VALUES('.$sqlVals.');';
return $this->objDB;
     }
     }
     /*----
     public function Engine() {
      HISTORY:
return $this->objDB;
2010-11-16 Added "array" requirement for iData
    }
2010-11-20 Calculation now takes place in SQL_forInsert()
    public function Name($iName=NULL) {
     */
if (!is_null($iName)) {
     public function Insert(array $iData) {
    $this->vTblName = $iName;
global $sql;
}
 
return $this->vTblName;
$sql = $this->SQL_forInsert($iData);
     }
$this->sqlExec = $sql;
     public function NameSQL() {
return $this->objDB->Exec($sql);
assert('is_string($this->vTblName); /* '.print_r($this->vTblName,TRUE).' */');
return '`'.$this->vTblName.'`';
    }
    public function ClassSng($iName=NULL) {
if (!is_null($iName)) {
    $this->vSngClass = $iName;
}
return $this->vSngClass;
     }
     }
     /*----
     /*----
       HISTORY:
       ACTION: Make sure the item is ready to be released in the wild
2011-02-02 created for deleting topic-title pairs
     */
     */
     public function Delete($iFilt) {
     protected function ReleaseItem(clsRecs_abstract $iItem) {
$sql = 'DELETE FROM `'.$this->Name().'` WHERE '.$iFilt;
$iItem->Table = $this;
$this->sqlExec = $sql;
$iItem->objDB = $this->objDB;
return $this->Engine()->Exec($sql);
$iItem->sqlMake = $this->sqlExec;
     }
     }
}
/*=============
  NAME: clsTable_keyed_abstract
  PURPOSE: adds abstract methods for dealing with keys
*/
abstract class clsTable_keyed_abstract extends clsTable_abstract {
    abstract public function GetItem();
     /*----
     /*----
       PURPOSE: method for setting a key which uniquely refers to this table
       ACTION: creates a new uninitialized singular object but sets the Table pointer back to self
Useful for logging, menus, and other text-driven contexts.
      RETURNS: created object
      FUTURE: maybe this should be renamed GetNew()?
     */
     */
     public function ActionKey($iName=NULL) {
     public function SpawnItem($iClass=NULL) {
if (!is_null($iName)) {
if (is_null($iClass)) {
    $this->ActionKey = $iName;
    $strCls = $this->ClassSng();
} else {
    $strCls = $iClass;
}
}
return $this->ActionKey;
assert('!empty($strCls);');
$objItem = new $strCls;
$this->ReleaseItem($objItem);
return $objItem;
     }
     }
     /*----
     /*----
       INPUT:
       RETURNS: dataset defined by the given SQL, wrapped in an object of the current class
$iData: array of data necessary to create a new record
      USAGE: primarily for joins where you want only records where there is no matching record
  or update an existing one, if found
in the joined table. (If other examples come up, maybe a DataNoJoin() method would
$iFilt: SQL defining what constitutes an existing record
be appropriate.)
  If NULL, MakeFilt() will be called to build this from $iData.
      HISTORY:
2011-02-22 created
2011-03-23 added madeNew and dataOld fields
  Nothing is actually using these yet, but that will probably change.
  For example, we might want to log when an existing record gets modified.
2011-03-31 why is this protected? Leaving it that way for now, but consider making it public.
     */
     */
     public $madeNew,$dataOld; // additional status output
     public function DataSQL($iSQL) {
    protected function Make(array $iData,$iFilt=NULL) {
$strCls = $this->ClassSng();
if (is_null($iFilt)) {
$obj = $this->Engine()->DataSet($iSQL,$strCls);
    $sqlFilt = $this->MakeFilt($iData);
$this->sqlExec = $iSQL;
} else {
$this->ReleaseItem($obj);
    $sqlFilt = $iFilt;
return $obj;
}
    }
$rs = $this->GetData($sqlFilt);
    /*----
if ($rs->HasRows()) {
      RETURNS: dataset containing all fields from the current table,
    assert('$rs->RowCount() == 1');
with additional options (everything after the table name) being
    $rs->NextRow();
defined by $iSQL, wrapped in the current object class.
    */
    public function DataSet($iSQL=NULL,$iClass=NULL) {
global $sql; // for debugging


    $this->madeNew = FALSE;
$sql = 'SELECT * FROM '.$this->NameSQL();
    $this->dataOld = $this->Values();
if (!is_null($iSQL)) {
 
    $sql .= ' '.$iSQL;
    $rs->Update($iData);
    $id = $rs->KeyValue();
} else {
    $this->Insert($iData);
    $id = $this->Engine()->NewID();
    $this->madeNew = TRUE;
}
}
return $id;
return $this->DataSQL($sql);
/*
$strCls = $this->vSngClass;
$obj = $this->objDB->DataSet($sql,$strCls);
$obj->Table = $this;
return $obj;
*/
     }
     }
// LATER:
    /*----
//    abstract protected function MakeFilt(array $iData);
      FUTURE: This *so* needs to have iClass LAST, or not at all.
}
    */
/*=============
    public function GetData($iWhere=NULL,$iClass=NULL,$iSort=NULL) {
  NAME: clsTable_key_single
global $sql; // for debugging
  PURPOSE: table with a single key field
 
*/
$sql = 'SELECT * FROM '.$this->NameSQL();
class clsTable_key_single extends clsTable_keyed_abstract {
if (!is_null($iWhere)) {
    protected $vKeyName;
    $sql .= ' WHERE '.$iWhere;
}
if (!is_null($iSort)) {
    $sql .= ' ORDER BY '.$iSort;
}


    public function __construct(clsDatabase $iDB) {
//$obj = $this->objDB->DataSet($sql,$strCls);
parent::__construct($iDB);
//$res = $this->DB()->Exec($sql);
$this->ClassSng('clsDataSet');
$obj = $this->SpawnItem($iClass);
    }
assert('is_object($obj->Table);');
$obj->Query($sql);


    public function KeyName($iName=NULL) {
$this->sqlExec = $sql;
if (!is_null($iName)) {
if (!is_null($obj)) {
    $this->vKeyName = $iName;
//     $obj->Table = $this; // 2011-01-20 this should be redundant now
    $obj->sqlMake = $sql;
}
}
return $this->vKeyName;
return $obj;
     }
     }
     /*----
     /*----
      RETURNS: SQL for creating a new record for the given data
       HISTORY:
       HISTORY:
2010-11-01 iID=NULL now means create new/blank object, i.e. SpawnItem()
2010-11-20 Created.
2011-11-15 tweak for clarity
     */
     */
     public function GetItem($iID=NULL,$iClass=NULL) {
     public function SQL_forInsert(array $iData) {
if (is_null($iID)) {
$sqlNames = '';
    $objItem = $this->SpawnItem($iClass);
$sqlVals = '';
    $objItem->KeyValue(NULL);
foreach($iData as $key=>$val) {
} else {
    if ($sqlNames != '') {
    $sqlFilt = $this->vKeyName.'='.SQLValue($iID);
$sqlNames .= ',';
    $objItem = $this->GetData($sqlFilt,$iClass);
$sqlVals .= ',';
    $objItem->NextRow();
    }
    $sqlNames .= $key;
    $sqlVals .= $val;
}
}
return $objItem;
return 'INSERT INTO `'.$this->Name().'` ('.$sqlNames.') VALUES('.$sqlVals.');';
     }
     }
     /*----
     /*----
      INPUT:
iFields: array of source fields and their output names - specified as iFields[output]=input, because you can
  have a single input used for multiple outputs, but not vice-versa. Yes, this is confusing but that's how
  arrays are indexed.
       HISTORY:
       HISTORY:
2010-10-16 Created for VbzAdminCartLog::AdminPage()
2010-11-16 Added "array" requirement for iData
2010-11-20 Calculation now takes place in SQL_forInsert()
     */
     */
     public function DataSetGroup(array $iFields, $iGroupBy, $iSort=NULL) {
     public function Insert(array $iData) {
global $sql; // for debugging
global $sql;


foreach ($iFields AS $fDest => $fSrce) {
$sql = $this->SQL_forInsert($iData);
    if(isset($sqlFlds)) {
$this->sqlExec = $sql;
$sqlFlds .= ', ';
return $this->objDB->Exec($sql);
    } else {
$sqlFlds = '';
    }
    $sqlFlds .= $fSrce.' AS '.$fDest;
}
$sql = 'SELECT '.$sqlFlds.' FROM '.$this->NameSQL().' GROUP BY '.$iGroupBy;
if (!is_null($iSort)) {
    $sql .= ' ORDER BY '.$iSort;
}
$obj = $this->objDB->DataSet($sql);
return $obj;
     }
     }
     /*----
     /*----
       HISTORY:
       HISTORY:
2010-11-20 Created
2011-02-02 created for deleting topic-title pairs
     */
     */
     public function SQL_forUpdate(array $iSet,$iWhere) {
     public function Delete($iFilt) {
$sqlSet = '';
$sql = 'DELETE FROM `'.$this->Name().'` WHERE '.$iFilt;
foreach($iSet as $key=>$val) {
$this->sqlExec = $sql;
    if ($sqlSet != '') {
return $this->Engine()->Exec($sql);
$sqlSet .= ',';
    }
    }
}
    $sqlSet .= ' `'.$key.'`='.$val;
/*=============
  NAME: clsTable_keyed_abstract
  PURPOSE: adds abstract methods for dealing with keys
*/
abstract class clsTable_keyed_abstract extends clsTable_abstract {
 
    //abstract public function GetItem_byArray();
    abstract protected function MakeFilt(array $iData);
    abstract protected function MakeFilt_direct(array $iData);
    /*----
      PURPOSE: method for setting a key which uniquely refers to this table
Useful for logging, menus, and other text-driven contexts.
    */
    public function ActionKey($iName=NULL) {
if (!is_null($iName)) {
    $this->ActionKey = $iName;
}
}
 
return $this->ActionKey;
return 'UPDATE `'.$this->Name().'` SET'.$sqlSet.' WHERE '.$iWhere;
     }
     }
     /*----
     /*----
      INPUT:
$iData: array of data necessary to create a new record
  or update an existing one, if found
$iFilt: SQL defining what constitutes an existing record
  If NULL, MakeFilt() will be called to build this from $iData.
      ASSUMES: iData has already been massaged for direct SQL use
       HISTORY:
       HISTORY:
2010-10-05 Commented out code which updated the row[] array from iSet's values.
2011-02-22 created
  * It doesn't work if the input is a string instead of an array.
2011-03-23 added madeNew and dataOld fields
  * Also, it seems like a better idea to actually re-read the data if
  Nothing is actually using these yet, but that will probably change.
    we really need to update the object.
  For example, we might want to log when an existing record gets modified.
2010-11-16 Added "array" requirement for iSet; removed code for handling
2011-03-31 why is this protected? Leaving it that way for now, but consider making it public.
  iSet as a string. If we want to support single-field updates, make a
2012-02-21 Needs to be public; making it so.
  new method: UpdateField($iField,$iVal,$iWhere). This makes it easier
  Also changed $this->Values() (which couldn't possibly have worked) to $rs->Values()
  to support automatic updates of certain fields in descendent classes
  (e.g. updating a WhenEdited timestamp).
2010-11-20 Calculation now takes place in SQL_forUpdate()
     */
     */
     public function Update(array $iSet,$iWhere) {
    public $madeNew,$dataOld; // additional status output
global $sql;
     public function Make(array $iData,$iFilt=NULL) {
if (is_null($iFilt)) {
    $sqlFilt = $this->MakeFilt_direct($iData);
//die( 'SQL='.$sqlFilt );
} else {
    $sqlFilt = $iFilt;
}
$rs = $this->GetData($sqlFilt);
if ($rs->HasRows()) {
    assert('$rs->RowCount() == 1');
    $rs->NextRow();


$sql = $this->SQL_forUpdate($iSet,$iWhere);
    $this->madeNew = FALSE;
$this->sqlExec = $sql;
    $this->dataOld = $rs->Values();
$ok = $this->objDB->Exec($sql);


return $ok;
    $rs->Update($iData);
    }
    $id = $rs->KeyString();
    public function LastID() {
$strKey = $this->vKeyName;
$sql = 'SELECT '.$strKey.' FROM `'.$this->Name().'` ORDER BY '.$strKey.' DESC LIMIT 1;';
 
$objRows = $this->objDB->DataSet($sql);
 
if ($objRows->HasRows()) {
    $objRows->NextRow();
    $intID = $objRows->$strKey;
    return $intID;
} else {
} else {
    return 0;
    $this->Insert($iData);
    $id = $this->Engine()->NewID();
    $this->madeNew = TRUE;
}
}
    }
return $id;
    /*----
      HISTORY:
2011-02-22 created
      IMPLEMENTATION:
KeyName must equal KeyValue
    */
    protected function MakeFilt(array $iData) {
return $this->KeyName().'='.SQLValue($this->KeyValue());
     }
     }
}
}
// alias -- sort of a default table type
/*=============
class clsTable extends clsTable_key_single {
  NAME: clsTable_key_single
}
  PURPOSE: table with a single key field
*/
class clsTable_key_single extends clsTable_keyed_abstract {
    protected $vKeyName;


// DEPRECATED -- use clsCache_Table helper class
    public function __construct(clsDatabase_abstract $iDB) {
class clsTableCache extends clsTable {
parent::__construct($iDB);
    private $arCache;
$this->ClassSng('clsDataSet');
    }


     public function GetItem($iID=NULL,$iClass=NULL) {
     public function KeyName($iName=NULL) {
if (!isset($this->arCache[$iID])) {
if (!is_null($iName)) {
    $objItem = $this->GetData($this->vKeyName.'='.SQLValue($iID),$iClass);
    $this->vKeyName = $iName;
    $objItem->NextRow();
    $this->arCache[$iID] = $objItem->RowCopy();
}
}
return $this->arCache[$iID];
return $this->vKeyName;
     }
     }
}
    /*----
/*====
      HISTORY:
  CLASS: cache for Tables
2010-11-01 iID=NULL now means create new/blank object, i.e. SpawnItem()
  ACTION: provides a cached GetItem()
2011-11-15 tweak for clarity
  USAGE: clsTable descendants should NOT override GetItem() or GetData() to use this class,
2012-03-04 getting rid of optional $iClass param
    as the class needs those methods to load data into the cache.
     */
  BOILERPLATE:
     public function GetItem($iID=NULL) {
     protected $objCache;
if (is_null($iID)) {
     protected function Cache() {
    $objItem = $this->SpawnItem();
if (!isset($this->objCache)) {
    $objItem->KeyValue(NULL);
    $this->objCache = new clsCache_Table($this);
} else {
    $sqlFilt = $this->vKeyName.'='.SQLValue($iID);
    $objItem = $this->GetData($sqlFilt);
    $objItem->NextRow();
}
}
return $this->objCache;
return $objItem;
     }
     }
     public function GetItem_Cached($iID=NULL,$iClass=NULL) {
     /*----
return $this->Cache()->GetItem($iID,$iClass);
      INPUT:
     }
iFields: array of source fields and their output names - specified as iFields[output]=input, because you can
     public function GetData_Cached($iWhere=NULL,$iClass=NULL,$iSort=NULL) {
  have a single input used for multiple outputs, but not vice-versa. Yes, this is confusing but that's how
return $this->Cache()->GetItem($iWhere,$iClass,$iSort);
  arrays are indexed.
    }
      HISTORY:
*/
2010-10-16 Created for VbzAdminCartLog::AdminPage()
/*----
     */
*/
     public function DataSetGroup(array $iFields, $iGroupBy, $iSort=NULL) {
class clsCache_Table {
global $sql; // for debugging
    protected $objTbl;
    protected $arRows; // arRows[id] = rows[]
    protected $arSets; // caches entire datasets


    public function __construct(clsTable $iTable) {
foreach ($iFields AS $fDest => $fSrce) {
$this->objTbl = $iTable;
    if(isset($sqlFlds)) {
$sqlFlds .= ', ';
    } else {
$sqlFlds = '';
    }
    $sqlFlds .= $fSrce.' AS '.$fDest;
}
$sql = 'SELECT '.$sqlFlds.' FROM '.$this->NameSQL().' GROUP BY '.$iGroupBy;
if (!is_null($iSort)) {
    $sql .= ' ORDER BY '.$iSort;
}
$obj = $this->objDB->DataSet($sql);
return $obj;
     }
     }
    public function GetItem($iID=NULL,$iClass=NULL) {
     /*----
$objTbl = $this->objTbl;
if (isset($this->arRows[$iID])) {
    $objItem = $objTbl->SpawnItem($iClass);
    $objItem->Row = $this->arCache[$iID];
} else {
    $objItem = $objTbl->GetItem($iID,$iClass);
    $this->arCache[$iID] = $objItem->Row;
}
return $objItem;
    }
     /*----
       HISTORY:
       HISTORY:
2011-02-11 Renamed GetData_Cached() to GetData()
2010-11-20 Created
  This was probably a leftover from before multiple inheritance
  Fixed some bugs. Renamed from GetData() to GetData_array()
    because caching the resource blob didn't seem to work very well.
  Now returns an array instead of an object.
      FUTURE: Possibly we should be reading all rows into memory, instead of just saving the Res.
That way, Res could be protected again instead of public.
     */
     */
     public function GetData_array($iWhere=NULL,$iClass=NULL,$iSort=NULL) {
     public function SQL_forUpdate(array $iSet,$iWhere) {
$objTbl = $this->objTbl;
$sqlSet = '';
$strKeyFilt = "$iWhere\t$iSort";
foreach($iSet as $key=>$val) {
$isCached = FALSE;
    if ($sqlSet != '') {
if (is_array($this->arSets)) {
$sqlSet .= ',';
    if (array_key_exists($strKeyFilt,$this->arSets)) {
$isCached = TRUE;
    }
    }
    $sqlSet .= ' `'.$key.'`='.$val;
}
}
if ($isCached) {
    //$objSet = $objTbl->SpawnItem($iClass);
    //$objSet->Res = $this->arSets[$strKey];
    //assert('is_resource($objSet->Res); /* KEY='.$strKey.'*/');


    // 2011-02-11 this code has not been tested yet
return 'UPDATE `'.$this->Name().'` SET'.$sqlSet.' WHERE '.$iWhere;
//echo '<pre>'.print_r($this->arSets,TRUE).'</pre>';
    foreach ($this->arSets[$strKeyFilt] as $key) {
$arOut[$key] = $this->arRows[$key];
    }
} else {
    $objSet = $objTbl->GetData($iWhere,$iClass,$iSort);
    while ($objSet->NextRow()) {
$strKeyRow = $objSet->KeyString();
$arOut[$strKeyRow] = $objSet->Values();
$this->arSets[$strKeyFilt][] = $strKeyRow;
    }
    if (is_array($this->arRows)) {
$this->arRows = array_merge($this->arRows,$arOut); // add to cached rows
    } else {
$this->arRows = $arOut; // start row cache
    }
}
return $arOut;
     }
     }
}
    /*----
/*=============
      HISTORY:
  NAME: clsRecs_abstract -- abstract recordset
2010-10-05 Commented out code which updated the row[] array from iSet's values.
    Does not deal with keys.
  * It doesn't work if the input is a string instead of an array.
  HISTORY:
  * Also, it seems like a better idea to actually re-read the data if
    2011-11-21 changed Table() from protected to public because Scripting needed to access it
    we really need to update the object.
*/
2010-11-16 Added "array" requirement for iSet; removed code for handling
abstract class clsRecs_abstract {
  iSet as a string. If we want to support single-field updates, make a
    public $objDB; // deprecated; use Engine()
  new method: UpdateField($iField,$iVal,$iWhere). This makes it easier
     public $sqlMake; // optional: SQL used to create the dataset -- used for reloading
  to support automatic updates of certain fields in descendent classes
     public $sqlExec; // last SQL executed on this dataset
  (e.g. updating a WhenEdited timestamp).
    public $Table; // public access deprecated; use Table()
2010-11-20 Calculation now takes place in SQL_forUpdate()
    protected $Res; // native result set
     */
    public $Row; // public access deprecated; use Values()/Value() (data from the active row)
     public function Update(array $iSet,$iWhere) {
global $sql;
 
$sql = $this->SQL_forUpdate($iSet,$iWhere);
$this->sqlExec = $sql;
$ok = $this->objDB->Exec($sql);


    public function __construct(clsDatabase $iDB=NULL, $iRes=NULL, array $iRow=NULL) {
return $ok;
$this->objDB = $iDB;
$this->Res = $iRes;
$this->Row = $iRow;
$this->InitVars();
     }
     }
     protected function InitVars() {
     public function LastID() {
     }
$strKey = $this->vKeyName;
     public function Table(clsTable_abstract $iTable=NULL) {
$sql = 'SELECT '.$strKey.' FROM `'.$this->Name().'` ORDER BY '.$strKey.' DESC LIMIT 1;';
if (!is_null($iTable)) {
 
    $this->Table = $iTable;
$objRows = $this->objDB->DataSet($sql);
 
if ($objRows->HasRows()) {
    $objRows->NextRow();
    $intID = $objRows->$strKey;
    return $intID;
} else {
    return 0;
}
     }
     /*----
      HISTORY:
2011-02-22 created
2012-02-21 this couldn't possibly have worked before, since it used $this->KeyValue(),
  which is a Record function, not a Table function.
  Replaced that with $iData[$strName].
      ASSUMES: $iData[$this->KeyName()] is set, but may need to be SQL-formatted
      IMPLEMENTATION:
KeyName must equal KeyValue
    */
    protected function MakeFilt(array $iData) {
$strName = $this->KeyName();
$val = $iData[$strName];
if ($iSQLify) {
    $val = SQLValue($val);
}
}
return $this->Table;
return $strName.'='.$val;
     }
     }
     /*----
     /*----
       PURPOSE: for debugging -- quick/easy way to see what data we have
       PURPOSE: same as MakeFilt(), but does no escaping of SQL data
       HISTORY:
       HISTORY:
2011-09-24 written for vbz order import routine rewrite
2012-03-04 created to replace $iSQLify option
      ASSUMES: $iData[$this->KeyName()] is set, and already SQL-formatted (i.e. quoted if necessary)
     */
     */
     public function DumpHTML() {
     protected function MakeFilt_direct(array $iData) {
$out = '<b>Table</b>: '.$this->Table->Name();
$strName = $this->KeyName();
if ($this->hasRows()) {
$val = $iData[$strName];
    $out .= '<ul>';
return $strName.'='.$val;
    $this->StartRows(); // make sure to start at the beginning
    }
    while ($this->NextRow()) {
}
$out .= "\n<li><ul>";
// alias -- sort of a default table type
foreach ($this->Row as $key => $val) {
class clsTable extends clsTable_key_single {
    $out .= "\n<li>[$key] = [$val]</li>";
}
}
 
$out .= "\n</ul></li>";
// DEPRECATED -- use clsCache_Table helper class
    }
class clsTableCache extends clsTable {
    $out .= "\n</ul>";
    private $arCache;
} else {
 
    $out .= " -- NO DATA";
    public function GetItem($iID=NULL,$iClass=NULL) {
if (!isset($this->arCache[$iID])) {
    $objItem = $this->GetData($this->vKeyName.'='.SQLValue($iID),$iClass);
    $objItem->NextRow();
    $this->arCache[$iID] = $objItem->RowCopy();
}
}
$out .= "\n<b>SQL</b>: ".$this->sqlMake;
return $this->arCache[$iID];
return $out;
     }
     }
     public function Engine() {
}
if (is_null($this->objDB)) {
/*====
    assert('!is_null($this->Table()); /* SQL: '.$this->sqlMake.' */');
  CLASS: cache for Tables
    return $this->Table()->Engine();
  ACTION: provides a cached GetItem()
} else {
  USAGE: clsTable descendants should NOT override GetItem() or GetData() to use this class,
    return $this->objDB;
     as the class needs those methods to load data into the cache.
  BOILERPLATE:
    protected $objCache;
    protected function Cache() {
if (!isset($this->objCache)) {
    $this->objCache = new clsCache_Table($this);
}
}
return $this->objCache;
     }
     }
    /*----
     public function GetItem_Cached($iID=NULL,$iClass=NULL) {
      RETURNS: associative array of fields/values for the current row
return $this->Cache()->GetItem($iID,$iClass);
      HISTORY:
2011-01-08 created
2011-01-09 actually working; added option to write values
    */
     public function Values(array $iRow=NULL) {
if (is_array($iRow)) {
    $this->Row = $iRow;
}
return $this->Row;
     }
     }
    /*----
     public function GetData_Cached($iWhere=NULL,$iClass=NULL,$iSort=NULL) {
      FUNCTION: Value(name)
return $this->Cache()->GetItem($iWhere,$iClass,$iSort);
      RETURNS: Value of named field
    }
      HISTORY:
*/
2010-11-19 Created to help with data-form processing.
/*----
2010-11-26 Added value-setting, so we can set defaults for new records
*/
2011-02-09 replaced direct call to array_key_exists() with call to new function HasValue()
class clsCache_Table {
    */
    protected $objTbl;
     public function Value($iName,$iVal=NULL) {
    protected $arRows; // arRows[id] = rows[]
if (is_null($iVal)) {
    protected $arSets; // caches entire datasets
    if (!$this->HasValue($iName)) {
 
echo '<pre>'.print_r($this->Row,TRUE).'</pre>';
    public function __construct(clsTable $iTable) {
throw new Exception('Attempted to read nonexistent field "'.$iName.'" in class '.get_class($this));
$this->objTbl = $iTable;
    }
} else {
    $this->Row[$iName] = $iVal;
}
return $this->Row[$iName];
     }
     }
    /*----
     public function GetItem($iID=NULL,$iClass=NULL) {
      PURPOSE: Like Value() but handles new records gracefully, and is read-only
$objTbl = $this->objTbl;
makes it easier for new-record forms not to throw exceptions
if (isset($this->arRows[$iID])) {
      RETURNS: Value of named field; if it isn't set, returns $iDefault instead of raising an exception
    $objItem = $objTbl->SpawnItem($iClass);
      HISTORY:
    $objItem->Row = $this->arCache[$iID];
2011-02-12 written
2011-10-17 rewritten by accident
    */
     public function ValueNz($iName,$iDefault=NULL) {
if ($this->HasValue($iName)) {
    return $this->Row[$iName];
} else {
} else {
    return $iDefault;
    $objItem = $objTbl->GetItem($iID,$iClass);
    $this->arCache[$iID] = $objItem->Row;
}
}
return $objItem;
     }
     }
     /*----
     /*----
       HISTORY:
       HISTORY:
2011-02-09 created so we can test for field existence before trying to access
2011-02-11 Renamed GetData_Cached() to GetData()
  This was probably a leftover from before multiple inheritance
  Fixed some bugs. Renamed from GetData() to GetData_array()
    because caching the resource blob didn't seem to work very well.
  Now returns an array instead of an object.
      FUTURE: Possibly we should be reading all rows into memory, instead of just saving the Res.
That way, Res could be protected again instead of public.
     */
     */
     public function HasValue($iName) {
     public function GetData_array($iWhere=NULL,$iClass=NULL,$iSort=NULL) {
if (is_array($this->Row)) {
$objTbl = $this->objTbl;
    return array_key_exists($iName,$this->Row);
$strKeyFilt = "$iWhere\t$iSort";
$isCached = FALSE;
if (is_array($this->arSets)) {
    if (array_key_exists($strKeyFilt,$this->arSets)) {
$isCached = TRUE;
    }
}
if ($isCached) {
    //$objSet = $objTbl->SpawnItem($iClass);
    //$objSet->Res = $this->arSets[$strKey];
    //assert('is_resource($objSet->Res); /* KEY='.$strKey.'*/');
 
    // 2011-02-11 this code has not been tested yet
//echo '<pre>'.print_r($this->arSets,TRUE).'</pre>';
    foreach ($this->arSets[$strKeyFilt] as $key) {
$arOut[$key] = $this->arRows[$key];
    }
} else {
} else {
    return FALSE;
    $objSet = $objTbl->GetData($iWhere,$iClass,$iSort);
    while ($objSet->NextRow()) {
$strKeyRow = $objSet->KeyString();
$arOut[$strKeyRow] = $objSet->Values();
$this->arSets[$strKeyFilt][] = $strKeyRow;
    }
    if (is_array($this->arRows)) {
$this->arRows = array_merge($this->arRows,$arOut); // add to cached rows
    } else {
$this->arRows = $arOut; // start row cache
    }
}
}
return $arOut;
     }
     }
    /*----
}
      FUNCTION: Clear();
/*=============
      ACTION: Clears Row[] of any leftover data
  NAME: clsRecs_abstract -- abstract recordset
    */
    Does not deal with keys.
     public function Clear() {
  NOTE: We have to maintain a local copy of Row because sometimes we don't have a Res object yet
    because we haven't done any queries yet.
  HISTORY:
    2011-11-21 changed Table() from protected to public because Scripting needed to access it
*/
abstract class clsRecs_abstract {
    public $objDB; // deprecated; use Engine()
    public $sqlMake; // optional: SQL used to create the dataset -- used for reloading
    public $sqlExec; // last SQL executed on this dataset
    public $Table; // public access deprecated; use Table()
    protected $objRes; // result object returned by Engine
    public $Row; // public access deprecated; use Values()/Value() (data from the active row)
 
//   public function __construct(clsDatabase $iDB=NULL, $iRes=NULL, array $iRow=NULL) {
     public function __construct(clsDatabase $iDB=NULL) {
$this->objDB = $iDB;
$this->objRes = NULL;
$this->Row = NULL;
$this->Row = NULL;
$this->InitVars();
    }
    protected function InitVars() {
    }
    public function ResultHandler($iRes=NULL) {
if (!is_null($iRes)) {
    $this->objRes = $iRes;
}
return $this->objRes;
     }
     }
     public function Query($iSQL) {
     public function Table(clsTable_abstract $iTable=NULL) {
$this->Res = $this->Engine()->_api_query($iSQL);
if (!is_null($iTable)) {
$this->sqlMake = $iSQL;
    $this->Table = $iTable;
if (!is_resource($this->Res)) {
    throw new exception ('SQL='.$iSQL);
}
}
return $this->Table;
     }
     }
     /*----
     /*----
       ACTION: Checks given values for any differences from current values
       PURPOSE: for debugging -- quick/easy way to see what data we have
       RETURNS: TRUE if all values are same
       HISTORY:
2011-09-24 written for vbz order import routine rewrite
     */
     */
     public function SameAs(array $iValues) {
     public function DumpHTML() {
$isSame = TRUE;
$out = '<b>Table</b>: '.$this->Table->Name();
foreach($iValues as $name => $value) {
if ($this->hasRows()) {
    $oldVal = $this->Row[$name];
    $out .= '<ul>';
    if ($oldVal != $value) {
    $this->StartRows(); // make sure to start at the beginning
$isSame = FALSE;
    while ($this->NextRow()) {
$out .= "\n<li><ul>";
foreach ($this->Row as $key => $val) {
    $out .= "\n<li>[$key] = [$val]</li>";
}
$out .= "\n</ul></li>";
    }
    }
 
    $out .= "\n</ul>";
} else {
    $out .= " -- NO DATA";
}
}
return $isSame;
$out .= "\n<b>SQL</b>: ".$this->sqlMake;
return $out;
     }
     }
    /*-----
     public function Engine() {
      RETURNS: # of rows iff result has rows, otherwise FALSE
if (is_null($this->objDB)) {
    */
    assert('!is_null($this->Table()); /* SQL: '.$this->sqlMake.' */');
     public function hasRows() {
    return $this->Table()->Engine();
$rows = $this->objDB->_api_count_rows($this->Res);
if ($rows === FALSE) {
    return FALSE;
} elseif ($rows == 0) {
    return FALSE;
} else {
} else {
    return $rows;
    return $this->objDB;
}
}
     }
     }
     public function hasRow() {
     /*----
return $this->objDB->_api_row_filled($this->Row);
      RETURNS: associative array of fields/values for the current row
    }
      HISTORY:
    public function RowCount() {
2011-01-08 created
return $this->objDB->_api_count_rows($this->Res);
2011-01-09 actually working; added option to write values
     }
     */
     public function StartRows() {
     public function Values(array $iRow=NULL) {
if ($this->hasRows()) {
if (is_array($iRow)) {
    $this->objDB->_api_rows_rewind($this->Res);
    $this->Row = $iRow;
    return TRUE;
    return $iRow;
} else {
} else {
    return FALSE;
    return $this->Row;
}
}
     }
     }
     public function FirstRow() {
    /*----
if ($this->StartRows()) {
      FUNCTION: Value(name)
    return $this->NextRow(); // get the first row of data
      RETURNS: Value of named field
      HISTORY:
2010-11-19 Created to help with data-form processing.
2010-11-26 Added value-setting, so we can set defaults for new records
2011-02-09 replaced direct call to array_key_exists() with call to new function HasValue()
    */
     public function Value($iName,$iVal=NULL) {
if (is_null($iVal)) {
    if (!$this->HasValue($iName)) {
if (is_object($this->Table())) {
    $htTable = ' from table "'.$this->Table()->Name().'"';
} else {
    $htTable = ' from query';
}
$strMsg = 'Attempted to read nonexistent field "'.$iName.'"'.$htTable.' in class '.get_class($this);
echo $strMsg.'<br>';
echo 'Source SQL: '.$this->sqlMake.'<br>';
echo 'Row contents:<pre>'.print_r($this->Values(),TRUE).'</pre>';
throw new Exception($strMsg);
    }
} else {
} else {
    return FALSE;
    $this->Row[$iName] = $iVal;
}
}
return $this->Row[$iName];
     }
     }
     /*=====
     /*----
       ACTION: Fetch the next row of data into $this->Row.
       PURPOSE: Like Value() but handles new records gracefully, and is read-only
If no data has been fetched yet, then fetch the first row.
makes it easier for new-record forms not to throw exceptions
       RETURN: TRUE if row was fetched; FALSE if there were no more rows
       RETURNS: Value of named field; if it isn't set, returns $iDefault instead of raising an exception
or the row could not be fetched.
      HISTORY:
2011-02-12 written
2011-10-17 rewritten by accident
     */
     */
     public function NextRow() {
     public function ValueNz($iName,$iDefault=NULL) {
$this->Row = $this->objDB->_api_fetch_row($this->Res);
if (array_key_exists($iName,$this->Row)) {
return $this->hasRow();
    return $this->Row[$iName];
} else {
    return $iDefault;
}
     }
     }
}
     /*----
/*=============
  NAME: clsRecs_keyed_abstract -- abstract recordset for keyed data
    Adds abstract and concrete methods for dealing with keys.
*/
abstract class clsRecs_keyed_abstract extends clsRecs_abstract {
    // ABSTRACT methods
    abstract public function SelfFilter();
    abstract public function KeyString();
    abstract public function SQL_forUpdate(array $iSet);
    abstract public function SQL_forMake(array $iSet);
 
     /*-----
      ACTION: Saves the data in $iSet to the current record (or records filtered by $iWhere)
       HISTORY:
       HISTORY:
2010-11-20 Calculation now takes place in SQL_forUpdate()
2011-02-09 created so we can test for field existence before trying to access
2010-11-28 SQL saved to table as well, for when we might be doing Insert() or Update()
  and need a single place to look up the SQL for whatever happened.
     */
     */
     public function Update(array $iSet,$iWhere=NULL) {
     public function HasValue($iName) {
//$ok = $this->Table->Update($iSet,$sqlWhere);
if (is_array($this->Row)) {
$sql = $this->SQL_forUpdate($iSet,$iWhere);
    return array_key_exists($iName,$this->Row);
//$this->sqlExec = $this->Table->sqlExec;
} else {
$this->sqlExec = $sql;
    return FALSE;
$this->Table->sql = $sql;
}
$ok = $this->objDB->Exec($sql);
return $ok;
     }
     }
}
 
/*=============
  NAME: clsDataSet_bare
  DEPRECATED - USE clsRecs_key_single INSTEAD
  PURPOSE: base class for datasets, with single key
    Does not add field overloading. Field overloading seems to have been a bad idea anyway;
      Use Value() instead.
*/
class clsRecs_key_single extends clsRecs_keyed_abstract {
     /*----
     /*----
       HISTORY:
       FUNCTION: Clear();
2010-11-01 iID=NULL now means object does not have data from an existing record
      ACTION: Clears Row[] of any leftover data
     */
     */
     public function IsNew() {
     public function Clear() {
return is_null($this->KeyValue());
$this->Row = NULL;
     }
     }
     /*-----
     /*----
       FUNCTION: KeyValue()
       USAGE: caller should always check this and throw an exception if it fails.
     */
     */
     public function KeyValue($iVal=NULL) {
     private function QueryOkay() {
if (!is_object($this->Table)) {
if (is_object($this->objRes)) {
    throw new Exception('Recordset needs a Table object to retrieve key value.');
    $ok = $this->objRes->is_okay();
}
$strKeyName = $this->Table->KeyName();
assert('!empty($strKeyName); /* TABLE: '.$this->Table->Name().' */');
assert('is_string($strKeyName); /* TABLE: '.$this->Table->Name().' */');
if (is_null($iVal)) {
    if (!isset($this->Row[$strKeyName])) {
$this->Row[$strKeyName] = NULL;
    }
} else {
} else {
    $this->Row[$strKeyName] = $iVal;
    $ok = FALSE;
}
}
return $this->Row[$strKeyName];
return $ok;
     }
     }
     public function KeyString() {
     public function Query($iSQL) {
return (string)$this->KeyValue();
$this->objRes = $this->Engine()->engine_db_query($iSQL);
$this->sqlMake = $iSQL;
if (!$this->QueryOkay()) {
    throw new exception ('Query failed -- SQL='.$iSQL);
}
     }
     }
     /*----
     /*----
      FUNCTION: Load_fromKey()
       ACTION: Checks given values for any differences from current values
       ACTION: Load a row of data whose key matches the given value
       RETURNS: TRUE if all values are same
       HISTORY:
2010-11-19 Created for form processing.
     */
     */
     public function Load_fromKey($iKeyValue) {
     public function SameAs(array $iValues) {
$this->sqlMake = NULL;
$isSame = TRUE;
$this->KeyValue($iKeyValue);
foreach($iValues as $name => $value) {
$this->Reload();
    $oldVal = $this->Row[$name];
    if ($oldVal != $value) {
$isSame = FALSE;
    }
 
}
return $isSame;
     }
     }
     /*-----
     /*-----
      FUNCTION: SelfFilter()
       RETURNS: # of rows iff result has rows, otherwise FALSE
       RETURNS: SQL for WHERE clause which will select only the current row, based on KeyValue()
      USED BY: Update(), Reload()
     */
     */
     public function SelfFilter() {
     public function hasRows() {
if (!is_object($this->Table)) {
$rows = $this->objRes->get_count();
    throw new exception('Table not set in class '.get_class($this));
if ($rows === FALSE) {
    return FALSE;
} elseif ($rows == 0) {
    return FALSE;
} else {
    return $rows;
}
}
$strKeyName = $this->Table->KeyName();
//$sqlWhere = $strKeyName.'='.$this->$strKeyName;
//$sqlWhere = $strKeyName.'='.$this->Row[$strKeyName];
$sqlWhere = '`'.$strKeyName.'`='.$this->KeyValue();
return $sqlWhere;
     }
     }
     /*-----
     public function hasRow() {
      ACTION: Reloads only the current row unless $iFilt is set
return $this->objRes->is_filled();
      TO DO: iFilt should probably be removed, now that we save
    }
the creation SQL in $this->sql.
    public function RowCount() {
     */
return $this->objRes->get_count();
     public function Reload($iFilt=NULL) {
     }
if (is_string($this->sqlMake)) {
     public function StartRows() {
    $sql = $this->sqlMake;
if ($this->hasRows()) {
    $this->objRes->do_rewind();
    return TRUE;
} else {
} else {
    $sql = 'SELECT * FROM `'.$this->Table->Name().'` WHERE ';
    return FALSE;
    if (is_null($iFilt)) {
$sql .= $this->SelfFilter();
    } else {
$sql .= $iFilt;
    }
}
}
$this->Query($sql);
$this->NextRow();
     }
     }
    /*----
     public function FirstRow() {
      HISTORY:
if ($this->StartRows()) {
2010-11-20 Created
    return $this->NextRow(); // get the first row of data
    */
} else {
     public function SQL_forUpdate(array $iSet,$iWhere=NULL) {
    return FALSE;
$doIns = FALSE;
if (is_null($iWhere)) {
// default: modify the current record
// build SQL filter for just the current record
    $sqlWhere = $this->SelfFilter();
} else {
    $sqlWhere = $iWhere;
}
}
return $this->Table->SQL_forUpdate($iSet,$sqlWhere);
     }
     }
     /*----
     /*=====
       HISTORY:
      ACTION: Fetch the next row of data into $this->Row.
2010-11-23 Created
If no data has been fetched yet, then fetch the first row.
       RETURN: TRUE if row was fetched; FALSE if there were no more rows
or the row could not be fetched.
     */
     */
     public function SQL_forMake(array $iarSet) {
     public function NextRow() {
$strKeyName = $this->Table->KeyName();
if (!is_object($this->objRes)) {
if ($this->IsNew()) {
    throw new exception('Result object not loaded');
    $sql = $this->Table->SQL_forInsert($iarSet);
} else {
    $sql = $this->SQL_forUpdate($iarSet);
}
}
return $sql;
$this->Row = $this->objRes->get_next();
return $this->hasRow();
     }
     }
     /*-----
}
       ACTION: Saves to the current record; creates a new record if ID is 0 or NULL
/*=============
  NAME: clsRecs_keyed_abstract -- abstract recordset for keyed data
    Adds abstract and concrete methods for dealing with keys.
*/
abstract class clsRecs_keyed_abstract extends clsRecs_abstract {
    // ABSTRACT methods
    abstract public function SelfFilter();
    abstract public function KeyString();
    abstract public function SQL_forUpdate(array $iSet);
    abstract public function SQL_forMake(array $iSet);
 
     /*-----
       ACTION: Saves the data in $iSet to the current record (or records filtered by $iWhere)
       HISTORY:
       HISTORY:
2010-11-03
2010-11-20 Calculation now takes place in SQL_forUpdate()
  Now uses this->IsNew() to determine whether to use Insert() or Update()
2010-11-28 SQL saved to table as well, for when we might be doing Insert() or Update()
  Loads new ID into KeyValue
  and need a single place to look up the SQL for whatever happened.
     */
     */
    public function Update(array $iSet,$iWhere=NULL) {
//$ok = $this->Table->Update($iSet,$sqlWhere);
$sql = $this->SQL_forUpdate($iSet,$iWhere);
//$this->sqlExec = $this->Table->sqlExec;
$this->sqlExec = $sql;
$this->Table->sql = $sql;
$ok = $this->objDB->Exec($sql);
return $ok;
    }
     public function Make(array $iarSet) {
     public function Make(array $iarSet) {
if ($this->IsNew()) {
return $this->Indexer()->Make($iarSet);
    $ok = $this->Table->Insert($iarSet);
    }
    $this->KeyValue($this->objDB->NewID());
    /*-----
    return $ok;
      ACTION: Reloads only the current row unless $iFilt is set
      HISTORY:
2012-03-04 moved from clsRecs_key_single to clsRecs_keyed_abstract
2012-03-05 removed code referencing iFilt -- it is no longer in the arg list
    */
    public function Reload() {
if (is_string($this->sqlMake)) {
    $sql = $this->sqlMake;
} else {
} else {
    return $this->Update($iarSet);
    $sql = 'SELECT * FROM `'.$this->Table->Name().'` WHERE ';
    $sql .= $this->SelfFilter();
}
}
$this->Query($sql);
$this->NextRow(); // load the data
     }
     }
     // DEPRECATED -- should be a function of Table type
}
     public function HasField($iName) {
/*=============
return isset($this->Row[$iName]);
  NAME: clsDataSet_bare
     }
  DEPRECATED - USE clsRecs_key_single INSTEAD
     // DEPRECATED - use Values()
  PURPOSE: base class for datasets, with single key
     public function RowCopy() {
     Does not add field overloading. Field overloading seems to have been a bad idea anyway;
$strClass = get_class($this);
      Use Value() instead.
if (is_array($this->Row)) {
*/
    $objNew = new $strClass;
class clsRecs_key_single extends clsRecs_keyed_abstract {
// copy critical object fields so methods will work:
    /*----
    $objNew->objDB = $this->objDB;
      HISTORY:
    $objNew->Table = $this->Table;
2010-11-01 iID=NULL now means object does not have data from an existing record
// copy data fields:
    */
    foreach ($this->Row AS $key=>$val) {
     public function IsNew() {
$objNew->Row[$key] = $val;
return is_null($this->KeyValue());
     }
     /*-----
      FUNCTION: KeyValue()
    */
     public function KeyValue($iVal=NULL) {
if (!is_object($this->Table)) {
    throw new Exception('Recordset needs a Table object to retrieve key value.');
}
if (!is_array($this->Row) && !is_null($this->Row)) {
    throw new Exception('Row needs to be an array or NULL, but type is '.gettype($this->Row).'. This may be an internal error.');
}
$strKeyName = $this->Table->KeyName();
assert('!empty($strKeyName); /* TABLE: '.$this->Table->Name().' */');
assert('is_string($strKeyName); /* TABLE: '.$this->Table->Name().' */');
if (is_null($iVal)) {
    if (!isset($this->Row[$strKeyName])) {
$this->Row[$strKeyName] = NULL;
    }
    }
    return $objNew;
} else {
} else {
    //echo 'RowCopy(): No data to copy in class '.$strClass;
    $this->Row[$strKeyName] = $iVal;
    return NULL;
}
}
return $this->Row[$strKeyName];
     }
     }
}
     public function KeyString() {
// alias -- a sort of default dataset type
return (string)$this->KeyValue();
class clsDataSet_bare extends clsRecs_key_single {
}
/*
  PURPOSE: clsDataSet with overloaded field access methods
  DEPRECATED -- This has turned out to be more problematic than useful.
    Retained only for compatibility with existing code; hope to eliminate eventually.
*/
class clsDataSet extends clsRecs_key_single {
  // -- accessing individual fields
     public function __set($iName, $iValue) {
$this->Row[$iName] = $iValue;
     }
     }
     public function __get($iName) {
    /*----
if (isset($this->Row[$iName])) {
      FUNCTION: Load_fromKey()
    return $this->Row[$iName];
      ACTION: Load a row of data whose key matches the given value
} else {
      HISTORY:
    return NULL;
2010-11-19 Created for form processing.
}
    */
     public function Load_fromKey($iKeyValue) {
$this->sqlMake = NULL;
$this->KeyValue($iKeyValue);
$this->Reload();
     }
     }
}
    /*-----
// HELPER CLASSES
      FUNCTION: SelfFilter()
 
      RETURNS: SQL for WHERE clause which will select only the current row, based on KeyValue()
/*====
      USED BY: Update(), Reload()
  CLASS: Table Indexer
      HISTORY:
*/
2012-02-21 KeyValue() is now run through SQLValue() so it can handle non-numeric keys
class clsIndexer_Table {
    private $objTbl; // clsTable object
 
    public function __construct(clsTable_abstract $iObj) {
$this->objTbl = $iObj;
    }
    /*----
      RETURNS: newly-created clsIndexer_Recs-descended object
Override this method to change how indexing works
     */
     */
     protected function NewRecsIdxer(clsRecs_indexed $iRecs) {
     public function SelfFilter() {
return new clsIndexer_Recs($iRecs,$this);
if (!is_object($this->Table)) {
    throw new exception('Table not set in class '.get_class($this));
}
$strKeyName = $this->Table->KeyName();
//$sqlWhere = $strKeyName.'='.$this->$strKeyName;
//$sqlWhere = $strKeyName.'='.$this->Row[$strKeyName];
$sqlWhere = '`'.$strKeyName.'`='.SQLValue($this->KeyValue());
return $sqlWhere;
     }
     }
     public function TableObj() {
    /*-----
return $this->objTbl;
      ACTION: Reloads only the current row unless $iFilt is set
      TO DO: iFilt should probably be removed, now that we save
the creation SQL in $this->sql.
    */
/*
     public function Reload($iFilt=NULL) {
if (is_string($this->sqlMake)) {
    $sql = $this->sqlMake;
} else {
    $sql = 'SELECT * FROM `'.$this->Table->Name().'` WHERE ';
    if (is_null($iFilt)) {
$sql .= $this->SelfFilter();
    } else {
$sql .= $iFilt;
    }
}
$this->Query($sql);
$this->NextRow();
     }
     }
 
*/
     public function InitRecs(clsRecs_indexed $iRecs) {
     /*----
$objIdx = $this->NewRecsIdxer($iRecs);
      HISTORY:
$iRecs->Indexer($objIdx);
2010-11-20 Created
return $objIdx;
    */
    }
     public function SQL_forUpdate(array $iSet,$iWhere=NULL) {
}
$doIns = FALSE;
/*====
if (is_null($iWhere)) {
  HISTORY:
// default: modify the current record
    2011-01-19 Started; not ready yet -- just saving bits of code I know I will need
// build SQL filter for just the current record
*/
    $sqlWhere = $this->SelfFilter();
class clsIndexer_Table_single_key extends clsIndexer_Table {
} else {
     protected function NewRecsIdxer(clsRecs_indexed $iRecs) {
    $sqlWhere = $iWhere;
return new clsIndexer_Recs_single_key($iRecs,$this);
    }
    public function KeyName($iName=NULL) {
if (!is_null($iName)) {
    $this->vKeyName = $iName;
}
}
return $this->vKeyName;
return $this->Table->SQL_forUpdate($iSet,$sqlWhere);
     }
     }
     public function GetItem($iID=NULL,$iClass=NULL) {
    /*----
if (is_null($iID)) {
      HISTORY:
    $objItem = $this->SpawnItem($iClass);
2010-11-23 Created
    $objItem->KeyValue(NULL);
    */
     public function SQL_forMake(array $iarSet) {
$strKeyName = $this->Table->KeyName();
if ($this->IsNew()) {
    $sql = $this->Table->SQL_forInsert($iarSet);
} else {
} else {
    assert('!is_array($iID); /* TABLE='.$this->TableObj()->Name().' */');
    $sql = $this->SQL_forUpdate($iarSet);
    $objItem = $this->TableObj()->GetData($this->vKeyName.'='.SQLValue($iID),$iClass);
    $objItem->NextRow();
}
}
return $objItem;
return $sql;
     }
     }
}
     /*-----
 
       ACTION: Saves to the current record; creates a new record if ID is 0 or NULL
class clsIndexer_Table_multi_key extends clsIndexer_Table {
      HISTORY:
    private $arKeys;
2010-11-03
 
  Now uses this->IsNew() to determine whether to use Insert() or Update()
     /*----
  Loads new ID into KeyValue
       RETURNS: newly-created clsIndexer_Recs-descended object
Override this method to change how indexing works
     */
     */
    protected function NewRecsIdxer(clsRecs_indexed $iRecs) {
     public function Make(array $iarSet) {
return new clsIndexer_Recs_multi_key($iRecs,$this);
if ($this->IsNew()) {
    }
    $ok = $this->Table->Insert($iarSet);
     public function KeyNames(array $iNames=NULL) {
    $this->KeyValue($this->objDB->NewID());
if (!is_null($iNames)) {
    return $ok;
    $this->arKeys = $iNames;
}
return $this->arKeys;
    }
    /*----
      HISTORY:
2011-01-08 written
    */
    public function GetItem(array $iVals=NULL) {
if (is_null($iVals)) {
    $objItem = $this->TableObj()->SpawnItem();
} else {
} else {
    $sqlFilt = $this->SQL_for_filter($iVals);
    return $this->Update($iarSet);
    $objItem = $this->TableObj()->GetData($sqlFilt);
    $objItem->NextRow();
}
}
return $objItem;
     }
     }
     /*----
     // DEPRECATED -- should be a function of Table type
      RETURNS: SQL to filter for the current record by key value(s)
    public function HasField($iName) {
'(name=value) AND (name=value) AND...'
return isset($this->Row[$iName]);
      INPUT: array of keynames and values
    }
array[name] = value
     // DEPRECATED - use Values()
      USED BY: GetItem()
     public function RowCopy() {
      HISTORY:
$strClass = get_class($this);
2011-01-08 written
if (is_array($this->Row)) {
2011-01-19 replaced with boilerplate call to indexer in clsIndexer_Table
    $objNew = new $strClass;
     */
// copy critical object fields so methods will work:
/*
    $objNew->objDB = $this->objDB;
     protected function SQL_for_filter(array $iVals) {
    $objNew->Table = $this->Table;
$arVals = $iVals;
// copy data fields:
$arKeys = $this->KeyNames();
    foreach ($this->Row AS $key=>$val) {
$sql = NULL;
$objNew->Row[$key] = $val;
foreach ($arKeys as $name) {
    $val = $arVals[$name];
    if (!is_null($sql)) {
$sql .= ' AND ';
    }
    }
    $sql .= '('.$name.'='.SQLValue($val).')';
    return $objNew;
} else {
    //echo 'RowCopy(): No data to copy in class '.$strClass;
    return NULL;
}
}
return $sql;
     }
     }
*/
    /*----
      RETURNS: SQL for creating a new record for the given data
      HISTORY:
2010-11-20 Created.
2011-01-08 adapted from clsTable::Insert()
    */
    public function SQL_forInsert(array $iData) {
$sqlNames = '';
$sqlVals = '';
foreach($iData as $key=>$val) {
    if ($sqlNames != '') {
$sqlNames .= ',';
$sqlVals .= ',';
    }
    $sqlNames .= '`'.$key.'`';
    $sqlVals .= $val;
}
return 'INSERT INTO `'.$this->Name().'` ('.$sqlNames.') VALUES('.$sqlVals.');';
    }
}
}
/*====
// alias -- a sort of default dataset type
   CLASS: clsIndexer_Recs -- record set indexer
class clsDataSet_bare extends clsRecs_key_single {
  PURPOSE: Handles record sets for tables with multiple keys
}
   HISTORY:
/*
     2010-?? written for clsCacheFlows/clsCacheFlow in cache.php
   PURPOSE: clsDataSet with overloaded field access methods
     2011-01-08 renamed, revised and clarified
  DEPRECATED -- This has turned out to be more problematic than useful.
    Retained only for compatibility with existing code; hope to eliminate eventually.
*/
class clsDataSet extends clsRecs_key_single {
   // -- accessing individual fields
     public function __set($iName, $iValue) {
$this->Row[$iName] = $iValue;
    }
     public function __get($iName) {
if (isset($this->Row[$iName])) {
    return $this->Row[$iName];
} else {
    return NULL;
}
    }
}
// HELPER CLASSES
 
/*====
  CLASS: Table Indexer
*/
*/
abstract class clsIndexer_Recs {
class clsIndexer_Table {
    private $objData;
     private $objTbl; // clsTable object
     private $objTbl; // table indexer
 
    // ABSTRACT functions
    abstract public function IndexIsSet();
    abstract public function KeyString();
    abstract public function SQL_forWhere();


    public function __construct(clsTable_abstract $iObj) {
$this->objTbl = $iObj;
    }
     /*----
     /*----
       INPUT:
       RETURNS: newly-created clsIndexer_Recs-descended object
iObj = DataSet
Override this method to change how indexing works
iKeys = array of field names
     */
     */
     public function __construct(clsRecs_keyed_abstract $iData,clsIndexer_Table $iTable) {
     protected function NewRecsIdxer(clsRecs_indexed $iRecs) {
$this->objData = $iData;
return new clsIndexer_Recs($iRecs,$this);
$this->objTbl = $iTable;
     }
     }
     public function DataObj() {
     public function TableObj() {
return $this->objData;
    }
    public function TblIdxObj() {
assert('is_a($this->objTbl,"clsIndexer_Table"); /* CLASS='.get_class($this->objTbl).' */');
return $this->objTbl;
return $this->objTbl;
     }
     }
     public function Engine() {
 
return $this->DataObj()->Engine();
     public function InitRecs(clsRecs_indexed $iRecs) {
$objIdx = $this->NewRecsIdxer($iRecs);
$iRecs->Indexer($objIdx);
return $objIdx;
     }
     }
     public function TableObj() {
}
return $this->TblIdxObj()->TableObj();
/*====
  HISTORY:
    2011-01-19 Started; not ready yet -- just saving bits of code I know I will need
*/
class clsIndexer_Table_single_key extends clsIndexer_Table {
     protected function NewRecsIdxer(clsRecs_indexed $iRecs) {
return new clsIndexer_Recs_single_key($iRecs,$this);
     }
     }
     public function TableName() {
     public function KeyName($iName=NULL) {
return $this->TableObj()->Name();
if (!is_null($iName)) {
    $this->vKeyName = $iName;
}
return $this->vKeyName;
     }
     }
/*
     public function GetItem($iID=NULL,$iClass=NULL) {
     public function Keys() {
if (is_null($iID)) {
$arKeys = $this->objTbl->Keys();
    $objItem = $this->SpawnItem($iClass);
reset($arKeys);
    $objItem->KeyValue(NULL);
return $arKeys;
} else {
    assert('!is_array($iID); /* TABLE='.$this->TableObj()->Name().' */');
    $objItem = $this->TableObj()->GetData($this->vKeyName.'='.SQLValue($iID),$iClass);
    $objItem->NextRow();
}
return $objItem;
     }
     }
*/
}
     /*-----
 
       FUNCTION: KeyValue()
class clsIndexer_Table_multi_key extends clsIndexer_Table {
      IN/OUT: array of key values
    private $arKeys;
array[key name] = value
 
      USED BY: clsCacheFlow::KeyValue()
     /*----
       RETURNS: newly-created clsIndexer_Recs-descended object
Override this method to change how indexing works
     */
     */
/*
     protected function NewRecsIdxer(clsRecs_indexed $iRecs) {
     public function KeyValue(array $iVals=NULL) {
return new clsIndexer_Recs_multi_key($iRecs,$this);
$arKeys = $this->KeyNames();
    }
$arRow = $this->DataObj()->Row;
    public function KeyNames(array $iNames=NULL) {
if (is_array($iVals)) {
if (!is_null($iNames)) {
    foreach ($iVals as $val) {
    $this->arKeys = $iNames;
list($key) = each($arKeys);
$arRow[$key] = $val;
    }
    $this->DataObj()->Row = $arRow;
}
}
foreach ($arKeys as $key) {
return $this->arKeys;
    $arOut[$key] = $arRow[$key];
}
return $arOut;
     }
     }
*/
     /*----
     /*----
       FUNCTION: KeyString()
       HISTORY:
       IN/OUT: prefix-delimited string of all key values
2011-01-08 written
       QUERY: What uses this?
    */
    /*----
       INPUT:
$iVals can be an array of index values, a prefix-marked string, or NULL
  NULL means "spawn a blank item".
       HISTORY:
2012-03-04 now calling MakeFilt() instead of SQL_for_filter()
     */
     */
/*
     public function GetItem($iVals=NULL) {
     public function KeyString($iVals=NULL) {
if (is_null($iVals)) {
if (is_string($iVals)) {
    $objItem = $this->TableObj()->SpawnItem();
    $xts = new xtString($iVals);
} else {
    $arVals = $xts->Xplode();
    if (is_array($iVals)) {
    $arKeys = $this->Keys();
$arVals = $iVals;
    foreach ($arVals as $val) {
    } else {
list($key) = each($arKeys);
$x = new xtString($iVals); // KLUGE to get strings.php to load >.<
$this->Row[$key] = $val;
$arVals = Xplode($iVals);
    }
    $arKeys = $this->KeyNames();
    $arVals = array_reverse($arVals); // pop takes the last item; we want to start with the first
    foreach ($arKeys as $key) {
$arData[$key] = array_pop($arVals); // get raw values to match
    }
    }
    $sqlFilt = $this->MakeFilt($arData,TRUE);
    $objItem = $this->TableObj()->GetData($sqlFilt);
    $objItem->NextRow();
}
}
$out = '';
return $objItem;
foreach ($this->arKeys as $key) {
    $val = $this->Row[$key];
    $out .= '.'.$val;
}
return $out;
     }
     }
*/
    /*----
/*
      RETURNS: SQL to filter for the current record by key value(s)
     public function KeyValue(array $iVals=NULL) {
'(name=value) AND (name=value) AND...'
      INPUT:
$iData: array of keynames and values
  iData[name] = value
$iSQLify: TRUE = massage with SQLValue() before using; FALSE = ready to use in SQL
      USED BY: GetItem()
      HISTORY:
2011-01-08 written
2011-01-19 replaced with boilerplate call to indexer in clsIndexer_Table
2012-03-04 I... don't know what I was talking about on 2011-01-19. Reinstating this,
  but renaming it from SQL_for_filter() to MakeFilt()
    */
     public function MakeFilt(array $iData,$iSQLify) {
$arKeys = $this->KeyNames();
$arKeys = $this->KeyNames();
$arRow = $this->DataObj()->Row;
$sql = NULL;
if (is_array($iVals)) {
foreach ($arKeys as $name) {
    foreach ($iVals as $val) {
    if (!array_key_exists($name,$iData)) {
list($key) = each($arKeys);
echo '<br>Key ['.$name.'] not found in passed data:<pre>'.print_r($iData,TRUE).'</pre>';
$arRow[$key] = $val;
throw new exception('Key ['.$name.'] not found in passed data.');
    }
    $val = $iData[$name];
    if (!is_null($sql)) {
$sql .= ' AND ';
    }
    if ($iSQLify) {
$val = SQLValue($val);
    }
    }
    $this->DataObj()->Row = $arRow;
    $sql .= '(`'.$name.'`='.$val.')';
}
}
foreach ($arKeys as $key) {
return $sql;
    $arOut[$key] = $arRow[$key];
}
return $arOut;
     }
     }
*/
 
/* There's no need for this here; it doesn't require indexing
    public function SQL_forInsert() {
return $this->TblIdxObj()->SQL_forInsert($this->KeyValues());
    }
*/
     /*----
     /*----
       INPUT:
       RETURNS: SQL for creating a new record for the given data
iSet: array specifying fields to update and the values to update them to
  iSet[field name] = value
       HISTORY:
       HISTORY:
2010-11-20 Created
2010-11-20 Created.
2011-01-09 Adapted from clsDataSet_bare
2011-01-08 adapted from clsTable::Insert()
     */
     */
     public function SQL_forUpdate(array $iSet) {
     public function SQL_forInsert(array $iData) {
$sqlSet = '';
$sqlNames = '';
foreach($iSet as $key=>$val) {
$sqlVals = '';
    if ($sqlSet != '') {
foreach($iData as $key=>$val) {
$sqlSet .= ',';
    if ($sqlNames != '') {
$sqlNames .= ',';
$sqlVals .= ',';
    }
    }
    $sqlSet .= ' `'.$key.'`='.$val;
    $sqlNames .= '`'.$key.'`';
    $sqlVals .= $val;
}
}
$sqlWhere = $this->SQL_forWhere();
return 'INSERT INTO `'.$this->Name().'` ('.$sqlNames.') VALUES('.$sqlVals.');';
 
return 'UPDATE `'.$this->TableName().'` SET'.$sqlSet.' WHERE '.$sqlWhere;
     }
     }
    /*----
      HISTORY:
2010-11-16 Added "array" requirement for iData
2010-11-20 Calculation now takes place in SQL_forInsert()
2011-01-08 adapted from clsTable::Insert()
    */
/* There's no need for this here; it doesn't require indexing
    public function Insert(array $iData) {
global $sql;


$sql = $this->SQL_forInsert($iData);
$this->sql = $sql;
return $this->Engine()->Exec($sql);
    }
*/
}
}
class clsIndexer_Recs_single_key extends clsIndexer_Recs {
/*====
     private $vKeyName;
  CLASS: clsIndexer_Recs -- record set indexer
 
  PURPOSE: Handles record sets for tables with multiple keys
     public function KeyName() {
  HISTORY:
return $this->TblIdxObj()->KeyName();
    2010-?? written for clsCacheFlows/clsCacheFlow in cache.php
    2011-01-08 renamed, revised and clarified
*/
abstract class clsIndexer_Recs {
     private $objData;
    private $objTbl; // table indexer
 
    // ABSTRACT functions
    abstract public function IndexIsSet();
    abstract public function KeyString();
    abstract public function SQL_forWhere();
 
    /*----
      INPUT:
iObj = DataSet
iKeys = array of field names
    */
     public function __construct(clsRecs_keyed_abstract $iData,clsIndexer_Table $iTable) {
$this->objData = $iData;
$this->objTbl = $iTable;
     }
     }
     public function KeyValue() {
     public function DataObj() {
return $this->DataObj()->Value($this->KeyName());
return $this->objData;
     }
     }
     public function KeyString() {
     public function TblIdxObj() {
return (string)$this->KeyValue();
assert('is_a($this->objTbl,"clsIndexer_Table"); /* CLASS='.get_class($this->objTbl).' */');
     }
return $this->objTbl;
     public function IndexIsSet() {
     }
return !is_null($this->KeyValue());
     public function Engine() {
return $this->DataObj()->Engine();
     }
     }
 
     public function TableObj() {
     public function SQL_forWhere() {
return $this->TblIdxObj()->TableObj();
$sql = $this->KeyName().'='.SQLValue($this->KeyValue());
return $sql;
     }
     }
}
     public function TableName() {
class clsIndexer_Recs_multi_key extends clsIndexer_Recs {
return $this->TableObj()->Name();
    /*----
      RETURNS: Array of values which constitute this row's key
array[key name] = key value
    */
     public function KeyArray() {
$arKeys = $this->TblIdxObj()->KeyNames();
$arRow = $this->DataObj()->Row;
foreach ($arKeys as $key) {
    $arOut[$key] = $arRow[$key];
}
return $arOut;
     }
     }
    /*----
/*
      ASSUMES: keys will always be returned in the same order
     public function Keys() {
If this changes, add field names.
$arKeys = $this->objTbl->Keys();
      POTENTIAL BUG: Non-numeric keys might contain the separator character
reset($arKeys);
that we are currently using ('.'). Some characters may not be appropriate
return $arKeys;
for some contexts. The caller should be able to specify what separator it wants.
    */
     public function KeyString() {
$arKeys = $this->KeyArray();
$out = NULL;
foreach ($arKeys as $name=>$val) {
    $out .= '.'.$val;
}
return $out;
     }
     }
  /*----
*/
       RETURNS: TRUE if any index fields are NULL
    /*-----
       ASSUMES: An index may not contain any NULL fields. Perhaps this is untrue, and it should
       FUNCTION: KeyValue()
only return TRUE if *all* index fields are NULL.
       IN/OUT: array of key values
array[key name] = value
      USED BY: clsCacheFlow::KeyValue()
     */
     */
     public function IndexIsSet() {
/*
$arKeys = $this->KeyArray();
     public function KeyValue(array $iVals=NULL) {
$isset = TRUE;
$arKeys = $this->KeyNames();
foreach ($arKeys as $key=>$val) {
$arRow = $this->DataObj()->Row;
    if (is_null($val)) { $isset = FALSE; }
if (is_array($iVals)) {
    foreach ($iVals as $val) {
list($key) = each($arKeys);
$arRow[$key] = $val;
    }
    $this->DataObj()->Row = $arRow;
}
foreach ($arKeys as $key) {
    $arOut[$key] = $arRow[$key];
}
}
return $isset;
return $arOut;
     }
     }
*/
     /*----
     /*----
       RETURNS: SQL to filter for the current record by key value(s)
       FUNCTION: KeyString()
       HISTORY:
       IN/OUT: prefix-delimited string of all key values
2011-01-08 written for Insert()
      QUERY: What uses this?
2011-01-19 moved from clsIndexer_Recs to clsIndexer_Recs_multi_key
     */
     */
     public function SQL_forWhere() {
/*
$arVals = $this->TblIdxObj()->KeyNames();
     public function KeyString($iVals=NULL) {
return SQL_for_filter($arVals);
if (is_string($iVals)) {
    $xts = new xtString($iVals);
    $arVals = $xts->Xplode();
    $arKeys = $this->Keys();
    foreach ($arVals as $val) {
list($key) = each($arKeys);
$this->Row[$key] = $val;
    }
}
$out = '';
foreach ($this->arKeys as $key) {
    $val = $this->Row[$key];
    $out .= '.'.$val;
}
return $out;
     }
     }
}
/*=============
  NAME: clsTable_indexed
  PURPOSE: handles indexes via a helper object
*/
*/
class clsTable_indexed extends clsTable_keyed_abstract {
/*
    protected $objIdx;
     public function KeyValue(array $iVals=NULL) {
 
$arKeys = $this->KeyNames();
    /*----
$arRow = $this->DataObj()->Row;
      NOTE: In practice, how would you ever have the Indexer object created before the Table object,
if (is_array($iVals)) {
since the Indexer object requires a Table object in its constructor? Possibly descendent classes
    foreach ($iVals as $val) {
can create the Indexer in their constructors and then pass it back to the parent constructor,
list($key) = each($arKeys);
which lets you have a default Indexer that you can override if you need, but how useful is this?
$arRow[$key] = $val;
    */
    }
     public function __construct(clsDatabase $iDB, clsIndexer_Table $iIndexer=NULL) {
    $this->DataObj()->Row = $arRow;
parent::__construct($iDB);
}
$this->Indexer($iIndexer);
foreach ($arKeys as $key) {
    }
    $arOut[$key] = $arRow[$key];
    // BOILERPLATE BEGINS
    protected function Indexer(clsIndexer_Table $iObj=NULL) {
if (!is_null($iObj)) {
    $this->objIdx = $iObj;
}
}
return $this->objIdx;
return $arOut;
     }
     }
     public function GetItem(array $iVals=NULL) {
*/
return $this->Indexer()->GetItem($iVals);
/* There's no need for this here; it doesn't require indexing
     public function SQL_forInsert() {
return $this->TblIdxObj()->SQL_forInsert($this->KeyValues());
     }
     }
    // BOILERPLATE ENDS
*/
    // OVERRIDES
     /*----
     /*----
       ADDS: spawns an indexer and attaches it to the item
       INPUT:
iSet: array specifying fields to update and the values to update them to
  iSet[field name] = value
      HISTORY:
2010-11-20 Created
2011-01-09 Adapted from clsDataSet_bare
     */
     */
     protected function ReleaseItem(clsRecs_abstract $iItem) {
     public function SQL_forUpdate(array $iSet) {
parent::ReleaseItem($iItem);
$sqlSet = '';
$this->Indexer()->InitRecs($iItem);
foreach($iSet as $key=>$val) {
    if ($sqlSet != '') {
$sqlSet .= ',';
    }
    $sqlSet .= ' `'.$key.'`='.$val;
}
$sqlWhere = $this->SQL_forWhere();
 
return 'UPDATE `'.$this->TableName().'` SET'.$sqlSet.' WHERE '.$sqlWhere;
     }
     }
     /*----
     /*----
       ADDS: spawns an indexer and attaches it to the item
       HISTORY:
2010-11-16 Added "array" requirement for iData
2010-11-20 Calculation now takes place in SQL_forInsert()
2011-01-08 adapted from clsTable::Insert()
     */
     */
/*
/* There's no need for this here; it doesn't require indexing
     public function SpawnItem($iClass=NULL) {
     public function Insert(array $iData) {
$obj = parent::SpawnItem($iClass);
global $sql;
return $obj;
 
$sql = $this->SQL_forInsert($iData);
$this->sql = $sql;
return $this->Engine()->Exec($sql);
     }
     }
*/
*/
}
}
/*=============
class clsIndexer_Recs_single_key extends clsIndexer_Recs {
  NAME: clsRecs_indexed
     private $vKeyName;
*/
class clsRecs_indexed extends clsRecs_keyed_abstract {
     protected $objIdx;


/* This is never used
     public function KeyName() {
     public function __construct(clsIndexer_Recs $iIndexer=NULL) {
return $this->TblIdxObj()->KeyName();
$this->Indexer($iIndexer);
     }
     }
*/
     public function KeyValue() {
    // BOILERPLATE BEGINS
return $this->DataObj()->Value($this->KeyName());
     public function Indexer(clsIndexer_Recs $iObj=NULL) {
if (!is_null($iObj)) {
    $this->objIdx = $iObj;
}
assert('is_object($this->objIdx);');
return $this->objIdx;
     }
     }
     public function IsNew() {
     public function KeyString() {
return !$this->Indexer()->IndexIsSet();
return (string)$this->KeyValue();
     }
     }
    /*----
     public function IndexIsSet() {
      USED BY: Administrative UI classes which need a string for referring to a particular record
return !is_null($this->KeyValue());
    */
     public function KeyString() {
return $this->Indexer()->KeyString();
     }
     }
     public function SelfFilter() {
 
return $this->Indexer()->SQL_forWhere();
     public function SQL_forWhere() {
    }
$sql = $this->KeyName().'='.SQLValue($this->KeyValue());
    public function SQL_forUpdate(array $iSet) {
return $sql;
return $this->Indexer()->SQL_forUpdate($iSet);
     }
     }
    // BOILERPLATE ENDS
    public function SQL_forMake(array $iarSet) { die('Not yet written.'); }
}
}
/*%%%%
class clsIndexer_Recs_multi_key extends clsIndexer_Recs {
  PURPOSE: for tracking whether a cached object has the expected data or not
    /*----
  HISTORY:
      RETURNS: Array of values which constitute this row's key
     2011-03-30 written
array[key name] = key value
*/
     */
class clsObjectCache {
     public function KeyArray() {
    private $vKey;
$arKeys = $this->TblIdxObj()->KeyNames();
    private $vObj;
$arRow = $this->DataObj()->Row;
 
foreach ($arKeys as $key) {
     public function __construct() {
    if (array_key_exists($key,$arRow)) {
$this->vKey = NULL;
$arOut[$key] = $arRow[$key];
$this->vObj = NULL;
    } else {
    }
echo "\nTrying to access nonexistent key [$key]. Available keys:";
    public function IsCached($iKey) {
echo '<pre>'.print_r($arRow,TRUE).'</pre>';
if (is_object($this->vObj)) {
throw new exception('Nonexistent key requested.');
    return ($this->vKey == $iKey);
    }
} else {
    return FALSE;
}
}
return $arOut;
     }
     }
     public function Object($iObj=NULL,$iKey=NULL) {
    /*----
if (!is_null($iObj)) {
      NOTE: The definition of "new" is a little more ambiguous with multikey tables;
    $this->vKey = $iKey;
for now, I'm saying that all keys must be NULL, because NULL keys are sometimes
    $this->vObj = $iObj;
valid in multikey contexts.
    */
     public function IsNew() {
$arData = $this->KeyArray();
foreach ($arData as $key => $val) {
    if (!is_null($val)) {
return FALSE;
    }
}
}
return $this->vObj;
return TRUE;
     }
     }
     public function Clear() {
     /*----
$this->vObj = NULL;
      ASSUMES: keys will always be returned in the same order
    }
If this changes, add field names.
}
      POTENTIAL BUG: Non-numeric keys might contain the separator character
class clsSQLFilt {
that we are currently using ('.'). Some characters may not be appropriate
    private $arFilt;
for some contexts. The caller should be able to specify what separator it wants.
    private $strConj;
 
    public function __construct($iConj) {
$this->strConj = $iConj;
    }
    /*-----
      ACTION: Add a condition
     */
     */
     public function AddCond($iSQL) {
     public function KeyString() {
$this->arFilt[] = $iSQL;
$arKeys = $this->KeyArray();
$out = NULL;
foreach ($arKeys as $name=>$val) {
    $out .= '.'.$val;
}
return $out;
     }
     }
     public function RenderFilter() {
  /*----
$out = '';
      RETURNS: TRUE if any index fields are NULL
foreach ($this->arFilt as $sql) {
      ASSUMES: An index may not contain any NULL fields. Perhaps this is untrue, and it should
    if ($out != '') {
only return TRUE if *all* index fields are NULL.
$out .= ' '.$this->strConj.' ';
    */
    }
     public function IndexIsSet() {
    $out .= '('.$sql.')';
$arKeys = $this->KeyArray();
$isset = TRUE;
foreach ($arKeys as $key=>$val) {
    if (is_null($val)) { $isset = FALSE; }
}
}
return $out;
return $isset;
     }
     }
}
    /*----
/* ========================
      RETURNS: SQL to filter for the current record by key value(s)
*** UTILITY FUNCTIONS ***
      HISTORY:
*/
2011-01-08 written for Insert()
/*----
2011-01-19 moved from clsIndexer_Recs to clsIndexer_Recs_multi_key
  PURPOSE: This gets around PHP's apparent lack of built-in object type-conversion.
2012-03-04 This couldn't have been working; it was calling SQL_for_filter() on a list of keys,
  ACTION: Copies all public fields from iSrce to iDest
  not keys-and-values. Fixed.
*/
    */
function CopyObj(object $iSrce, object $iDest) {
    public function SQL_forWhere() {
    foreach($iSrce as $key => $val) {
$arVals = $this->TblIdxObj()->KeyNames();
$iDest->$key = $val;
//return SQL_for_filter($arVals);
$sql = $this->TblIdxObj()->MakeFilt($this->DataObj()->Values(),TRUE);
return $sql;
    }
    /*----
      NOTE: This is slightly different from the single-keyed Make() in that it assumes there are no autonumber keys.
All keys must be specified in the initial data.
    */
    public function Make(array $iarSet) {
if ($this->IsNew()) {
    $ok = $this->Table->Insert($iarSet);
    //$this->KeyValue($this->objDB->NewID());
    $this->DataObj()->Values($iarSet); // do we need to preserve any existing values? for now, assuming not.
    return $ok;
} else {
    return $this->DataObj()->Update($iarSet);
}
     }
     }
}
}
if (!function_exists('Pluralize')) {
/*=============
    function Pluralize($iQty,$iSingular='',$iPlural='s') {
  NAME: clsTable_indexed
  if ($iQty == 1) {
  PURPOSE: handles indexes via a helper object
  return $iSingular;
*/
  } else {
class clsTable_indexed extends clsTable_keyed_abstract {
  return $iPlural;
    protected $objIdx;
  }
  }
}


function SQLValue($iVal) {
    /*----
    if (is_array($iVal)) {
      NOTE: In practice, how would you ever have the Indexer object created before the Table object,
foreach ($iVal as $key => $val) {
since the Indexer object requires a Table object in its constructor? Possibly descendent classes
    $arOut[$key] = SQLValue($val);
can create the Indexer in their constructors and then pass it back to the parent constructor,
}
which lets you have a default Indexer that you can override if you need, but how useful is this?
return $arOut;
    */
     } else {
    public function __construct(clsDatabase $iDB, clsIndexer_Table $iIndexer=NULL) {
if (is_null($iVal)) {
parent::__construct($iDB);
    return 'NULL';
$this->Indexer($iIndexer);
} else if (is_bool($iVal)) {
    }
    return $iVal?'TRUE':'FALSE';
    // BOILERPLATE BEGINS
} else if (is_string($iVal)) {
     protected function Indexer(clsIndexer_Table $iObj=NULL) {
    $oVal = '"'.mysql_real_escape_string($iVal).'"';
if (!is_null($iObj)) {
    return $oVal;
    $this->objIdx = $iObj;
} else {
    // numeric can be raw
    // all others, we don't know how to handle, so return raw as well
    return $iVal;
}
}
return $this->objIdx;
    }
    /*----
      INPUT:
$iVals can be an array of index values, a prefix-marked string, or NULL
  NULL means "spawn a blank item".
    */
    public function GetItem($iVals=NULL) {
return $this->Indexer()->GetItem($iVals);
     }
     }
}
    protected function MakeFilt(array $iData) {
function SQL_for_filter(array $iVals) {
return $this->Indexer()->MakeFilt($iData,TRUE);
    $sql = NULL;
    foreach ($arVals as $name => $val) {
if (!is_null($sql)) {
    $sql .= ' AND ';
}
$sql .= '('.$name.'='.SQLValue($val).')';
     }
     }
     return $sql;
     protected function MakeFilt_direct(array $iData) {
}
return $this->Indexer()->MakeFilt($iData,FALSE);
function NoYes($iBool,$iNo='no',$iYes='yes') {
    if ($iBool) {
return $iYes;
    } else {
return $iNo;
     }
     }
    // BOILERPLATE ENDS
    // OVERRIDES
    /*----
      ADDS: spawns an indexer and attaches it to the item
    */
    protected function ReleaseItem(clsRecs_abstract $iItem) {
parent::ReleaseItem($iItem);
$this->Indexer()->InitRecs($iItem);
    }
    /*----
      ADDS: spawns an indexer and attaches it to the item
    */
/*
    public function SpawnItem($iClass=NULL) {
$obj = parent::SpawnItem($iClass);
return $obj;
    }
*/
}
}
 
/*=============
function nz(&$iVal,$default=NULL) {
   NAME: clsRecs_indexed
    return empty($iVal)?$default:$iVal;
}
/*-----
  FUNCTION: nzAdd -- NZ Add
  RETURNS: ioVal += iAmt, but assumes ioVal is zero if not set (prevents runtime error)
   NOTE: iAmt is a reference so that we can pass variables which might not be set.
    Need to document why this is better than being able to pass constants.
*/
*/
function nzAdd(&$ioVal,&$iAmt=NULL) {
class clsRecs_indexed extends clsRecs_keyed_abstract {
     $intAmt = empty($iAmt)?0:$iAmt;
     protected $objIdx;
     if (empty($ioVal)) {
 
$ioVal = $intAmt;
/* This is never used
    } else {
     public function __construct(clsIndexer_Recs $iIndexer=NULL) {
$ioVal += $intAmt;
$this->Indexer($iIndexer);
     }
     }
    return $ioVal;
}
/*-----
  FUNCTION: nzApp -- NZ Append
  PURPOSE: Like nzAdd(), but appends strings instead of adding numbers
*/
*/
function nzApp(&$ioVal,$iTxt=NULL) {
     // BOILERPLATE BEGINS
    if (empty($ioVal)) {
     public function Indexer(clsIndexer_Recs $iObj=NULL) {
$ioVal = $iTxt;
if (!is_null($iObj)) {
    } else {
    $this->objIdx = $iObj;
$ioVal .= $iTxt;
     }
     return $ioVal;
}
function nzArray(array $iArr=NULL,$iKey,$iDefault=NULL) {
    $out = $iDefault;
    if (is_array($iArr)) {
if (array_key_exists($iKey,$iArr)) {
    $out = $iArr[$iKey];
}
}
assert('is_object($this->objIdx);');
return $this->objIdx;
     }
     }
     return $out;
    public function IsNew() {
}
return !$this->Indexer()->IndexIsSet();
/*----
    }
   PURPOSE: combines the two arrays without changing any keys
    /*----
     If entries in arTwo have the same keys as arOne, the values in
      USED BY: Administrative UI classes which need a string for referring to a particular record
       arTwo will overwrite those in arOne.
    */
   RETURNS: the combined array
    public function KeyString() {
   NOTE: You'd think one of the native array functions could do this...
return $this->Indexer()->KeyString();
     array_merge(): "Values in the input array with numeric keys will be
    }
       renumbered with incrementing keys starting from zero in the result array."
    public function SelfFilter() {
       (This is a problem if the keys are significant, e.g. record ID numbers.)
return $this->Indexer()->SQL_forWhere();
   HISTORY:
    }
     2011-12-22 written because I keep needing it for cache mapping functions
    public function SQL_forUpdate(array $iSet) {
*/
return $this->Indexer()->SQL_forUpdate($iSet);
function ArrayJoin(array $arOne, array $arTwo) {
    }
     $arOut = $arOne;
    // BOILERPLATE ENDS
     foreach ($arTwo as $id => $val) {
    public function SQL_forMake(array $iarSet) { die('Not yet written.'); }
if (!array_key_exists($id,$arOut)) {
}
    $arOut[$id] = $val;
/*%%%%
}
  PURPOSE: for tracking whether a cached object has the expected data or not
     }
  HISTORY:
     return $arOut;
    2011-03-30 written
}
*/
function ifEmpty(&$iVal,$iDefault) {
class clsObjectCache {
     if (empty($iVal)) {
    private $vKey;
return $iDefault;
    private $vObj;
     } else {
 
return $iVal;
    public function __construct() {
     }
$this->vKey = NULL;
}
$this->vObj = NULL;
function FirstNonEmpty(array $iList) {
    }
     foreach ($iList as $val) {
    public function IsCached($iKey) {
if (!empty($val)) {
if (is_object($this->vObj)) {
    return $val;
    return ($this->vKey == $iKey);
}
} else {
     }
    return FALSE;
}
}
/*----
    }
   ACTION: Takes a two-dimensional array and returns it flipped diagonally,
    public function Object($iObj=NULL,$iKey=NULL) {
     i.e. each element out[x][y] is element in[y][x].
if (!is_null($iObj)) {
   EXAMPLE:
    $this->vKey = $iKey;
     INPUT      OUTPUT
    $this->vObj = $iObj;
     +---+---+  +---+---+---+
}
     | A | 1 |  | A | B | C |
return $this->vObj;
     +---+---+  +---+---+---+
    }
     | B | 2 |  | 1 | 2 | 3 |
    public function Clear() {
$this->vObj = NULL;
    }
}
class clsSQLFilt {
    private $arFilt;
    private $strConj;
 
    public function __construct($iConj) {
$this->strConj = $iConj;
    }
    /*-----
      ACTION: Add a condition
    */
    public function AddCond($iSQL) {
$this->arFilt[] = $iSQL;
    }
    public function RenderFilter() {
$out = '';
foreach ($this->arFilt as $sql) {
    if ($out != '') {
$out .= ' '.$this->strConj.' ';
    }
    $out .= '('.$sql.')';
}
return $out;
    }
}
/* ========================
*** UTILITY FUNCTIONS ***
*/
/*----
  PURPOSE: This gets around PHP's apparent lack of built-in object type-conversion.
  ACTION: Copies all public fields from iSrce to iDest
*/
function CopyObj(object $iSrce, object $iDest) {
    foreach($iSrce as $key => $val) {
$iDest->$key = $val;
    }
}
if (!function_exists('Pluralize')) {
    function Pluralize($iQty,$iSingular='',$iPlural='s') {
  if ($iQty == 1) {
  return $iSingular;
  } else {
  return $iPlural;
  }
  }
}
 
function SQLValue($iVal) {
    if (is_array($iVal)) {
foreach ($iVal as $key => $val) {
    $arOut[$key] = SQLValue($val);
}
return $arOut;
    } else {
if (is_null($iVal)) {
    return 'NULL';
} else if (is_bool($iVal)) {
    return $iVal?'TRUE':'FALSE';
} else if (is_string($iVal)) {
    $oVal = '"'.mysql_real_escape_string($iVal).'"';
    return $oVal;
} else {
    // numeric can be raw
    // all others, we don't know how to handle, so return raw as well
    return $iVal;
}
    }
}
function SQL_for_filter(array $iVals) {
    $sql = NULL;
    foreach ($iVals as $name => $val) {
if (!is_null($sql)) {
    $sql .= ' AND ';
}
$sql .= '('.$name.'='.SQLValue($val).')';
    }
throw new exception('How did we get here?');
    return $sql;
}
function NoYes($iBool,$iNo='no',$iYes='yes') {
    if ($iBool) {
return $iYes;
    } else {
return $iNo;
    }
}
 
function nz(&$iVal,$default=NULL) {
    return empty($iVal)?$default:$iVal;
}
/*-----
  FUNCTION: nzAdd -- NZ Add
  RETURNS: ioVal += iAmt, but assumes ioVal is zero if not set (prevents runtime error)
  NOTE: iAmt is a reference so that we can pass variables which might not be set.
    Need to document why this is better than being able to pass constants.
*/
function nzAdd(&$ioVal,&$iAmt=NULL) {
    $intAmt = empty($iAmt)?0:$iAmt;
    if (empty($ioVal)) {
$ioVal = $intAmt;
    } else {
$ioVal += $intAmt;
    }
    return $ioVal;
}
/*-----
  FUNCTION: nzApp -- NZ Append
  PURPOSE: Like nzAdd(), but appends strings instead of adding numbers
*/
function nzApp(&$ioVal,$iTxt=NULL) {
    if (empty($ioVal)) {
$ioVal = $iTxt;
    } else {
$ioVal .= $iTxt;
    }
    return $ioVal;
}
/*----
  HISTORY:
    2012-03-11 iKey can now be an array, for multidimensional iArr
*/
function nzArray(array $iArr=NULL,$iKey,$iDefault=NULL) {
    $out = $iDefault;
    if (is_array($iArr)) {
if (is_array($iKey)) {
    $out = $iArr;
    foreach ($iKey as $key) {
if (array_key_exists($key,$out)) {
    $out = $out[$key];
} else {
    return $iDefault;
}
    }
} else {
    if (array_key_exists($iKey,$iArr)) {
$out = $iArr[$iKey];
    }
}
    }
    return $out;
}
function nzArray_debug(array $iArr=NULL,$iKey,$iDefault=NULL) {
    $out = $iDefault;
    if (is_array($iArr)) {
if (is_array($iKey)) {
    $out = $iArr;
    foreach ($iKey as $key) {
if (array_key_exists($key,$out)) {
    $out = $out[$key];
} else {
    return $iDefault;
}
    }
} else {
    if (array_key_exists($iKey,$iArr)) {
$out = $iArr[$iKey];
    }
}
    }
//echo '<br>IARR:<pre>'.print_r($iArr,TRUE).'</pre> KEY=['.$iKey.'] RETURNING <pre>'.print_r($out,TRUE).'</pre>';
     return $out;
}
/*----
   PURPOSE: combines the two arrays without changing any keys
     If entries in arAdd have the same keys as arStart, the result
      depends on the value of iReplace
    If entries in arAdd have keys that don't exist in arStart,
      the result depends on the value of iAppend
    This probably means that there are some equivalent ways of doing
      things by reversing order and changing flags, but I haven't
      worked through it yet.
  INPUT:
    arStart: the starting array - key-value pairs
    arAdd: additional key-value pairs to add to arStart
    iReplace:
       TRUE = arAdd values replace same keys in arStart
   RETURNS: the combined array
   NOTE: You'd think one of the native array functions could do this...
     array_merge(): "Values in the input array with numeric keys will be
       renumbered with incrementing keys starting from zero in the result array."
       (This is a problem if the keys are significant, e.g. record ID numbers.)
   HISTORY:
     2011-12-22 written because I keep needing it for cache mapping functions
    2012-03-05 added iReplace, iAppend
*/
function ArrayJoin(array $arStart=NULL, array $arAdd=NULL, $iReplace, $iAppend) {
    if (is_null($arStart)) {
$arOut = $arAdd;
     } elseif (is_null($arAdd)) {
$arOut = $arStart;
     } else {
$arOut = $arStart;
foreach ($arAdd as $key => $val) {
    if (array_key_exists($key,$arOut)) {
        if ($iReplace) {
    $arOut[$key] = $val;
    }
    } else {
    if ($iAppend) {
    $arOut[$key] = $val;
    }
    }
}
    }
    return $arOut;
}
/*----
  RETURNS: an array consisting only of keys from $arKeys
    plus the associated values from $arData
*/
function ArrayFilter_byKeys(array $arData, array $arKeys) {
    foreach ($arKeys as $key) {
if (array_key_exists($key,$arData)) {
    $arOut[$key] = $arData[$key];
} else {
    echo 'KEY ['.$key,'] not found.';
    echo ' Array contents:<pre>'.print_r($arData,TRUE).'</pre>';
    throw new exception('Expected key not found.');
}
     }
     return $arOut;
}
function ifEmpty(&$iVal,$iDefault) {
     if (empty($iVal)) {
return $iDefault;
     } else {
return $iVal;
     }
}
function FirstNonEmpty(array $iList) {
     foreach ($iList as $val) {
if (!empty($val)) {
    return $val;
}
     }
}
/*----
   ACTION: Takes a two-dimensional array and returns it flipped diagonally,
     i.e. each element out[x][y] is element in[y][x].
   EXAMPLE:
     INPUT      OUTPUT
     +---+---+  +---+---+---+
     | A | 1 |  | A | B | C |
     +---+---+  +---+---+---+
     | B | 2 |  | 1 | 2 | 3 |
     +---+---+  +---+---+---+
     +---+---+  +---+---+---+
     | C | 3 |
     | C | 3 |
Line 1,910: Line 2,408:
     }
     }
     return $arOut;
     return $arOut;
}
/*----
  ACTION: convert an array to SQL for filtering
  INPUT: iarFilt = array of filter terms; key is ignored
*/
function Array_toFilter($iarFilt) {
    $out = NULL;
    if (is_array($iarFilt)) {
foreach ($iarFilt as $key => $cond) {
    if (!is_null($out)) {
$out .= ' AND ';
    }
    $out .= '('.$cond.')';
}
    }
    return $out;
}
}
/* ========================
/* ========================
Line 2,019: Line 2,533:
   }
   }
}
}
</php>
</syntaxhighlight>

Latest revision as of 16:42, 22 May 2022

About

Database abstraction classes; used by VbzCart, SpamFerret, AudioFerret, WorkFerret

History

  • 2013-01-25 Working version from HostGator 1: seems to have added the data-engine-handling classes
  • 2013-01-27 Working version from Rizzo: minor changes to handle indirect access to database engine better

Code

<?php
/* ===========================
 *** DATA UTILITY CLASSES ***
  AUTHOR: Woozle Staddon
  HISTORY:
    2007-05-20 (wzl) These classes have been designed to be db-engine agnostic, but I wasn't able
	  to test against anything other than MySQL nor was I able to implement the usage of
	  the dbx_ functions, as the system that I was using didn't have them installed.
    2007-08-30 (wzl) posting this version at http://htyp.org/User:Woozle/data.php
    2007-12-24 (wzl) Some changes seem to have been made as recently as 12/17, so posting updated version
    2008-02-06 (wzl) Modified to use either mysqli or (standard) mysql library depending on flag; the latter isn't working yet
    2009-03-10 (wzl) adding some static functions to gradually get rid of the need for object factories
    2009-03-18 (wzl) debug constants now have defaults
    2009-03-26 (wzl) clsDataSet.Query() no longer fetches first row; this will require some rewriting
      NextRow() now returns TRUE if data was fetched; use if (data->NextRow()) {..} to loop through data.
    2009-05-02 (wzl) undocumented changes -- looks like:
      assert-checks return ID of an insertion
      function ExecUpdate($iSet,$iWhere)
      function SQLValue($iVal)
    2009-05-03 (wzl) more undocumented changes -- looks like mainly $iWhere is now optional in GetData()
    2009-07-05 (wzl) DataSet->__get now returns NULL if no field found; DataSet->HasField()
    2009-07-18 (wzl) clsTable::ExecUpdate() -> Update(); clsTable::Insert()
    2009-08-02 (wzl) clsDatabase::RowsAffected()
    2009-10-07 (wzl) minor: $dbg global added to clsTable Update() and Insert() methods
    2009-10-27 (wzl) clsTableCache
    2009-11-23 (wzl) clsDatabase.LogSQL(); some format-tidying
    2009-12-29 (wzl) clsDataSet_bare
    2010-01-02 (wzl) clsTable::DataSet()
    2010-01-08 (wzl) ifEmpty()
    2010-01-09 (wzl) fixed bug in clsDataSet_bare::Reload()
    2010-02-07 (wzl) clsTable::LastID()
    2010-04-11 (wzl) clsTable::KeyValue()
    2010-05-28 (wzl) split() is now deprecated -- replacing it with preg_split()
    2010-06-14 (wzl) added $iClass=NULL parameter to clsTable::SpawnItem
    2010-06-16 (wzl) nzApp()
    2010-07-19 (wzl) clsDatabase::Make()
    2010-10-04 (wzl) clsTable::ActionKey()
    2010-10-05 (wzl) removed reloading code from clsDataSet::Update()
    2010-10-16 (wzl) added clsTable::NameSQL(), clsTable::DataSetGroup()
    2010-10-19 (wzl) clsTable::DataSQL()
    2010-11-01 (wzl) clsTable::GetItem iID=NULL now means create new/blank object, i.e. SpawnItem()
    2010-11-14 (wzl) clsDataSet_bare::SameAs()
    2010-11-21 (wzl) caching helper class
    2011-02-07 (wzl) SQLValue() now handles arrays too
    2011-09-24 (wzl) Data Scripting classes created
    2011-10-07 (wzl) Data Scripting extracted to data-script.php
    2011-10-17 (wzl) ValueNz() rewritten (now goes directly to Row array instead of calling Value())
    2012-01-22 (wzl) clsDatabase_abstract
    2012-01-28 (wzl) clsDataEngine classes
    2012-12-31 (wzl) improved error handling in clsDataEngine_MySQL.db_open()
    2013-01-24 (wzl) clsDatabase_abstract:: SelfClass() and Spawn()
  FUTURE:
    API FIXES:
      GetData() should not have an $iClass parameter, or it should be the last parameter.
*/
// Select which DB library to use --
//	exactly one of the following must be true:
/*

These have been replaced by KS_DEFAULT_ENGINE

define('KF_USE_MYSQL',TRUE);	// in progress
define('KF_USE_MYSQLI',FALSE);	// complete & tested
define('KF_USE_DBX',false);	// not completely written; stalled
*/

define('KS_DEFAULT_ENGINE','clsDataEngine_MySQL');	// classname of default database engine

if (!defined('KDO_DEBUG')) {		define('KDO_DEBUG',FALSE); }
if (!defined('KDO_DEBUG_STACK')) {	define('KDO_DEBUG_STACK',FALSE); }
if (!defined('KDO_DEBUG_IMMED')) {	define('KDO_DEBUG_IMMED',FALSE); }
if (!defined('KS_DEBUG_HTML')) {	define('KS_DEBUG_HTML',FALSE); }
if (!defined('KDO_DEBUG_DARK')) {	define('KDO_DEBUG_DARK',FALSE); }

abstract class clsDatabase_abstract {
    protected $objEng;	// engine object
    protected $objRes;	// result object
    protected $cntOpen;


    public function InitBase() {
	$this->cntOpen = 0;
    }
    protected function DefaultEngine() {
	$cls = KS_DEFAULT_ENGINE;
	return new $cls;
    }
    public function Engine(clsDataEngine $iEngine=NULL) {
	if (!is_null($iEngine)) {
	    $this->objEng = $iEngine;
	} else {
	    if (!isset($this->objEng)) {
		$this->objEng = $this->DefaultEngine();
	    }
	}
	return $this->objEng;
    }
    public function Open() {
	if ($this->cntOpen == 0) {
	    $this->Engine()->db_open();
	}
	if (!$this->isOk()) {
	    $this->Engine()->db_get_error();
	}
	$this->cntOpen++;
    }
    public function Shut() {
	$this->cntOpen--;
	if ($this->cntOpen == 0) {
	    $this->Engine()->db_shut();
	}
    }

    /*-----
      PURPOSE: generic table-creation function
      HISTORY:
	2010-12-01 Added iID parameter to get singular item
	2011-02-23 Changed from protected to public, to support class registration
	2012-01-23 Moved from clsDatabase to (new) clsDatabase_abstract
    */
    public function Make($iName,$iID=NULL) {
	if (!isset($this->$iName)) {
	    if (class_exists($iName)) {
		$this->$iName = new $iName($this);
	    } else {
		throw new exception('Unknown class "'.$iName.'" requested.');
	    }
	}
	if (!is_null($iID)) {
	    return $this->$iName->GetItem($iID);
	} else {
	    return $this->$iName;
	}
    }
    abstract public function Exec($iSQL);
    abstract public function DataSet($iSQL=NULL,$iClass=NULL);

    // ENGINE WRAPPER FUNCTIONS
    public function engine_db_query($iSQL) {
//echo '#2: ENGINE CLASS=['.get_class($this->Engine()).']<br>';		// comes up as clsDataEngine_MySQL
	return $this->Engine()->db_query($iSQL);
    }
    public function engine_db_query_ok() {
	return $this->objRes->is_okay();
    }
    public function engine_db_get_new_id() {
	return $this->Engine()->db_get_new_id();
    }
    public function engine_db_rows_affected() {
	return $this->Engine()->db_get_qty_rows_chgd();
    }
/*
    public function engine_row_rewind() {
	return $this->objRes->do_rewind();
    }
    public function engine_row_get_next() { throw new exception('how did we get here?');
	return $this->objRes->get_next();
    }
    public function engine_row_get_count() {
	return $this->objRes->get_count();
    }
    public function engine_row_was_filled() {
	return $this->objRes->is_filled();
    }
*/
}
/*%%%%
  HISTORY:
    2013-01-25 InitSpec() only makes sense for _CliSrv, which is the first descendant that needs connection credentials.
*/
abstract class clsDataEngine {
    private $arSQL;

    //abstract public function InitSpec($iSpec);
    abstract public function db_open();
    abstract public function db_shut();

    /*----
      RETURNS: clsDataResult descendant
    */
    public function db_query($iSQL) {
	$this->LogSQL($iSQL);
	return $this->db_do_query($iSQL);
    }
    /*----
      RETURNS: clsDataResult descendant
    */
    abstract protected function db_do_query($iSQL);
    //abstract public function db_get_error();
    abstract public function db_get_new_id();
    abstract public function db_safe_param($iVal);
    //abstract public function db_query_ok(array $iBox);
    abstract public function db_get_error();
    abstract public function db_get_qty_rows_chgd();

    // LOGGING -- eventually split this off into handler class
    protected function LogSQL($iSQL) {
	$this->sql = $iSQL;
	$this->arSQL[] = $iSQL;
    }
    public function ListSQL($iPfx=NULL) {
	$out = '';
	foreach ($this->arSQL as $sql) {
	    $out .= $iPfx.$sql;
	}
	return $out;
    }
}
/*%%%%
  PURPOSE: encapsulates the results of a query
*/
abstract class clsDataResult {
    protected $box;

    public function __construct(array $iBox=NULL) {
	$this->box = $iBox;
    }
    /*----
      PURPOSE: The "Box" is an array containing information which this class needs but which
	the calling class has to be responsible for. The caller doesn't need to know what's
	in the box, it just needs to keep it safe.
    */
    public function Box(array $iBox=NULL) {
	if (!is_null($iBox)) {
	    $this->box = $iBox;
	}
	return $this->box;
    }
    public function Row(array $iRow=NULL) {
	if (!is_null($iRow)) {
	    $this->box['row'] = $iRow;
	    return $iRow;
	}
	if ($this->HasRow()) {
	    return $this->box['row'];
	} else {
	    return NULL;
	}
    }
    /*----
      USAGE: used internally when row retrieval comes back FALSE
    */
    protected function RowClear() {
	$this->box['row'] = NULL;
    }
    public function Val($iKey,$iVal=NULL) {
	if (!is_null($iVal)) {
	    $this->box['row'][$iKey] = $iVal;
	    return $iVal;
	} else {
	    if (!array_key_exists('row',$this->box)) {
		throw new exception('Row data not loaded yet.');
	    }
	    return $this->box['row'][$iKey];
	}
    }
    public function HasRow() {
	if (array_key_exists('row',$this->box)) {
	    return (!is_null($this->box['row']));
	} else {
	    return FALSE;
	}
    }
/* might be useful, but not actually needed now
    public function HasVal($iKey) {
	$row = $this->Row();
	return array_key_exists($iKey,$this->box['row']);
    }
*/
    abstract public function is_okay();
    /*----
      ACTION: set the record pointer so the first row in the set will be read next
    */
    abstract public function do_rewind();
    /*----
      ACTION: Fetch the first/next row of data from a result set
    */
    abstract public function get_next();
    /*----
      ACTION: Return the number of rows in the result set
    */
    abstract public function get_count();
    /*----
      ACTION: Return whether row currently has data.
    */
    abstract public function is_filled();
}

/*%%%%%
  PURPOSE: clsDataEngine that is specific to client-server databases
    This type will always need host and schema names, username, and password.
*/
abstract class clsDataEngine_CliSrv extends clsDataEngine {
    protected $strType, $strHost, $strUser, $strPass;

    public function InitSpec($iSpec) {
	$ar = preg_split('/@/',$iSpec);
	if (array_key_exists(1,$ar)) {
	    list($part1,$part2) = preg_split('/@/',$iSpec);
	} else {
	    throw new exception('Connection string not formatted right: ['.$iSpec.']');
	}
	list($this->strType,$this->strUser,$this->strPass) = preg_split('/:/',$part1);
	list($this->strHost,$this->strName) = explode('/',$part2);
	$this->strType = strtolower($this->strType);	// make sure it is lowercased, for comparison
	$this->strErr = NULL;
    }
    public function Host() {
	return $this->strHost;
    }
    public function User() {
	return $this->strUser;
    }
}

class clsDataResult_MySQL extends clsDataResult {

    /*----
      HISTORY:
	2012-09-06 This needs to be public so descendant helper classes can transfer the resource.
    */
    public function Resource($iRes=NULL) {
	if (!is_null($iRes)) {
	    $this->box['res'] = $iRes;
	}
	return $this->box['res'];
    }
    /*----
      NOTES:
	* For queries returning a resultset, mysql_query() returns a resource on success, or FALSE on error.
	* For other SQL statements, INSERT, UPDATE, DELETE, DROP, etc, mysql_query() returns TRUE on success or FALSE on error. 
      HISTORY:
	2012-02-04 revised to use box['ok']
    */
    public function do_query($iConn,$iSQL) {
//$ok = mysql_select_db('igov_app');
//echo 'OK=['.$ok.']<br>';
	$res = mysql_query($iSQL,$iConn);
	if (is_resource($res)) {
//echo 'GOT TO LINE '.__LINE__.'<br>';
	    $this->Resource($res);
	    $this->box['ok'] = TRUE;
	} else {
//echo 'GOT TO LINE '.__LINE__.' - SQL='.$iSQL.'<br>';
	    $this->Resource(NULL);
	    $this->box['ok'] = $res;	// TRUE if successful, false otherwise
	}
    }
    /*----
      USAGE: call after do_query()
      FUTURE: should probably reflect status of other operations besides do_query()
      HISTORY:
	2012-02-04 revised to use box['ok']
    */
    public function is_okay() {
	return $this->box['ok'];
    }
    public function do_rewind() {
	$res = $this->Resource();
	mysql_data_seek($res, 0);
    }
    public function get_next() {
	$res = $this->Resource();
	if (is_resource($res)) {
	    $row = mysql_fetch_assoc($res);
	    if ($row === FALSE) {
		$this->RowClear();
	    } else {
		$this->Row($row);
	    }
	    return $row;
	} else {
	    return NULL;
	}
    }
    /*=====
      ACTION: Return the number of rows in the result set
    */
    public function get_count() {
	$res = $this->Resource();
	if ($res === FALSE) {
	    return NULL;
	} else {
	    if (is_resource($res)) {
		$arRow = mysql_num_rows($res);
		return $arRow;
	    } else {
		return NULL;
	    }
	}
    }
    public function is_filled() {
	return $this->HasRow();
    }
}

class clsDataEngine_MySQL extends clsDataEngine_CliSrv {
    private $objConn;	// connection object

    public function db_open() {
	$this->objConn = @mysql_connect( $this->strHost, $this->strUser, $this->strPass, false );
	if ($this->objConn === FALSE) {
	    $arErr = error_get_last();
	    throw new exception('MySQL could not connect: '.$arErr['message']);
	} else {
	    $ok = mysql_select_db($this->strName, $this->objConn);
	    if (!$ok) {
		throw new exception('MySQL could not select database "'.$this->strName.'": '.mysql_error());
	    }
	}
    }
    public function db_shut() {
	mysql_close($this->objConn);
    }
    protected function Spawn_ResultObject() {
	return new clsDataResult_MySQL();
    }
    /*----
      RETURNS: clsDataResult descendant
    */
    protected function db_do_query($iSQL) {
	if (is_resource($this->objConn)) {
	    $obj = $this->Spawn_ResultObject();
	    $obj->do_query($this->objConn,$iSQL);
	    return $obj;
	} else {
	    throw new Exception('Database Connection object is a '.gettype($this->objConn).', not a resource');
	}
    }
    public function db_get_new_id() {
	$id = mysql_insert_id($this->objConn);
	return $id;
    }
/*
    public function db_query_ok(array $iBox) {
	$obj = new clsDataQuery_MySQL($iBox);
	return $obj->QueryOkay();
    }
*/
    public function db_get_error() {
	return mysql_error();
    }
    public function db_safe_param($iVal) {
	if (is_resource($this->objConn)) {
	    $out = mysql_real_escape_string($iVal,$this->objConn);
	} else {
	    throw new exception(get_class($this).'.SafeParam("'.$iString.'") has no connection.');
	}
	return $out;
    }
    public function db_get_qty_rows_chgd() {
	return mysql_affected_rows($this->objConn);
    }
}

/*
  These interfaces marked "abstract" have not been completed or tested.
    They're mainly here as a place to stick the partial code I wrote for them
    back when I first started writing the data.php library.
*/
abstract class clsDataEngine_MySQLi extends clsDataEngine_CliSrv {
    private $objConn;	// connection object

    public function db_open() {
	$this->objConn = new mysqli($this->strHost,$this->strUser,$this->strPass,$this->strName);
    }
    public function db_shut() {
	$this->objConn->close();
    }
    public function db_get_error() {
	return $this->objConn->error;
    }
    public function db_safe_param($iVal) {
	return $this->objConn->escape_string($iVal);
    }
    protected function db_do_query($iSQL) {
	$this->objConn->real_query($iSQL);
	return $this->objConn->store_result();
    }
    public function db_get_new_id() {
	$id = $this->objConn->insert_id;
	return $id;
    }
    public function row_do_rewind(array $iBox) {
    }
    public function row_get_next(array $iBox) {
	return $iRes->fetch_assoc();
    }
    public function row_get_count(array $iBox) {
	return $iRes->num_rows;
    }
    public function row_was_filled(array $iBox) {
	return ($this->objData !== FALSE) ;
    }
}
abstract class clsDataEngine_DBX extends clsDataEngine_CliSrv {
    private $objConn;	// connection object

    public function db_open() {
	$this->objConn = dbx_connect($this->strType,$this->strHost,$this->strName,$this->strUser,$this->strPass);
    }
    public function db_shut() {
	dbx_close($this->Conn);
    }

    protected function db_do_query($iSQL) {
	return dbx_query($this->objConn,$iSQL,DBX_RESULT_ASSOC);
    }
    public function db_get_new_id() {
    }
    public function row_do_rewind(array $iBox) {
    }
    public function row_get_next(array $iBox) {
    }
    public function row_get_count(array $iBox) {
    }
    public function row_was_filled(array $iBox) {
    }
}

/*====
  TODO: this is actually specific to a particular library for MySQL, so it should probably be renamed
    to reflect that.
*/
class clsDatabase extends clsDatabase_abstract {
    private $strType;	// type of db (MySQL etc.)
    private $strUser;	// database user
    private $strPass;	// password
    private $strHost;	// host (database server domain-name or IP address)
    private $strName;	// database (schema) name

    private $Conn;	// connection object
  // status
    private $strErr;	// latest error message
    public $sql;	// last SQL executed (or attempted)
    public $arSQL;	// array of all SQL statements attempted
    public $doAllowWrite;	// FALSE = don't execute UPDATE or INSERT commands, just log them

    public function __construct($iConn) {
	$this->Init($iConn);
	$this->doAllowWrite = TRUE;	// default
    }
    /*=====
      INPUT: 
	$iConn: type:user:pass@server/dbname
      TO DO:
	Init() -> InitSpec()
	InitBase() -> Init()
    */
    public function Init($iConn) {
	$this->InitBase();
	$this->Engine()->InitSpec($iConn);
    }

    /*=====
      PURPOSE: For debugging, mainly
      RETURNS: TRUE if database connection is supposed to be open
    */
    public function isOpened() {
	return ($this->cntOpen > 0);
    }
    /*=====
      PURPOSE: Checking status of a db operation
      RETURNS: TRUE if last operation was successful
    */
    public function isOk() {
	if (empty($this->strErr)) {
	    return TRUE;
	} elseif ($this->Conn == FALSE) {
	    return FALSE;
	} else {
	    return FALSE;
	}
    }
    public function getError() {
      if (is_null($this->strErr)) {
      // avoid having an ok status overwrite an actual error
	  $this->strErr = $this->Engine()->db_get_error();
      }
      return $this->strErr;
    }
    public function ClearError() {
	$this->strErr = NULL;
    }
    protected function LogSQL($iSQL) {
	$this->sql = $iSQL;
	$this->arSQL[] = $iSQL;
    }
    public function ListSQL($iPfx=NULL) {
	$out = '';
	foreach ($this->arSQL as $sql) {
	    $out .= $iPfx.$sql;
	}
	return $out;
    }
    /*----
      HISTORY:
	2011-03-04 added DELETE to list of write commands; rewrote to be more robust
    */
    protected function OkToExecSQL($iSQL) {
	if ($this->doAllowWrite) {
	    return TRUE;
	} else {
	    // this is a bit of a kluge... need to strip out comments and whitespace
	    // but basically, if the SQL starts with UPDATE, INSERT, or DELETE, then it's a write command so forbid it
	    $sql = strtoupper(trim($iSQL));
	    $cmd = preg_split (' ',$sql,1);	// get just the first word
	    switch ($cmd) {
	      case 'UPDATE':
	      case 'INSERT':
	      case 'DELETE':
		return FALSE;
	      default:
		return TRUE;
	    }
	}
    }
    /*=====
      HISTORY:
	2011-02-24 Now passing $this->Conn to mysql_query() because somehow the connection was getting set
	  to the wiki database instead of the original.
    */
    public function Exec($iSQL) {
	CallEnter($this,__LINE__,__CLASS__.'.'.__METHOD__.'('.$iSQL.')');
	$this->LogSQL($iSQL);
	$ok = TRUE;
	if ($this->OkToExecSQL($iSQL)) {
    /*----
      RETURNS: clsDataResult descendant
    */
	    $res = $this->Engine()->db_query($iSQL);
	    if (!$res->is_okay()) {
		$this->getError();
		$ok = FALSE;
	    }
	}
	CallExit(__CLASS__.'.'.__METHOD__.'()');
	return $ok;
    }

    public function RowsAffected() {
	return $this->Engine()->db_get_qty_rows_chgd();
    }
    public function NewID($iDbg=NULL) {
	return $this->engine_db_get_new_id();
    }
    public function SafeParam($iVal) {
	if (is_object($iVal)) {
	    echo '<b>Internal error</b>: argument is an object of class '.get_class($iVal).', not a string.<br>';
	    throw new exception('Unexpected argument type.');
	}
	$out = $this->Engine()->db_safe_param($iVal);
	return $out;
    }
    public function ErrorText() {
	if ($this->strErr == '') {
	    $this->_api_getError();
	}
	return $this->strErr;
    }

/******
 SECTION: OBJECT FACTORY
*/
    public function DataSet($iSQL=NULL,$iClass=NULL) {
	if (is_string($iClass)) {
	    $objData = new $iClass($this);
	    assert('is_object($objData)');
	    if (!($objData instanceof clsDataSet)) {
		LogError($iClass.' is not a clsDataSet subclass.');
	    }
	} else {
	    $objData = new clsDataSet($this);
	    assert('is_object($objData)');
	}
	assert('is_object($objData->Engine())');
	if (!is_null($iSQL)) {
	    if (is_object($objData)) {
		$objData->Query($iSQL);
	    }
	}
	return $objData;
    }
}
/*=============
  NAME: clsTable_abstract
  PURPOSE: objects for operating on particular tables
    Does not attempt to deal with keys.
*/
abstract class clsTable_abstract {
    protected $objDB;
    protected $vTblName;
    protected $vSngClass;	// name of singular class
    public $sqlExec;		// last SQL executed on this table

    public function __construct(clsDatabase_abstract $iDB) {
	$this->objDB = $iDB;
    }
    public function DB() {	// DEPRECATED - use Engine()
	return $this->objDB;
    }
    public function Engine() {
	return $this->objDB;
    }
    public function Name($iName=NULL) {
	if (!is_null($iName)) {
	    $this->vTblName = $iName;
	}
	return $this->vTblName;
    }
    public function NameSQL() {
	assert('is_string($this->vTblName); /* '.print_r($this->vTblName,TRUE).' */');
	return '`'.$this->vTblName.'`';
    }
    public function ClassSng($iName=NULL) {
	if (!is_null($iName)) {
	    $this->vSngClass = $iName;
	}
	return $this->vSngClass;
    }
    /*----
      ACTION: Make sure the item is ready to be released in the wild
    */
    protected function ReleaseItem(clsRecs_abstract $iItem) {
	$iItem->Table = $this;
	$iItem->objDB = $this->objDB;
	$iItem->sqlMake = $this->sqlExec;
    }
    /*----
      ACTION: creates a new uninitialized singular object but sets the Table pointer back to self
      RETURNS: created object
      FUTURE: maybe this should be renamed GetNew()?
    */
    public function SpawnItem($iClass=NULL) {
	if (is_null($iClass)) {
	    $strCls = $this->ClassSng();
	} else {
	    $strCls = $iClass;
	}
	assert('!empty($strCls);');
	$objItem = new $strCls;
	$this->ReleaseItem($objItem);
	return $objItem;
    }
    /*----
      RETURNS: dataset defined by the given SQL, wrapped in an object of the current class
      USAGE: primarily for joins where you want only records where there is no matching record
	in the joined table. (If other examples come up, maybe a DataNoJoin() method would
	be appropriate.)
    */
    public function DataSQL($iSQL) {
	$strCls = $this->ClassSng();
	$obj = $this->Engine()->DataSet($iSQL,$strCls);
	$this->sqlExec = $iSQL;
	$this->ReleaseItem($obj);
	return $obj;
    }
    /*----
      RETURNS: dataset containing all fields from the current table,
	with additional options (everything after the table name) being
	defined by $iSQL, wrapped in the current object class.
    */
    public function DataSet($iSQL=NULL,$iClass=NULL) {
	global $sql;	// for debugging

	$sql = 'SELECT * FROM '.$this->NameSQL();
	if (!is_null($iSQL)) {
	    $sql .= ' '.$iSQL;
	}
	return $this->DataSQL($sql);
/*
	$strCls = $this->vSngClass;
	$obj = $this->objDB->DataSet($sql,$strCls);
	$obj->Table = $this;
	return $obj;
*/
    }
    /*----
      FUTURE: This *so* needs to have iClass LAST, or not at all.
    */
    public function GetData($iWhere=NULL,$iClass=NULL,$iSort=NULL) {
	global $sql; 	// for debugging

	$sql = 'SELECT * FROM '.$this->NameSQL();
	if (!is_null($iWhere)) {
	    $sql .= ' WHERE '.$iWhere;
	}
	if (!is_null($iSort)) {
	    $sql .= ' ORDER BY '.$iSort;
	}

	//$obj = $this->objDB->DataSet($sql,$strCls);
	//$res = $this->DB()->Exec($sql);
	$obj = $this->SpawnItem($iClass);
	assert('is_object($obj->Table);');
	$obj->Query($sql);

	$this->sqlExec = $sql;
	if (!is_null($obj)) {
//	    $obj->Table = $this;	// 2011-01-20 this should be redundant now
	    $obj->sqlMake = $sql;
	}
	return $obj;
    }
    /*----
      RETURNS: SQL for creating a new record for the given data
      HISTORY:
	2010-11-20 Created.
    */
    public function SQL_forInsert(array $iData) {
	$sqlNames = '';
	$sqlVals = '';
	foreach($iData as $key=>$val) {
	    if ($sqlNames != '') {
		$sqlNames .= ',';
		$sqlVals .= ',';
	    }
	    $sqlNames .= $key;
	    $sqlVals .= $val;
	}
	return 'INSERT INTO `'.$this->Name().'` ('.$sqlNames.') VALUES('.$sqlVals.');';
    }
    /*----
      HISTORY:
	2010-11-16 Added "array" requirement for iData
	2010-11-20 Calculation now takes place in SQL_forInsert()
    */
    public function Insert(array $iData) {
	global $sql;

	$sql = $this->SQL_forInsert($iData);
	$this->sqlExec = $sql;
	return $this->objDB->Exec($sql);
    }
    /*----
      HISTORY:
	2011-02-02 created for deleting topic-title pairs
    */
    public function Delete($iFilt) {
	$sql = 'DELETE FROM `'.$this->Name().'` WHERE '.$iFilt;
	$this->sqlExec = $sql;
	return $this->Engine()->Exec($sql);
    }
}
/*=============
  NAME: clsTable_keyed_abstract
  PURPOSE: adds abstract methods for dealing with keys
*/
abstract class clsTable_keyed_abstract extends clsTable_abstract {

    //abstract public function GetItem_byArray();
    abstract protected function MakeFilt(array $iData);
    abstract protected function MakeFilt_direct(array $iData);
    /*----
      PURPOSE: method for setting a key which uniquely refers to this table
	Useful for logging, menus, and other text-driven contexts.
    */
    public function ActionKey($iName=NULL) {
	if (!is_null($iName)) {
	    $this->ActionKey = $iName;
	}
	return $this->ActionKey;
    }
    /*----
      INPUT:
	$iData: array of data necessary to create a new record
	  or update an existing one, if found
	$iFilt: SQL defining what constitutes an existing record
	  If NULL, MakeFilt() will be called to build this from $iData.
      ASSUMES: iData has already been massaged for direct SQL use
      HISTORY:
	2011-02-22 created
	2011-03-23 added madeNew and dataOld fields
	  Nothing is actually using these yet, but that will probably change.
	  For example, we might want to log when an existing record gets modified.
	2011-03-31 why is this protected? Leaving it that way for now, but consider making it public.
	2012-02-21 Needs to be public; making it so.
	  Also changed $this->Values() (which couldn't possibly have worked) to $rs->Values()
    */
    public $madeNew,$dataOld;	// additional status output
    public function Make(array $iData,$iFilt=NULL) {
	if (is_null($iFilt)) {
	    $sqlFilt = $this->MakeFilt_direct($iData);
//die( 'SQL='.$sqlFilt );
	} else {
	    $sqlFilt = $iFilt;
	}
	$rs = $this->GetData($sqlFilt);
	if ($rs->HasRows()) {
	    assert('$rs->RowCount() == 1');
	    $rs->NextRow();

	    $this->madeNew = FALSE;
	    $this->dataOld = $rs->Values();

	    $rs->Update($iData);
	    $id = $rs->KeyString();
	} else {
	    $this->Insert($iData);
	    $id = $this->Engine()->NewID();
	    $this->madeNew = TRUE;
	}
	return $id;
    }
}
/*=============
  NAME: clsTable_key_single
  PURPOSE: table with a single key field
*/
class clsTable_key_single extends clsTable_keyed_abstract {
    protected $vKeyName;

    public function __construct(clsDatabase_abstract $iDB) {
	parent::__construct($iDB);
	$this->ClassSng('clsDataSet');
    }

    public function KeyName($iName=NULL) {
	if (!is_null($iName)) {
	    $this->vKeyName = $iName;
	}
	return $this->vKeyName;
    }
    /*----
      HISTORY:
	2010-11-01 iID=NULL now means create new/blank object, i.e. SpawnItem()
	2011-11-15 tweak for clarity
	2012-03-04 getting rid of optional $iClass param
    */
    public function GetItem($iID=NULL) {
	if (is_null($iID)) {
	    $objItem = $this->SpawnItem();
	    $objItem->KeyValue(NULL);
	} else {
	    $sqlFilt = $this->vKeyName.'='.SQLValue($iID);
	    $objItem = $this->GetData($sqlFilt);
	    $objItem->NextRow();
	}
	return $objItem;
    }
    /*----
      INPUT:
	iFields: array of source fields and their output names - specified as iFields[output]=input, because you can
	  have a single input used for multiple outputs, but not vice-versa. Yes, this is confusing but that's how
	  arrays are indexed.
      HISTORY:
	2010-10-16 Created for VbzAdminCartLog::AdminPage()
    */
    public function DataSetGroup(array $iFields, $iGroupBy, $iSort=NULL) {
	global $sql;	// for debugging

	foreach ($iFields AS $fDest => $fSrce) {
	    if(isset($sqlFlds)) {
		$sqlFlds .= ', ';
	    } else {
		$sqlFlds = '';
	    }
	    $sqlFlds .= $fSrce.' AS '.$fDest;
	}
	$sql = 'SELECT '.$sqlFlds.' FROM '.$this->NameSQL().' GROUP BY '.$iGroupBy;
	if (!is_null($iSort)) {
	    $sql .= ' ORDER BY '.$iSort;
	}
	$obj = $this->objDB->DataSet($sql);
	return $obj;
    }
    /*----
      HISTORY:
	2010-11-20 Created
    */
    public function SQL_forUpdate(array $iSet,$iWhere) {
	$sqlSet = '';
	foreach($iSet as $key=>$val) {
	    if ($sqlSet != '') {
		$sqlSet .= ',';
	    }
	    $sqlSet .= ' `'.$key.'`='.$val;
	}

	return 'UPDATE `'.$this->Name().'` SET'.$sqlSet.' WHERE '.$iWhere;
    }
    /*----
      HISTORY:
	2010-10-05 Commented out code which updated the row[] array from iSet's values.
	  * It doesn't work if the input is a string instead of an array.
	  * Also, it seems like a better idea to actually re-read the data if
	    we really need to update the object.
	2010-11-16 Added "array" requirement for iSet; removed code for handling
	  iSet as a string. If we want to support single-field updates, make a 
	  new method: UpdateField($iField,$iVal,$iWhere). This makes it easier
	  to support automatic updates of certain fields in descendent classes
	  (e.g. updating a WhenEdited timestamp).
	2010-11-20 Calculation now takes place in SQL_forUpdate()
    */
    public function Update(array $iSet,$iWhere) {
	global $sql;

	$sql = $this->SQL_forUpdate($iSet,$iWhere);
	$this->sqlExec = $sql;
	$ok = $this->objDB->Exec($sql);

	return $ok;
    }
    public function LastID() {
	$strKey = $this->vKeyName;
	$sql = 'SELECT '.$strKey.' FROM `'.$this->Name().'` ORDER BY '.$strKey.' DESC LIMIT 1;';

	$objRows = $this->objDB->DataSet($sql);

	if ($objRows->HasRows()) {
	    $objRows->NextRow();
	    $intID = $objRows->$strKey;
	    return $intID;
	} else {
	    return 0;
	}
    }
    /*----
      HISTORY:
	2011-02-22 created
	2012-02-21 this couldn't possibly have worked before, since it used $this->KeyValue(),
	  which is a Record function, not a Table function.
	  Replaced that with $iData[$strName].
      ASSUMES: $iData[$this->KeyName()] is set, but may need to be SQL-formatted
      IMPLEMENTATION:
	KeyName must equal KeyValue
    */
    protected function MakeFilt(array $iData) {
	$strName = $this->KeyName();
	$val = $iData[$strName];
	if ($iSQLify) {
	    $val = SQLValue($val);
	}
	return $strName.'='.$val;
    }
    /*----
      PURPOSE: same as MakeFilt(), but does no escaping of SQL data
      HISTORY:
	2012-03-04 created to replace $iSQLify option
      ASSUMES: $iData[$this->KeyName()] is set, and already SQL-formatted (i.e. quoted if necessary)
    */
    protected function MakeFilt_direct(array $iData) {
	$strName = $this->KeyName();
	$val = $iData[$strName];
	return $strName.'='.$val;
    }
}
// alias -- sort of a default table type
class clsTable extends clsTable_key_single {
}

// DEPRECATED -- use clsCache_Table helper class
class clsTableCache extends clsTable {
    private $arCache;

    public function GetItem($iID=NULL,$iClass=NULL) {
	if (!isset($this->arCache[$iID])) {
	    $objItem = $this->GetData($this->vKeyName.'='.SQLValue($iID),$iClass);
	    $objItem->NextRow();
	    $this->arCache[$iID] = $objItem->RowCopy();
	}
	return $this->arCache[$iID];
    }
}
/*====
  CLASS: cache for Tables
  ACTION: provides a cached GetItem()
  USAGE: clsTable descendants should NOT override GetItem() or GetData() to use this class,
    as the class needs those methods to load data into the cache.
  BOILERPLATE:
    protected $objCache;
    protected function Cache() {
	if (!isset($this->objCache)) {
	    $this->objCache = new clsCache_Table($this);
	}
	return $this->objCache;
    }
    public function GetItem_Cached($iID=NULL,$iClass=NULL) {
	return $this->Cache()->GetItem($iID,$iClass);
    }
    public function GetData_Cached($iWhere=NULL,$iClass=NULL,$iSort=NULL) {
	return $this->Cache()->GetItem($iWhere,$iClass,$iSort);
    }
*/
/*----
*/
class clsCache_Table {
    protected $objTbl;
    protected $arRows;	// arRows[id] = rows[]
    protected $arSets;	// caches entire datasets

    public function __construct(clsTable $iTable) {
	$this->objTbl = $iTable;
    }
    public function GetItem($iID=NULL,$iClass=NULL) {
	$objTbl = $this->objTbl;
	if (isset($this->arRows[$iID])) {
	    $objItem = $objTbl->SpawnItem($iClass);
	    $objItem->Row = $this->arCache[$iID];
	} else {
	    $objItem = $objTbl->GetItem($iID,$iClass);
	    $this->arCache[$iID] = $objItem->Row;
	}
	return $objItem;
    }
    /*----
      HISTORY:
	2011-02-11 Renamed GetData_Cached() to GetData()
	  This was probably a leftover from before multiple inheritance
	  Fixed some bugs. Renamed from GetData() to GetData_array()
	    because caching the resource blob didn't seem to work very well.
	  Now returns an array instead of an object.
      FUTURE: Possibly we should be reading all rows into memory, instead of just saving the Res.
	That way, Res could be protected again instead of public.
    */
    public function GetData_array($iWhere=NULL,$iClass=NULL,$iSort=NULL) {
	$objTbl = $this->objTbl;
	$strKeyFilt = "$iWhere\t$iSort";
	$isCached = FALSE;
	if (is_array($this->arSets)) {
	    if (array_key_exists($strKeyFilt,$this->arSets)) {
		$isCached = TRUE;
	    }
	}
	if ($isCached) {
	    //$objSet = $objTbl->SpawnItem($iClass);
	    //$objSet->Res = $this->arSets[$strKey];
	    //assert('is_resource($objSet->Res); /* KEY='.$strKey.'*/');

	    // 2011-02-11 this code has not been tested yet
//echo '<pre>'.print_r($this->arSets,TRUE).'</pre>';
	    foreach ($this->arSets[$strKeyFilt] as $key) {
		$arOut[$key] = $this->arRows[$key];
	    }
	} else {
	    $objSet = $objTbl->GetData($iWhere,$iClass,$iSort);
	    while ($objSet->NextRow()) {
		$strKeyRow = $objSet->KeyString();
		$arOut[$strKeyRow] = $objSet->Values();
		$this->arSets[$strKeyFilt][] = $strKeyRow;
	    }
	    if (is_array($this->arRows)) {
		$this->arRows = array_merge($this->arRows,$arOut);	// add to cached rows
	    } else {
		$this->arRows = $arOut;	// start row cache
	    }
	}
	return $arOut;
    }
}
/*=============
  NAME: clsRecs_abstract -- abstract recordset
    Does not deal with keys.
  NOTE: We have to maintain a local copy of Row because sometimes we don't have a Res object yet
    because we haven't done any queries yet.
  HISTORY:
    2011-11-21 changed Table() from protected to public because Scripting needed to access it
*/
abstract class clsRecs_abstract {
    public $objDB;	// deprecated; use Engine()
    public $sqlMake;	// optional: SQL used to create the dataset -- used for reloading
    public $sqlExec;	// last SQL executed on this dataset
    public $Table;	// public access deprecated; use Table()
    protected $objRes;	// result object returned by Engine
    public $Row;	// public access deprecated; use Values()/Value() (data from the active row)

//    public function __construct(clsDatabase $iDB=NULL, $iRes=NULL, array $iRow=NULL) {
    public function __construct(clsDatabase $iDB=NULL) {
	$this->objDB = $iDB;
	$this->objRes = NULL;
	$this->Row = NULL;
	$this->InitVars();
    }
    protected function InitVars() {
    }
    public function ResultHandler($iRes=NULL) {
	if (!is_null($iRes)) {
	    $this->objRes = $iRes;
	}
	return $this->objRes;
    }
    public function Table(clsTable_abstract $iTable=NULL) {
	if (!is_null($iTable)) {
	    $this->Table = $iTable;
	}
	return $this->Table;
    }
    /*----
      PURPOSE: for debugging -- quick/easy way to see what data we have
      HISTORY:
	2011-09-24 written for vbz order import routine rewrite
    */
    public function DumpHTML() {
	$out = '<b>Table</b>: '.$this->Table->Name();
	if ($this->hasRows()) {
	    $out .= '<ul>';
	    $this->StartRows();	// make sure to start at the beginning
	    while ($this->NextRow()) {
		$out .= "\n<li><ul>";
		foreach ($this->Row as $key => $val) {
		    $out .= "\n<li>[$key] = [$val]</li>";
		}
		$out .= "\n</ul></li>";
	    }
	    $out .= "\n</ul>";
	} else {
	    $out .= " -- NO DATA";
	}
	$out .= "\n<b>SQL</b>: ".$this->sqlMake;
	return $out;
    }
    public function Engine() {
	if (is_null($this->objDB)) {
	    assert('!is_null($this->Table()); /* SQL: '.$this->sqlMake.' */');
	    return $this->Table()->Engine();
	} else {
	    return $this->objDB;
	}
    }
    /*----
      RETURNS: associative array of fields/values for the current row
      HISTORY:
	2011-01-08 created
	2011-01-09 actually working; added option to write values
    */
    public function Values(array $iRow=NULL) {
	if (is_array($iRow)) {
	    $this->Row = $iRow;
	    return $iRow;
	} else {
	    return $this->Row;
	}
    }
    /*----
      FUNCTION: Value(name)
      RETURNS: Value of named field
      HISTORY:
	2010-11-19 Created to help with data-form processing.
	2010-11-26 Added value-setting, so we can set defaults for new records
	2011-02-09 replaced direct call to array_key_exists() with call to new function HasValue()
    */
    public function Value($iName,$iVal=NULL) {
	if (is_null($iVal)) {
	    if (!$this->HasValue($iName)) {
		if (is_object($this->Table())) {
		    $htTable = ' from table "'.$this->Table()->Name().'"';
		} else {
		    $htTable = ' from query';
		}
		$strMsg = 'Attempted to read nonexistent field "'.$iName.'"'.$htTable.' in class '.get_class($this);
		echo $strMsg.'<br>';
		echo 'Source SQL: '.$this->sqlMake.'<br>';
		echo 'Row contents:<pre>'.print_r($this->Values(),TRUE).'</pre>';
		throw new Exception($strMsg);
	    }
	} else {
	    $this->Row[$iName] = $iVal;
	}
	return $this->Row[$iName];
    }
    /*----
      PURPOSE: Like Value() but handles new records gracefully, and is read-only
	makes it easier for new-record forms not to throw exceptions
      RETURNS: Value of named field; if it isn't set, returns $iDefault instead of raising an exception
      HISTORY:
	2011-02-12 written
	2011-10-17 rewritten by accident
    */
    public function ValueNz($iName,$iDefault=NULL) {
	if (array_key_exists($iName,$this->Row)) {
	    return $this->Row[$iName];
	} else {
	    return $iDefault;
	}
    }
    /*----
      HISTORY:
	2011-02-09 created so we can test for field existence before trying to access
    */
    public function HasValue($iName) {
	if (is_array($this->Row)) {
	    return array_key_exists($iName,$this->Row);
	} else {
	    return FALSE;
	}
    }

    /*----
      FUNCTION: Clear();
      ACTION: Clears Row[] of any leftover data
    */
    public function Clear() {
	$this->Row = NULL;
    }
    /*----
      USAGE: caller should always check this and throw an exception if it fails.
    */
    private function QueryOkay() {
	if (is_object($this->objRes)) {
	    $ok = $this->objRes->is_okay();
	} else {
	    $ok = FALSE;
	}
	return $ok;
    }
    public function Query($iSQL) {
	$this->objRes = $this->Engine()->engine_db_query($iSQL);
	$this->sqlMake = $iSQL;
	if (!$this->QueryOkay()) {
	    throw new exception ('Query failed -- SQL='.$iSQL);
	}
    }
    /*----
      ACTION: Checks given values for any differences from current values
      RETURNS: TRUE if all values are same
    */
    public function SameAs(array $iValues) {
	$isSame = TRUE;
	foreach($iValues as $name => $value) {
	    $oldVal = $this->Row[$name];
	    if ($oldVal != $value) {
		$isSame = FALSE;
	    }

	}
	return $isSame;
    }
    /*-----
      RETURNS: # of rows iff result has rows, otherwise FALSE
    */
    public function hasRows() {
	$rows = $this->objRes->get_count();
	if ($rows === FALSE) {
	    return FALSE;
	} elseif ($rows == 0) {
	    return FALSE;
	} else {
	    return $rows;
	}
    }
    public function hasRow() {
	return $this->objRes->is_filled();
    }
    public function RowCount() {
	return $this->objRes->get_count();
    }
    public function StartRows() {
	if ($this->hasRows()) {
	    $this->objRes->do_rewind();
	    return TRUE;
	} else {
	    return FALSE;
	}
    }
    public function FirstRow() {
	if ($this->StartRows()) {
	    return $this->NextRow();	// get the first row of data
	} else {
	    return FALSE;
	}
    }
    /*=====
      ACTION: Fetch the next row of data into $this->Row.
	If no data has been fetched yet, then fetch the first row.
      RETURN: TRUE if row was fetched; FALSE if there were no more rows
	or the row could not be fetched.
    */
    public function NextRow() {
	if (!is_object($this->objRes)) {
	    throw new exception('Result object not loaded');
	}
	$this->Row = $this->objRes->get_next();
	return $this->hasRow();
    }
}
/*=============
  NAME: clsRecs_keyed_abstract -- abstract recordset for keyed data
    Adds abstract and concrete methods for dealing with keys.
*/
abstract class clsRecs_keyed_abstract extends clsRecs_abstract {
    // ABSTRACT methods
    abstract public function SelfFilter();
    abstract public function KeyString();
    abstract public function SQL_forUpdate(array $iSet);
    abstract public function SQL_forMake(array $iSet);

    /*-----
      ACTION: Saves the data in $iSet to the current record (or records filtered by $iWhere)
      HISTORY:
	2010-11-20 Calculation now takes place in SQL_forUpdate()
	2010-11-28 SQL saved to table as well, for when we might be doing Insert() or Update()
	  and need a single place to look up the SQL for whatever happened.
    */
    public function Update(array $iSet,$iWhere=NULL) {
	//$ok = $this->Table->Update($iSet,$sqlWhere);
	$sql = $this->SQL_forUpdate($iSet,$iWhere);
	//$this->sqlExec = $this->Table->sqlExec;
	$this->sqlExec = $sql;
	$this->Table->sql = $sql;
	$ok = $this->objDB->Exec($sql);
	return $ok;
    }
    public function Make(array $iarSet) {
	return $this->Indexer()->Make($iarSet);
    }
    /*-----
      ACTION: Reloads only the current row unless $iFilt is set
      HISTORY:
	2012-03-04 moved from clsRecs_key_single to clsRecs_keyed_abstract
	2012-03-05 removed code referencing iFilt -- it is no longer in the arg list
    */
    public function Reload() {
	if (is_string($this->sqlMake)) {
	    $sql = $this->sqlMake;
	} else {
	    $sql = 'SELECT * FROM `'.$this->Table->Name().'` WHERE ';
	    $sql .= $this->SelfFilter();
	}
	$this->Query($sql);
	$this->NextRow();	// load the data
    }
}
/*=============
  NAME: clsDataSet_bare
  DEPRECATED - USE clsRecs_key_single INSTEAD
  PURPOSE: base class for datasets, with single key
    Does not add field overloading. Field overloading seems to have been a bad idea anyway;
      Use Value() instead.
*/
class clsRecs_key_single extends clsRecs_keyed_abstract {
    /*----
      HISTORY:
	2010-11-01 iID=NULL now means object does not have data from an existing record
    */
    public function IsNew() {
	return is_null($this->KeyValue());
    }
    /*-----
      FUNCTION: KeyValue()
    */
    public function KeyValue($iVal=NULL) {
	if (!is_object($this->Table)) {
	    throw new Exception('Recordset needs a Table object to retrieve key value.');
	}
	if (!is_array($this->Row) && !is_null($this->Row)) {
	    throw new Exception('Row needs to be an array or NULL, but type is '.gettype($this->Row).'. This may be an internal error.');
	}
	$strKeyName = $this->Table->KeyName();
	assert('!empty($strKeyName); /* TABLE: '.$this->Table->Name().' */');
	assert('is_string($strKeyName); /* TABLE: '.$this->Table->Name().' */');
	if (is_null($iVal)) {
	    if (!isset($this->Row[$strKeyName])) {
		$this->Row[$strKeyName] = NULL;
	    }
	} else {
	    $this->Row[$strKeyName] = $iVal;
	}
	return $this->Row[$strKeyName];
    }
    public function KeyString() {
	return (string)$this->KeyValue();
    }
    /*----
      FUNCTION: Load_fromKey()
      ACTION: Load a row of data whose key matches the given value
      HISTORY:
	2010-11-19 Created for form processing.
    */
    public function Load_fromKey($iKeyValue) {
	$this->sqlMake = NULL;
	$this->KeyValue($iKeyValue);
	$this->Reload();
    }
    /*-----
      FUNCTION: SelfFilter()
      RETURNS: SQL for WHERE clause which will select only the current row, based on KeyValue()
      USED BY: Update(), Reload()
      HISTORY:
	2012-02-21 KeyValue() is now run through SQLValue() so it can handle non-numeric keys
    */
    public function SelfFilter() {
	if (!is_object($this->Table)) {
	    throw new exception('Table not set in class '.get_class($this));
	}
	$strKeyName = $this->Table->KeyName();
	//$sqlWhere = $strKeyName.'='.$this->$strKeyName;
	//$sqlWhere = $strKeyName.'='.$this->Row[$strKeyName];
	$sqlWhere = '`'.$strKeyName.'`='.SQLValue($this->KeyValue());
	return $sqlWhere;
    }
    /*-----
      ACTION: Reloads only the current row unless $iFilt is set
      TO DO: iFilt should probably be removed, now that we save
	the creation SQL in $this->sql.
    */
/*
    public function Reload($iFilt=NULL) {
	if (is_string($this->sqlMake)) {
	    $sql = $this->sqlMake;
	} else {
	    $sql = 'SELECT * FROM `'.$this->Table->Name().'` WHERE ';
	    if (is_null($iFilt)) {
		$sql .= $this->SelfFilter();
	    } else {
		$sql .= $iFilt;
	    }
	}
	$this->Query($sql);
	$this->NextRow();
    }
*/
    /*----
      HISTORY:
	2010-11-20 Created
    */
    public function SQL_forUpdate(array $iSet,$iWhere=NULL) {
	$doIns = FALSE;
	if (is_null($iWhere)) {
// default: modify the current record
//	build SQL filter for just the current record
	    $sqlWhere = $this->SelfFilter();
	} else {
	    $sqlWhere = $iWhere;
	}
	return $this->Table->SQL_forUpdate($iSet,$sqlWhere);
    }
    /*----
      HISTORY:
	2010-11-23 Created
    */
    public function SQL_forMake(array $iarSet) {
	$strKeyName = $this->Table->KeyName();
	if ($this->IsNew()) {
	    $sql = $this->Table->SQL_forInsert($iarSet);
	} else {
	    $sql = $this->SQL_forUpdate($iarSet);
	}
	return $sql;
    }
    /*-----
      ACTION: Saves to the current record; creates a new record if ID is 0 or NULL
      HISTORY:
	2010-11-03
	  Now uses this->IsNew() to determine whether to use Insert() or Update()
	  Loads new ID into KeyValue
    */
    public function Make(array $iarSet) {
	if ($this->IsNew()) {
	    $ok = $this->Table->Insert($iarSet);
	    $this->KeyValue($this->objDB->NewID());
	    return $ok;
	} else {
	    return $this->Update($iarSet);
	}
    }
    // DEPRECATED -- should be a function of Table type
    public function HasField($iName) {
	return isset($this->Row[$iName]);
    }
    // DEPRECATED - use Values()
    public function RowCopy() {
	$strClass = get_class($this);
	if (is_array($this->Row)) {
	    $objNew = new $strClass;
// copy critical object fields so methods will work:
	    $objNew->objDB = $this->objDB;
	    $objNew->Table = $this->Table;
// copy data fields:
	    foreach ($this->Row AS $key=>$val) {
		$objNew->Row[$key] = $val;
	    }
	    return $objNew;
	} else {
	    //echo 'RowCopy(): No data to copy in class '.$strClass;
	    return NULL;
	}
    }
}
// alias -- a sort of default dataset type
class clsDataSet_bare extends clsRecs_key_single {
}
/*
  PURPOSE: clsDataSet with overloaded field access methods
  DEPRECATED -- This has turned out to be more problematic than useful.
    Retained only for compatibility with existing code; hope to eliminate eventually.
*/
class clsDataSet extends clsRecs_key_single {
  // -- accessing individual fields
    public function __set($iName, $iValue) {
	$this->Row[$iName] = $iValue;
    }
    public function __get($iName) {
	if (isset($this->Row[$iName])) {
	    return $this->Row[$iName];
	} else {
	    return NULL;
	}
    }
}
// HELPER CLASSES

/*====
  CLASS: Table Indexer
*/
class clsIndexer_Table {
    private $objTbl;	// clsTable object

    public function __construct(clsTable_abstract $iObj) {
	$this->objTbl = $iObj;
    }
    /*----
      RETURNS: newly-created clsIndexer_Recs-descended object
	Override this method to change how indexing works
    */
    protected function NewRecsIdxer(clsRecs_indexed $iRecs) {
	return new clsIndexer_Recs($iRecs,$this);
    }
    public function TableObj() {
	return $this->objTbl;
    }

    public function InitRecs(clsRecs_indexed $iRecs) {
	$objIdx = $this->NewRecsIdxer($iRecs);
	$iRecs->Indexer($objIdx);
	return $objIdx;
    }
}
/*====
  HISTORY:
    2011-01-19 Started; not ready yet -- just saving bits of code I know I will need
*/
class clsIndexer_Table_single_key extends clsIndexer_Table {
    protected function NewRecsIdxer(clsRecs_indexed $iRecs) {
	return new clsIndexer_Recs_single_key($iRecs,$this);
    }
    public function KeyName($iName=NULL) {
	if (!is_null($iName)) {
	    $this->vKeyName = $iName;
	}
	return $this->vKeyName;
    }
    public function GetItem($iID=NULL,$iClass=NULL) {
	if (is_null($iID)) {
	    $objItem = $this->SpawnItem($iClass);
	    $objItem->KeyValue(NULL);
	} else {
	    assert('!is_array($iID); /* TABLE='.$this->TableObj()->Name().' */');
	    $objItem = $this->TableObj()->GetData($this->vKeyName.'='.SQLValue($iID),$iClass);
	    $objItem->NextRow();
	}
	return $objItem;
    }
}

class clsIndexer_Table_multi_key extends clsIndexer_Table {
    private $arKeys;

    /*----
      RETURNS: newly-created clsIndexer_Recs-descended object
	Override this method to change how indexing works
    */
    protected function NewRecsIdxer(clsRecs_indexed $iRecs) {
	return new clsIndexer_Recs_multi_key($iRecs,$this);
    }
    public function KeyNames(array $iNames=NULL) {
	if (!is_null($iNames)) {
	    $this->arKeys = $iNames;
	}
	return $this->arKeys;
    }
    /*----
      HISTORY:
	2011-01-08 written
    */
    /*----
      INPUT:
	$iVals can be an array of index values, a prefix-marked string, or NULL
	  NULL means "spawn a blank item".
      HISTORY:
	2012-03-04 now calling MakeFilt() instead of SQL_for_filter()
    */
    public function GetItem($iVals=NULL) {
	if (is_null($iVals)) {
	    $objItem = $this->TableObj()->SpawnItem();
	} else {
	    if (is_array($iVals)) {
		$arVals = $iVals;
	    } else {
		$x = new xtString($iVals);	// KLUGE to get strings.php to load >.<
		$arVals = Xplode($iVals);
	    }
	    $arKeys = $this->KeyNames();
	    $arVals = array_reverse($arVals);	// pop takes the last item; we want to start with the first
	    foreach ($arKeys as $key) {
		$arData[$key] = array_pop($arVals);	// get raw values to match
	    }
	    $sqlFilt = $this->MakeFilt($arData,TRUE);
	    $objItem = $this->TableObj()->GetData($sqlFilt);
	    $objItem->NextRow();
	}
	return $objItem;
    }
    /*----
      RETURNS: SQL to filter for the current record by key value(s)
	'(name=value) AND (name=value) AND...'
      INPUT:
	$iData: array of keynames and values
	  iData[name] = value
	$iSQLify: TRUE = massage with SQLValue() before using; FALSE = ready to use in SQL
      USED BY: GetItem()
      HISTORY:
	2011-01-08 written
	2011-01-19 replaced with boilerplate call to indexer in clsIndexer_Table
	2012-03-04 I... don't know what I was talking about on 2011-01-19. Reinstating this,
	  but renaming it from SQL_for_filter() to MakeFilt()
    */
    public function MakeFilt(array $iData,$iSQLify) {
	$arKeys = $this->KeyNames();
	$sql = NULL;
	foreach ($arKeys as $name) {
	    if (!array_key_exists($name,$iData)) {
		echo '<br>Key ['.$name.'] not found in passed data:<pre>'.print_r($iData,TRUE).'</pre>';
		throw new exception('Key ['.$name.'] not found in passed data.');
	    }
	    $val = $iData[$name];
	    if (!is_null($sql)) {
		$sql .= ' AND ';
	    }
	    if ($iSQLify) {
		$val = SQLValue($val);
	    }
	    $sql .= '(`'.$name.'`='.$val.')';
	}
	return $sql;
    }

    /*----
      RETURNS: SQL for creating a new record for the given data
      HISTORY:
	2010-11-20 Created.
	2011-01-08 adapted from clsTable::Insert()
    */
    public function SQL_forInsert(array $iData) {
	$sqlNames = '';
	$sqlVals = '';
	foreach($iData as $key=>$val) {
	    if ($sqlNames != '') {
		$sqlNames .= ',';
		$sqlVals .= ',';
	    }
	    $sqlNames .= '`'.$key.'`';
	    $sqlVals .= $val;
	}
	return 'INSERT INTO `'.$this->Name().'` ('.$sqlNames.') VALUES('.$sqlVals.');';
    }

}
/*====
  CLASS: clsIndexer_Recs -- record set indexer
  PURPOSE: Handles record sets for tables with multiple keys
  HISTORY:
    2010-?? written for clsCacheFlows/clsCacheFlow in cache.php
    2011-01-08 renamed, revised and clarified
*/
abstract class clsIndexer_Recs {
    private $objData;
    private $objTbl;	// table indexer

    // ABSTRACT functions
    abstract public function IndexIsSet();
    abstract public function KeyString();
    abstract public function SQL_forWhere();

    /*----
      INPUT:
	iObj = DataSet
	iKeys = array of field names
    */
    public function __construct(clsRecs_keyed_abstract $iData,clsIndexer_Table $iTable) {
	$this->objData = $iData;
	$this->objTbl = $iTable;
    }
    public function DataObj() {
	return $this->objData;
    }
    public function TblIdxObj() {
	assert('is_a($this->objTbl,"clsIndexer_Table"); /* CLASS='.get_class($this->objTbl).' */');
	return $this->objTbl;
    }
    public function Engine() {
	return $this->DataObj()->Engine();
    }
    public function TableObj() {
	return $this->TblIdxObj()->TableObj();
    }
    public function TableName() {
	return $this->TableObj()->Name();
    }
/*
    public function Keys() {
	$arKeys = $this->objTbl->Keys();
	reset($arKeys);
	return $arKeys;
    }
*/
    /*-----
      FUNCTION: KeyValue()
      IN/OUT: array of key values
	array[key name] = value
      USED BY: clsCacheFlow::KeyValue()
    */
/*
    public function KeyValue(array $iVals=NULL) {
	$arKeys = $this->KeyNames();
	$arRow = $this->DataObj()->Row;
	if (is_array($iVals)) {
	    foreach ($iVals as $val) {
		list($key) = each($arKeys);
		$arRow[$key] = $val;
	    }
	    $this->DataObj()->Row = $arRow;
	}
	foreach ($arKeys as $key) {
	    $arOut[$key] = $arRow[$key];
	}
	return $arOut;
    }
*/
    /*----
      FUNCTION: KeyString()
      IN/OUT: prefix-delimited string of all key values
      QUERY: What uses this?
    */
/*
    public function KeyString($iVals=NULL) {
	if (is_string($iVals)) {
	    $xts = new xtString($iVals);
	    $arVals = $xts->Xplode();
	    $arKeys = $this->Keys();
	    foreach ($arVals as $val) {
		list($key) = each($arKeys);
		$this->Row[$key] = $val;
	    }
	}
	$out = '';
	foreach ($this->arKeys as $key) {
	    $val = $this->Row[$key];
	    $out .= '.'.$val;
	}
	return $out;
    }
*/
/*
    public function KeyValue(array $iVals=NULL) {
	$arKeys = $this->KeyNames();
	$arRow = $this->DataObj()->Row;
	if (is_array($iVals)) {
	    foreach ($iVals as $val) {
		list($key) = each($arKeys);
		$arRow[$key] = $val;
	    }
	    $this->DataObj()->Row = $arRow;
	}
	foreach ($arKeys as $key) {
	    $arOut[$key] = $arRow[$key];
	}
	return $arOut;
    }
*/
/* There's no need for this here; it doesn't require indexing
    public function SQL_forInsert() {
	return $this->TblIdxObj()->SQL_forInsert($this->KeyValues());
    }
*/
    /*----
      INPUT:
	iSet: array specifying fields to update and the values to update them to
	  iSet[field name] = value
      HISTORY:
	2010-11-20 Created
	2011-01-09 Adapted from clsDataSet_bare
    */
    public function SQL_forUpdate(array $iSet) {
	$sqlSet = '';
	foreach($iSet as $key=>$val) {
	    if ($sqlSet != '') {
		$sqlSet .= ',';
	    }
	    $sqlSet .= ' `'.$key.'`='.$val;
	}
	$sqlWhere = $this->SQL_forWhere();

	return 'UPDATE `'.$this->TableName().'` SET'.$sqlSet.' WHERE '.$sqlWhere;
    }
    /*----
      HISTORY:
	2010-11-16 Added "array" requirement for iData
	2010-11-20 Calculation now takes place in SQL_forInsert()
	2011-01-08 adapted from clsTable::Insert()
    */
/* There's no need for this here; it doesn't require indexing
    public function Insert(array $iData) {
	global $sql;

	$sql = $this->SQL_forInsert($iData);
	$this->sql = $sql;
	return $this->Engine()->Exec($sql);
    }
*/
}
class clsIndexer_Recs_single_key extends clsIndexer_Recs {
    private $vKeyName;

    public function KeyName() {
	return $this->TblIdxObj()->KeyName();
    }
    public function KeyValue() {
	return $this->DataObj()->Value($this->KeyName());
    }
    public function KeyString() {
	return (string)$this->KeyValue();
    }
    public function IndexIsSet() {
	return !is_null($this->KeyValue());
    }

    public function SQL_forWhere() {
	$sql = $this->KeyName().'='.SQLValue($this->KeyValue());
	return $sql;
    }
}
class clsIndexer_Recs_multi_key extends clsIndexer_Recs {
    /*----
      RETURNS: Array of values which constitute this row's key
	array[key name] = key value
    */
    public function KeyArray() {
	$arKeys = $this->TblIdxObj()->KeyNames();
	$arRow = $this->DataObj()->Row;
	foreach ($arKeys as $key) {
	    if (array_key_exists($key,$arRow)) {
		$arOut[$key] = $arRow[$key];
	    } else {
		echo "\nTrying to access nonexistent key [$key]. Available keys:";
		echo '<pre>'.print_r($arRow,TRUE).'</pre>';
		throw new exception('Nonexistent key requested.');
	    }
	}
	return $arOut;
    }
    /*----
      NOTE: The definition of "new" is a little more ambiguous with multikey tables;
	for now, I'm saying that all keys must be NULL, because NULL keys are sometimes
	valid in multikey contexts.
    */
    public function IsNew() {
	$arData = $this->KeyArray();
	foreach ($arData as $key => $val) {
	    if (!is_null($val)) {
		return FALSE;
	    }
	}
	return TRUE;
    }
    /*----
      ASSUMES: keys will always be returned in the same order
	If this changes, add field names.
      POTENTIAL BUG: Non-numeric keys might contain the separator character
	that we are currently using ('.'). Some characters may not be appropriate
	for some contexts. The caller should be able to specify what separator it wants.
    */
    public function KeyString() {
	$arKeys = $this->KeyArray();
	$out = NULL;
	foreach ($arKeys as $name=>$val) {
	    $out .= '.'.$val;
	}
	return $out;
    }
   /*----
      RETURNS: TRUE if any index fields are NULL
      ASSUMES: An index may not contain any NULL fields. Perhaps this is untrue, and it should
	only return TRUE if *all* index fields are NULL.
    */
    public function IndexIsSet() {
	$arKeys = $this->KeyArray();
	$isset = TRUE;
	foreach ($arKeys as $key=>$val) {
	    if (is_null($val)) { $isset = FALSE; }
	}
	return $isset;
    }
    /*----
      RETURNS: SQL to filter for the current record by key value(s)
      HISTORY:
	2011-01-08 written for Insert()
	2011-01-19 moved from clsIndexer_Recs to clsIndexer_Recs_multi_key
	2012-03-04 This couldn't have been working; it was calling SQL_for_filter() on a list of keys,
	  not keys-and-values. Fixed.
    */
    public function SQL_forWhere() {
	$arVals = $this->TblIdxObj()->KeyNames();
	//return SQL_for_filter($arVals);
	$sql = $this->TblIdxObj()->MakeFilt($this->DataObj()->Values(),TRUE);
	return $sql;
    }
    /*----
      NOTE: This is slightly different from the single-keyed Make() in that it assumes there are no autonumber keys.
	All keys must be specified in the initial data.
    */
    public function Make(array $iarSet) {
	if ($this->IsNew()) {
	    $ok = $this->Table->Insert($iarSet);
	    //$this->KeyValue($this->objDB->NewID());
	    $this->DataObj()->Values($iarSet);	// do we need to preserve any existing values? for now, assuming not.
	    return $ok;
	} else {
	    return $this->DataObj()->Update($iarSet);
	}
    }
}
/*=============
  NAME: clsTable_indexed
  PURPOSE: handles indexes via a helper object
*/
class clsTable_indexed extends clsTable_keyed_abstract {
    protected $objIdx;

    /*----
      NOTE: In practice, how would you ever have the Indexer object created before the Table object,
	since the Indexer object requires a Table object in its constructor? Possibly descendent classes
	can create the Indexer in their constructors and then pass it back to the parent constructor,
	which lets you have a default Indexer that you can override if you need, but how useful is this?
    */
    public function __construct(clsDatabase $iDB, clsIndexer_Table $iIndexer=NULL) {
	parent::__construct($iDB);
	$this->Indexer($iIndexer);
    }
    // BOILERPLATE BEGINS
    protected function Indexer(clsIndexer_Table $iObj=NULL) {
	if (!is_null($iObj)) {
	    $this->objIdx = $iObj;
	}
	return $this->objIdx;
    }
    /*----
      INPUT:
	$iVals can be an array of index values, a prefix-marked string, or NULL
	  NULL means "spawn a blank item".
    */
    public function GetItem($iVals=NULL) {
	return $this->Indexer()->GetItem($iVals);
    }
    protected function MakeFilt(array $iData) {
	return $this->Indexer()->MakeFilt($iData,TRUE);
    }
    protected function MakeFilt_direct(array $iData) {
	return $this->Indexer()->MakeFilt($iData,FALSE);
    }
    // BOILERPLATE ENDS
    // OVERRIDES
    /*----
      ADDS: spawns an indexer and attaches it to the item
    */
    protected function ReleaseItem(clsRecs_abstract $iItem) {
	parent::ReleaseItem($iItem);
	$this->Indexer()->InitRecs($iItem);
    }
    /*----
      ADDS: spawns an indexer and attaches it to the item
    */
/*
    public function SpawnItem($iClass=NULL) {
	$obj = parent::SpawnItem($iClass);
	return $obj;
    }
*/
}
/*=============
  NAME: clsRecs_indexed
*/
class clsRecs_indexed extends clsRecs_keyed_abstract {
    protected $objIdx;

/* This is never used
    public function __construct(clsIndexer_Recs $iIndexer=NULL) {
	$this->Indexer($iIndexer);
    }
*/
    // BOILERPLATE BEGINS
    public function Indexer(clsIndexer_Recs $iObj=NULL) {
	if (!is_null($iObj)) {
	    $this->objIdx = $iObj;
	}
	assert('is_object($this->objIdx);');
	return $this->objIdx;
    }
    public function IsNew() {
	return !$this->Indexer()->IndexIsSet();
    }
    /*----
      USED BY: Administrative UI classes which need a string for referring to a particular record
    */
    public function KeyString() {
	return $this->Indexer()->KeyString();
    }
    public function SelfFilter() {
	return $this->Indexer()->SQL_forWhere();
    }
    public function SQL_forUpdate(array $iSet) {
	return $this->Indexer()->SQL_forUpdate($iSet);
    }
    // BOILERPLATE ENDS
    public function SQL_forMake(array $iarSet) { die('Not yet written.'); }
}
/*%%%%
  PURPOSE: for tracking whether a cached object has the expected data or not
  HISTORY:
    2011-03-30 written
*/
class clsObjectCache {
    private $vKey;
    private $vObj;

    public function __construct() {
	$this->vKey = NULL;
	$this->vObj = NULL;
    }
    public function IsCached($iKey) {
	if (is_object($this->vObj)) {
	    return ($this->vKey == $iKey);
	} else {
	    return FALSE;
	}
    }
    public function Object($iObj=NULL,$iKey=NULL) {
	if (!is_null($iObj)) {
	    $this->vKey = $iKey;
	    $this->vObj = $iObj;
	}
	return $this->vObj;
    }
    public function Clear() {
	$this->vObj = NULL;
    }
}
class clsSQLFilt {
    private $arFilt;
    private $strConj;

    public function __construct($iConj) {
	$this->strConj = $iConj;
    }
    /*-----
      ACTION: Add a condition
    */
    public function AddCond($iSQL) {
	$this->arFilt[] = $iSQL;
    }
    public function RenderFilter() {
	$out = '';
	foreach ($this->arFilt as $sql) {
	    if ($out != '') {
		$out .= ' '.$this->strConj.' ';
	    }
	    $out .= '('.$sql.')';
	}
	return $out;
    }
}
/* ========================
 *** UTILITY FUNCTIONS ***
*/
/*----
  PURPOSE: This gets around PHP's apparent lack of built-in object type-conversion.
  ACTION: Copies all public fields from iSrce to iDest
*/
function CopyObj(object $iSrce, object $iDest) {
    foreach($iSrce as $key => $val) {
	$iDest->$key = $val;
    }
}
if (!function_exists('Pluralize')) {
    function Pluralize($iQty,$iSingular='',$iPlural='s') {
	  if ($iQty == 1) {
		  return $iSingular;
	  } else {
		  return $iPlural;
	  }
  }
}

function SQLValue($iVal) {
    if (is_array($iVal)) {
	foreach ($iVal as $key => $val) {
	    $arOut[$key] = SQLValue($val);
	}
	return $arOut;
    } else {
	if (is_null($iVal)) {
	    return 'NULL';
	} else if (is_bool($iVal)) {
	    return $iVal?'TRUE':'FALSE';
	} else if (is_string($iVal)) {
	    $oVal = '"'.mysql_real_escape_string($iVal).'"';
	    return $oVal;
	} else {
    // numeric can be raw
    // all others, we don't know how to handle, so return raw as well
	    return $iVal;
	}
    }
}
function SQL_for_filter(array $iVals) {
    $sql = NULL;
    foreach ($iVals as $name => $val) {
	if (!is_null($sql)) {
	    $sql .= ' AND ';
	}
	$sql .= '('.$name.'='.SQLValue($val).')';
    }
throw new exception('How did we get here?');
    return $sql;
}
function NoYes($iBool,$iNo='no',$iYes='yes') {
    if ($iBool) {
	return $iYes;
    } else {
	return $iNo;
    }
}

function nz(&$iVal,$default=NULL) {
    return empty($iVal)?$default:$iVal;
}
/*-----
  FUNCTION: nzAdd -- NZ Add
  RETURNS: ioVal += iAmt, but assumes ioVal is zero if not set (prevents runtime error)
  NOTE: iAmt is a reference so that we can pass variables which might not be set.
    Need to document why this is better than being able to pass constants.
*/
function nzAdd(&$ioVal,&$iAmt=NULL) {
    $intAmt = empty($iAmt)?0:$iAmt;
    if (empty($ioVal)) {
	$ioVal = $intAmt;
    } else {
	$ioVal += $intAmt;
    }
    return $ioVal;
}
/*-----
  FUNCTION: nzApp -- NZ Append
  PURPOSE: Like nzAdd(), but appends strings instead of adding numbers
*/
function nzApp(&$ioVal,$iTxt=NULL) {
    if (empty($ioVal)) {
	$ioVal = $iTxt;
    } else {
	$ioVal .= $iTxt;
    }
    return $ioVal;
}
/*----
  HISTORY:
    2012-03-11 iKey can now be an array, for multidimensional iArr
*/
function nzArray(array $iArr=NULL,$iKey,$iDefault=NULL) {
    $out = $iDefault;
    if (is_array($iArr)) {
	if (is_array($iKey)) {
	    $out = $iArr;
	    foreach ($iKey as $key) {
		if (array_key_exists($key,$out)) {
		    $out = $out[$key];
		} else {
		    return $iDefault;
		}
	    }
	} else {
	    if (array_key_exists($iKey,$iArr)) {
		$out = $iArr[$iKey];
	    }
	}
    }
    return $out;
}
function nzArray_debug(array $iArr=NULL,$iKey,$iDefault=NULL) {
    $out = $iDefault;
    if (is_array($iArr)) {
	if (is_array($iKey)) {
	    $out = $iArr;
	    foreach ($iKey as $key) {
		if (array_key_exists($key,$out)) {
		    $out = $out[$key];
		} else {
		    return $iDefault;
		}
	    }
	} else {
	    if (array_key_exists($iKey,$iArr)) {
		$out = $iArr[$iKey];
	    }
	}
    }
//echo '<br>IARR:<pre>'.print_r($iArr,TRUE).'</pre> KEY=['.$iKey.'] RETURNING <pre>'.print_r($out,TRUE).'</pre>';
    return $out;
}
/*----
  PURPOSE: combines the two arrays without changing any keys
    If entries in arAdd have the same keys as arStart, the result
      depends on the value of iReplace
    If entries in arAdd have keys that don't exist in arStart,
      the result depends on the value of iAppend
    This probably means that there are some equivalent ways of doing
      things by reversing order and changing flags, but I haven't
      worked through it yet.
  INPUT:
    arStart: the starting array - key-value pairs
    arAdd: additional key-value pairs to add to arStart
    iReplace:
      TRUE = arAdd values replace same keys in arStart
  RETURNS: the combined array
  NOTE: You'd think one of the native array functions could do this...
    array_merge(): "Values in the input array with numeric keys will be
      renumbered with incrementing keys starting from zero in the result array."
      (This is a problem if the keys are significant, e.g. record ID numbers.)
  HISTORY:
    2011-12-22 written because I keep needing it for cache mapping functions
    2012-03-05 added iReplace, iAppend
*/
function ArrayJoin(array $arStart=NULL, array $arAdd=NULL, $iReplace, $iAppend) {
    if (is_null($arStart)) {
	$arOut = $arAdd;
    } elseif (is_null($arAdd)) {
	$arOut = $arStart;
    } else {
	$arOut = $arStart;
	foreach ($arAdd as $key => $val) {
	    if (array_key_exists($key,$arOut)) {
        	if ($iReplace) {
		    $arOut[$key] = $val;
    		}
	    } else {
    		if ($iAppend) {
		    $arOut[$key] = $val;
    		}
	    }
	}
    }
    return $arOut;
}
/*----
  RETURNS: an array consisting only of keys from $arKeys
    plus the associated values from $arData
*/
function ArrayFilter_byKeys(array $arData, array $arKeys) {
    foreach ($arKeys as $key) {
	if (array_key_exists($key,$arData)) {
	    $arOut[$key] = $arData[$key];
	} else {
	    echo 'KEY ['.$key,'] not found.';
	    echo ' Array contents:<pre>'.print_r($arData,TRUE).'</pre>';
	    throw new exception('Expected key not found.');
	}
    }
    return $arOut;
}
function ifEmpty(&$iVal,$iDefault) {
    if (empty($iVal)) {
	return $iDefault;
    } else {
	return $iVal;
    }
}
function FirstNonEmpty(array $iList) {
    foreach ($iList as $val) {
	if (!empty($val)) {
	    return $val;
	}
    }
}
/*----
  ACTION: Takes a two-dimensional array and returns it flipped diagonally,
    i.e. each element out[x][y] is element in[y][x].
  EXAMPLE:
    INPUT      OUTPUT
    +---+---+  +---+---+---+
    | A | 1 |  | A | B | C |
    +---+---+  +---+---+---+
    | B | 2 |  | 1 | 2 | 3 |
    +---+---+  +---+---+---+
    | C | 3 |
    +---+---+
*/ 
function ArrayPivot($iArray) {
    foreach ($iArray as $row => $col) {
	if (is_array($col)) {
	    foreach ($col as $key => $val) {
		$arOut[$key][$row] = $val;
	    }
	}
    }
    return $arOut;
}
/*----
  ACTION: convert an array to SQL for filtering
  INPUT: iarFilt = array of filter terms; key is ignored
*/
function Array_toFilter($iarFilt) {
    $out = NULL;
    if (is_array($iarFilt)) {
	foreach ($iarFilt as $key => $cond) {
	    if (!is_null($out)) {
		$out .= ' AND ';
	    }
	    $out .= '('.$cond.')';
	}
    }
    return $out;
}
/* ========================
 *** DEBUGGING FUNCTIONS ***
*/

// these could later be expanded to create a call-path for errors, etc.

function CallEnter($iObj,$iLine,$iName) {
  global $intCallDepth, $debug;
  if (KDO_DEBUG_STACK) {
    $strDescr =  ' line '.$iLine.' ('.get_class($iObj).')'.$iName;
    _debugLine('enter','&gt;',$strDescr);
    $intCallDepth++;
    _debugDump();
  }
}
function CallExit($iName) {
  global $intCallDepth, $debug;
  if (KDO_DEBUG_STACK) {
    $intCallDepth--;
    _debugLine('exit','&lt;',$iName);
    _debugDump();
  }
}
function CallStep($iDescr) {
  global $intCallDepth, $debug;
  if (KDO_DEBUG_STACK) {
    _debugLine('step',':',$iDescr);
    _debugDump();
  }
}
function LogError($iDescr) {
  global $intCallDepth, $debug;

  if (KDO_DEBUG_STACK) {
    _debugLine('error',':',$iDescr);
    _debugDump();
  }
}
function _debugLine($iType,$iSfx,$iText) {
    global $intCallDepth, $debug;

    if (KDO_DEBUG_HTML) {
      $debug .= '<span class="debug-'.$iType.'"><b>'.str_repeat('&mdash;',$intCallDepth).$iSfx.'</b> '.$iText.'</span><br>';
    } else {
      $debug .= str_repeat('*',$intCallDepth).'++ '.$iText."\n";
    }
}
function _debugDump() {
    global $debug;

    if (KDO_DEBUG_IMMED) {
	DoDebugStyle();
	echo $debug;
	$debug = '';
    }
}
function DumpArray($iArr) {
  global $intCallDepth, $debug;

  if (KDO_DEBUG) {
    while (list($key, $val) = each($iArr)) {
      if (KS_DEBUG_HTML) {
        $debug .= '<br><span class="debug-dump"><b>'.str_repeat('-- ',$intCallDepth+1).'</b>';
        $debug .= " $key => $val";
        $debug .= '</span>';
      } else {
        $debug .= "/ $key => $val /";
      }
      if (KDO_DEBUG_IMMED) {
        DoDebugStyle();
        echo $debug;
        $debug = '';
      }
    }
  }
}
function DumpValue($iName,$iVal) {
  global $intCallDepth, $debug;

  if (KDO_DEBUG) {
    if (KS_DEBUG_HTML) {
      $debug .= '<br><span class="debug-dump"><b>'.str_repeat('-- ',$intCallDepth+1);
      $debug .= " $iName</b>: [$iVal]";
      $debug .= '</span>';
    } else {
      $debug .= "/ $iName => $iVal /";
    }
    if (KDO_DEBUG_IMMED) {
      DoDebugStyle();
      echo $debug;
      $debug = '';
    }
  }
}
function DoDebugStyle() {
  static $isStyleDone = false;

  if (!$isStyleDone) {
    echo '<style type="text/css"><!--';
    if (KDO_DEBUG_DARK) {
      echo '.debug-enter { background: #666600; }';	// dark highlight
    } else {
      echo '.debug-enter { background: #ffff00; }';	// regular yellow highlight
    }
    echo '--></style>';
    $isStyleDone = true;
  }
}