Skip to content

Commit a33c27f

Browse files
author
Benjamin Doherty
committedDec 29, 2014
initial commit.
0 parents  commit a33c27f

File tree

2 files changed

+456
-0
lines changed

2 files changed

+456
-0
lines changed
 

‎database.inc

+455
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,455 @@
1+
<?php
2+
3+
/**
4+
* @file
5+
* Database interface code for PDO database servers.
6+
*/
7+
8+
/**
9+
* @addtogroup database
10+
* @{
11+
*/
12+
class DatabaseConnection_pdo extends DatabaseConnection
13+
{
14+
/** @var \PDO */
15+
protected $pdo;
16+
17+
/** @var \DatabaseHelper */
18+
protected $helper;
19+
20+
public function __construct(array $connection_options = array())
21+
{
22+
$this->connectionOptions = $connection_options;
23+
24+
// Initialize and prepare the connection prefix.
25+
$this->setPrefix(isset($this->connectionOptions['prefix']) ? $this->connectionOptions['prefix'] : '');
26+
27+
// Because the other methods don't seem to work right.
28+
$driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
29+
30+
// Call PDO::__construct and PDO::setAttribute.
31+
$this->pdo = $connection_options['pdo'];
32+
33+
$helper_class = 'DatabaseHelper_'. $this->driver();
34+
$this->helper = new $helper_class($this);
35+
36+
// Set a Statement class, unless the driver opted out.
37+
if (!empty($this->statementClass)) {
38+
$this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this)));
39+
}
40+
}
41+
42+
/**
43+
* {@inheritdocs}
44+
*/
45+
public function prepareQuery($query) {
46+
$query = $this->prefixTables($query);
47+
48+
// Call PDO::prepare.
49+
return $this->prepare($query);
50+
}
51+
52+
/**
53+
* {@inheritdocs}
54+
*/
55+
public function queryRange(
56+
$query,
57+
$from,
58+
$count,
59+
array $args = array(),
60+
array $options = array()
61+
) {
62+
return $this->helper->queryRange($query, $from, $count, $args, $options);
63+
}
64+
65+
/**
66+
* {@inheritdocs}
67+
*/
68+
function queryTemporary(
69+
$query,
70+
array $args = array(),
71+
array $options = array()
72+
) {
73+
return $this->helper->queryTemporary($query, $args, $options);
74+
}
75+
76+
/**
77+
* {@inheritdocs}
78+
*/
79+
public function driver()
80+
{
81+
return $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
82+
}
83+
84+
/**
85+
* {@inheritdocs}
86+
*/
87+
public function databaseType()
88+
{
89+
return $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
90+
}
91+
92+
/**
93+
* {@inheritdocs}
94+
*/
95+
public function mapConditionOperator($operator)
96+
{
97+
return $this->helper->mapConditionOperator($operator);
98+
}
99+
100+
/**
101+
* {@inheritdocs}
102+
*/
103+
public function nextId($existing_id = 0)
104+
{
105+
return $this->helper->nextId($existing_id);
106+
}
107+
108+
/**
109+
* {@inheritdocs}
110+
*/
111+
public function prepare($statement, $driver_options = array())
112+
{
113+
return $this->pdo->prepare($statement, $driver_options);
114+
}
115+
116+
/**
117+
* {@inheritdocs}
118+
*/
119+
public function beginTransaction()
120+
{
121+
return $this->pdo->beginTransaction();
122+
}
123+
124+
/**
125+
* {@inheritdocs}
126+
*/
127+
public function commit()
128+
{
129+
return $this->pdo->commit();
130+
}
131+
132+
/**
133+
* {@inheritdocs}
134+
*/
135+
public function rollback($savepoint_name = 'drupal_transaction')
136+
{
137+
if (!$this->supportsTransactions()) {
138+
return;
139+
}
140+
if (!$this->inTransaction()) {
141+
throw new DatabaseTransactionNoActiveException();
142+
}
143+
// A previous rollback to an earlier savepoint may mean that the savepoint
144+
// in question has already been accidentally committed.
145+
if (!isset($this->transactionLayers[$savepoint_name])) {
146+
throw new DatabaseTransactionNoActiveException();
147+
}
148+
149+
// We need to find the point we're rolling back to, all other savepoints
150+
// before are no longer needed. If we rolled back other active savepoints,
151+
// we need to throw an exception.
152+
$rolled_back_other_active_savepoints = FALSE;
153+
while ($savepoint = array_pop($this->transactionLayers)) {
154+
if ($savepoint == $savepoint_name) {
155+
// If it is the last the transaction in the stack, then it is not a
156+
// savepoint, it is the transaction itself so we will need to roll back
157+
// the transaction rather than a savepoint.
158+
if (empty($this->transactionLayers)) {
159+
break;
160+
}
161+
$this->query('ROLLBACK TO SAVEPOINT ' . $savepoint);
162+
$this->popCommittableTransactions();
163+
if ($rolled_back_other_active_savepoints) {
164+
throw new DatabaseTransactionOutOfOrderException();
165+
}
166+
return;
167+
}
168+
else {
169+
$rolled_back_other_active_savepoints = TRUE;
170+
}
171+
}
172+
$this->pdo->rollBack();
173+
if ($rolled_back_other_active_savepoints) {
174+
throw new DatabaseTransactionOutOfOrderException();
175+
}
176+
}
177+
178+
/**
179+
* {@inheritdocs}
180+
*/
181+
public function pushTransaction($name) {
182+
if (!$this->supportsTransactions()) {
183+
return;
184+
}
185+
if (isset($this->transactionLayers[$name])) {
186+
throw new DatabaseTransactionNameNonUniqueException($name . " is already in use.");
187+
}
188+
// If we're already in a transaction then we want to create a savepoint
189+
// rather than try to create another transaction.
190+
if ($this->inTransaction()) {
191+
$this->pdo->query('SAVEPOINT ' . $name);
192+
}
193+
else {
194+
$this->pdo->beginTransaction();
195+
}
196+
$this->transactionLayers[$name] = $name;
197+
}
198+
199+
/**
200+
* {@inheritdocs}
201+
*/
202+
public function inTransaction()
203+
{
204+
return $this->pdo->inTransaction();
205+
}
206+
207+
/**
208+
* {@inheritdocs}
209+
*/
210+
public function setAttribute($attribute, $value)
211+
{
212+
return $this->pdo->setAttribute($attribute, $value);
213+
}
214+
215+
/**
216+
* {@inheritdocs}
217+
*/
218+
public function exec($statement)
219+
{
220+
return $this->pdo->exec($statement);
221+
}
222+
223+
/**
224+
* {@inheritdocs}
225+
*/
226+
public function lastInsertId($name = null)
227+
{
228+
return $this->pdo->lastInsertId($name);
229+
}
230+
231+
/**
232+
* {@inheritdocs}
233+
*/
234+
public function errorCode()
235+
{
236+
return $this->pdo->errorCode();
237+
}
238+
239+
/**
240+
* {@inheritdocs}
241+
*/
242+
public function errorInfo()
243+
{
244+
return $this->pdo->errorInfo();
245+
}
246+
247+
/**
248+
* {@inheritdocs}
249+
*/
250+
public function getAttribute($attribute)
251+
{
252+
return $this->pdo->getAttribute($attribute);
253+
}
254+
255+
/**
256+
* {@inheritdocs}
257+
*/
258+
public function quote($string, $parameter_type = PDO::PARAM_STR)
259+
{
260+
return $this->pdo->quote($string, $parameter_type);
261+
}
262+
263+
/**
264+
* {@inheritdocs}
265+
*/
266+
protected function popCommittableTransactions()
267+
{
268+
if ($this->helper instanceof DatabaseTransactionHelper) {
269+
$this->helper->popCommittableTransactions($this->transactionLayers);
270+
}
271+
else {
272+
parent::popCommittableTransactions();
273+
}
274+
}
275+
}
276+
277+
interface DatabaseHelper
278+
{
279+
public function __construct(DatabaseConnection_pdo $conn);
280+
public function queryRange($query, $from, $count, array $args = array(), array $options = array());
281+
public function queryTemporary($query, array $args = array(), array $options = array());
282+
public function mapConditionOperator($operator);
283+
public function nextId($existing_id = 0);
284+
}
285+
286+
interface DatabaseTransactionHelper
287+
{
288+
public function popCommittableTransactions(&$transactionLayers);
289+
}
290+
291+
abstract class DatabaseHelper_pdo implements DatabaseHelper
292+
{
293+
/** @var DatabaseConnection_pdo */
294+
protected $conn;
295+
296+
/**
297+
* An index used to generate unique temporary table names.
298+
*
299+
* @var integer
300+
*/
301+
protected $temporaryNameIndex = 0;
302+
303+
public function __construct(\DatabaseConnection_pdo $conn)
304+
{
305+
$this->conn = $conn;
306+
}
307+
308+
/**
309+
* Generates a temporary table name.
310+
*
311+
* @return
312+
* A table name.
313+
*/
314+
protected function generateTemporaryTableName() {
315+
return "db_temporary_" . $this->temporaryNameIndex++;
316+
}
317+
}
318+
319+
class DatabaseHelper_mysql extends DatabaseHelper_pdo implements DatabaseTransactionHelper
320+
{
321+
/**
322+
* Flag to indicate if the cleanup function in __destruct() should run.
323+
*
324+
* @var boolean
325+
*/
326+
protected $needsCleanup = FALSE;
327+
328+
public function queryRange(
329+
$query,
330+
$from,
331+
$count,
332+
array $args = array(),
333+
array $options = array()
334+
) {
335+
return $this->conn->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
336+
}
337+
338+
public function __destruct() {
339+
if ($this->needsCleanup) {
340+
$this->nextIdDelete();
341+
}
342+
}
343+
344+
public function queryTemporary(
345+
$query,
346+
array $args = array(),
347+
array $options = array()
348+
) {
349+
$tablename = $this->generateTemporaryTableName();
350+
$this->conn->query('CREATE TEMPORARY TABLE {' . $tablename . '} Engine=MEMORY ' . $query, $args, $options);
351+
return $tablename;
352+
}
353+
354+
public function mapConditionOperator($operator)
355+
{
356+
// We don't want to override any of the defaults.
357+
return NULL;
358+
}
359+
360+
public function nextId($existing_id = 0)
361+
{
362+
$new_id = $this->conn->query('INSERT INTO {sequences} () VALUES ()', array(), array('return' => Database::RETURN_INSERT_ID));
363+
// This should only happen after an import or similar event.
364+
if ($existing_id >= $new_id) {
365+
// If we INSERT a value manually into the sequences table, on the next
366+
// INSERT, MySQL will generate a larger value. However, there is no way
367+
// of knowing whether this value already exists in the table. MySQL
368+
// provides an INSERT IGNORE which would work, but that can mask problems
369+
// other than duplicate keys. Instead, we use INSERT ... ON DUPLICATE KEY
370+
// UPDATE in such a way that the UPDATE does not do anything. This way,
371+
// duplicate keys do not generate errors but everything else does.
372+
$this->conn->query('INSERT INTO {sequences} (value) VALUES (:value) ON DUPLICATE KEY UPDATE value = value', array(':value' => $existing_id));
373+
$new_id = $this->conn->query('INSERT INTO {sequences} () VALUES ()', array(), array('return' => Database::RETURN_INSERT_ID));
374+
}
375+
$this->needsCleanup = TRUE;
376+
return $new_id;
377+
}
378+
379+
public function nextIdDelete() {
380+
// While we want to clean up the table to keep it up from occupying too
381+
// much storage and memory, we must keep the highest value in the table
382+
// because InnoDB uses an in-memory auto-increment counter as long as the
383+
// server runs. When the server is stopped and restarted, InnoDB
384+
// reinitializes the counter for each table for the first INSERT to the
385+
// table based solely on values from the table so deleting all values would
386+
// be a problem in this case. Also, TRUNCATE resets the auto increment
387+
// counter.
388+
try {
389+
$max_id = $this->conn->query('SELECT MAX(value) FROM {sequences}')->fetchField();
390+
// We know we are using MySQL here, no need for the slower db_delete().
391+
$this->conn->query('DELETE FROM {sequences} WHERE value < :value', array(':value' => $max_id));
392+
}
393+
// During testing, this function is called from shutdown with the
394+
// simpletest prefix stored in $this->connection, and those tables are gone
395+
// by the time shutdown is called so we need to ignore the database
396+
// errors. There is no problem with completely ignoring errors here: if
397+
// these queries fail, the sequence will work just fine, just use a bit
398+
// more database storage and memory.
399+
catch (PDOException $e) {
400+
}
401+
}
402+
403+
/**
404+
* Overridden to work around issues to MySQL not supporting transactional DDL.
405+
* @param $transactionLayers
406+
* @throws DatabaseTransactionCommitFailedException
407+
*/
408+
public function popCommittableTransactions(&$transactionLayers) {
409+
// Commit all the committable layers.
410+
foreach (array_reverse($transactionLayers) as $name => $active) {
411+
// Stop once we found an active transaction.
412+
if ($active) {
413+
break;
414+
}
415+
416+
// If there are no more layers left then we should commit.
417+
unset($transactionLayers[$name]);
418+
if (empty($transactionLayers)) {
419+
if (!$this->conn->commit()) {
420+
throw new DatabaseTransactionCommitFailedException();
421+
}
422+
}
423+
else {
424+
// Attempt to release this savepoint in the standard way.
425+
try {
426+
$this->conn->query('RELEASE SAVEPOINT ' . $name);
427+
}
428+
catch (PDOException $e) {
429+
// However, in MySQL (InnoDB), savepoints are automatically committed
430+
// when tables are altered or created (DDL transactions are not
431+
// supported). This can cause exceptions due to trying to release
432+
// savepoints which no longer exist.
433+
//
434+
// To avoid exceptions when no actual error has occurred, we silently
435+
// succeed for MySQL error code 1305 ("SAVEPOINT does not exist").
436+
if ($e->errorInfo[1] == '1305') {
437+
// If one SAVEPOINT was released automatically, then all were.
438+
// Therefore, clean the transaction stack.
439+
$transactionLayers = array();
440+
// We also have to explain to PDO that the transaction stack has
441+
// been cleaned-up.
442+
$this->conn->commit();
443+
}
444+
else {
445+
throw $e;
446+
}
447+
}
448+
}
449+
}
450+
}
451+
}
452+
453+
/**
454+
* @} End of "addtogroup database".
455+
*/

‎query.inc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<?php

0 commit comments

Comments
 (0)
Please sign in to comment.