3 * SPDX-License-Identifier: GPL-2.0-only
5 * This file is part of Nominatim. (https://nominatim.org)
7 * Copyright (C) 2022 by the Nominatim developer community.
8 * For a full list of authors see the git log.
13 require_once(CONST_LibDir.'/DatabaseError.php');
16 * Uses PDO to access the database specified in the CONST_Database_DSN
21 protected $connection;
23 public function __construct($sDSN = null)
25 $this->sDSN = $sDSN ?? getSetting('DATABASE_DSN');
28 public function connect($bNew = false, $bPersistent = true)
30 if (isset($this->connection) && !$bNew) {
33 $aConnOptions = array(
34 \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
35 \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
36 \PDO::ATTR_PERSISTENT => $bPersistent
39 // https://secure.php.net/manual/en/ref.pdo-pgsql.connection.php
41 $this->connection = new \PDO($this->sDSN, null, null, $aConnOptions);
42 } catch (\PDOException $e) {
43 $sMsg = 'Failed to establish database connection:' . $e->getMessage();
44 throw new \Nominatim\DatabaseError($sMsg, 500, null, $e->getMessage());
47 $this->connection->exec("SET DateStyle TO 'sql,european'");
48 $this->connection->exec("SET client_encoding TO 'utf-8'");
49 // Disable JIT and parallel workers. They interfere badly with search SQL.
50 $this->connection->exec('SET max_parallel_workers_per_gather TO 0');
51 if ($this->getPostgresVersion() >= 11) {
52 $this->connection->exec('SET jit_above_cost TO -1');
55 $iMaxExecution = ini_get('max_execution_time');
56 if ($iMaxExecution > 0) {
57 $this->connection->setAttribute(\PDO::ATTR_TIMEOUT, $iMaxExecution); // seconds
63 // returns the number of rows that were modified or deleted by the SQL
64 // statement. If no rows were affected returns 0.
65 public function exec($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
69 if (isset($aInputVars)) {
70 $stmt = $this->connection->prepare($sSQL);
71 $stmt->execute($aInputVars);
73 $val = $this->connection->exec($sSQL);
75 } catch (\PDOException $e) {
76 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
82 * Executes query. Returns first row as array.
83 * Returns false if no result found.
89 public function getRow($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
92 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
93 $row = $stmt->fetch();
94 } catch (\PDOException $e) {
95 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
101 * Executes query. Returns first value of first result.
102 * Returns false if no results found.
104 * @param string $sSQL
108 public function getOne($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
111 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
112 $row = $stmt->fetch(\PDO::FETCH_NUM);
113 if ($row === false) {
116 } catch (\PDOException $e) {
117 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
123 * Executes query. Returns array of results (arrays).
124 * Returns empty array if no results found.
126 * @param string $sSQL
130 public function getAll($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
133 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
134 $rows = $stmt->fetchAll();
135 } catch (\PDOException $e) {
136 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
142 * Executes query. Returns array of the first value of each result.
143 * Returns empty array if no results found.
145 * @param string $sSQL
149 public function getCol($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
153 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
155 while (($val = $stmt->fetchColumn(0)) !== false) { // returns first column or false
158 } catch (\PDOException $e) {
159 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
165 * Executes query. Returns associate array mapping first value to second value of each result.
166 * Returns empty array if no results found.
168 * @param string $sSQL
172 public function getAssoc($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
175 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
178 while ($aRow = $stmt->fetch(\PDO::FETCH_NUM)) {
179 $aList[$aRow[0]] = $aRow[1];
181 } catch (\PDOException $e) {
182 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
188 * Executes query. Returns a PDO statement to iterate over.
190 * @param string $sSQL
192 * @return PDOStatement
194 public function getQueryStatement($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
197 if (isset($aInputVars)) {
198 $stmt = $this->connection->prepare($sSQL);
199 $stmt->execute($aInputVars);
201 $stmt = $this->connection->query($sSQL);
203 } catch (\PDOException $e) {
204 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
210 * St. John's Way => 'St. John\'s Way'
212 * @param string $sVal Text to be quoted.
216 public function getDBQuoted($sVal)
218 return $this->connection->quote($sVal);
222 * Like getDBQuoted, but takes an array.
224 * @param array $aVals List of text to be quoted.
228 public function getDBQuotedList($aVals)
230 return array_map(function ($sVal) {
231 return $this->getDBQuoted($sVal);
236 * [1,2,'b'] => 'ARRAY[1,2,'b']''
238 * @param array $aVals List of text to be quoted.
242 public function getArraySQL($a)
244 return 'ARRAY['.join(',', $a).']';
248 * Check if a table exists in the database. Returns true if it does.
250 * @param string $sTableName
254 public function tableExists($sTableName)
256 $sSQL = 'SELECT count(*) FROM pg_tables WHERE tablename = :tablename';
257 return ($this->getOne($sSQL, array(':tablename' => $sTableName)) == 1);
261 * Deletes a table. Returns true if deleted or didn't exist.
263 * @param string $sTableName
267 public function deleteTable($sTableName)
269 return $this->exec('DROP TABLE IF EXISTS '.$sTableName.' CASCADE') == 0;
273 * Tries to connect to the database but on failure doesn't throw an exception.
277 public function checkConnection()
281 $this->connect(true);
282 } catch (\Nominatim\DatabaseError $e) {
293 public function getPostgresVersion()
295 $sVersionString = $this->getOne('SHOW server_version_num');
296 preg_match('#([0-9]?[0-9])([0-9][0-9])[0-9][0-9]#', $sVersionString, $aMatches);
297 return (float) ($aMatches[1].'.'.$aMatches[2]);
305 public function getPostgisVersion()
307 $sVersionString = $this->getOne('select postgis_lib_version()');
308 preg_match('#^([0-9]+)[.]([0-9]+)[.]#', $sVersionString, $aMatches);
309 return (float) ($aMatches[1].'.'.$aMatches[2]);
313 * Returns an associate array of postgresql database connection settings. Keys can
314 * be 'database', 'hostspec', 'port', 'username', 'password'.
315 * Returns empty array on failure, thus check if at least 'database' is set.
319 public static function parseDSN($sDSN)
321 // https://secure.php.net/manual/en/ref.pdo-pgsql.connection.php
323 if (preg_match('/^pgsql:(.+)$/', $sDSN, $aMatches)) {
324 foreach (explode(';', $aMatches[1]) as $sKeyVal) {
325 list($sKey, $sVal) = explode('=', $sKeyVal, 2);
326 if ($sKey == 'host') {
328 } elseif ($sKey == 'dbname') {
330 } elseif ($sKey == 'user') {
333 $aInfo[$sKey] = $sVal;
340 * Takes an array of settings and return the DNS string. Key names can be
341 * 'database', 'hostspec', 'port', 'username', 'password' but aliases
342 * 'dbname', 'host' and 'user' are also supported.
347 public static function generateDSN($aInfo)
350 'pgsql:host=%s;port=%s;dbname=%s;user=%s;password=%s;',
351 $aInfo['host'] ?? $aInfo['hostspec'] ?? '',
352 $aInfo['port'] ?? '',
353 $aInfo['dbname'] ?? $aInfo['database'] ?? '',
354 $aInfo['user'] ?? '',
355 $aInfo['password'] ?? ''
357 $sDSN = preg_replace('/\b\w+=;/', '', $sDSN);
358 $sDSN = preg_replace('/;\Z/', '', $sDSN);