5 require_once(CONST_LibDir.'/DatabaseError.php');
8 * Uses PDO to access the database specified in the CONST_Database_DSN
13 protected $connection;
15 public function __construct($sDSN = null)
17 $this->sDSN = $sDSN ?? getSetting('DATABASE_DSN');
20 public function connect($bNew = false, $bPersistent = true)
22 if (isset($this->connection) && !$bNew) {
25 $aConnOptions = array(
26 \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
27 \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
28 \PDO::ATTR_PERSISTENT => $bPersistent
31 // https://secure.php.net/manual/en/ref.pdo-pgsql.connection.php
33 $conn = new \PDO($this->sDSN, null, null, $aConnOptions);
34 } catch (\PDOException $e) {
35 $sMsg = 'Failed to establish database connection:' . $e->getMessage();
36 throw new \Nominatim\DatabaseError($sMsg, 500, null, $e->getMessage());
39 $conn->exec("SET DateStyle TO 'sql,european'");
40 $conn->exec("SET client_encoding TO 'utf-8'");
41 // Disable JIT and parallel workers. They interfere badly with search SQL.
42 $conn->exec("UPDATE pg_settings SET setting = -1 WHERE name = 'jit_above_cost'");
43 $conn->exec("UPDATE pg_settings SET setting = 0 WHERE name = 'max_parallel_workers_per_gather'");
44 $iMaxExecution = ini_get('max_execution_time');
45 if ($iMaxExecution > 0) {
46 $conn->setAttribute(\PDO::ATTR_TIMEOUT, $iMaxExecution); // seconds
49 $this->connection = $conn;
53 // returns the number of rows that were modified or deleted by the SQL
54 // statement. If no rows were affected returns 0.
55 public function exec($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
59 if (isset($aInputVars)) {
60 $stmt = $this->connection->prepare($sSQL);
61 $stmt->execute($aInputVars);
63 $val = $this->connection->exec($sSQL);
65 } catch (\PDOException $e) {
66 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
72 * Executes query. Returns first row as array.
73 * Returns false if no result found.
79 public function getRow($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
82 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
83 $row = $stmt->fetch();
84 } catch (\PDOException $e) {
85 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
91 * Executes query. Returns first value of first result.
92 * Returns false if no results found.
98 public function getOne($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
101 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
102 $row = $stmt->fetch(\PDO::FETCH_NUM);
103 if ($row === false) {
106 } catch (\PDOException $e) {
107 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
113 * Executes query. Returns array of results (arrays).
114 * Returns empty array if no results found.
116 * @param string $sSQL
120 public function getAll($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
123 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
124 $rows = $stmt->fetchAll();
125 } catch (\PDOException $e) {
126 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
132 * Executes query. Returns array of the first value of each result.
133 * Returns empty array if no results found.
135 * @param string $sSQL
139 public function getCol($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
143 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
145 while (($val = $stmt->fetchColumn(0)) !== false) { // returns first column or false
148 } catch (\PDOException $e) {
149 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
155 * Executes query. Returns associate array mapping first value to second value of each result.
156 * Returns empty array if no results found.
158 * @param string $sSQL
162 public function getAssoc($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
165 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
168 while ($aRow = $stmt->fetch(\PDO::FETCH_NUM)) {
169 $aList[$aRow[0]] = $aRow[1];
171 } catch (\PDOException $e) {
172 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
178 * Executes query. Returns a PDO statement to iterate over.
180 * @param string $sSQL
182 * @return PDOStatement
184 public function getQueryStatement($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
187 if (isset($aInputVars)) {
188 $stmt = $this->connection->prepare($sSQL);
189 $stmt->execute($aInputVars);
191 $stmt = $this->connection->query($sSQL);
193 } catch (\PDOException $e) {
194 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
200 * St. John's Way => 'St. John\'s Way'
202 * @param string $sVal Text to be quoted.
206 public function getDBQuoted($sVal)
208 return $this->connection->quote($sVal);
212 * Like getDBQuoted, but takes an array.
214 * @param array $aVals List of text to be quoted.
218 public function getDBQuotedList($aVals)
220 return array_map(function ($sVal) {
221 return $this->getDBQuoted($sVal);
226 * [1,2,'b'] => 'ARRAY[1,2,'b']''
228 * @param array $aVals List of text to be quoted.
232 public function getArraySQL($a)
234 return 'ARRAY['.join(',', $a).']';
238 * Check if a table exists in the database. Returns true if it does.
240 * @param string $sTableName
244 public function tableExists($sTableName)
246 $sSQL = 'SELECT count(*) FROM pg_tables WHERE tablename = :tablename';
247 return ($this->getOne($sSQL, array(':tablename' => $sTableName)) == 1);
251 * Deletes a table. Returns true if deleted or didn't exist.
253 * @param string $sTableName
257 public function deleteTable($sTableName)
259 return $this->exec('DROP TABLE IF EXISTS '.$sTableName.' CASCADE') == 0;
263 * Tries to connect to the database but on failure doesn't throw an exception.
267 public function checkConnection()
271 $this->connect(true);
272 } catch (\Nominatim\DatabaseError $e) {
283 public function getPostgresVersion()
285 $sVersionString = $this->getOne('SHOW server_version_num');
286 preg_match('#([0-9]?[0-9])([0-9][0-9])[0-9][0-9]#', $sVersionString, $aMatches);
287 return (float) ($aMatches[1].'.'.$aMatches[2]);
295 public function getPostgisVersion()
297 $sVersionString = $this->getOne('select postgis_lib_version()');
298 preg_match('#^([0-9]+)[.]([0-9]+)[.]#', $sVersionString, $aMatches);
299 return (float) ($aMatches[1].'.'.$aMatches[2]);
303 * Returns an associate array of postgresql database connection settings. Keys can
304 * be 'database', 'hostspec', 'port', 'username', 'password'.
305 * Returns empty array on failure, thus check if at least 'database' is set.
309 public static function parseDSN($sDSN)
311 // https://secure.php.net/manual/en/ref.pdo-pgsql.connection.php
313 if (preg_match('/^pgsql:(.+)$/', $sDSN, $aMatches)) {
314 foreach (explode(';', $aMatches[1]) as $sKeyVal) {
315 list($sKey, $sVal) = explode('=', $sKeyVal, 2);
316 if ($sKey == 'host') {
318 } elseif ($sKey == 'dbname') {
320 } elseif ($sKey == 'user') {
323 $aInfo[$sKey] = $sVal;
330 * Takes an array of settings and return the DNS string. Key names can be
331 * 'database', 'hostspec', 'port', 'username', 'password' but aliases
332 * 'dbname', 'host' and 'user' are also supported.
337 public static function generateDSN($aInfo)
340 'pgsql:host=%s;port=%s;dbname=%s;user=%s;password=%s;',
341 $aInfo['host'] ?? $aInfo['hostspec'] ?? '',
342 $aInfo['port'] ?? '',
343 $aInfo['dbname'] ?? $aInfo['database'] ?? '',
344 $aInfo['user'] ?? '',
345 $aInfo['password'] ?? ''
347 $sDSN = preg_replace('/\b\w+=;/', '', $sDSN);
348 $sDSN = preg_replace('/;\Z/', '', $sDSN);