From 966fd66f217911d079c4bd6a87b09f4a0c5c4736 Mon Sep 17 00:00:00 2001 From: "godzilla80@gmx.net" <> Date: Sat, 13 Feb 2010 19:04:45 +0000 Subject: NEW: Add Beta of master/slave senario solution - add package System.Testing.Data.Analysis - add package System.Testing.Data.Distributed - add sub package System.Testing.Data.Distributed.MasterSlave - add unittest for System.Testing.Data.Analysis --- .../Testing/Data/Analysis/TDbStatementAnalysis.php | 238 ++++++++++ .../Data/Analysis/TSimpleDbStatementAnalysis.php | 134 ++++++ .../MasterSlave/TMasterSlaveDataSourceConfig.php | 154 +++++++ .../MasterSlave/TMasterSlaveDbConnection.php | 477 +++++++++++++++++++++ .../MasterSlave/TMasterSlaveDbTransaction.php | 83 ++++ .../Distributed/TDistributedDataSourceConfig.php | 181 ++++++++ .../Data/Distributed/TDistributedDbConnection.php | 189 ++++++++ 7 files changed, 1456 insertions(+) create mode 100644 framework/Testing/Data/Analysis/TDbStatementAnalysis.php create mode 100644 framework/Testing/Data/Analysis/TSimpleDbStatementAnalysis.php create mode 100644 framework/Testing/Data/Distributed/MasterSlave/TMasterSlaveDataSourceConfig.php create mode 100644 framework/Testing/Data/Distributed/MasterSlave/TMasterSlaveDbConnection.php create mode 100644 framework/Testing/Data/Distributed/MasterSlave/TMasterSlaveDbTransaction.php create mode 100644 framework/Testing/Data/Distributed/TDistributedDataSourceConfig.php create mode 100644 framework/Testing/Data/Distributed/TDistributedDbConnection.php (limited to 'framework/Testing') diff --git a/framework/Testing/Data/Analysis/TDbStatementAnalysis.php b/framework/Testing/Data/Analysis/TDbStatementAnalysis.php new file mode 100644 index 00000000..46f9b745 --- /dev/null +++ b/framework/Testing/Data/Analysis/TDbStatementAnalysis.php @@ -0,0 +1,238 @@ + + * @link http://www.pradosoft.com/ + * @copyright Copyright © 2005-2010 PradoSoft + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @package System.Testing.Data.Analysis + */ + + /** + * IDbStatementAnalysis interface + * + * @author Yves Berkholz + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @package System.Testing.Data.Analysis + * @since 4.0 + */ + interface IDbStatementAnalysis + { + /** + * @param TDbStatementAnalysisParamete + * @return TDbStatementClassification + */ + public static function doClassificationAnalysis(TDbStatementAnalysisParameter $param); + + /** + * @param TDbStatementAnalysisParameter + * @return TDbStatementClassification + */ + public function getClassificationAnalysis(TDbStatementAnalysisParameter $param); + + /** + * @param string PDO drivername of connection + */ + public function setDriverName($value); + + /** + * @return string PDO drivername of connection + */ + public function getDriverName(); + } + + /** + * TDbStatementAnalysisParameter class + * + * + * @author Yves Berkholz + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @package System.Testing.Data.Analysis + * @since 4.0 + */ + class TDbStatementAnalysisParameter + { + /** + * @var string The SQL statement that should be analysed + */ + protected $_statement = null; + + /** + * TDbStatementClassification Defaults to 'UNKNOWN' + */ + protected $_defaultClassification = TDbStatementClassification::UNKNOWN; + + /** + * string|null PDO drivername of connection + */ + protected $_drivername = null; + + /** + * @param string The SQL statement that should be analysed + * @param TDbStatementClassification + * @param string|null PDO drivername of connection + */ + public function __construct($statement='', $defaultClassification=null, $drivername=null) + { + $this->setStatement($statement); + $this->setDefaultClassification($defaultClassification); + $this->setDriverName($drivername); + } + + /** + * @param string The SQL statement that should be analysed + */ + public function setStatement($value) + { + $this->_statement = (string)$value; + } + + /** + * @return string The SQL statement that should be analysed + */ + public function getStatement() + { + return $this->_statement; + } + + /** + * @param string|null PDO drivername of connection + */ + public function setDriverName($value) + { + $this->_drivername = ($value===null) ? null : (string)$value; + } + + /** + * @return string|null PDO drivername of connection + */ + public function getDriverName() + { + return $this->_drivername; + } + + /** + * @param TDbStatementClassification Defaults to 'UNKNOWN' + */ + public function setDefaultClassification($value) + { + if($value!==null) + $this->_defaultClassification = (string)$value; + else + $this->_defaultClassification = TDbStatementClassification::UNKNOWN; + } + + /** + * @return TDbStatementClassification + */ + public function getDefaultClassification() + { + return $this->_defaultClassification; + } + } + + /** + * TDbStatementAnalysis class + * + * Basic "dummy" implementation allways return {@link TDbStatementAnalysisParameter::getDefaultClassification DefaultClassification} + * + * @author Yves Berkholz + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @package System.Testing.Data.Analysis + * @since 4.0 + */ + class TDbStatementAnalysis implements IDbStatementAnalysis + { + /** + * @var string|null PDO drivername of connection + */ + protected $_drivername = null; + + /** + * @param string|null PDO drivername of connection + */ + public function setDriverName($value) + { + $this->_drivername = ($value===null) ? null : (string)$value; + } + + /** + * @return string|null PDO drivername of connection + */ + public function getDriverName() + { + return $this->_drivername; + } + + /** + * @param TDbStatementAnalysisParamete + * @return TDbStatementClassification + */ + public static function doClassificationAnalysis(TDbStatementAnalysisParameter $param) + { + return $param->getDefaultClassification(); + } + + /** + * @param TDbStatementAnalysisParameter + * @return TDbStatementClassification + */ + public function getClassificationAnalysis(TDbStatementAnalysisParameter $param) + { + return $param->getDefaultClassification(); + } + } + + /** + * TDbStatementClassification + * + * @author Yves Berkholz + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @package System.Testing.Data.Analysis + * @since 4.0 + */ + class TDbStatementClassification extends TEnumerable + { + /** + * Structured Query Language + */ + const SQL = 'SQL'; + + /** + * Data Definition Language + */ + const DDL = 'DDL'; + + /** + * Data Manipulation Language + */ + const DML = 'DML'; + + /** + * Data Control Language + */ + const DCL = 'DCL'; + + /** + * Transaction Control Language + */ + const TCL = 'TCL'; + + /** + * classification depends on subsequent statement(s) + */ + const CONTEXT = 'CONTEXT'; + + /** + * unable to detect real classification or multiple possibilities + */ + const UNKNOWN = 'UNKNOWN'; + } +?> diff --git a/framework/Testing/Data/Analysis/TSimpleDbStatementAnalysis.php b/framework/Testing/Data/Analysis/TSimpleDbStatementAnalysis.php new file mode 100644 index 00000000..e4ab3391 --- /dev/null +++ b/framework/Testing/Data/Analysis/TSimpleDbStatementAnalysis.php @@ -0,0 +1,134 @@ + + * @link http://www.pradosoft.com/ + * @copyright Copyright © 2005-2010 PradoSoft + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @package System.Testing.Data.Analysis + */ + + Prado::using('System.Testing.Data.Analysis.TDbStatementAnalysis'); + + /** + * TSimpleDbStatementAnalysis class + * + * IMPORTANT!!! + * BETA Version - Use with care and NOT in production environment (only tested with MySql) + * + * @author Yves Berkholz + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @package System.Testing.Data.Analysis + * @since 4.0 + * @todo SELECT * FOR UPDATE (row lock) + * @todo SELECT * INTO [ TEMPORARY | TEMP ] [ TABLE ] new_table (PostgreSQL) + * @todo mysql conditional commands in multiline comments e.g. / *! MySQL specific code * / + */ + class TSimpleDbStatementAnalysis extends TDbStatementAnalysis + { + /** + * @var array mapping of commands to classification + */ + protected static $mappingClassificationAnalysis = array( + 'CREATE' => TDbStatementClassification::DDL, + 'DROP' => TDbStatementClassification::DDL, + 'ALTER' => TDbStatementClassification::DDL, + 'RENAME' => TDbStatementClassification::DDL, + + 'INSERT' => TDbStatementClassification::DML, + 'UPDATE' => TDbStatementClassification::DML, + 'DELETE' => TDbStatementClassification::DML, + 'REPLACE' => TDbStatementClassification::DML, + 'TRUNCATE' => TDbStatementClassification::DML, + 'LOAD' => TDbStatementClassification::DML, + + 'GRANT' => TDbStatementClassification::DCL, + 'REVOKE' => TDbStatementClassification::DCL, + + 'XA' => TDbStatementClassification::TCL, + 'SAVEPOINT' => TDbStatementClassification::TCL, + 'CHECKPOINT' => TDbStatementClassification::TCL, + 'RELEASE SAVEPOINT' => TDbStatementClassification::TCL, + 'START TRANSACTION' => TDbStatementClassification::TCL, + 'BEGIN' => TDbStatementClassification::TCL, + 'COMMIT' => TDbStatementClassification::TCL, + 'ROLLBACK' => TDbStatementClassification::TCL, + 'LOCK' => TDbStatementClassification::TCL, + 'UNLOCK' => TDbStatementClassification::TCL, + 'ABORT' => TDbStatementClassification::TCL, + 'END' => TDbStatementClassification::TCL, + + 'SELECT' => TDbStatementClassification::SQL, + + 'SHOW' => TDbStatementClassification::SQL, + 'DESCRIBE' => TDbStatementClassification::SQL, + 'EXPLAIN' => TDbStatementClassification::SQL, + 'PRAGMA' => TDbStatementClassification::SQL, + + 'SET' => TDbStatementClassification::CONTEXT, + 'USE' => TDbStatementClassification::CONTEXT, + + 'CALL' => TDbStatementClassification::UNKNOWN, + 'EXEC' => TDbStatementClassification::UNKNOWN, + 'PREPARE' => TDbStatementClassification::UNKNOWN, + 'EXECUTE' => TDbStatementClassification::UNKNOWN, + 'DEALLOCATE' => TDbStatementClassification::UNKNOWN, + ); + + /** + * @var array + */ + protected static $cacheClassificationAnalysis = array(); + + /** + * @var string + */ + protected static $regExpClassificationAnalysis = null; + + /** + * @param TDbStatementAnalysisParamete + * @return TDbStatementClassification + */ + public static function doClassificationAnalysis(TDbStatementAnalysisParameter $param) + { + $statement = $param->getStatement(); + $default = $param->getDefaultClassification(); + + $hash = md5($statement . '-' . $default); + + if( isset(self::$cacheClassificationAnalysis[$hash]) ) + return self::$cacheClassificationAnalysis[$hash]; + + self::$cacheClassificationAnalysis[$hash] = $default; + + $statement = preg_replace('/(?:--|\\#)[\x20\\t\\S]*\s+|\/\\*[\x20\\t\\n\\r\\S]*?\\*\//Ssmux', '', $statement); + $statement = preg_replace('/[\s]+/Smu', ' ', $statement); + $statement = trim($statement); + + if(self::$regExpClassificationAnalysis===null) + self::$regExpClassificationAnalysis = '/^(' . str_replace(' ', '\x20', implode('|', array_keys(self::$mappingClassificationAnalysis))) . ')+[\s]+.*|\k1/Siu'; + + $cmd = strToUpper(preg_replace(self::$regExpClassificationAnalysis, '\1', $statement)); + + if( isset(self::$mappingClassificationAnalysis[$cmd]) ) + self::$cacheClassificationAnalysis[$hash] = self::$mappingClassificationAnalysis[$cmd]; + + return self::$cacheClassificationAnalysis[$hash]; + } + + /** + * @param TDbStatementAnalysisParameter + * @return TDbStatementClassification + */ + public function getClassificationAnalysis(TDbStatementAnalysisParameter $param) + { + if( ($drivername = $this->getDriverName())!== null ) + $param->setDriverName($drivername); + + return self::doClassificationAnalysis($param); + } + } +?> \ No newline at end of file diff --git a/framework/Testing/Data/Distributed/MasterSlave/TMasterSlaveDataSourceConfig.php b/framework/Testing/Data/Distributed/MasterSlave/TMasterSlaveDataSourceConfig.php new file mode 100644 index 00000000..ee486fe8 --- /dev/null +++ b/framework/Testing/Data/Distributed/MasterSlave/TMasterSlaveDataSourceConfig.php @@ -0,0 +1,154 @@ + + * @link http://www.pradosoft.com/ + * @copyright Copyright © 2005-2010 PradoSoft + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @package System.Testing.Data.Distributed.MasterSlave + */ + + Prado::using('System.Testing.Data.Distributed.TDistributedDataSourceConfig'); + Prado::using('System.Testing.Data.Distributed.MasterSlave.TMasterSlaveDbConnection'); + + /** + * TMasterSlaveDataSourceConfig module class provides configuration for database connections in master/slave senario. + * + * IMPORTANT!!! + * BETA Version - Use with care and NOT in production environment (only tested with MySql) + * + * Example usage: mysql connection + * + * + * + * + * + * + * + * + * + * + * + * @author Yves Berkholz + * @version $Id$ + * @package System.Testing.Data.Distributed.MasterSlave + * @since 4.0 + */ + class TMasterSlaveDataSourceConfig extends TDistributedDataSourceConfig + { + /** + * @var boolean + */ + private $_bMasterInitialized = false; + + /** + * @var boolean + */ + private $_bSlaveInitialized = false; + + /** + * Constructor + */ + public function __construct() + { + $this->setConnectionClass('System.Testing.Data.Distributed.MasterSlave.TMasterSlaveDbConnection'); + $this->setDistributedConnectionClass('System.Testing.Data.Distributed.MasterSlave.TSlaveDbConnection'); + } + + /** + * Initalize the database connection properties from attributes in slave tag. + * @param TXmlDocument xml configuration. + */ + protected function initChildConnectionData($xml) + { + parent::initChildConnectionData($xml, 'slave'); + } + + /** + * @return IMasterSlaveDbConnection + */ + public function getDbConnection() { + $id = $this->getID(); + static $result = array(); + + if(!isset($result[$id])) + $result[$id] = parent::getDbConnection(); + + if(!$this->bInitialized) + return $result[$id]; + + if($this->_bMasterInitialized) + return $result[$id]; + + $this->_bMasterInitialized = true; + + if(!$result[$id] instanceof IMasterSlaveDbConnection) + return $result[$id]; + + $slave = parent::getDistributedDbConnection(); + + if($slave instanceof ISlaveDbConnection && $slave->getMasterConnection()===null) + $slave->setMasterConnection($result[$id]); + + if($result[$id]->getSlaveConnection()===null) + $result[$id]->setSlaveConnection($slave); + + return $result[$id]; + } + + /** + * @return ISlaveDbConnection + */ + public function getDistributedDbConnection() { + $id = $this->getID(); + static $result = array(); + + if(!isset($result[$id])) + $result[$id] = parent::getDistributedDbConnection(); + + if(!$this->bInitialized) + return $result[$id]; + + if($this->_bSlaveInitialized) + return $result[$id]; + + $this->_bSlaveInitialized = true; + + if(!$result[$id] instanceof ISlaveDbConnection) + return $result[$id]; + + $master = parent::getDbConnection(); + + if($master instanceof IMasterSlaveDbConnection && ($master->getSlaveConnection()===null)) + $master->setSlaveConnection($result[$id]); + + if($result[$id]->getMasterConnection()===null) + $result[$id]->setMasterConnection($master); + + return $result[$id]; + } + + /** + * Alias for getDbConnection(). + * @return IMasterSlaveDbConnection database connection. + */ + public function getMasterDbConnection() + { + return $this->getDbConnection(); + } + + /** + * Alias for getDistributedDbConnection(). + * @return ISlaveDbConnection database connection. + */ + public function getSlaveDbConnection() + { + return $this->getDistributedDbConnection(); + } + } +?> \ No newline at end of file diff --git a/framework/Testing/Data/Distributed/MasterSlave/TMasterSlaveDbConnection.php b/framework/Testing/Data/Distributed/MasterSlave/TMasterSlaveDbConnection.php new file mode 100644 index 00000000..e6b71931 --- /dev/null +++ b/framework/Testing/Data/Distributed/MasterSlave/TMasterSlaveDbConnection.php @@ -0,0 +1,477 @@ + + * @link http://www.pradosoft.com/ + * @copyright Copyright © 2005-2010 PradoSoft + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @package System.Testing.Data.Distributed.MasterSlave + * @todo Test with Backport of Yii's DBO + */ + + Prado::using('System.Data.TDbConnection'); + Prado::using('System.Testing.Data.Distributed.TDistributedDbConnection'); + Prado::using('System.Collections.TQueue'); + Prado::using('System.Collections.TStack'); + Prado::using('System.Testing.Data.Distributed.MasterSlave.TMasterSlaveDbTransaction'); + + /** + * IMasterSlaveDbConnection interface + * + * @author Yves Berkholz + * @version $Id$ + * @package System.Testing.Data.Distributed.MasterSlave + * @since 4.0 + */ + interface IMasterSlaveDbConnection extends IDistributedDbConnection + { + /** + * @return TQueue + */ + public function getStatementQueue(); + + /** + * @return ISlaveDbConnection|null + */ + public function getSlaveConnection(); + + /** + * @param ISlaveDbConnection|null + */ + public function setSlaveConnection($conn); + + /** + * @return TMasterSlaveDbConnectionForceMaster + */ + public function getForceMaster(); + + /** + * @param TMasterSlaveDbConnectionForceMaster + */ + public function setForceMaster($value); + + } + + /** + * ISlaveDbConnection interface + * + * @author Yves Berkholz + * @version $Id$ + * @package System.Testing.Data.Distributed.MasterSlave + * @since 4.0 + */ + interface ISlaveDbConnection extends IDistributedDbConnection + { + /** + * @return IMasterSlaveDbConnection|null + */ + public function getMasterConnection(); + + /** + * @param IMasterSlaveDbConnection|null + */ + public function setMasterConnection($conn); + + /** + * @return TMasterSlaveDbConnectionForceMaster + */ + public function getForceMaster(); + + /** + * @param TMasterSlaveDbConnectionForceMaster + */ + public function setForceMaster($value); + } + + /** + * TMasterSlaveDbConnection class + * + * IMPORTANT!!! + * BETA Version - Use with care and NOT in production environment (only tested with MySql) + * + * TMasterSlaveDbConnection represents a master connection to a database in master/slave senario. + * + * @author Yves Berkholz + * @version $Id$ + * @package System.Testing.Data.Distributed.MasterSlave + * @since 4.0 + */ + class TMasterSlaveDbConnection extends TDistributedDbConnection implements IMasterSlaveDbConnection + { + /** + * @var TSlaveDbConnection|null + */ + private $_connSlave = null; + + /** + * @var TQueue + */ + private $_statementQueue = null; + + /** + * @var integer + * @see TMasterSlaveDbConnectionForceMaster + */ + private $_forceMaster = TMasterSlaveDbConnectionForceMaster::OFF_AUTOMATIC; + + private $_forceMasterStack = null; + + /** + * Constructor. + * Note, the DB connection is not established when this connection + * instance is created. Set {@link setActive Active} property to true + * to establish the connection. + * + * @param string The Data Source Name, or DSN, contains the information required to connect to the database. + * @param string The user name for the DSN string. + * @param string The password for the DSN string. + * @param string Charset used for DB Connection (MySql & pgsql only). If not set, will use the default charset of your database server + * @see http://www.php.net/manual/en/function.PDO-construct.php + */ + public function __construct($dsn='', $username='', $password='', $charset='') + { + parent::__construct($dsn, $username, $password, $charset); + parent::setTransactionClass('System.Testing.Data.Distributed.MasterSlave.TMasterSlaveDbTransaction'); + } + + /** + * @return TQueue + */ + public function getStatementQueue() + { + if($this->_statementQueue===null) + $this->_statementQueue = new TQueue(); + return $this->_statementQueue; + } + + /** + * @return ISlaveDbConnection|null + */ + public function getSlaveConnection() + { + return $this->_connSlave; + } + + /** + * @param ISlaveDbConnection|null + * @throws TDbConnectionException if the slave connection already exists + * @throws TDbConnectionException connection not instance of ISlaveDbConnection + */ + public function setSlaveConnection($conn) + { + if($this->_connSlave !== null) + throw new TDbConnectionException('masterslavedbconnection_connection_exists', get_class($this), 'SlaveConnection'); + + if($conn!==null && !$conn instanceof ISlaveDbConnection) + throw new TDbConnectionException('masterslavedbconnection_interface_required', get_class($this), 'SlaveConnection', 'ISlaveDbConnection'); + + $this->_connSlave = $conn; + + if($this->_connSlave===null) return; + if($this->_connSlave->getMasterConnection()!==null) return; + + $this->_connSlave->setMasterConnection($this); + } + + /** + * Creates a command for execution. + * @param string SQL statement associated with the new command. + * @return TDistributedDbCommand the DB command + * @throws TDbException if the connection is not active + */ + public function createCommand($sql) + { + $force = $this->getForceMaster(); + if($force == TMasterSlaveDbConnectionForceMaster::ON_MANUAL || $force == TMasterSlaveDbConnectionForceMaster::ON_TRANSACTION) + { + Prado::log('ForceMaster: ' . $force, TLogger::DEBUG, 'System.Testing.Data.Distributed.MasterSlave.TMasterSlaveDbConnection'); + return new TDistributedDbCommand($this, $sql, TDbStatementClassification::UNKNOWN); + } + + $bEnqueue = false; + $bMaster = true; + + $classification = $this->getStatementClassification($sql); + + switch($classification) { + case TDbStatementClassification::CONTEXT: + $bEnqueue = true; + $bMaster = true; + break; + case TDbStatementClassification::SQL: + $bMaster = false; + break; + case TDbStatementClassification::TCL: + $this->setForceMaster(TMasterSlaveDbConnectionForceMaster::ON_TCL); + case TDbStatementClassification::DDL: + case TDbStatementClassification::DML: + case TDbStatementClassification::DCL: + case TDbStatementClassification::UNKNOWN: + default: + $bMaster = true; + break; + } + + $bMaster = $bMaster || $this->getForceMaster(); + + $result = new TDistributedDbCommand(($bMaster ? $this : $this->getSlaveConnection()), $sql, $classification); + //$result = new TDistributedDbCommand($this, $sql, $classification); + + if($bEnqueue) + $this->getStatementQueue()->enqueue($result); + + return $result; + } + + /** + * @return TMasterSlaveDbConnectionForceMaster + */ + public function getForceMaster() + { + return $this->_forceMaster; + } + + /** + * @param TMasterSlaveDbConnectionForceMaster + */ + public function setForceMaster($value) + { + if($this->_forceMasterStack===null) + $this->_forceMasterStack = new TStack(); + + if($value) + { + $this->_forceMaster = (integer)$value; + $this->_forceMasterStack->push((integer)$value); + } + elseif($this->_forceMasterStack->count() > 0) + $this->_forceMaster = $this->_forceMasterStack->pop(); + else + $this->_forceMaster = (integer)$value; + } + + /** + * @return TDbConnectionServerRole + */ + public function getServerRole() + { + return TDbConnectionServerRole::Master; + } + } + + /** + * TSlaveDbConnection class + * + * IMPORTANT!!! + * BETA Version - Use with care and NOT in production environment (only tested with MySql) + * + * TSlaveDbConnection represents a readonly connection to a database in master/slave senario. + * + * @author Yves Berkholz + * @version $Id$ + * @package System.Testing.Data.Distributed.MasterSlave + * @since 4.0 + */ + class TSlaveDbConnection extends TDbConnection implements ISlaveDbConnection + { + /** + * @var TMasterSlaveDbConnection|null + */ + private $_connMaster = null; + + /** + * @return IMasterSlaveDbConnection|null + */ + public function getMasterConnection() + { + return $this->_connMaster; + } + + /** + * @param IMasterSlaveDbConnection|null + * @throws TDbConnectionException if the master connection already exists + * @throws TDbConnectionException connection not instance of IMasterSlaveDbConnection + */ + public function setMasterConnection($conn) + { + if($this->_connMaster!==null) + throw new TDbConnectionException('masterslavedbconnection_connection_exists', get_class($this), 'MasterConnection'); + + if($conn!==null && !$conn instanceof IMasterSlaveDbConnection) + throw new TDbConnectionException('masterslavedbconnection_interface_required', get_class($this), 'MasterConnection', 'IMasterSlaveDbConnection'); + + $this->_connMaster = $conn; + } + + /** + * Creates a command for execution. + * @param string SQL statement associated with the new command. + * @return TDistributedDbCommand the DB command + * @throws TDbException if the connection is not active + */ + public function createCommand($sql) + { + $force = $this->getForceMaster(); + if($force == TMasterSlaveDbConnectionForceMaster::ON_MANUAL || $force == TMasterSlaveDbConnectionForceMaster::ON_TRANSACTION) + { + Prado::log('ForceMaster: ' . $force, TLogger::DEBUG, 'System.Testing.Data.Distributed.MasterSlave.TSlaveDbConnection'); + return new TDistributedDbCommand($this->getMasterConnection(), $sql, TDbStatementClassification::UNKNOWN); + } + + $bEnqueue = false; + $bMaster = false; + + $classification = $this->getStatementClassification($sql); + + switch($classification) { + case TDbStatementClassification::SQL: + $bMaster = false; + break; + case TDbStatementClassification::CONTEXT: + $bEnqueue = true; + $bMaster = true; + break; + case TDbStatementClassification::TCL: + $this->setForceMaster(TMasterSlaveDbConnectionForceMaster::ON_TCL); + case TDbStatementClassification::DDL: + case TDbStatementClassification::DML: + case TDbStatementClassification::DCL: + case TDbStatementClassification::UNKNOWN: + default: + $bMaster = true; + break; + } + + $bMaster = $bMaster || $this->getForceMaster(); + + $result = new TDistributedDbCommand(($bMaster ? $this->getMasterConnection() : $this), $sql, $classification); + + if($bEnqueue) + $this->getMasterConnection()->getStatementQueue()->enqueue($result); + + return $result; + } + + /** + * Starts a transaction. + * @return TDbTransaction the transaction initiated + * @throws TDbException if no master connection exists or the connection is not active + */ + public function beginTransaction() + { + if($this->getMasterConnection() === null) + throw new TDbException('slavedbconnection_requires_master', getclass($this), 'MasterConnection'); + + return $this->getMasterConnection()->beginTransaction(); + } + + /** + * @return string Transaction class name to be created by calling {@link TDbConnection::beginTransaction}. + * @throws TDbException if no master connection exists + */ + public function getTransactionClass() + { + if($this->getMasterConnection() === null) + throw new TDbException('slavedbconnection_requires_master', getclass($this), 'MasterConnection'); + return $this->getMasterConnection()->getTransactionClass(); + } + + /** + * @param string Transaction class name to be created by calling {@link TDbConnection::beginTransaction}. + * @throws TDbException if no master connection exists + */ + public function setTransactionClass($value) + { + if($this->getMasterConnection() === null) + throw new TDbException('slavedbconnection_requires_master', getclass($this), 'MasterConnection'); + $this->getMasterConnection()->setTransactionClass($value); + } + + /** + * Gets the statement analyser of type given by + * {@link setStatementAnalyserClass StatementAnalyserClass }. + * @return IDbStatementAnalysis statement analyser. + * @throws TDbException if no master connection exists + */ + public function getStatementAnalyser() + { + if($this->getMasterConnection() === null) + throw new TDbException('slavedbconnection_requires_master', getclass($this), 'MasterConnection'); + return $this->getMasterConnection()->getStatementAnalyser(); + } + + /** + * The statement analyser class name to be created when {@link getStatementAnalyserClass} + * method is called. The {@link setStatementAnalyserClass StatementAnalyserClass} + * property must be set before calling {@link getStatementAnalyser} if you wish to + * create the connection using the given class name. + * @param string Statement analyser class name. + * @throws TDbException if no master connection exists + */ + public function setStatementAnalyserClass($value) + { + if($this->getMasterConnection() === null) + throw new TDbException('slavedbconnection_requires_master', getclass($this), 'MasterConnection'); + $this->getMasterConnection()->setStatementAnalyserClass($value); + } + + /** + * @param string Statement analyser class name to be created. + * @throws TDbException if no master connection exists + */ + public function getStatementAnalyserClass() + { + if($this->getMasterConnection() === null) + throw new TDbException('slavedbconnection_requires_master', getclass($this), 'MasterConnection'); + return $this->getMasterConnection()->getStatementAnalyserClass(); + } + + /** + * @return TMasterSlaveDbConnectionForceMaster + * @throws TDbException if no master connection exists + */ + public function getForceMaster() + { + if($this->getMasterConnection() === null) + throw new TDbException('slavedbconnection_requires_master', getclass($this), 'MasterConnection'); + return $this->getMasterConnection()->getForceMaster(); + } + + /** + * @param TMasterSlaveDbConnectionForceMaster + * @throws TDbException if no master connection exists + */ + public function setForceMaster($value) + { + if($this->getMasterConnection() === null) + throw new TDbException('slavedbconnection_requires_master', getclass($this), 'MasterConnection'); + $this->getMasterConnection()->setForceMaster($value); + } + + /** + * @return TDbConnectionServerRole + */ + public function getServerRole() + { + return TDbConnectionServerRole::Slave; + } + } + + /** + * TMasterSlaveDbConnectionForceMaster class + * + * @author Yves Berkholz + * @version $Id$ + * @package System.Testing.Data.Distributed.MasterSlave + * @since 4.0 + */ + class TMasterSlaveDbConnectionForceMaster extends TEnumerable + { + const OFF_AUTOMATIC = 0; + const ON_MANUAL = 1; + const ON_TCL = -1; + const ON_TRANSACTION = -2; + } diff --git a/framework/Testing/Data/Distributed/MasterSlave/TMasterSlaveDbTransaction.php b/framework/Testing/Data/Distributed/MasterSlave/TMasterSlaveDbTransaction.php new file mode 100644 index 00000000..254afdb5 --- /dev/null +++ b/framework/Testing/Data/Distributed/MasterSlave/TMasterSlaveDbTransaction.php @@ -0,0 +1,83 @@ + + * @link http://www.pradosoft.com/ + * @copyright Copyright © 2005-2010 PradoSoft + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @package System.Testing.Data.Distributed.MasterSlave + */ + Prado::using('System.Data.TDbTransaction'); + + /** + * TMasterSlaveDbTransaction class + * + * IMPORTANT!!! + * BETA Version - Use with care and NOT in production environment (only tested with MySql) + * + * TMasterSlaveDbTransaction represents a DB transaction in master/slave senario. + * + * @author Yves Berkholz + * @version $Id$ + * @package System.Testing.Data.Distributed.MasterSlave + * @since 4.0 + */ + class TMasterSlaveDbTransaction extends TDbTransaction + { + /** + * @var boolean + */ + private $_compatible = false; + + /** + * Constructor. + * @param TDbConnection the connection associated with this transaction + * @see TDbConnection::beginTransaction + */ + public function __construct(TDbConnection $connection) + { + if($connection instanceof ISlaveDbConnection) + { + $this->_compatible = true; + $master = $connection->getMasterConnection(); + $master->setForceMaster(TMasterSlaveDbConnectionForceMaster::ON_TRANSACTION); + Prado::log('contstuct, ForceMaster: ON_TRANSACTION', TLogger::DEBUG, 'System.Testing.Data.Distributed.MasterSlave.TMasterSlaveDbTransaction'); + parent::__construct($master); + } + else + { + if($connection instanceof IMasterSlaveDbConnection) + { + $this->_compatible = true; + $connection->setForceMaster(TMasterSlaveDbConnectionForceMaster::ON_TRANSACTION); + Prado::log('contstuct, ForceMaster: ON_TRANSACTION', TLogger::DEBUG, 'System.TestingData.Distributed.MasterSlave.TMasterSlaveDbTransaction'); + } + parent::__construct($connection); + } + } + + /** + * Commits a transaction. + * @throws TDbException if the transaction or the DB connection is not active. + */ + public function commit() + { + if($this->_compatible) $this->getConnection()->setForceMaster(TMasterSlaveDbConnectionForceMaster::OFF_AUTOMATIC); + Prado::log('commit, ForceMaster: OFF_AUTOMATIC', TLogger::DEBUG, 'System.Testing.Data.Distributed.MasterSlave.TMasterSlaveDbTransaction'); + parent::commit(); + } + + /** + * Rolls back a transaction. + * @throws TDbException if the transaction or the DB connection is not active. + */ + public function rollback() + { + if($this->_compatible) $this->getConnection()->setForceMaster(TMasterSlaveDbConnectionForceMaster::OFF_AUTOMATIC); + Prado::log('rollback, ForceMaster: OFF_AUTOMATIC', TLogger::DEBUG, 'System.Testing.Data.Distributed.MasterSlave.TMasterSlaveDbTransaction'); + parent::rollback(); + } + } +?> \ No newline at end of file diff --git a/framework/Testing/Data/Distributed/TDistributedDataSourceConfig.php b/framework/Testing/Data/Distributed/TDistributedDataSourceConfig.php new file mode 100644 index 00000000..b8c4e5a9 --- /dev/null +++ b/framework/Testing/Data/Distributed/TDistributedDataSourceConfig.php @@ -0,0 +1,181 @@ + + * @link http://www.pradosoft.com/ + * @copyright Copyright © 2005-2010 PradoSoft + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @package System.Testing.Data.Distributed + */ + + Prado::using('System.Data.TDataSourceConfig'); + Prado::using('System.Testing.Data.Distributed.TDistributedDbConnection'); + + /** + * IDistributedDataSourceConfig module interface provides configuration for database connections. + * + * @author Yves Berkholz + * @version $Id$ + * @package System.Testing.Data.Distributed + * @since 4.0 + */ + interface IDistributedDataSourceConfig /*extends IDataSourceConfig*/ { + /** + * @return string Database connection class name to be created for child connection. + */ + public function getDistributedConnectionClass(); + + /** + * @param string Database connection class name to be created for child connection. + */ + public function setDistributedConnectionClass($value); + } + + /** + * TDistributedDataSourceConfig module class provides configuration for database connections. + * + * @author Yves Berkholz + * @version $Id$ + * @package System.Testing.Data.Distributed + * @since 4.0 + */ + class TDistributedDataSourceConfig extends TDataSourceConfig implements IDistributedDataSourceConfig + { + /** + * @var array + */ + private $_childConnectionAttributes = array(); + + /** + * @var string + */ + private $_connDistributedClass = 'System.Data.TDbConnection'; + + /** + * @var IDistributedDbConnection + */ + private $_connDistributed = null; + + /** + * @var boolean + */ + protected $bInitialized = false; + + /** + * @var boolean + */ + private $_hasDistributedConnectionData = false; + + /** + * Initalize the database connection properties from attributes in tag. + * @param TXmlDocument xml configuration. + */ + public function init($xml) + { + parent::init($xml); + $this->initChildConnectionData($xml); + $this->bInitialized = true; + } + + /** + * Initalize the database connection properties from attributes in $tagName tag. + * @param TXmlDocument xml configuration. + * @param string Tagnames to parse. Defaults to 'child' + */ + protected function initChildConnectionData($xml, $tagName='child') + { + $c = 0; + foreach($xml->getElementsByTagName($tagName) as $item) + { + ++$c; + $this->_childConnectionAttributes[] = $item->getAttributes(); + } + + if($c===0) + throw new TConfigurationException('distributeddatasource_child_required', get_class($this), $tagName); + } + + /** + * @return string Database connection class name to be created for child connection. + */ + public function getDistributedConnectionClass() + { + return $this->_connDistributedClass; + } + + /** + * @param string Database connection class name to be created for child connection. + */ + public function setDistributedConnectionClass($value) + { + $this->_connDistributedClass=$value; + } + + /** + * @return IDistributedDbConnection + */ + public function getDistributedDbConnection() + { + $this->_hasDistributedConnectionData = false; + if($this->_connDistributed===null) + $this->_connDistributed = Prado::createComponent($this->getDistributedConnectionClass()); + + if($this->_hasDistributedConnectionData) + return $this->_connDistributed; + + $attribs = $this->getDistributedDbConnectionAttributes(); + + if($attribs===null) + return $this->_connDistributed; + + foreach($attribs as $name => $value) + $this->_connDistributed->setSubproperty($name, $value); + + $this->_hasDistributedConnectionData = true; + + return $this->_connDistributed; + } + + /** + * @return TMap + */ + protected function getDistributedDbConnectionAttributes() + { + $index = 0; + $c = count($this->_childConnectionAttributes); + + if($c > 1) { + $aSrc = array(); + $aTmp = array(); + + foreach($this->_childConnectionAttributes as $k => $item) + { + $weight = 1; + if( isset($item['Weight']) ) + $weight = $item['Weight']; + $aSrc[$k] = $weight; + } + + asort($aSrc); + + foreach($aSrc as $idx => $weight) + $aTmp = array_merge($aTmp, array_pad(array(), $weight*5, $idx)); + + $min = 0; + $max = count($aTmp)-1; + $factor = array_sum($aSrc) / count($aSrc); + $wrand = round($min + (pow(rand(0, $max) / $max, $factor) * ($max - $min))); + $index = $aTmp[$wrand]; + } + + $result = $this->_childConnectionAttributes[$index]; + + if( isset($result['Weight']) ) + unset($result['Weight']); + + return $result; + } + } +?> \ No newline at end of file diff --git a/framework/Testing/Data/Distributed/TDistributedDbConnection.php b/framework/Testing/Data/Distributed/TDistributedDbConnection.php new file mode 100644 index 00000000..bb105d50 --- /dev/null +++ b/framework/Testing/Data/Distributed/TDistributedDbConnection.php @@ -0,0 +1,189 @@ + + * @link http://www.pradosoft.com/ + * @copyright Copyright © 2005-2010 PradoSoft + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @package System.Testing.Data.Distributed + */ + + Prado::using('System.Data.TDbConnection'); + Prado::using('System.Testing.Data.Analysis.TDbStatementAnalysis'); + + /** + * TDistributedDbConnection interface + * + * @author Yves Berkholz + * @version $Id$ + * @package System.Testing.Data.Distributed + * @since 4.0 + */ + interface IDistributedDbConnection /*extends IDbConnection*/ + { + /** + * Gets the statement analyser of type given by + * {@link setStatementAnalyserClass StatementAnalyserClass }. + * @return IDbStatementAnalysis statement analyser. + */ + public function getStatementAnalyser(); + + /** + * The statement analyser class name to be created when {@link getStatementAnalyserClass} + * method is called. The {@link setStatementAnalyserClass StatementAnalyserClass} + * property must be set before calling {@link getStatementAnalyser} if you wish to + * create the connection using the given class name. + * @param string Statement analyser class name. + */ + public function setStatementAnalyserClass($value); + + /** + * @param string Statement analyser class name to be created. + */ + public function getStatementAnalyserClass(); + + /** + * @return TDbConnectionServerRole + */ + public function getServerRole(); + } + + /** + * TDistributedDbConnection class + * + * TDistributedDbConnection represents a conditional base connection class to a database + * + * @author Yves Berkholz + * @version $Id$ + * @package System.Testing.Data.Distributed + * @since 4.0 + */ + abstract class TDistributedDbConnection extends TDbConnection implements IDistributedDbConnection + { + /** + * @var string + */ + private $_statementAnalyserClass = 'System.Testing.Data.Analysis.TDbStatementAnalysis'; + + /** + * @var IDbStatementAnalysis + */ + private $_statementAnalyser = null; + + /** + * Gets the statement analyser of type given by + * {@link setStatementAnalyserClass StatementAnalyserClass }. + * @return IDbStatementAnalysis statement analyser. + */ + public function getStatementAnalyser() + { + if($this->_statementAnalyser === null) + { + $this->setActive(true); + $this->_statementAnalyser = Prado::createComponent($this->getStatementAnalyserClass()); + + if($this->getActive()) + $this->_statementAnalyser->setDriverName($this->getDriverName()); + } + return $this->_statementAnalyser; + } + + /** + * The statement analyser class name to be created when {@link getStatementAnalyser} + * method is called. The {@link setStatementAnalyserClass StatementAnalyserClass} + * property must be set before calling {@link getStatementAnalyser} if you wish to + * create the connection using the given class name. + * @param string Statement analyser class name. + */ + public function setStatementAnalyserClass($value) + { + if($this->_statementAnalyser === null) + $this->_statementAnalyserClass = $value; + } + + /** + * @param string Statement analyser class name to be created. + */ + public function getStatementAnalyserClass() + { + return $this->_statementAnalyserClass; + } + + /** + * @param string The SQL statement that should be analysed + * @param TDbStatementClassification + */ + protected function getStatementClassification($statement='', $defaultClassification=null) { + return $this->getStatementAnalyser()->getClassificationAnalysis(new TDbStatementAnalysisParameter($statement, $defaultClassification)); + } + } + + /** + * TDistributedDbCommand + * + * @author Yves Berkholz + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @package System.Testing.Data.Distributed + * @since 4.0 + */ + class TDistributedDbCommand extends TDbCommand + { + /** + * @var TDbStatementClassification + */ + private $_statementClassification; + + /** + * Constructor. + * @param TDbConnection the database connection + * @param string the SQL statement to be executed + * @param TDbStatementClassification Defaults to 'UNKNOWN' + */ + public function __construct(TDbConnection $connection, $text, $classification=TDbStatementClassification::UNKNOWN) + { + $connection->setActive(true); + parent::__construct($connection, $text); + $this->_statementClassification = $classification; + Prado::log($classification . ', ' . $connection->getServerRole() . ': ' . preg_replace('/[\s]+/', ' ', $text), TLogger::DEBUG, 'System.Testing.Data.Distributed.TDistributedDbCommand'); + } + + /** + * @return TDbStatementClassification + */ + public function getStatementClassification() + { + return $this->_statementClassification; + } + } + + + /** + * TDbConnectionServerRole + * + * @author Yves Berkholz + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @package System.Testing.Data.Distributed + * @since 4.0 + */ + class TDbConnectionServerRole extends TEnumerable + { + /** + * Master Server (Read/Write) + */ + const Master = 'Master'; + + /** + * Slave Server (Read only) + */ + const Slave = 'Slave'; + + /** + * Mirror Server (Read/Write) for further use + */ + //const Mirror = 'Mirror'; + } +?> \ No newline at end of file -- cgit v1.2.3