1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
/** |
5
|
|
|
* Database Access Layer. |
6
|
|
|
* @author Doug Wright |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
namespace DVDoug\DB; |
10
|
|
|
|
11
|
|
|
/** |
12
|
|
|
* PDO-backed database connection (common parts). |
13
|
|
|
* @author Doug Wright |
14
|
|
|
*/ |
15
|
|
|
abstract class PDODatabase extends \PDO implements DatabaseInterface |
16
|
|
|
{ |
17
|
|
|
/** |
18
|
|
|
* Character to use when quoting identifiers. |
19
|
|
|
*/ |
20
|
|
|
public const IDENTIFIER_OPENQUOTE = '"'; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Character to use when quoting identifiers. |
24
|
|
|
*/ |
25
|
|
|
public const IDENTIFIER_CLOSEQUOTE = '"'; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Constructor. |
29
|
|
|
* @param string $aDSN |
30
|
|
|
* @param string $aUsername |
31
|
|
|
* @param string $aPassword |
32
|
|
|
*/ |
33
|
40 |
|
public function __construct($aDSN, $aUsername, $aPassword, array $aDriverOptions = null) |
34
|
|
|
{ |
35
|
40 |
|
parent::__construct($aDSN, $aUsername, $aPassword, $aDriverOptions); |
36
|
40 |
|
$this->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); |
37
|
40 |
|
$this->setAttribute(\PDO::ATTR_STATEMENT_CLASS, ['\DVDoug\DB\PDOStatement']); |
38
|
40 |
|
} |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Executes an SQL statement, returning the result set if any as a StatementInterface object. |
42
|
|
|
* @param string $aSQL the SQL statement to execute |
43
|
|
|
* @return StatementInterface|bool |
44
|
|
|
*/ |
45
|
20 |
|
public function query($aSQL) |
46
|
|
|
{ |
47
|
20 |
|
return parent::query($aSQL); |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Returns the ID of the last inserted row or sequence value. |
52
|
|
|
* @param string $aName name of the sequence object (if any) from which the ID should be returned |
53
|
|
|
*/ |
54
|
|
|
public function getLastInsertId($aName = null): string |
55
|
|
|
{ |
56
|
|
|
return parent::lastInsertID($aName); |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Escapes/quotes a parameter for use in a query. |
61
|
|
|
* |
62
|
|
|
* @param mixed $aParam the parameter to be quoted |
63
|
|
|
* @param int $aParamType data type hint for drivers |
64
|
|
|
* |
65
|
|
|
* @throws \RuntimeException |
66
|
|
|
* @return string a quoted string that is theoretically safe to pass into an SQL statement |
67
|
|
|
*/ |
68
|
12 |
|
public function escape($aParam, $aParamType = DatabaseInterface::PARAM_IS_STR) |
69
|
|
|
{ |
70
|
|
|
switch ($aParamType) { |
71
|
12 |
|
case self::PARAM_IS_INT: |
72
|
8 |
|
if (is_int($aParam) || ctype_digit($aParam)) { |
73
|
4 |
|
return (int) $aParam; |
74
|
|
|
} else { |
75
|
4 |
|
throw new \RuntimeException("Parameter {$aParam} is not an integer"); |
76
|
|
|
} |
77
|
|
|
break; |
78
|
|
|
|
79
|
|
|
default: |
80
|
8 |
|
return parent::quote($aParam, $aParamType); |
81
|
|
|
} |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Adds appropriate quotes to an identifier so it can be safely used in an SQL statement. |
86
|
|
|
* @param mixed $aIdentifier the parameter to be quoted |
87
|
|
|
*/ |
88
|
20 |
|
public function quoteIdentifier($aIdentifier): string |
89
|
|
|
{ |
90
|
20 |
|
return static::IDENTIFIER_OPENQUOTE . $aIdentifier . static::IDENTIFIER_CLOSEQUOTE; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* Get MySQL table definition. |
95
|
|
|
* @param string $aDatabase database/schema name |
96
|
|
|
* @param string $aTable table name |
97
|
|
|
* @param bool $aSkipUnusedCols whether to skip unused columns |
98
|
|
|
*/ |
99
|
8 |
|
public function getMySQLTableDef($aDatabase, $aTable, $aSkipUnusedCols = true): string |
100
|
|
|
{ |
101
|
8 |
|
$table = strtolower($aTable); |
102
|
|
|
|
103
|
8 |
|
$columns = $this->getTableColumns($aDatabase, $aTable); |
104
|
|
|
|
105
|
8 |
|
$colDefs = []; |
106
|
|
|
|
107
|
8 |
|
foreach ($columns as $columnName => $column) { |
108
|
8 |
|
if ($aSkipUnusedCols && $column->getDistinctValueCount() <= 1) { |
109
|
|
|
unset($columns[$columnName]); |
110
|
|
|
continue; |
111
|
|
|
} |
112
|
|
|
|
113
|
8 |
|
$colDefs[] = $column->getMySQLColumnDef(); |
114
|
|
|
} |
115
|
|
|
|
116
|
8 |
|
$primaryKey = $this->getPrimaryKey($aDatabase, $aTable); |
117
|
8 |
|
$indexes = $this->getIndexes($aDatabase, $aTable); |
118
|
|
|
|
119
|
8 |
|
$tableDef = "CREATE TABLE `{$table}` (" . "\n"; |
120
|
8 |
|
$tableDef .= implode(',' . "\n", $colDefs); |
121
|
|
|
|
122
|
8 |
|
if ($primaryKey) { |
|
|
|
|
123
|
|
|
$length = 0; |
124
|
|
|
foreach ($primaryKey as $primaryCol) { |
125
|
|
|
$length += $columns[$primaryCol]->getLength(); |
126
|
|
|
} |
127
|
|
|
if ($length <= 191) { //skip index if too long for MySQL |
128
|
|
|
$tableDef .= ',' . "\n" . "\n"; |
129
|
|
|
$tableDef .= 'PRIMARY KEY ('; |
130
|
|
|
$tableDef .= implode(', ' . "\n", array_map(function ($c) {return '`' . strtolower($c) . '`'; }, $primaryKey)); |
131
|
|
|
$tableDef .= ')'; |
132
|
|
|
} |
133
|
|
|
} |
134
|
|
|
|
135
|
8 |
|
if ($indexes) { |
|
|
|
|
136
|
|
|
foreach ($indexes as $indexName => $indexColumns) { |
137
|
|
|
$length = 0; |
138
|
|
|
foreach ($indexColumns as &$col) { |
139
|
|
|
if (!in_array($col, array_keys($columns))) { //skip index if it includes a skipped column |
140
|
|
|
continue 2; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
$length += $columns[$col]->getLength(); |
144
|
|
|
if ($length > 191) { //skip index if too long for MySQL |
145
|
|
|
continue 2; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
if (preg_match('/(BLOB|TEXT)$/', $columns[$col]->getMySQLType())) { |
149
|
|
|
continue 2; |
150
|
|
|
} |
151
|
|
|
} |
152
|
|
|
$tableDef .= ',' . "\n"; |
153
|
|
|
$tableDef .= 'KEY `' . strtolower($indexName) . '` ('; |
154
|
|
|
$tableDef .= implode(', ', array_map(function ($c) {return '`' . strtolower($c) . '`'; }, $indexColumns)); |
155
|
|
|
$tableDef .= ')'; |
156
|
|
|
} |
157
|
|
|
} |
158
|
8 |
|
$tableDef .= ') ENGINE=InnoDB ROW_FORMAT=COMPRESSED'; |
159
|
|
|
|
160
|
8 |
|
return $tableDef; |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* Get Oracle table definition. |
165
|
|
|
* @param string $aDatabase database/schema name |
166
|
|
|
* @param string $aTable table name |
167
|
|
|
* @param bool $aSkipUnusedCols whether to skip unused columns |
168
|
|
|
*/ |
169
|
8 |
|
public function getOracleTableDef($aDatabase, $aTable, $aSkipUnusedCols = true): string |
170
|
|
|
{ |
171
|
8 |
|
$table = strtolower($aTable); |
172
|
|
|
|
173
|
8 |
|
$columns = $this->getTableColumns($aDatabase, $aTable); |
174
|
|
|
|
175
|
8 |
|
$colDefs = []; |
176
|
|
|
|
177
|
8 |
|
foreach ($columns as $columnName => $column) { |
178
|
8 |
|
if ($aSkipUnusedCols && $column->getDistinctValueCount() <= 1) { |
179
|
|
|
unset($columns[$columnName]); |
180
|
|
|
continue; |
181
|
|
|
} |
182
|
|
|
|
183
|
8 |
|
$colDefs[] = $column->getOracleColumnDef(); |
184
|
|
|
} |
185
|
|
|
|
186
|
8 |
|
$primaryKey = $this->getPrimaryKey($aDatabase, $aTable); |
187
|
8 |
|
$indexes = $this->getIndexes($aDatabase, $aTable); |
188
|
|
|
|
189
|
8 |
|
$tableDef = "CREATE TABLE `{$table}` (" . "\n"; |
190
|
8 |
|
$tableDef .= implode(',' . "\n", $colDefs); |
191
|
|
|
|
192
|
8 |
|
if ($primaryKey) { |
|
|
|
|
193
|
|
|
$tableDef .= ',' . "\n" . "\n"; |
194
|
|
|
$tableDef .= 'PRIMARY KEY ('; |
195
|
|
|
$tableDef .= implode(', ' . "\n", array_map(function ($c) {return '"' . strtolower($c) . '"'; }, $primaryKey)); |
196
|
|
|
$tableDef .= ')'; |
197
|
|
|
} |
198
|
|
|
|
199
|
8 |
|
if ($indexes) { |
|
|
|
|
200
|
|
|
foreach ($indexes as $indexName => $indexColumns) { |
201
|
|
|
foreach ($indexColumns as &$col) { |
202
|
|
|
if (!in_array($col, array_keys($columns))) { //skip index if it includes a skipped column |
203
|
|
|
continue 2; |
204
|
|
|
} |
205
|
|
|
} |
206
|
|
|
$tableDef .= ',' . "\n"; |
207
|
|
|
$tableDef .= 'KEY `' . strtolower($indexName) . '` ('; |
208
|
|
|
$tableDef .= implode(', ', array_map(function ($c) {return '"' . strtolower($c) . '"'; }, $indexColumns)); |
209
|
|
|
$tableDef .= ')'; |
210
|
|
|
} |
211
|
|
|
} |
212
|
8 |
|
$tableDef .= ')'; |
213
|
|
|
|
214
|
8 |
|
return $tableDef; |
215
|
|
|
} |
216
|
|
|
} |
217
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.