egroupware/phpgwapi/inc/class.schema_proc.inc.php
Ralf Becker 3bef4b2a26 * Tracker: dropping unique index(es) on escalations to not limit creating same escalations eg. on different queues
Was previously done by modifying index to contain more columns in update, but not new installations.
Now droping all existing unique indexes completly.
2014-01-14 11:06:31 +00:00

1390 lines
43 KiB
PHP

<?php
/**
* EGroupware - Setup - db-schema-processor
*
* Originaly written by
* - Michael Dean <mdean@users.sourceforge.net>
* - Miles Lott<milosch@groupwhere.org>
* Rewritten and adapted to ADOdb's schema processor by Ralf Becker.
*
* @link http://www.egroupware.org
* @author Ralf Becker <RalfBecker@outdoor-training.de>
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package api
* @subpackage db
* @version $Id$
*/
/**
* eGW's ADOdb based schema-processor
*/
class schema_proc
{
/**
* @deprecated formerly used translator class, now a reference to ourself
*/
var $m_oTranslator;
/**
* egw_db-object
*
* @var egw_db
*/
var $m_odb;
/**
* reference to the global ADOdb object
*
* @var ADOConnection
*/
var $adodb;
/**
* adodb's datadictionary object for the used db-type
*
* @var ADODB_DataDict
*/
var $dict;
/**
* Debuglevel: 0=Off, 1=some, eg. primary function calls, 2=lots incl. the SQL used
*
* @var int
*/
var $debug = 0;
/**
* Array with db => max. length of indexes pairs (if there is a considerable low limit for a db)
*
* @var array
*/
var $max_index_length=array(
'maxdb' => 32,
'oracle' => 30,
);
/**
* type of the database, set by the the constructor: 'mysql','pgsql','mssql','maxdb'
*
* @var string
*/
var $sType;
/**
* maximum length of a varchar column, everything above get converted to text
*
* @var int
*/
var $max_varchar_length = 255;
/**
* system-charset if set
*
* @var string
*/
var $system_charset;
/**
* reference to the capabilities array of the db-class
*
* @var array
*/
var $capabilities;
/**
* preserve value of old sequences in PostgreSQL
*
* @var int
*/
var $pgsql_old_seq;
/**
* Constructor of schema-processor
*
* @param string $dbms type of the database: 'mysql','pgsql','mssql','maxdb'
* @param object $db=null database class, if null we use $GLOBALS['egw']->db
* @return schema_proc
*/
function __construct($dbms=False,$db=null)
{
if(is_object($db))
{
$this->m_odb = $db;
}
else
{
$this->m_odb = isset($GLOBALS['egw']->db) && is_object($GLOBALS['egw']->db) ? $GLOBALS['egw']->db : $GLOBALS['egw_setup']->db;
}
if (!($this->m_odb instanceof egw_db))
{
throw new egw_exception_assertion_failed('no egw_db object!');
}
$this->m_odb->connect();
$this->capabilities =& $this->m_odb->capabilities;
$this->sType = $dbms ? $dmbs : $this->m_odb->Type;
$this->adodb = &$this->m_odb->Link_ID;
$this->dict = NewDataDictionary($this->adodb);
// enable the debuging in ADOdb's datadictionary if the debug-level is greater then 1
if ($this->debug > 1) $this->dict->debug = True;
// to allow some of the former translator-functions to be called, we assign ourself as the translator
$this->m_oTranslator = &$this;
switch($this->sType)
{
case 'maxdb':
$this->max_varchar_length = 8000;
break;
case 'mysql':
// since MySQL 5.0 65535, but with utf8 and row-size-limit of 64k:
// it's effective 65535/3 - size of other columns, so we use 20000 (mysql silently convert to text anyway)
if ((float)$this->m_odb->ServerInfo['version'] >= 5.0)
{
$this->max_varchar_length = 20000;
}
break;
}
if (is_object($GLOBALS['egw_setup']))
{
$this->system_charset =& $GLOBALS['egw_setup']->system_charset;
}
elseif (isset($GLOBALS['egw_info']['server']['system_charset']))
{
$this->system_charset = $GLOBALS['egw_info']['server']['system_charset'];
}
}
/**
* Check if the given $columns exist as index in the index array $indexes
*
* @param string/array $columns column-name as string or array of column-names plus optional options key
* @param array $indexs array of indexes (column-name as string or array of column-names plus optional options key)
* @return boolean true if index over $columns exist in the $indexes array
*/
function _in_index($columns,$indexs)
{
if (is_array($columns))
{
unset($columns['options']);
$columns = implode('-',$columns);
}
foreach($indexs as $index)
{
if (is_array($index))
{
unset($index['options']);
$index = implode('-',$index);
}
if ($columns == $index) return true;
}
return false;
}
/**
* Created a table named $sTableName as defined in $aTableDef
*
* @param string $sTableName
* @param array $aTableDef
* @param bool $preserveSequence
* @return int 2: no error, 1: errors, but continued, 0: errors aborted
*/
function CreateTable($sTableName, $aTableDef, $preserveSequence=False)
{
if ($this->debug)
{
$this->debug_message('schema_proc::CreateTable(%1,%2)',False,$sTableName, $aTableDef);
}
// for mysql 4.0+ we set the charset for the table
if ($this->system_charset && substr($this->sType,0,5) == 'mysql' &&
(float) $this->m_odb->ServerInfo['version'] >= 4.0 && $this->m_odb->Link_ID->charset2mysql[$this->system_charset])
{
$set_table_charset = array($this->sType => 'CHARACTER SET '.$this->m_odb->Link_ID->charset2mysql[$this->system_charset]);
}
// creating the table
$aSql = $this->dict->CreateTableSQL($sTableName,$ado_cols = $this->_egw2adodb_columndef($aTableDef),$set_table_charset);
if (!($retVal = $this->ExecuteSQLArray($aSql,2,'CreateTableSQL(%1,%2) sql=%3',False,$sTableName,$ado_cols,$aSql)))
{
return $retVal;
}
// creating unique indices/constrains
foreach ($aTableDef['uc'] as $name => $mFields)
{
if (empty($mFields))
{
continue; // cant create an index without fields (was observed in broken backups)
}
if ($this->_in_index($mFields,array($aTableDef['pk'])))
{
continue; // is already created as primary key
}
if (!($retVal = $this->CreateIndex($sTableName,$mFields,true,'',$name)))
{
return $retVal;
}
}
// creation indices
foreach ($aTableDef['ix'] as $name => $mFields)
{
if (empty($mFields))
{
continue; // cant create an index without fields (was observed in broken backups)
}
if ($this->_in_index($mFields,array($aTableDef['pk'])) ||
$this->_in_index($mFields,$aTableDef['uc']))
{
continue; // is already created as primary key or unique index
}
$options = False;
if (is_array($mFields))
{
if (isset($mFields['options'])) // array sets additional options
{
if (isset($mFields['options'][$this->sType]))
{
$options = $mFields['options'][$this->sType]; // db-specific options, eg. index-type
if (!$options) continue; // no index for our db-type
}
unset($mFields['options']);
}
}
foreach((array)$mFields as $k => $col)
{
// only create indexes on text-columns, if (db-)specifiy options are given or FULLTEXT for mysql
// most DB's cant do them and give errors
if (in_array($aTableDef['fd'][$col]['type'],array('text','longtext')))
{
if (is_array($mFields)) // index over multiple columns including a text column
{
$mFields[$k] .= '(32)'; // 32=limit of egw_addressbook_extra.extra_value to fix old backups
}
elseif (!$options) // index over a single text column and no options given
{
if ($this->sType == 'mysql')
{
$options = 'FULLTEXT';
}
else
{
continue 2; // ignore that index, 2=not just column but whole index!
}
}
}
}
if (!($retVal = $this->CreateIndex($sTableName,$mFields,false,$options,$name)))
{
return $retVal;
}
}
// preserve last value of an old sequence
if ($this->sType == 'pgsql' && $preserveSequence && $this->pgsql_old_seq)
{
if ($seq = $this->_PostgresHasOldSequence($sTableName))
{
$this->pgsql_old_seq = $this->pgsql_old_seq + 1;
$this->m_odb->query("ALTER SEQUENCE $seq RESTART WITH " . $this->pgsql_old_seq,__LINE__,__FILE__);
}
$this->pgsql_old_seq = 0;
}
return $retVal;
}
/**
* Drops all tables in $aTables
*
* @param array $aTables array of eGW table-definitions
* @param boolean $bOutputHTML should we give diagnostics, default False
* @return boolean True if no error, else False
*/
function DropAllTables($aTables, $bOutputHTML=False)
{
if(!is_array($aTables) || !isset($this->m_odb))
{
return False;
}
// set our debug-mode or $bOutputHTML is the other one is set
if ($this->debug) $bOutputHTML = True;
if ($bOutputHTML && !$this->debug) $this->debug = 2;
foreach($aTables as $sTableName => $aTableDef)
{
if($this->DropTable($sTableName))
{
if($bOutputHTML)
{
echo '<br>Drop Table <b>' . $sTableSQL . '</b>';
}
}
else
{
return False;
}
}
return True;
}
/**
* Drops the table $sTableName
*
* @param string $sTableName
* @return int 2: no error, 1: errors, but continued, 0: errors aborted
*/
function DropTable($sTableName)
{
if ($this->sType == 'pgsql') $this->_PostgresTestDropOldSequence($sTableName);
$aSql = $this->dict->DropTableSql($sTableName);
return $this->ExecuteSQLArray($aSql,2,'DropTable(%1) sql=%2',False,$sTableName,$aSql);
}
/**
* Drops column $sColumnName from table $sTableName
*
* @param string $sTableName table-name
* @param array $aTableDef eGW table-defintion
* @param string $sColumnName column-name
* @param boolean $bCopyData ???
* @return int 2: no error, 1: errors, but continued, 0: errors aborted
*/
function DropColumn($sTableName, $aTableDef, $sColumnName, $bCopyData = true)
{
$table_def = $this->GetTableDefinition($sTableName);
unset($table_def['fd'][$sColumnName]);
$aSql = $this->dict->DropColumnSql($sTableName,$sColumnName,$ado_table=$this->_egw2adodb_columndef($table_def));
return $this->ExecuteSQLArray($aSql,2,'DropColumnSQL(%1,%2,%3) sql=%4',False,$sTableName,$sColumnName,$ado_table,$aSql);
}
/**
* Renames table $sOldTableName to $sNewTableName
*
* @param string $sOldTableName old (existing) table-name
* @param string $sNewTableName new table-name
* @return int 2: no error, 1: errors, but continued, 0: errors aborted
*/
function RenameTable($sOldTableName, $sNewTableName)
{
// if we have an old postgres sequence or index (the ones not linked to the table),
// we create a new table, copy the content and drop the old one
if ($this->sType == 'pgsql')
{
$table_def = $this->GetTableDefinition($sOldTableName);
if ($this->_PostgresHasOldSequence($sOldTableName,True) || count($table_def['pk']) ||
count($table_def['ix']) || count($table_def['uc']))
{
if ($this->adodb->BeginTrans() &&
$this->CreateTable($sNewTableName,$table_def,True) &&
$this->m_odb->query("INSERT INTO $sNewTableName SELECT * FROM $sOldTableName",__LINE__,__FILE__) &&
$this->DropTable($sOldTableName))
{
$this->adodb->CommitTrans();
return 2;
}
$this->adodb->RollbackTrans();
return 0;
}
}
$aSql = $this->dict->RenameTableSQL($sOldTableName, $sNewTableName);
return $this->ExecuteSQLArray($aSql,2,'RenameTableSQL(%1,%2) sql=%3',False,$sOldTableName,$sNewTableName,$aSql);
}
/**
* Check if we have an old, not automaticaly droped sequence
*
* @param string $sTableName
* @param bool $preserveValue
* @return boolean/string sequence-name or false
*/
function _PostgresHasOldSequence($sTableName,$preserveValue=False)
{
if ($this->sType != 'pgsql') return false;
$seq = $this->adodb->GetOne("SELECT d.adsrc FROM pg_attribute a, pg_class c, pg_attrdef d WHERE c.relname='$sTableName' AND c.oid=d.adrelid AND d.adsrc LIKE '%seq_$sTableName''::text)' AND a.attrelid=c.oid AND d.adnum=a.attnum");
$seq2 = $this->adodb->GetOne("SELECT d.adsrc FROM pg_attribute a, pg_class c, pg_attrdef d WHERE c.relname='$sTableName' AND c.oid=d.adrelid AND d.adsrc LIKE '%$sTableName%_seq''::text)' AND a.attrelid=c.oid AND d.adnum=a.attnum");
if ($seq && preg_match('/^nextval\(\'(.*)\'/',$seq,$matches))
{
if ($preserveValue) $this->pgsql_old_seq = $this->adodb->GetOne("SELECT last_value FROM " . $matches[1]);
return $matches[1];
}
if ($seq2 && preg_match('/^nextval\(\'public\.(.*)\'/',$seq2,$matches))
{
if ($preserveValue) $this->pgsql_old_seq = $this->adodb->GetOne("SELECT last_value FROM " . $matches[1]);
return $matches[1];
}
return false;
}
/**
* Check if we have an old, not automaticaly droped sequence and drop it
*
* @param $sTableName
* @param bool $preserveValue
*/
function _PostgresTestDropOldSequence($sTableName,$preserveValue=False)
{
$this->pgsql_old_seq = 0;
if ($this->sType == 'pgsql' && ($seq = $this->_PostgresHasOldSequence($sTableName)))
{
// only drop sequence, if there is no dependency on it
if (!$this->adodb->GetOne("SELECT relname FROM pg_class JOIN pg_depend ON pg_class.relfilenode=pg_depend.objid WHERE relname='$seq' AND relkind='S' AND deptype='i'"))
{
$this->query('DROP SEQUENCE '.$seq,__LINE__,__FILE__);
}
}
}
/**
* Changes one (exiting) column in a table
*
* @param string $sTableName table-name
* @param string $sColumnName column-name
* @param array $aColumnDef new column-definition
* @param boolean $bCopyData ???
* @return int 2: no error, 1: errors, but continued, 0: errors aborted
*/
function AlterColumn($sTableName, $sColumnName, $aColumnDef, $bCopyData=True)
{
$table_def = $this->GetTableDefinition($sTableName);
$table_def['fd'][$sColumnName] = $aColumnDef;
$aSql = $this->dict->AlterColumnSQL($sTableName,$ado_col = $this->_egw2adodb_columndef(array(
'fd' => array($sColumnName => $aColumnDef),
'pk' => array(),
)),$ado_table=$this->_egw2adodb_columndef($table_def));
return $this->ExecuteSQLArray($aSql,2,'AlterColumnSQL(%1,%2,%3) sql=%4',False,$sTableName,$ado_col,$ado_table,$aSql);
}
/**
* Renames column $sOldColumnName to $sNewColumnName in table $sTableName
*
* @param string $sTableName table-name
* @param string $sOldColumnName old (existing) column-name
* @param string $sNewColumnName new column-name
* @param boolean $bCopyData ???
* @return int 2: no error, 1: errors, but continued, 0: errors aborted
*/
function RenameColumn($sTableName, $sOldColumnName, $sNewColumnName, $bCopyData=True)
{
$table_def = $this->GetTableDefinition($sTableName);
$old_def = array();
if (isset($table_def['fd'][$sOldColumnName]))
{
$old_def = $table_def['fd'][$sOldColumnName];
}
else
{
foreach($table_def['fd'] as $col => $def)
{
if (strtolower($col) == strtolower($sOldColumnName))
{
$old_def = $def;
break;
}
}
}
$col_def = $this->_egw2adodb_columndef(array(
'fd' => array($sNewColumnName => $old_def),
'pk' => array(),
));
$aSql = $this->dict->RenameColumnSQL($sTableName,$sOldColumnName,$sNewColumnName,$col_def);
return $this->ExecuteSQLArray($aSql,2,'RenameColumnSQL(%1,%2,%3) sql=%4',False,$sTableName,$sOldColumnName, $sNewColumnName,$aSql);
}
/**
* Add one (new) column to a table
*
* @param string $sTableName table-name
* @param string $sColumnName column-name
* @param array $aColumnDef column-definition
* @return int 2: no error, 1: errors, but continued, 0: errors aborted
*/
function AddColumn($sTableName, $sColumnName, $aColumnDef)
{
$aSql = $this->dict->AddColumnSQL($sTableName,$ado_cols = $this->_egw2adodb_columndef(array(
'fd' => array($sColumnName => $aColumnDef),
'pk' => array(),
)));
return $this->ExecuteSQLArray($aSql,2,'AlterColumnSQL(%1,%2,%3) sql=%4',False,$sTableName,$sColumnName, $aColumnDef,$aSql);
}
/**
* Create an (unique) Index over one or more columns
*
* @param string $sTablename table-name
* @param array $aColumnNames columns for the index
* @param boolean $bUnique=false true for a unique index, default false
* @param array/string $options='' db-sepecific options, default '' = none
* @param string $sIdxName='' name of the index, if not given (default) its created automaticaly
* @return int 2: no error, 1: errors, but continued, 0: errors aborted
*/
function CreateIndex($sTableName,$aColumnNames,$bUnique=false,$options='',$sIdxName='')
{
// remove length limits from column names, if DB type is NOT MySQL
if ($this->sType != 'mysql')
{
$aColumnNames = preg_replace('/ *\(\d+\)$/','',$aColumnNames);
}
if (!$sIdxName || is_numeric($sIdxName))
{
$sIdxName = $this->_index_name($sTableName,$aColumnNames);
}
if (!is_array($options)) $options = $options ? array($options) : array();
if ($bUnique) $options[] = 'UNIQUE';
$aSql = $this->dict->CreateIndexSQL($sIdxName,$sTableName,$aColumnNames,$options);
return $this->ExecuteSQLArray($aSql,2,'CreateIndexSQL(%1,%2,%3,%4) sql=%5',False,$name,$sTableName,$aColumnNames,$options,$aSql);
}
/**
* Drop an Index
*
* @param string $sTablename table-name
* @param array/string $aColumnNames columns of the index or the name of the index
* @return int 2: no error, 1: errors, but continued, 0: errors aborted
*/
function DropIndex($sTableName,$aColumnNames)
{
if (is_array($aColumnNames))
{
$indexes = $this->dict->MetaIndexes($sTableName);
if ($indexes === False)
{
// if MetaIndexes is not availible for the DB, we try the name the index was created with
// this fails if one of the columns have been renamed
$sIdxName = $this->_index_name($sTableName,$aColumnNames);
}
else
{
foreach($indexes as $idx => $idx_data)
{
if (strtolower(implode(':',$idx_data['columns'])) == implode(':',$aColumnNames))
{
$sIdxName = $idx;
break;
}
}
}
}
else
{
$sIdxName = $aColumnNames;
}
if(!$sIdxName)
{
return True;
}
$aSql = $this->dict->DropIndexSQL($sIdxName,$sTableName);
return $this->ExecuteSQLArray($aSql,2,'DropIndexSQL(%1(%2),%3) sql=%4',False,$sIdxName,$aColumnNames,$sTableName,$aSql);
}
/**
* Updating the sequence-value, after eg. copying data via RefreshTable
* @param string $sTableName table-name
* @param string $sColumnName column-name, which default is set to nextval()
*/
function UpdateSequence($sTableName,$sColumnName)
{
switch($this->sType)
{
case 'pgsql':
// identify the sequence name, ADOdb uses a different name or it might be renamed
$columns = $this->dict->MetaColumns($sTableName);
$seq_name = 'seq_'.$sTableName;
if (preg_match("/nextval\('([^']+)'::(text|regclass)\)/",$columns[strtoupper($sColumnName)]->default_value,$matches))
{
$seq_name = $matches[1];
}
$sql = "SELECT setval('$seq_name',MAX($sColumnName)) FROM $sTableName";
if($this->debug) { echo "<br>Updating sequence '$seq_name using: $sql"; }
return $this->query($sql,__LINE__,__FILE__);
}
return True;
}
/**
* This function manually re-created the table incl. primary key and all other indices
*
* It is meant to use if the primary key, existing indices or column-order changes or
* columns are not longer used or new columns need to be created (with there default value or NULL)
* Beside the default-value in the schema, one can give extra defaults via $aDefaults to eg. use an
* other colum or function to set the value of a new or changed column
*
* @param string $sTableName table-name
* @param array $aTableDef eGW table-defintion
* @param array/boolean $aDefaults array with default for the colums during copying, values are either (old) column-names or quoted string-literals
*/
function RefreshTable($sTableName, $aTableDef, $aDefaults=False)
{
if($this->debug) { echo "<p>schema_proc::RefreshTable('$sTableName',"._debug_array($aTableDef,False).")<p>$sTableName="._debug_array($old_table_def,False)."\n"; }
$old_table_def = $this->GetTableDefinition($sTableName);
$tmp_name = 'tmp_'.$sTableName;
$this->m_odb->transaction_begin();
$select = array();
$blob_column_included = $auto_column_included = False;
foreach($aTableDef['fd'] as $name => $data)
{
// new auto column with no default or explicit NULL as default (can be an existing column too!)
if ($data['type'] == 'auto' &&
(!isset($old_table_def['fd'][$name]) && (!$aDefaults || !isset($aDefaults[$name])) ||
$aDefaults && strtoupper($aDefaults[$name]) == 'NULL'))
{
$sequence_name = $sTableName.'_'.$name.'_seq';
switch($GLOBALS['egw_setup']->db->Type)
{
case 'mysql':
$value = 'NULL'; break;
case 'pgsql':
$value = "nextval('$sequence_name'::regclass)"; break;
default:
$value = "nextval('$sequence_name')"; break;
}
}
elseif ($aDefaults && isset($aDefaults[$name])) // use given default
{
$value = $aDefaults[$name];
}
elseif (isset($old_table_def['fd'][$name])) // existing column, use its value => column-name in query
{
$value = $name;
if ($this->sType == 'pgsql') // some postgres specific code
{
// this is eg. necessary to change a varchar into an int column under postgres
if (in_array($old_table_def['fd'][$name]['type'],array('char','varchar','text','blob')) &&
in_array($data['type'],array('int','decimal')))
{
$value = "to_number($name,'S9999999999999D99')";
}
// blobs cant be casted to text
elseif($old_table_def['fd'][$name]['type'] == 'blob' && $data['type'] == 'text')
{
$value = "ENCODE($value,'escape')";
}
// cast everything which is a different type
elseif($old_table_def['fd'][$name]['type'] != $data['type'] && ($type_translated = $this->TranslateType($data['type'])))
{
$value = "CAST($value AS $type_translated)";
}
}
}
else // new column => use default value or NULL
{
if (!isset($data['default']) && (!isset($data['nullable']) || $data['nullable']))
{
$value = 'NULL';
}
// some stuff is NOT to be quoted
elseif (in_array(strtoupper($data['default']),array('CURRENT_TIMESTAMP','CURRENT_DATE','NULL','NOW()')))
{
$value = $data['default'];
}
else
{
$value = $this->m_odb->quote(isset($data['default']) ? $data['default'] : '',$data['type']);
}
if ($this->sType == 'pgsql')
{
// fix for postgres error "no '<' operator for type 'unknown'"
if(($type_translated = $this->TranslateType($data['type'])))
{
$value = "CAST($value AS $type_translated)";
}
}
}
$blob_column_included = $blob_column_included || in_array($data['type'],array('blob','text','longtext'));
$auto_column_included = $auto_column_included || $data['type'] == 'auto';
$select[] = $value;
}
$select = implode(',',$select);
$extra = '';
$distinct = 'DISTINCT';
switch($this->sType)
{
case 'mssql':
if ($auto_column_included) $extra = "SET IDENTITY_INSERT $sTableName ON\n";
if ($blob_column_included) $distinct = ''; // no distinct on blob-columns
break;
}
// because of all the trouble with sequences and indexes in the global namespace,
// we use an additional temp. table for postgres and not rename the existing one, but drop it.
if ($this->sType == 'pgsql')
{
$Ok = $this->m_odb->query("SELEcT * INTO TEMPORARY TABLE $tmp_name FROM $sTableName",__LINE__,__FILE__) &&
$this->DropTable($sTableName);
}
else
{
$Ok = $this->RenameTable($sTableName,$tmp_name);
}
$Ok = $Ok && $this->CreateTable($sTableName,$aTableDef) &&
$this->m_odb->query($sql_copy_data="$extra INSERT INTO $sTableName (".
implode(',',array_keys($aTableDef['fd'])).
") SELEcT $distinct $select FROM $tmp_name",__LINE__,__FILE__) &&
$this->DropTable($tmp_name);
//error_log($sql_copy_data);
if (!$Ok)
{
$this->m_odb->transaction_abort();
return False;
}
// do we need to update the new sequences value ?
if (count($aTableDef['pk']) == 1 && $aTableDef['fd'][$aTableDef['pk'][0]]['type'] == 'auto')
{
$this->UpdateSequence($sTableName,$aTableDef['pk'][0]);
}
$this->m_odb->transaction_commit();
return True;
}
/**
* depricated Function does nothing any more
* @depricated
*/
function GenerateScripts($aTables, $bOutputHTML=False)
{
return True;
}
/**
* Creates all tables for one application
*
* @param array $aTables array of eGW table-definitions
* @param boolean $bOutputHTML=false should we give diagnostics, default False
* @return boolean True on success, False if an (fatal) error occured
*/
function ExecuteScripts($aTables, $bOutputHTML=False)
{
if(!is_array($aTables) || !IsSet($this->m_odb))
{
return False;
}
// set our debug-mode or $bOutputHTML is the other one is set
if ($this->debug) $bOutputHTML = True;
if ($bOutputHTML && !$this->debug) $this->debug = 2;
foreach($aTables as $sTableName => $aTableDef)
{
if($this->CreateTable($sTableName, $aTableDef))
{
if($bOutputHTML)
{
echo '<br>Create Table <b>' . $sTableName . '</b>';
}
}
else
{
if($bOutputHTML)
{
echo '<br>Create Table Failed For <b>' . $sTableName . '</b>';
}
return False;
}
}
return True;
}
/**
* Return the value of a column
*
* @param string/integer $Name name of field or positional index starting from 0
* @param bool $strip_slashes string escape chars from field(optional), default false
* @return string the field value
*/
function f($value,$strip_slashes=False)
{
return $this->m_odb->f($value,$strip_slashes);
}
/**
* Number of rows in current result set
*
* @return int number of rows
*/
function num_rows()
{
return $this->m_odb->num_rows();
}
/**
* Move to the next row in the results set
*
* @return bool was another row found?
*/
function next_record()
{
return $this->m_odb->next_record();
}
/**
* Execute a query
*
* @param string $Query_String the query to be executed
* @param mixed $line the line method was called from - use __LINE__
* @param string $file the file method was called from - use __FILE__
* @param int $offset row to start from
* @param int $num_rows number of rows to return (optional), if unset will use $GLOBALS['phpgw_info']['user']['preferences']['common']['maxmatchs']
* @return ADORecordSet or false, if the query fails
*/
function query($sQuery, $line='', $file='')
{
return $this->m_odb->query($sQuery, $line, $file);
}
/**
* Insert a row of data into a table or updates it if $where is given, all data is quoted according to it's type
*
* @param string $table name of the table
* @param array $data with column-name / value pairs
* @param mixed $where string with where clause or array with column-name / values pairs to check if a row with that keys already exists, or false for an unconditional insert
* if the row exists db::update is called else a new row with $date merged with $where gets inserted (data has precedence)
* @param int $line line-number to pass to query
* @param string $file file-name to pass to query
* @param string $app=false string with name of app, this need to be set in setup anyway!!!
* @return ADORecordSet or false, if the query fails
*/
function insert($table,$data,$where,$line,$file,$app=False,$use_prepared_statement=false)
{
return $this->m_odb->insert($table,$data,$where,$line,$file,$app,$use_prepared_statement);
}
/**
* Execute the Sql statements in an array and give diagnostics, if any error occures
*
* @param $aSql array of SQL strings to execute
* @param $debug_level int for which debug_level (and higher) should the diagnostics always been printed
* @param $debug string variable number of arguments for the debug_message functions in case of an error
* @return int 2: no error, 1: errors, but continued, 0: errors aborted
*/
function ExecuteSqlArray($aSql,$debug_level,$debug)
{
if ($this->m_odb->query_log) // we use egw_db::query to log the queries
{
$retval = 2;
foreach($aSql as $sql)
{
if (!$this->m_odb->query($sql,__LINE__,__FILE__))
{
$retval = 1;
}
}
}
else
{
$retval = $this->dict->ExecuteSQLArray($aSql);
}
if ($retval < 2 || $this->debug >= $debug_level || $this->debug > 3)
{
$debug_params = func_get_args();
array_shift($debug_params);
array_shift($debug_params);
call_user_func_array(array($this,'debug_message'),$debug_params);
if ($retval < 2 && !$this->dict->debug)
{
echo '<p><b>'.$this->adodb->ErrorMsg()."</b></p>\n";
}
}
return $retval;
}
/**
* Created a (unique) name for an index
*
* As the length of the index name is limited on some databases, we use two algorithms:
* a) we use just the first column-name with and added _2, _3, ... if more indexes uses that column
* b) we use the table-names plus all column-names and remove dublicate parts
*
* @internal
* @param $sTableName string name of the table
* @param $aColumnNames array of column-names or string with a single column-name
* @return string the index-name
*/
function _index_name($sTableName,$aColumnNames)
{
// this code creates extrem short index-names, eg. for MaxDB
// if (isset($this->max_index_length[$this->sType]) && $this->max_index_length[$this->sType] <= 32)
// {
// static $existing_indexes=array();
//
// if (!isset($existing_indexes[$sTableName]) && method_exists($this->adodb,'MetaIndexes'))
// {
// $existing_indexes[$sTableName] = $this->adodb->MetaIndexes($sTableName);
// }
// $i = 0;
// $firstCol = is_array($aColumnNames) ? $aColumnNames[0] : $aColumnNames;
// do
// {
// ++$i;
// $name = $firstCol . ($i > 1 ? '_'.$i : '');
// }
// while (isset($existing_indexes[$sTableName][$name]) || isset($existing_indexes[strtoupper($sTableName)][strtoupper($name)]));
//
// $existing_indexes[$sTableName][$name] = True; // mark it as existing now
//
// return $name;
// }
// This code creates longer index-names incl. the table-names and the used columns
$table = str_replace(array('phpgw_','egw_'),'',$sTableName);
// if the table-name or a part of it is repeated in the column-name, remove it there
$remove[] = $table.'_';
// also remove 3 or 4 letter shortcuts of the table- or app-name
$remove[] = substr($table,0,3).'_';
$remove[] = substr($table,0,4).'_';
// if the table-name consists of '_' limtied parts, remove occurences of these parts too
foreach (explode('_',$table) as $part)
{
$remove[] = $part.'_';
}
$aColumnNames = str_replace($remove,'',$aColumnNames);
$name = $sTableName.'_'.(is_array($aColumnNames) ? implode('_',$aColumnNames) : $aColumnNames);
// remove length limits from column names
$name = preg_replace('/ *\(\d+\)/','',$name);
// this code creates a fixed short index-names (30 chars) from the long and unique name, eg. for MaxDB or Oracle
if (isset($this->max_index_length[$this->sType]) && $this->max_index_length[$this->sType] <= 32 && strlen($name) > 30 ||
strlen($name) >= 64) // even mysql has a limit here ;-)
{
$name = "i".substr(md5($name),0,29);
}
return $name;
}
/**
* Giving a non-fatal error-message
*/
function error($str)
{
echo "<p><b>Error:</b> $str</p>";
}
/**
* Giving a fatal error-message and exiting
*/
function fatal($str)
{
echo "<p><b>Fatal Error:</b> $str</p>";
exit;
}
/**
* Gives out a debug-message with certain parameters
*
* All permanent debug-messages in the calendar should be done by this function !!!
* (In future they may be logged or sent as xmlrpc-faults back.)
*
* Permanent debug-message need to make sure NOT to give secret information like passwords !!!
*
* This function do NOT honor the setting of the debug variable, you may use it like
* if ($this->debug > N) $this->debug_message('Error ;-)');
*
* The parameters get formated depending on their type.
*
* @param $msg string message with parameters/variables like lang(), eg. '%1'
* @param $backtrace include a function-backtrace, default True=On
* should only be set to False=Off, if your code ensures a call with backtrace=On was made before !!!
* @param $param mixed a variable number of parameters, to be inserted in $msg
* arrays get serialized with print_r() !
*/
function debug_message($msg,$backtrace=True)
{
for($i = 2; $i < func_num_args(); ++$i)
{
$param = func_get_arg($i);
if (is_null($param))
{
$param='NULL';
}
else
{
switch(gettype($param))
{
case 'string':
$param = "'$param'";
break;
case 'array':
case 'object':
list(,$content) = @each($param);
$do_pre = is_array($param) ? count($param) > 6 || is_array($content)&&count($content) : True;
$param = ($do_pre ? '<pre>' : '').print_r($param,True).($do_pre ? '</pre>' : '');
break;
case 'boolean':
$param = $param ? 'True' : 'False';
break;
}
}
$msg = str_replace('%'.($i-1),$param,$msg);
}
echo '<p>'.$msg."<br>\n".($backtrace ? 'Backtrace: '.function_backtrace(1)."</p>\n" : '');
}
/**
* Converts an eGW table-definition array into an ADOdb column-definition string
*
* @internal
* @param array $aTableDef eGW table-defintion
* @return string ADOdb column-definition string (comma separated)
*/
function _egw2adodb_columndef($aTableDef)
{
$ado_defs = array();
foreach($aTableDef['fd'] as $col => $col_data)
{
$ado_col = False;
switch($col_data['type'])
{
case 'auto':
$ado_col = 'I AUTOINCREMENT NOTNULL';
unset($col_data['nullable']); // else we set it twice
break;
case 'blob':
$ado_col = 'B';
break;
case 'bool':
$ado_col = 'L';
break;
case 'char':
// ADOdb does not differ between char and varchar
case 'varchar':
$ado_col = "C";
if(0 < $col_data['precision'] && $col_data['precision'] <= $this->max_varchar_length)
{
$ado_col .= "($col_data[precision])";
}
if($col_data['precision'] > $this->max_varchar_length)
{
$ado_col = 'X';
}
break;
case 'date':
$ado_col = 'D';
// allow to use now() beside current_date, as Postgres backups contain it and it's easier to remember anyway
if (in_array($col_data['default'],array('current_date','now()')))
{
$ado_col .= ' DEFDATE';
unset($col_data['default']);
}
break;
case 'decimal':
$ado_col = "N($col_data[precision].$col_data[scale])";
break;
case 'double':
case 'float':
// ADOdb does not differ between float and double
$ado_col = 'F';
break;
case 'int':
$ado_col = 'I';
switch($col_data['precision'])
{
case 1:
case 2:
case 4:
case 8:
$ado_col .= $col_data['precision'];
break;
}
break;
case 'longtext':
$ado_col = 'XL';
break;
case 'text':
$ado_col = 'X';
break;
case 'timestamp':
$ado_col = 'T';
// allow to use now() beside current_timestamp, as Postgres backups contain it and it's easier to remember anyway
if (in_array($col_data['default'],array('current_timestamp','now()')))
{
$ado_col .= ' DEFTIMESTAMP';
unset($col_data['default']);
}
break;
}
if (!$ado_col)
{
$this->error("Ignoring unknown column-type '$col_data[type]($col_data[precision])' !!!<br>".function_backtrace());
continue;
}
if (isset($col_data['nullable']) && !$col_data['nullable'])
{
$ado_col .= ' NOTNULL';
}
if (isset($col_data['default']))
{
$ado_col .= (in_array($col_data['type'],array('bool','int','decimal','float','double')) && $col_data['default'] != 'NULL' ? ' NOQUOTE' : '').
' DEFAULT '.$this->m_odb->quote($col_data['default'],$col_data['type']);
}
if (in_array($col,$aTableDef['pk']))
{
$ado_col .= ' PRIMARY';
}
$ado_defs[] = $col . ' ' . $ado_col;
}
//print_r($aTableDef); echo implode(",\n",$ado_defs)."\n";
return implode(",\n",$ado_defs);
}
/**
* Translates an eGW type into the DB's native type
*
* @param string $egw_type eGW name of type
* @param string/boolean DB's name of the type or false if the type could not be identified (should not happen)
*/
function TranslateType($egw_type)
{
$ado_col = $this->_egw2adodb_columndef(array(
'fd' => array('test' => array('type' => $egw_type)),
'pk' => array(),
));
return preg_match('/test ([A-Z0-9]+)/i',$ado_col,$matches) ? $this->dict->ActualType($matches[1]) : false;
}
/**
* Read the table-definition direct from the database
*
* The definition might not be as accurate, depending on the DB!
*
* @param string $sTableName table-name
* @return array/boolean table-defition, like $phpgw_baseline[$sTableName] after including tables_current, or false on error
*/
function GetTableDefinition($sTableName)
{
// MetaType returns all varchar >= blobSize as blob, it's by default 100, which is wrong
$this->dict->blobSize = $this->max_varchar_length;
if (!method_exists($this->dict,'MetaColumns') ||
!($columns = $this->dict->MetaColumns($sTableName)))
{
return False;
}
$definition = array(
'fd' => array(),
'pk' => array(),
'fk' => array(),
'ix' => array(),
'uc' => array(),
);
//echo "$sTableName: <pre>".print_r($columns,true)."</pre>";
foreach($columns as $column)
{
$name = $this->capabilities['name_case'] == 'upper' ? strtolower($column->name) : $column->name;
$type = method_exists($this->dict,'MetaType') ? $this->dict->MetaType($column) : strtoupper($column->type);
// fix longtext not correctly handled by ADOdb
if ($type == 'X' && $column->type == 'longtext') $type = 'XL';
static $ado_type2egw = array(
'C' => 'varchar',
'C2' => 'varchar',
'X' => 'text',
'X2' => 'text',
'XL' => 'longtext',
'B' => 'blob',
'I' => 'int',
'T' => 'timestamp',
'D' => 'date',
'F' => 'float',
'N' => 'decimal',
'R' => 'auto',
'L' => 'bool',
);
$definition['fd'][$name]['type'] = $ado_type2egw[$type];
switch($type)
{
case 'D': case 'T':
// detecting the automatic timestamps again
if ($column->has_default && preg_match('/(0000-00-00|timestamp)/i',$column->default_value))
{
$column->default_value = $type == 'D' ? 'current_date' : 'current_timestamp';
}
break;
case 'C': case 'C2':
$definition['fd'][$name]['type'] = 'varchar';
$definition['fd'][$name]['precision'] = $column->max_length;
break;
case 'B':
case 'X': case 'XL': case 'X2':
// text or blob's need to be nullable for most databases
$column->not_null = false;
break;
case 'F':
$definition['fd'][$name]['precision'] = $column->max_length;
break;
case 'N':
$definition['fd'][$name]['precision'] = $column->max_length;
$definition['fd'][$name]['scale'] = $column->scale;
break;
case 'R':
$column->auto_increment = true;
// fall-through
case 'I': case 'I1': case 'I2': case 'I4': case 'I8':
switch($type)
{
case 'I1': case 'I2': case 'I4': case 'I8':
$definition['fd'][$name]['precision'] = (int) $type[1];
break;
default:
if ($column->max_length > 11)
{
$definition['fd'][$name]['precision'] = 8;
}
elseif ($column->max_length > 6 || !$column->max_length)
{
$definition['fd'][$name]['precision'] = 4;
}
elseif ($column->max_length > 2)
{
$definition['fd'][$name]['precision'] = 2;
}
else
{
$definition['fd'][$name]['precision'] = 1;
}
break;
}
if ($column->auto_increment)
{
// no precision for auto!
$definition['fd'][$name] = array(
'type' => 'auto',
'nullable' => False,
);
$column->has_default = False;
$definition['pk'][] = $name;
}
else
{
$definition['fd'][$name]['type'] = 'int';
// detect postgres type-spec and remove it
if ($this->sType == 'pgsql' && $column->has_default && preg_match('/\(([^)])\)::/',$column->default_value,$matches))
{
$definition['fd'][$name]['default'] = $matches[1];
$column->has_default = False;
}
}
// fix MySQL stores bool columns as smallint
if ($this->sType == 'mysql' && $definition['fd'][$name]['precision'] == 1 &&
$this->m_odb->get_column_attribute($name, $sTableName, true, 'type') === 'bool')
{
$definition['fd'][$name]['type'] = 'bool';
unset($definition['fd'][$name]['precision']);
$column->default_value = (bool)$column->default_value;
}
break;
}
if ($column->has_default)
{
if (preg_match("/^'(.*)'::.*$/",$column->default_value,$matches)) // postgres
{
$column->default_value = $matches[1];
}
$definition['fd'][$name]['default'] = $column->default_value;
}
if ($column->not_null)
{
$definition['fd'][$name]['nullable'] = False;
}
if ($column->primary_key && !in_array($name,$definition['pk']))
{
$definition['pk'][] = $name;
}
}
if ($this->debug > 2) $this->debug_message("schema_proc::GetTableDefintion: MetaColumns(%1) = %2",False,$sTableName,$columns);
// not all DB's (odbc) return the primary keys via MetaColumns
if (!count($definition['pk']) && method_exists($this->dict,'MetaPrimaryKeys') &&
is_array($primary = $this->dict->MetaPrimaryKeys($sTableName)) && count($primary))
{
if($this->capabilities['name_case'] == 'upper')
{
array_walk($primary,create_function('&$s','$s = strtolower($s);'));
}
$definition['pk'] = $primary;
}
if ($this->debug > 1) $this->debug_message("schema_proc::GetTableDefintion: MetaPrimaryKeys(%1) = %2",False,$sTableName,$primary);
$this->GetIndexes($sTableName, $definition);
if ($this->debug > 1) $this->debug_message("schema_proc::GetTableDefintion(%1) = %2",False,$sTableName,$definition);
return $definition;
}
/**
* Query indexes (not primary index) from database
*
* @param string $sTableName
* @param array& $definition=array()
* @return array of arrays with keys 'ix' and 'uc'
*/
public function GetIndexes($sTableName, array &$definition=array())
{
if (method_exists($this->dict,'MetaIndexes') &&
is_array($indexes = $this->dict->MetaIndexes($sTableName)) && count($indexes))
{
foreach($indexes as $index_name => $index)
{
if($this->capabilities['name_case'] == 'upper')
{
array_walk($index['columns'],create_function('&$s','$s = strtolower($s);'));
}
if (count($definition['pk']) && (implode(':',$definition['pk']) == implode(':',$index['columns']) ||
$index['unique'] && count(array_intersect($definition['pk'],$index['columns'])) == count($definition['pk'])))
{
continue; // is already the primary key => ignore it
}
$kind = $index['unique'] ? 'uc' : 'ix';
$definition[$kind][$index_name] = count($index['columns']) > 1 ? $index['columns'] : $index['columns'][0];
}
if ($this->debug > 2) $this->debug_message("schema_proc::GetTableDefintion: MetaIndexes(%1) = %2",False,$sTableName,$indexes);
}
return $definition;
}
/**
* Get actual columnnames as a comma-separated string in $sColumns and set indices as class-vars pk,fk,ix,uc
*
* old translator function, use GetTableDefition() instead
* @depricated
*/
function _GetColumns($oProc,$sTableName,&$sColumns)
{
$this->sCol = $this->pk = $this->fk = $this->ix = $this->uc = array();
$tabledef = $this->GetTableDefinition($sTableName);
$sColumns = implode(',',array_keys($tabledef['fd']));
foreach($tabledef['fd'] as $column => $data)
{
$col_def = "'type' => '$data[type]'";
unset($data['type']);
foreach($data as $key => $val)
{
$col_def .= ", '$key' => ".(is_bool($val) ? ($val ? 'true' : 'false') :
(is_int($val) ? $val : "'$val'"));
}
$this->sCol[] = "\t\t\t\t'$column' => array($col_def),\n";
}
foreach(array('pk','fk','ix','uc') as $kind)
{
$this->$kind = $tabledef[$kind];
}
}
}