3 /** @package verysimple::Phreeze */
6 * import supporting libraries
8 require_once("IObservable.php");
9 require_once("ConnectionSetting.php");
10 require_once("verysimple/DB/DataDriver/IDataDriver.php");
13 * DataAdapter abstracts and provides access to the data store
15 * @package verysimple::Phreeze
16 * @author VerySimple Inc. <noreply@verysimple.com>
17 * @copyright 1997-2005 VerySimple Inc.
18 * @license http://www.gnu.org/licenses/lgpl.html LGPL
21 class DataAdapter
implements IObservable
25 * @var ConnectionSetting
27 public $ConnectionSetting;
28 private $_observers = array ();
33 private $_transactionInProgress;
34 private $_masterAdapter;
36 /** @var used internally to keep track of communication error re-tries */
37 private $_num_retries = 0;
39 /** @var static singleton instance of the data adapter */
40 static $ADAPTER_INSTANCE = null;
42 /** @var instance of the driver class, used for escaping */
43 static $DRIVER_INSTANCE = null;
45 /** @var bool if true the data adapter attempt one retry when a communication error occurs */
46 static $RETRY_ON_COMMUNICATION_ERROR = false;
49 * Contructor initializes the object
52 * @param ConnectionSetting $csetting
53 * @param Observable $listener
55 * IDataDriver (optional) if not provided, then DataAdapter will attempt to instantiate one based on ConnectionSetting->Type
57 * string (optional) a label for the DataAdapter used in debug messages (if empty a random label will be generated)
59 function __construct($csetting, $listener = null, IDataDriver
$driver = null, $label = null)
61 $this->_driver
= $driver;
63 DataAdapter
::$DRIVER_INSTANCE = $this->_driver
;
66 $this->_label
= $label ?
$label : 'db-' . mt_rand(10000, 99999);
68 $this->ConnectionSetting
= & $csetting;
71 $this->AttachObserver($listener);
74 $this->Observe("DataAdapter ($this->_label) Instantiated", OBSERVE_DEBUG
);
76 // set the singleton reference
77 DataAdapter
::$ADAPTER_INSTANCE = $this;
81 * Destructor closes the db connection.
87 $this->Observe("DataAdapter ($this->_label) Destructor Firing...", OBSERVE_DEBUG
);
92 * Load the data driver
96 public function LoadDriver()
98 if ($this->_driver
== null) {
99 require_once("verysimple/IO/Includer.php");
101 // the driver was not explicitly provided so we will try to create one from
102 // the connection setting based on the database types that we do know about
103 switch ($this->ConnectionSetting
->Type
) {
105 include_once("verysimple/DB/DataDriver/MySQL.php");
106 $this->_driver
= new DataDriverMySQL();
109 include_once("verysimple/DB/DataDriver/MySQLi.php");
110 $this->_driver
= new DataDriverMySQLi();
113 include_once("verysimple/DB/DataDriver/SQLite.php");
114 $this->_driver
= new DataDriverSQLite();
118 Includer
::IncludeFile("verysimple/DB/DataDriver/" . $this->ConnectionSetting
->Type
. ".php");
119 $classname = "DataDriver" . $this->ConnectionSetting
->Type
;
120 $this->_driver
= new $classname();
121 } catch (IncludeException
$ex) {
122 throw new Exception('Unknown DataDriver "' . $this->ConnectionSetting
->Type
. '" specified in connection settings');
127 DataAdapter
::$DRIVER_INSTANCE = $this->_driver
;
132 * Returns name of the DB currently in use
139 return $this->ConnectionSetting
->DBName
;
143 * Opens a connection to the data server and selects the specified database
149 $this->Observe("DataAdapter ($this->_label) Opening Connection...", OBSERVE_DEBUG
);
151 if ($this->_dbopen
) {
152 $this->Observe("DataAdapter ($this->_label) Connection Already Open", OBSERVE_WARN
);
154 if (! $this->_driver
) {
159 $this->_dbconn
= $this->_driver
->Open($this->ConnectionSetting
->ConnectionString
, $this->ConnectionSetting
->DBName
, $this->ConnectionSetting
->Username
, $this->ConnectionSetting
->Password
, $this->ConnectionSetting
->Charset
, $this->ConnectionSetting
->BootstrapSQL
);
161 $this->_num_retries
= 0;
162 } catch (Exception
$ex) {
163 // retry one time a communication error occurs
164 if ($this->_num_retries
== 0 && DataAdapter
::$RETRY_ON_COMMUNICATION_ERROR && $this->IsCommunicationError($ex)) {
165 $this->_num_retries ++
;
166 $this->Observe("DataAdapter ($this->_label) Communication error. Retry attempt " . $this->_num_retries
, OBSERVE_WARN
);
167 sleep(2); // slight delay to prevent throttling
168 return $this->Open();
171 $msg = "DataAdapter ($this->_label) Error Opening DB: " . $ex->getMessage() . ' (retry attempts: ' . $this->_num_retries
. ')';
173 $this->Observe($msg, OBSERVE_FATAL
);
174 throw new Exception($msg, $ex->getCode());
177 $this->_dbopen
= true;
178 $this->Observe("DataAdapter ($this->_label) Connection Open", OBSERVE_DEBUG
);
183 * Closing the connection to the data Server
189 $this->Observe("DataAdapter ($this->_label) Closing Connection...", OBSERVE_DEBUG
);
191 if ($this->_dbopen
) {
192 $this->_driver
->Close($this->_dbconn
); // ignore warnings
193 $this->_dbopen
= false;
194 $this->Observe("DataAdapter ($this->_label) Connection Closed", OBSERVE_DEBUG
);
196 $this->Observe("DataAdapter ($this->_label) Connection Not Open", OBSERVE_DEBUG
);
201 * Checks that the connection is open and if not, crashes
205 * Automatically try to connect if connection isn't already open
207 private function RequireConnection($auto = false)
209 if ($this->_dbopen
) {
210 // $this->_driver->Ping($this->_dbconn);
215 $this->Observe("DataAdapter ($this->_label) DB is not connected. Please call DBConnection->Open() first.", OBSERVE_FATAL
);
216 throw new Exception("DataAdapter ($this->_label) DB is not connected. Please call DBConnection->Open() first.");
222 * Executes a SQL select statement and returns a resultset that can be read
227 * @return resultset (dependent on the type of driver used)
229 function Select($sql)
231 $this->RequireConnection(true);
232 $this->Observe("DataAdapter ($this->_label) (DataAdapter.Select) " . $sql, OBSERVE_QUERY
);
235 $rs = $this->_driver
->Query($this->_dbconn
, $sql);
236 $this->_num_retries
= 0;
237 } catch (Exception
$ex) {
238 // retry one time a communication error occurs
239 if ($this->_num_retries
== 0 && DataAdapter
::$RETRY_ON_COMMUNICATION_ERROR && $this->IsCommunicationError($ex)) {
240 $this->_num_retries ++
;
241 $this->Observe("DataAdapter ($this->_label) Communication error. Retry attempt " . $this->_num_retries
, OBSERVE_WARN
);
242 sleep(2); // slight delay to prevent throttling
243 return $this->Select($sql);
246 $msg = "DataAdapter ($this->_label)" . ' Error Selecting SQL: ' . $ex->getMessage() . ' (retry attempts: ' . $this->_num_retries
. ')';
248 $this->Observe($msg, OBSERVE_FATAL
);
249 throw new Exception($msg, $ex->getCode());
256 * Executes a SQL query that does not return a resultset
260 * @return int number of records affected
262 function Execute($sql)
266 if ($this->ConnectionSetting
->IsReadOnlySlave
) {
267 // this is a read-only slave connection attempting a write operation. we
268 // will only proceed if the connection specifies a "master" delegate connection
269 if (! $this->_masterAdapter
) {
270 if ($this->ConnectionSetting
->MasterConnectionDelegate
) {
271 $this->Observe("DataAdapter ($this->_label) (DataAdapter.Execute) Delegating write operation from Slave to Master Connection", OBSERVE_INFO
);
273 $this->_masterAdapter
= new DataAdapter($this->ConnectionSetting
->MasterConnectionDelegate
);
275 foreach ($this->_observers
as $observer) {
276 $this->_masterAdapter
->AttachObserver($observer);
279 throw new Exception('DB Write operation was attempted on a read-only slave connection');
283 // we have a master connection initialized and ready to use
284 $result = $this->_masterAdapter
->Execute($sql);
286 $this->RequireConnection(true);
287 $this->Observe("DataAdapter ($this->_label) (DataAdapter.Execute) " . $sql, OBSERVE_QUERY
);
291 $result = $this->_driver
->Execute($this->_dbconn
, $sql);
292 $this->_num_retries
= 0;
293 } catch (Exception
$ex) {
294 // retry one time a communication error occurs
295 if ($this->_num_retries
== 0 && DataAdapter
::$RETRY_ON_COMMUNICATION_ERROR && $this->IsCommunicationError($ex)) {
296 $this->_num_retries ++
;
297 $this->Observe("DataAdapter ($this->_label) Communication error. Retry attempt " . $this->_num_retries
, OBSERVE_WARN
);
298 sleep(2); // slight delay to prevent throttling
299 return $this->Execute($sql);
302 $msg = "DataAdapter ($this->_label)" . ' Error Executing SQL: ' . $ex->getMessage() . ' (retry attempts: ' . $this->_num_retries
. ')';
304 $this->Observe($msg, OBSERVE_FATAL
);
305 throw new Exception($msg, $ex->getCode());
313 * Return true if a transaction is in progress
317 function IsTransactionInProgress()
319 return $this->_transactionInProgress
;
323 * Start a DB transaction, disabling auto-commit if necessar)
327 function StartTransaction()
329 if ($this->IsTransactionInProgress()) {
330 throw new Exception('Transaction is already in progress. Commit or rollback must be called before beginning a new transaction');
333 if ($this->ConnectionSetting
->IsReadOnlySlave
) {
334 throw new Exception('Transactions are not allowed on a read-only slave');
337 $this->RequireConnection(true);
338 $this->Observe("DataAdapter ($this->_label) (DataAdapter.StartTransaction)", OBSERVE_QUERY
);
339 $this->_transactionInProgress
= true;
340 return $this->_driver
->StartTransaction($this->_dbconn
);
344 * Commit the current DB transaction and re-enable auto-commit if necessary
348 function CommitTransaction()
350 if ($this->ConnectionSetting
->IsReadOnlySlave
) {
351 throw new Exception('Transactions are not allowed on a read-only slave');
354 $this->RequireConnection(true);
355 $this->Observe("DataAdapter ($this->_label) (DataAdapter.CommitTransaction)", OBSERVE_QUERY
);
356 $this->_transactionInProgress
= false;
357 return $this->_driver
->CommitTransaction($this->_dbconn
);
361 * Rollback the current DB transaction and re-enable auto-commit if necessary
365 function RollbackTransaction()
367 if ($this->ConnectionSetting
->IsReadOnlySlave
) {
368 throw new Exception('Transactions are not allowed on a read-only slave');
371 $this->RequireConnection(true);
372 $this->Observe("DataAdapter ($this->_label) (DataAdapter.RollbackTransaction)", OBSERVE_QUERY
);
373 $this->_transactionInProgress
= false;
374 return $this->_driver
->RollbackTransaction($this->_dbconn
);
378 * Return true if the error with the given message is a communication/network error
381 * variant string or Exception $msg
384 public function IsCommunicationError($error)
386 $msg = is_a($error, 'Exception') ?
$error->getMessage() : $error;
387 return strpos(strtolower($msg), 'lost connection') !== false;
391 * Returns an array of all table names in the current database
394 * bool true to ommit tables that are empty (default = false)
397 public function GetTableNames($ommitEmptyTables = false)
399 return $this->_driver
->GetTableName($this->_dbconn
, $this->GetDBName(), $ommitEmptyTables);
403 * Runs OPTIMIZE TABLE on all tables in the current database
405 * @return array results for each table
407 public function OptimizeTables()
409 if ($this->ConnectionSetting
->IsReadOnlySlave
) {
410 throw new Exception('Optimizing tables is allowed on a read-only slave');
414 $table_names = $this->_driver
->GetTableNames($this->_dbconn
, $this->GetDBName());
416 foreach ($table_names as $table_name) {
417 $results [$table_name] = $this->_driver
->Optimize($this->_dbconn
, $table_name);
423 * Returns last auto-inserted Id.
424 * If this is a read-only slave and a write operation was made to a Master Delegate
425 * then the last insert id from that connection will be returned
430 function GetLastInsertId()
434 if ($this->ConnectionSetting
->IsReadOnlySlave
&& $this->_masterAdapter
) {
435 $id = $this->_masterAdapter
->GetLastInsertId();
437 $this->RequireConnection();
438 $this->Observe("DataAdapter ($this->_label) GetLastInsertId", OBSERVE_QUERY
);
439 $id = $this->_driver
->GetLastInsertId($this->_dbconn
);
446 * Moves the database curser forward and returns the current row as an associative array
447 * the resultset passed in must have been created by the same database driver that
448 * was connected when Select was called
451 * @param resultset $rs
456 $this->RequireConnection();
458 $this->Observe("DataAdapter ($this->_label) Fetching next result as array", OBSERVE_DEBUG
);
459 return $this->_driver
->Fetch($this->_dbconn
, $rs);
463 * Releases the resources for the given resultset.
464 * the resultset must have
465 * been created by the same database driver
468 * @param resultset $rs
470 function Release($rs)
472 $this->RequireConnection();
474 $this->Observe("DataAdapter ($this->_label) Releasing result resources", OBSERVE_DEBUG
);
475 $this->_driver
->Release($this->_dbconn
, $rs);
479 * Removes any illegal chars from a value to prepare it for use in SQL
485 public static function Escape($val)
487 if (DataAdapter
::$ADAPTER_INSTANCE) {
488 DataAdapter
::$ADAPTER_INSTANCE->LoadDriver();
491 // this is an unfortunate leftover from poor design of making this function static
492 // we cannon use the driver's escape method without a static reference
493 if (! DataAdapter
::$DRIVER_INSTANCE) {
494 throw new Exception("DataAdapter must be instantiated before Escape can be called");
497 // $driver->RequireConnection(true);
498 return DataAdapter
::$DRIVER_INSTANCE->Escape($val);
502 * Quote and escape value to prepare it for use in SQL
508 public static function GetQuotedSql($val)
510 if (DataAdapter
::$ADAPTER_INSTANCE) {
511 DataAdapter
::$ADAPTER_INSTANCE->LoadDriver();
514 // this is an unfortunate leftover from poor design of making this function static
515 // we cannon use the driver's escape method without a static reference
516 if (! DataAdapter
::$DRIVER_INSTANCE) {
517 throw new Exception("DataAdapter must be instantiated before Escape can be called");
520 // $driver->RequireConnection(true);
521 return DataAdapter
::$DRIVER_INSTANCE->GetQuotedSql($val);
525 * Registers/attaches an IObserver to this object
528 * @param IObserver $observer
530 public function AttachObserver($listener)
533 $this->_observers
[] = & $listener;
534 if ($this->_masterAdapter
) {
535 $this->_masterAdapter
->AttachObserver($listener);
541 * Fires the Observe event on all registered observers
544 * @param variant $obj
545 * the $obj or message that you want to log/listen to, etc.
549 public function Observe($obj, $ltype = OBSERVE_INFO
)
551 foreach ($this->_observers
as $observer) {
552 @$observer->Observe($obj, $ltype);