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 if ($this->getPostgresVersion() < 11) {
51 $this->connection->exec("UPDATE pg_settings SET setting = -1 WHERE name = 'jit_above_cost'");
52 $this->connection->exec("UPDATE pg_settings SET setting = 0 WHERE name = 'max_parallel_workers_per_gather'");
54 $this->connection->exec("SET jit_above_cost TO -1");
55 $this->connection->exec("SET max_parallel_workers_per_gather TO 0");
58 $iMaxExecution = ini_get('max_execution_time');
59 if ($iMaxExecution > 0) {
60 $this->connection->setAttribute(\PDO::ATTR_TIMEOUT, $iMaxExecution); // seconds
66 // returns the number of rows that were modified or deleted by the SQL
67 // statement. If no rows were affected returns 0.
68 public function exec($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
72 if (isset($aInputVars)) {
73 $stmt = $this->connection->prepare($sSQL);
74 $stmt->execute($aInputVars);
76 $val = $this->connection->exec($sSQL);
78 } catch (\PDOException $e) {
79 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
85 * Executes query. Returns first row as array.
86 * Returns false if no result found.
92 public function getRow($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
95 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
96 $row = $stmt->fetch();
97 } catch (\PDOException $e) {
98 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
104 * Executes query. Returns first value of first result.
105 * Returns false if no results found.
107 * @param string $sSQL
111 public function getOne($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
114 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
115 $row = $stmt->fetch(\PDO::FETCH_NUM);
116 if ($row === false) {
119 } catch (\PDOException $e) {
120 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
126 * Executes query. Returns array of results (arrays).
127 * Returns empty array if no results found.
129 * @param string $sSQL
133 public function getAll($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
136 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
137 $rows = $stmt->fetchAll();
138 } catch (\PDOException $e) {
139 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
145 * Executes query. Returns array of the first value of each result.
146 * Returns empty array if no results found.
148 * @param string $sSQL
152 public function getCol($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
156 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
158 while (($val = $stmt->fetchColumn(0)) !== false) { // returns first column or false
161 } catch (\PDOException $e) {
162 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
168 * Executes query. Returns associate array mapping first value to second value of each result.
169 * Returns empty array if no results found.
171 * @param string $sSQL
175 public function getAssoc($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
178 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
181 while ($aRow = $stmt->fetch(\PDO::FETCH_NUM)) {
182 $aList[$aRow[0]] = $aRow[1];
184 } catch (\PDOException $e) {
185 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
191 * Executes query. Returns a PDO statement to iterate over.
193 * @param string $sSQL
195 * @return PDOStatement
197 public function getQueryStatement($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
200 if (isset($aInputVars)) {
201 $stmt = $this->connection->prepare($sSQL);
202 $stmt->execute($aInputVars);
204 $stmt = $this->connection->query($sSQL);
206 } catch (\PDOException $e) {
207 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
213 * St. John's Way => 'St. John\'s Way'
215 * @param string $sVal Text to be quoted.
219 public function getDBQuoted($sVal)
221 return $this->connection->quote($sVal);
225 * Like getDBQuoted, but takes an array.
227 * @param array $aVals List of text to be quoted.
231 public function getDBQuotedList($aVals)
233 return array_map(function ($sVal) {
234 return $this->getDBQuoted($sVal);
239 * [1,2,'b'] => 'ARRAY[1,2,'b']''
241 * @param array $aVals List of text to be quoted.
245 public function getArraySQL($a)
247 return 'ARRAY['.join(',', $a).']';
251 * Check if a table exists in the database. Returns true if it does.
253 * @param string $sTableName
257 public function tableExists($sTableName)
259 $sSQL = 'SELECT count(*) FROM pg_tables WHERE tablename = :tablename';
260 return ($this->getOne($sSQL, array(':tablename' => $sTableName)) == 1);
264 * Deletes a table. Returns true if deleted or didn't exist.
266 * @param string $sTableName
270 public function deleteTable($sTableName)
272 return $this->exec('DROP TABLE IF EXISTS '.$sTableName.' CASCADE') == 0;
276 * Tries to connect to the database but on failure doesn't throw an exception.
280 public function checkConnection()
284 $this->connect(true);
285 } catch (\Nominatim\DatabaseError $e) {
296 public function getPostgresVersion()
298 $sVersionString = $this->getOne('SHOW server_version_num');
299 preg_match('#([0-9]?[0-9])([0-9][0-9])[0-9][0-9]#', $sVersionString, $aMatches);
300 return (float) ($aMatches[1].'.'.$aMatches[2]);
308 public function getPostgisVersion()
310 $sVersionString = $this->getOne('select postgis_lib_version()');
311 preg_match('#^([0-9]+)[.]([0-9]+)[.]#', $sVersionString, $aMatches);
312 return (float) ($aMatches[1].'.'.$aMatches[2]);
316 * Returns an associate array of postgresql database connection settings. Keys can
317 * be 'database', 'hostspec', 'port', 'username', 'password'.
318 * Returns empty array on failure, thus check if at least 'database' is set.
322 public static function parseDSN($sDSN)
324 // https://secure.php.net/manual/en/ref.pdo-pgsql.connection.php
326 if (preg_match('/^pgsql:(.+)$/', $sDSN, $aMatches)) {
327 foreach (explode(';', $aMatches[1]) as $sKeyVal) {
328 list($sKey, $sVal) = explode('=', $sKeyVal, 2);
329 if ($sKey == 'host') {
331 } elseif ($sKey == 'dbname') {
333 } elseif ($sKey == 'user') {
336 $aInfo[$sKey] = $sVal;
343 * Takes an array of settings and return the DNS string. Key names can be
344 * 'database', 'hostspec', 'port', 'username', 'password' but aliases
345 * 'dbname', 'host' and 'user' are also supported.
350 public static function generateDSN($aInfo)
353 'pgsql:host=%s;port=%s;dbname=%s;user=%s;password=%s;',
354 $aInfo['host'] ?? $aInfo['hostspec'] ?? '',
355 $aInfo['port'] ?? '',
356 $aInfo['dbname'] ?? $aInfo['database'] ?? '',
357 $aInfo['user'] ?? '',
358 $aInfo['password'] ?? ''
360 $sDSN = preg_replace('/\b\w+=;/', '', $sDSN);
361 $sDSN = preg_replace('/;\Z/', '', $sDSN);