Source for file Table.php
Documentation is available at Table.php
/* vim: set expandtab tabstop=4 shiftwidth=4: */
// +----------------------------------------------------------------------+
// | Copyright (c) 2002-2003 Brent Cook |
// +----------------------------------------------------------------------+
// | This library is free software; you can redistribute it and/or |
// | modify it under the terms of the GNU Lesser General Public |
// | License as published by the Free Software Foundation; either |
// | version 2.1 of the License, or (at your option) any later version. |
// | This library is distributed in the hope that it will be useful, |
// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
// | Lesser General Public License for more details. |
// | You should have received a copy of the GNU Lesser General Public |
// | License along with this library; if not, write to the Free Software |
// | Foundation, Inc., 59 Temple Place, Suite 330,Boston,MA 02111-1307 USA|
// +----------------------------------------------------------------------+
// $Id: Table.php,v 1.21 2003/01/30 04:16:10 busterb Exp $
require_once 'DBA/Functions.php';
if (!isset ($_dba_functions)) {
if (!isset ($_dba_operators)) {
$_dba_operators = array_flip(array ('=','<','>','!','(',')','&','|',
'*','/','+','-','%',','));
if (!isset ($_dba_keywords)) {
$_dba_keywords = array_flip(array ('and','or','null','false','true'));
* Reserved key used to store the schema record
define('DBA_SCHEMA_KEY', '__schema__');
* Reserved character used to separate fields in a row
define('DBA_FIELD_SEPARATOR', '|');
define('DBA_OPTION_SEPARATOR', ';');
define('DBA_DOMAIN_SEPARATOR', ',');
define('DBA_KEY_SEPARATOR', '.');
* type domain length primary_key auto_increment default default_type not_null
* This class provides a table storage object.
* It uses a DBA storage object as returned by DBA::create().
* This class is used extensively in DBA_Relational as part of
* a complete, relational database system.
* Enough functionality exists within this class to create and delete tables,
* insert, retrieve, remove and validate data based on a table schema, and
* perform basic database operations such as selects, projects and sorts.
* @author Brent Cook <busterb@mail.utexas.edu>
// {{{ instance variables
* Describes the types of fields in a table
var $_dateFormat = 'D M j G:i:s T Y';
* When no primary key is specified, this is used to generate a unique
* Field names to use as the primary key. An empty array indicates that
* the _rowid is to be used as the primary key
var $_primaryKey=array ();
* Name of the DBA driver to use
// {{{ DBA_Table($driver = 'file')
* @param string $driver dba driver to use for storage
// call the base constructor
// initialize the internal dba object
$this->_dba = & DBA ::create ($driver);
$this->_driver = $driver;
* PEAR emulated destructor calls close on PHP shutdown
// {{{ raiseError($message)
return PEAR ::raiseError ('DBA_Table: '. $message);
// {{{ open($tableName, $mode = 'r')
* @param string $tableName name of the table to open
* @param char $mode mode to open the table; one of r,w,c,n
* @return object PEAR_Error on failure
function open($tableName, $mode = 'r')
if (($mode != 'w') && ($mode != 'r')) {
return $this->raiseError(" table open mode '$mode' is invalid" );
$result = $this->_dba->open ($tableName, $mode);
if (PEAR ::isError ($result)) {
// fetch the field descriptor at the key, DBA_SCHEMA_KEY
// unpack the field schema
$schema = $this->_unpackSchema ($schema);
if (PEAR ::isError ($schema)) {
$this->_schema = $schema;
return $this->raiseError('Table is missing field descriptor.'.
$this->_tableName = $tableName;
* @return object PEAR_Error on failure
if ($this->_dba->isWritable ()) {
// pack up the field structure and store it back in the table
if (isset ($this->_schema)) {
$schema = $this->_packSchema ($this->_schema);
if (DBA ::isError ($schema)) {
return PEAR ::raiseError ("No schema, what's the point :P\n");
$this->_tableName = null;
return $this->_dba->close ();
// {{{ create($tableName, $schema)
* Creates a new table. Note, this is a static function, and operates
* independently of a table object. It no longer closes an open table.
* @param string $tableName name of the table to create
* @param array $schema field schema for the table
* @return object PEAR_Error on failure
function create($tableName, $schema, $driver, $format= 'php')
$v_schema = DBA_Table::_validateSchema ($schema);
if (PEAR ::isError ($v_schema)) {
$packedSchema = DBA_Table::_packSchema ($v_schema +
array ('_rowid'=>array ('type' => 'integer',
'auto_increment' => true ),
'_timestamp'=>array ('type' => 'timestamp',
'default_type' => 'function')));
if (PEAR ::isError ($packedSchema)) {
$dba = DBA ::create ($driver);
if (PEAR ::isError ($dba)) {
$r = $dba->open ($tableName, 'n');
// {{{ _validateSchema($schema)
* @returns the validated schema, PEAR_Error
function _validateSchema ($schema) {
foreach ($schema as $field=> $meta) {
if (($field == '_rowid') || ($field == '_timestamp')) {
return $this->raiseError(" Cannot use $field as a field name" );
if (isset ($meta['auto_increment'])) {
if ($meta['type'] == 'integer') {
if (!isset ($meta['default'])) {
$meta['default_type'] = 'integer';
return $this->raiseError('Cannot use autoincrement with a non-integer');
if (isset ($meta['default_type']) && ($meta['default_type'] == 'function') &&
($meta['default'] != 'time()')) {
return $this->raiseError($meta['default']. ' is not a valid function');
if (!isset ($meta['default_type'])) {
$meta['default_type'] = $meta['type'];
$meta['default'] = "\x00";
* Returns the stored schema for the table
* @return mixed an array of the form 'fieldname'=>array(fieldmeta) on
* success, PEAR_Error on failure
return $this->raiseError('table not open, no schema available');
// {{{ tableExists($tableName)
* Check whether table exists
* @param string $tableName
* @return boolean true if the table exists, false otherwise
return DBA ::db_exists ($tableName, $this->_driver);
* Check if a row with a primary key exists
* @return boolean true if the row exists
return $this->dba->exists ($key);
* Returns the current open status for the database
* @returns true if the table exists, false otherwise
* @return boolean true if the table is open, false if it is closed
return $this->_dba->isOpen ();
* Returns the current read status for the database
* @return boolean true if the table is readable, false otherwise
return $this->_dba->isReadable ();
* Returns the current write status for the database
* @return boolean true if the table is writable, false otherwise
return $this->_dba->isWritable ();
// {{{ fieldExists($fieldName)
* Returns whether a field exists in the current table's schema
* @return boolean true if the field exists, false otherwise
return($this->isOpen() && isset ($this->_schema[$fieldName]));
* Aquire an exclusive lock on the table
* @return object PEAR_Error on failure
return $this->_dba->reopen ('w');
* Aquire a shared lock on the table
* @return object PEAR_Error on failure
return $this->_dba->reopen ('r');
// {{{ _packField($field, $value)
* Returns a string for a raw field
* @param array $field field to pack
* @param string $value value of this field to pack
* @return string packed version of value, as per $field spec
function _packField ($field, $value)
switch ($this->_schema[$field]['type']) {
foreach ($value as $element) {
$this->_schema[$field]['domain']);
// check for both null and false for backwards
if (!is_null($c_element) && ($c_element!==false )) {
$this->_schema[$field]['domain']);
// convert a 'boolean' string into a string 1 or 0
array ('t','true','y','yes','1')) ? '1': '0';
str_pad($value, $this->_schema[$field]['length'])
,0 , $this->_schema[$field]['length']));
// size is optional for varchars
if (isset ($this->_schema[$field]['length'])) {
$this->_schema[$field]['length']);
case 'integer': case 'float': case 'fixed':
// {{{ _unpackField($field, $value)
* Converts a field from its packed representation to its original value
* @param string $field field to convert
* @return mixed string $field packed value
function _unpackField ($field, $value)
switch ($this->_schema[$field]['type']) {
foreach ($value as $element) {
$c_value[] = $this->_schema[$field]['domain'][$element];
return $this->_schema[$field]['domain'][$value];
// {{{ _packSchema($schema)
* Returns a string for a field structure
* This function uses the following is the grammar to pack elements:
* enum => name;type=enum;domain=[element1,...]
* set => name;type=set;domain=[element1,...]
* timestamp => name;type=timestamp;format=<string>;init=<num>
* boolean => name;type=bool;default=[true, false]
* text => name;type=text;default=<string>
* varchar => name;varchar;size=<num>;default=<string>
* numeric => name;[autoincrement];size=<num>;default=<string>
* @param array $schema schema to pack
* @return string the packed schema
function _packSchema ($schema, $format = 'php')
return $this->raiseError('wddx extension not found!');
return $this->raiseError('Unknown schema format: '. $format);
// {{{ _unpackSchema($rawFieldString)
* Unpacks a raw string as created by _packSchema into an array
* structure for use as $this->_schema
* @param string $rawFieldString data to be unpacked into the schema
function _unpackSchema ($rawSchema)
if ($rawSchema{0 } == 'a') {
} elseif ($rawSchema{0 } == '<') {
return $this->raiseError('wddx extension not found!');
return $this->raiseError('Unknown schema format');
foreach ($schema as $name => $meta) {
if (isset ($meta['primary_key'])) {
$primaryKey[$name] = true;
$this->_primaryKey = $primaryKey;
$this->_primaryKey = array ('_rowid'=>true );
// {{{ _packRow($data, &$key)
* Packs a fields from a raw row into an internal representation suitable
* for storing in the table. Think of this as a cross-language version of
* @param array $data row data to pack, key=>field pairs
function _packRow ($data, &$key)
foreach ($this->_schema as $fieldName=> $fieldMeta) {
if (isset ($data[$fieldName])) {
// data ordering is explicit, e.g. field=>data
$c_value = $this->_packField ($fieldName, $data[$fieldName]);
} elseif (isset ($data[$i])) {
// data ordering is implicit, e.g. $i=>data
$c_value = $this->_packField ($fieldName, $data[$i]);
} elseif (isset ($fieldMeta['default'])) {
if (isset ($fieldMeta['auto_increment'])) {
// set the default value to 0 if none has been set before
if (!isset ($this->_schema[$fieldName]['default'])) {
$this->_schema[$fieldName]['default'] = 0;
// use the autoincrement value
$c_value = $this->_packField ($fieldName,
$this->_schema[$fieldName]['default']++ );
if ($fieldMeta['default_type'] == 'function') {
$c_value = $this->_packField ($fieldName,
eval ('return DBA_Functions::'.
$this->_schema[$fieldName]['default']. ';'));
$c_value = $this->_packField ($fieldName,
$this->_schema[$fieldName]['default']);
} elseif (isset ($fieldMeta['not_null'])) {
return $this->raiseError(" $fieldName cannot be null" );
} elseif (!isset ($data[$fieldName]) || is_null($data[$fieldName])) {
$c_value = "\x00"; // \x00 is the null value placeholder
if (isset ($this->_primaryKey[$fieldName])) {
// {{{ _unpackRow($packedData)
* Unpacks a string into an array containing the data from the original
* packed row. Think of this as a cross-language version of deserialize.
* @param array packedData row data to unpack
* @return array field=>value pairs
function _unpackRow ($packedData)
foreach ($this->_schema as $fieldName=> $fieldMeta) {
if ($data[$i] == "\x00") { // it's a placeholder
$buffer[$fieldName] = null;
$buffer[$fieldName] = $this->_unpackField ($fieldName, $data[$i]);
* Inserts a new row in a table
* @param array $data assoc array or ordered list of data to insert
* @return mixed PEAR_Error on failure, the row index on success
$packedRow = $this->_packRow ($data, $key);
if (PEAR ::isError ($packedRow)) {
$result = $this->_dba->insert ($key, $packedRow);
if (PEAR ::isError ($result)) {
// {{{ replace($rawQuery, $data, $rows=null)
* Replaces rows that match $rawQuery with $data
* @param string $rawQuery query expression for performing the replace
* @param array $rows rows to select on
* @return object PEAR_Error on failure
function replace($rawQuery, $data, $rows=null )
$rows = & $this->select($rawQuery, $rows);
if (PEAR ::isError ($rows)) {
// _packRow will calculate a new primary key for each row;
// passing a dummy value preserves the current keys
$packedRow = & $this->_packRow ($data, $dummy);
if (PEAR ::isError ($packedRow)) {
$result = $this->_dba->replace ($rowKey, $packedRow);
if (PEAR ::isError ($result)) {
// {{{ replaceKey($key, $data)
* Replaces an existing row in a table, inserts if the row does not exist
* @param string $key row id to replace
* @param array $data assoc array or ordered list of data to insert
* @return mixed PEAR_Error on failure, the row index on success
// _packRow will calculate a new primary key for each row;
// passing a dummy value preserves the current keys
$packedRow = & $this->_packRow ($data, $dummy);
if (PEAR ::isError ($packedRow)) {
return $this->_dba->replace ($key, $packedRow);
// {{{ remove($rawQuery, $rows=null)
* Removes existing rows from table that match $rawQuery
* @param string $rawQuery query expression for performing the remove
* @param array $rows rows to select on
* @return object PEAR_Error on failure
function remove($rawQuery, $rows=null )
$removableRows = & $this->select($rawQuery, $rows);
if (PEAR ::isError ($rows)) {
$result = $this->_dba->remove ($rowKey);
if (PEAR ::isError ($result)) {
* Removes an existing row from a table, referenced by the row key
* @param string $key row id to remove
* @return object PEAR_Error on failure
return $this->_dba->remove ($key);
* Fetches an existing row from a table
* @param string $key row id to fetch
* @return mixed PEAR_Error on failure, the row array on success
$result = $this->_dba->fetch ($key);
if (!PEAR ::isError ($result)) {
return $this->_unpackRow ($result);
// {{{ _finalizeField($field, $value)
* Converts a field from its native value to a value, that is
* sets are converted to strings, bools are converted to 'true' and 'false'
* timestamps are converted to a readable date. No more operations
* should be performed on a field after this though.
* @return string the string representation of $field
function _finalizeField ($field, $value)
switch ($this->_schema[$field]['type']) {
foreach ($value as $element) {
return $value ? 'true' : 'false';
return date($this->_dateFormat, $value);
// {{{ finalize($rows=null)
* Converts the results from any of the row operations to a 'finalized'
* display-ready form. That means that timestamps, sets and enums are
* converted into strings. This obviously has some consequences if you plan
* on chaining the results into another row operation, so don't call this
* unless it is the final operation.
* @param array $rows rows to finalize, if none are specified, returns
* @return mixed PEAR_Error on failure, the row array on success
if ($this->_dba->isOpen ()) {
return $this->raiseError('table not open / no rows specified');
foreach ($rows as $key=> $row) {
foreach ($row as $field=> $data) {
$row[$field] = $this->_finalizeField ($field, $row[$field]);
// {{{ getRows($rowKeys=null)
* Returns the specified rows. A multiple-value version of getRow
* @param array $rowKeys keys of rows to get, if none are specified,
* returns the whole table
* @return mixed PEAR_Error on failure, the row array on success
if ($this->_dba->isOpen ()) {
$key = $this->_dba->firstkey ();
$rows[$key] = $this->_unpackRow ($this->_dba->fetch ($key));
$key = $this->_dba->nextkey ();
foreach ($rowKeys as $key) {
$rows[$key] = $this->unpackRow ($this->_dba->fetch ($key));
* Returns the first row in the table. The built-in cursor for the
* internal _dba object is used, so no other operations should be performed
* other than nextRow() if iterating through the rows is the desired
* @return mixed PEAR_Error on failure, the row array on success
* or false if there are no elements in the table
* i.e (firstRow() === false) = table is empty
if ($this->_dba->isOpen ()) {
$key = $this->_dba->firstkey ();
return $this->_unpackRow ($this->_dba->fetch ($key));
* Returns the next row in the table after a firstRow or nextRow.
* @return mixed PEAR_Error on failure, the row array on success
* or false if there are no more elements in the table
if ($this->_dba->isOpen ()) {
$key = $this->_dba->nextkey ();
return $this->_unpackRow ($this->_dba->fetch ($key));
* Returns all keys in the table
* Similar to PostgrSQL's row ID's
if ($this->_dba->isOpen ()) {
return $this->_dba->getkeys ();
* Returns an array of the defined field names in the table
* @return array the field names in an array
// {{{ function _cookToken($token, &$isField)
function _cookToken ($token, &$isField)
global $_dba_functions, $_dba_keywords;
if ($token{0 } == '"' || $token{0 } == "'"
|| isset ($_dba_keywords[$token])) {
} elseif (isset ($_dba_functions[$token])) {
return 'DBA_Function::'. $token;
return '$row[\''. $token. '\']';
// {{{ function _parsePHPQuery($rawQuery)
function _parsePHPQuery ($rawQuery, &$fields)
global $_dba_functions, $_dba_operators;
for($i=0; $i < strlen($rawQuery); $i++ ) {
if ($c == "'" || $c == '"') {
} elseif ($c == $quote) {
} elseif (isset ($_dba_operators[$c]) || $c == ' ') {
if (!$inQuote && strlen($token)) {
$PHPQuery .= DBA_Table::_cookToken ($token, $isField);
} elseif ($c == "\t" || $c == "\n" || $c == "\r") {
if (!$inQuote && strlen($token)) {
$PHPQuery .= DBA_Table::_cookToken ($token, $isField);
$PHPQuery .= DBA_Table::_cookToken ($token, $isField);
// {{{ &select($rawQuery, $rows=null)
* Performs a select on a table. This means that a subset of rows in a
* table are filtered and returned based on the query. Accepts any valid
* expression of the form '(field == field) || (field > 3)', etc. Using the
* expression '*' returns the entire table
* SQL analog: 'select * from rows where rawQuery'
* @param string $rawQuery query expression for performing the select
* @param array $rows rows to select on
* @return mixed PEAR_Error on failure, the row array on success
function &select($rawQuery, $rows=null )
if ($this->_dba->isOpen ()) {
// if we haven't passed any rows to select from, use the whole table
// handle the special case of requesting all rows
// convert the query into a php statement
$PHPQuery = $this->_parsePHPQuery ($rawQuery, $fields);
foreach ($fields as $field) {
return $this->raiseError($field . ' is not a field name');
$PHPSelect = 'foreach ($rows as $key=>$row) if ('.
$PHPQuery. ') $results[$key] = $row;';
// {{{ _parseFieldString($fieldString, $possibleFields)
* explodes a string of field names into an array
* @param string $fieldString field names to explode
function _parseFieldString ($fieldString, $possibleFields)
foreach ($tokens as $token) {
if (isset ($possibleFields[$token])) {
// {{{ sort($fields, $order, $rows)
* Sorts rows by field in either ascending or descending order
* SQL analog: 'select * from rows, order by fields'
* @param mixed $fields a string with the field name to sort by or an
* array of fields to sort by in order of preference
* @param string $order 'a' for ascending, 'd' for descending
* @param array $rows rows to sort
* @return mixed PEAR_Error on failure, the row array on success
function sort($fields, $order, $rows)
global $_dba_sort_fields;
// {{{ _dbaSortCmpA($a, $b)
* Comparison function for sorting ascending order
function _dbaSortCmpA ($a, $b)
global $_dba_sort_fields;
foreach ($_dba_sort_fields as $field) {
if ($a[$field] < $b[$field]) return -1;
if ($a[$field] > $b[$field]) return 1;
// {{{ _dbaSortCmpD($a, $b)
* Comparison function for sorting descending order
function _dbaSortCmpD ($a, $b)
global $_dba_sort_fields;
foreach ($_dba_sort_fields as $field) {
if ($a[$field] < $b[$field]) return 1;
if ($a[$field] > $b[$field]) return -1;
// parse the sort string to produce an array of sort fields
// we pass the sortfields as a member variable because
// uasort does not pass extra parameters to the comparison
$_dba_sort_fields = DBA_Table::_parseFieldString ($fields,
// we already have an array of sort fields
$_dba_sort_fieldssortFields = $fields;
uasort($rows, '_dbaSortCmpA');
uasort($rows, '_dbaSortCmpD');
// {{{ project($fields, $rows)
* Projects rows by field. This means that a subset of the possible fields i
* are in the resulting rows. The SQL analog is 'select fields from table'
* @param array $fields fields to project
* @param array $rows rows to project
* @return mixed PEAR_Error on failure, the row array on success
$projectFields = array ();
$projectFields = DBA_Table::_parseFieldString ($fields,
// we already have an array of fields
$projectFields = $fields;
foreach ($rows as $key=> $row) {
foreach ($projectFields as $field) {
$projectedRows[$key][$field] = $row[$field];
* @return bool true if they are the same, false if they are not
foreach ($a as $field=> $value) {
if ($value != $b[$field]) {
* Returns the unique rows from a set of rows
* @param array $rows rows to process
* @return mixed PEAR_Error on failure, the row array on success
foreach ($rows as $key=> $row) {
if (!isset ($current) || ($current != $row)) {
// {{{ _getColumn($field, $rows)
function _getColumn ($field, $rows)
foreach ($rows as $row) {
Documentation generated on Mon, 11 Mar 2019 14:59:55 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.
|