mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-23 08:23:12 +01:00
1167 lines
37 KiB
PHP
1167 lines
37 KiB
PHP
<?php
|
|
/**************************************************************************\
|
|
* eGroupWare - Setup - db-schema-processor *
|
|
* http://www.egroupware.org *
|
|
* -------------------------------------------- *
|
|
* Rewritten and adopted to ADOdb's data-dictionary by Ralf Becker *
|
|
* <RalfBecker@outdoor-training.de> *
|
|
* This file was originaly written by *
|
|
* - Michael Dean <mdean@users.sourceforge.net> *
|
|
* - Miles Lott<milosch@groupwhere.org> *
|
|
* -------------------------------------------- *
|
|
* This program is free software; you can redistribute it and/or modify it *
|
|
* under the terms of the GNU General Public License as published by the *
|
|
* Free Software Foundation; either version 2 of the License, or (at your *
|
|
* option) any later version. *
|
|
\**************************************************************************/
|
|
|
|
/* $Id$ */
|
|
|
|
/**
|
|
* eGW's ADOdb based schema-processor
|
|
*
|
|
* @class schema_proc
|
|
* @author RalfBecker-AT-outdoor-training.de and others
|
|
* @license GPL
|
|
*/
|
|
class schema_proc
|
|
{
|
|
var $m_oTranslator;
|
|
var $m_odb;
|
|
var $m_bDeltaOnly;
|
|
var $debug = 0; // 0=Off, 1=some, eg. primary function calls, 2=lots incl. the SQL used
|
|
var $max_index_length=array( // if known
|
|
'sapdb' => 32,
|
|
'oracle' => 30,
|
|
);
|
|
var $sType; // type of the database, set by the the constructor
|
|
var $max_varchar_length = 255; // maximum length of a varchar column, everything above get converted to text
|
|
|
|
/**
|
|
* Constructor of schema-processor
|
|
*
|
|
* @param string $dbms type of the database: 'mysql','pgsql','mssql','sapdb'
|
|
*/
|
|
function schema_proc($dbms=False)
|
|
{
|
|
$this->m_odb = is_object($GLOBALS['phpgw']->db) ? $GLOBALS['phpgw']->db : $GLOBALS['phpgw_setup']->db;
|
|
$this->m_odb->connect();
|
|
|
|
$this->sType = $dbms ? $dmbs : $this->m_odb->Type;
|
|
|
|
$this->adodb = &$GLOBALS['phpgw']->ADOdb;
|
|
$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 'sapdb':
|
|
case 'maxdb':
|
|
$this->max_varchar_length = 8000;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Created a table named $sTableName as defined in $aTableDef
|
|
*
|
|
* @param string $sTableName
|
|
* @param array $aTableDef
|
|
* @return int 2: no error, 1: errors, but continued, 0: errors aborted
|
|
*/
|
|
function CreateTable($sTableName, $aTableDef)
|
|
{
|
|
if ($this->debug)
|
|
{
|
|
$this->debug_message('schema_proc::CreateTable(%1,%2)',False,$sTableName, $aTableDef);
|
|
}
|
|
// creating the table
|
|
$aSql = $this->dict->CreateTableSQL($sTableName,$ado_cols = $this->_egw2adodb_columndef($aTableDef));
|
|
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 (is_numeric($name))
|
|
{
|
|
$name = $this->_index_name($sTableName,$mFields);
|
|
}
|
|
$aSql = $this->dict->CreateIndexSQL($name,$sTableName,$mFields,array('UNIQUE'));
|
|
if (!($retVal = $this->ExecuteSQLArray($aSql,2,'CreateIndexSql(%1,%2,%3,%4) sql=%5',False,$name,$sTableName,$mFields,array('UNIQUE'),$aSql)))
|
|
{
|
|
return $retVal;
|
|
}
|
|
}
|
|
// creation indices
|
|
foreach ($aTableDef['ix'] as $name => $mFields)
|
|
{
|
|
$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']);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 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 ($aTableDef['fd'][$mFields]['type'] == 'text')
|
|
{
|
|
if ($this->sType == 'mysql')
|
|
{
|
|
$options = 'FULLTEXT';
|
|
}
|
|
else
|
|
{
|
|
continue; // ignore that index
|
|
}
|
|
}
|
|
}
|
|
|
|
if (is_numeric($name))
|
|
{
|
|
$name = $this->_index_name($sTableName,$mFields);
|
|
}
|
|
$aSql = $this->dict->CreateIndexSQL($name,$sTableName,$mFields,array($options));
|
|
if (!($retVal = $this->ExecuteSQLArray($aSql,2,'CreateIndexSql(%1,%2,%3,%4) sql=%5',False,$name,$sTableName,$mFields,$options,$aSql)))
|
|
{
|
|
return $retVal;
|
|
}
|
|
}
|
|
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) || count($table_def['pk']) ||
|
|
count($table_def['ix']) || count($table_def['uc']))
|
|
{
|
|
if ($this->adodb->BeginTrans() &&
|
|
$this->CreateTable($sNewTableName,$table_def) &&
|
|
$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 boolean/string sequence-name or false
|
|
*/
|
|
function _PostgresHasOldSequence($sTableName)
|
|
{
|
|
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%' AND a.attrelid=c.oid AND d.adnum=a.attnum");
|
|
|
|
if ($seq && preg_match('/^nextval\(\'(.*)\'/',$seq,$matches))
|
|
{
|
|
|
|
return $matches[1];
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if we have an old, not automaticaly droped sequence and drop it
|
|
*
|
|
* @param $sTableName
|
|
*/
|
|
function _PostgresTestDropOldSequence($sTableName)
|
|
{
|
|
if ($this->sType == 'pgsql' && ($seq = $this->_PostgresHasOldSequence($sTableName)))
|
|
{
|
|
$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);
|
|
|
|
if (isset($table_def['fd'][$sOldColumnName]))
|
|
{
|
|
$old_def = $table_def['fd'][$sOldColumnName];
|
|
}
|
|
else
|
|
{
|
|
foreach($table_def['fd'] as $col => $def)
|
|
{
|
|
if (strlower($col) == strtolower($OldColumnName))
|
|
{
|
|
$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='')
|
|
{
|
|
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($name,$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\)/",$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)
|
|
{
|
|
if ($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;
|
|
// this is eg. necessary to change a varchar into an int column under postgres
|
|
if ($this->sType == 'pgsql' &&
|
|
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')";
|
|
}
|
|
}
|
|
else // new column => use default value or NULL
|
|
{
|
|
if (!isset($data['default']) && (!isset($data['nullable']) || $data['nullable']))
|
|
{
|
|
$value = 'NULL';
|
|
}
|
|
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("$extra INSERT INTO $sTableName (".
|
|
implode(',',array_keys($aTableDef['fd'])).
|
|
") SELEcT $distinct $select FROM $tmp_name",__LINE__,__FILE__) &&
|
|
$this->DropTable($tmp_name);
|
|
|
|
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 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 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)
|
|
{
|
|
return $this->m_odb->insert($table,$data,$where,$line,$file,$app);
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
$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_method_array('debug_message',$this,$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);
|
|
|
|
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';
|
|
if ($col_data['default'] == 'current_date')
|
|
{
|
|
$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';
|
|
if ($col_data['default'] == 'current_timestamp')
|
|
{
|
|
$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 .= " DEFAULT '$col_data[default]'";
|
|
}
|
|
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
|
|
if ($this->dict->blobSize < 255) $this->dict->blobSize = 255;
|
|
|
|
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)
|
|
{
|
|
switch($this->sType)
|
|
{
|
|
case 'sapdb':
|
|
case 'maxdb':
|
|
case 'oracle':
|
|
$name = strtolower($column->name);
|
|
break;
|
|
default:
|
|
$name = $column->name;
|
|
break;
|
|
}
|
|
|
|
$type = method_exists($this->dict,'MetaType') ? $this->dict->MetaType($column) : strtoupper($column->type);
|
|
|
|
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',
|
|
);
|
|
$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;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
if ($column->has_default)
|
|
{
|
|
$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))
|
|
{
|
|
switch($this->sType)
|
|
{
|
|
case 'sapdb':
|
|
case 'maxdb':
|
|
case 'oracle':
|
|
array_walk($primary,create_function('&$s','$s = strtolower($s);'));
|
|
break;
|
|
}
|
|
$definition['pk'] = $primary;
|
|
}
|
|
if ($this->debug > 1) $this->debug_message("schema_proc::GetTableDefintion: MetaPrimaryKeys(%1) = %2",False,$sTableName,$primary);
|
|
|
|
if (method_exists($this->dict,'MetaIndexes') &&
|
|
is_array($indexes = $this->dict->MetaIndexes($sTableName)) && count($indexes))
|
|
{
|
|
foreach($indexes as $index)
|
|
{
|
|
switch($this->sType)
|
|
{
|
|
case 'sapdb':
|
|
case 'maxdb':
|
|
case 'oracle':
|
|
array_walk($index['columns'],create_function('&$s','$s = strtolower($s);'));
|
|
break;
|
|
}
|
|
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][] = 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);
|
|
if ($this->debug > 1) $this->debug_message("schema_proc::GetTableDefintion(%1) = %2",False,$sTableName,$definition);
|
|
|
|
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];
|
|
}
|
|
}
|
|
}
|
|
?>
|