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 $conn = 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 $conn->exec("SET DateStyle TO 'sql,european'");
48 $conn->exec("SET client_encoding TO 'utf-8'");
49 // Disable JIT and parallel workers. They interfere badly with search SQL.
50 $conn->exec("SET jit_above_cost TO -1");
51 $conn->exec("SET max_parallel_workers_per_gather TO 0");
52 $iMaxExecution = ini_get('max_execution_time');
53 if ($iMaxExecution > 0) {
54 $conn->setAttribute(\PDO::ATTR_TIMEOUT, $iMaxExecution); // seconds
57 $this->connection = $conn;
61 // returns the number of rows that were modified or deleted by the SQL
62 // statement. If no rows were affected returns 0.
63 public function exec($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
67 if (isset($aInputVars)) {
68 $stmt = $this->connection->prepare($sSQL);
69 $stmt->execute($aInputVars);
71 $val = $this->connection->exec($sSQL);
73 } catch (\PDOException $e) {
74 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
80 * Executes query. Returns first row as array.
81 * Returns false if no result found.
87 public function getRow($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
90 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
91 $row = $stmt->fetch();
92 } catch (\PDOException $e) {
93 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
99 * Executes query. Returns first value of first result.
100 * Returns false if no results found.
102 * @param string $sSQL
106 public function getOne($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
109 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
110 $row = $stmt->fetch(\PDO::FETCH_NUM);
111 if ($row === false) {
114 } catch (\PDOException $e) {
115 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
121 * Executes query. Returns array of results (arrays).
122 * Returns empty array if no results found.
124 * @param string $sSQL
128 public function getAll($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
131 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
132 $rows = $stmt->fetchAll();
133 } catch (\PDOException $e) {
134 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
140 * Executes query. Returns array of the first value of each result.
141 * Returns empty array if no results found.
143 * @param string $sSQL
147 public function getCol($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
151 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
153 while (($val = $stmt->fetchColumn(0)) !== false) { // returns first column or false
156 } catch (\PDOException $e) {
157 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
163 * Executes query. Returns associate array mapping first value to second value of each result.
164 * Returns empty array if no results found.
166 * @param string $sSQL
170 public function getAssoc($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
173 $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
176 while ($aRow = $stmt->fetch(\PDO::FETCH_NUM)) {
177 $aList[$aRow[0]] = $aRow[1];
179 } catch (\PDOException $e) {
180 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
186 * Executes query. Returns a PDO statement to iterate over.
188 * @param string $sSQL
190 * @return PDOStatement
192 public function getQueryStatement($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
195 if (isset($aInputVars)) {
196 $stmt = $this->connection->prepare($sSQL);
197 $stmt->execute($aInputVars);
199 $stmt = $this->connection->query($sSQL);
201 } catch (\PDOException $e) {
202 throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
208 * St. John's Way => 'St. John\'s Way'
210 * @param string $sVal Text to be quoted.
214 public function getDBQuoted($sVal)
216 return $this->connection->quote($sVal);
220 * Like getDBQuoted, but takes an array.
222 * @param array $aVals List of text to be quoted.
226 public function getDBQuotedList($aVals)
228 return array_map(function ($sVal) {
229 return $this->getDBQuoted($sVal);
234 * [1,2,'b'] => 'ARRAY[1,2,'b']''
236 * @param array $aVals List of text to be quoted.
240 public function getArraySQL($a)
242 return 'ARRAY['.join(',', $a).']';
246 * Check if a table exists in the database. Returns true if it does.
248 * @param string $sTableName
252 public function tableExists($sTableName)
254 $sSQL = 'SELECT count(*) FROM pg_tables WHERE tablename = :tablename';
255 return ($this->getOne($sSQL, array(':tablename' => $sTableName)) == 1);
259 * Deletes a table. Returns true if deleted or didn't exist.
261 * @param string $sTableName
265 public function deleteTable($sTableName)
267 return $this->exec('DROP TABLE IF EXISTS '.$sTableName.' CASCADE') == 0;
271 * Tries to connect to the database but on failure doesn't throw an exception.
275 public function checkConnection()
279 $this->connect(true);
280 } catch (\Nominatim\DatabaseError $e) {
291 public function getPostgresVersion()
293 $sVersionString = $this->getOne('SHOW server_version_num');
294 preg_match('#([0-9]?[0-9])([0-9][0-9])[0-9][0-9]#', $sVersionString, $aMatches);
295 return (float) ($aMatches[1].'.'.$aMatches[2]);
303 public function getPostgisVersion()
305 $sVersionString = $this->getOne('select postgis_lib_version()');
306 preg_match('#^([0-9]+)[.]([0-9]+)[.]#', $sVersionString, $aMatches);
307 return (float) ($aMatches[1].'.'.$aMatches[2]);
311 * Returns an associate array of postgresql database connection settings. Keys can
312 * be 'database', 'hostspec', 'port', 'username', 'password'.
313 * Returns empty array on failure, thus check if at least 'database' is set.
317 public static function parseDSN($sDSN)
319 // https://secure.php.net/manual/en/ref.pdo-pgsql.connection.php
321 if (preg_match('/^pgsql:(.+)$/', $sDSN, $aMatches)) {
322 foreach (explode(';', $aMatches[1]) as $sKeyVal) {
323 list($sKey, $sVal) = explode('=', $sKeyVal, 2);
324 if ($sKey == 'host') {
326 } elseif ($sKey == 'dbname') {
328 } elseif ($sKey == 'user') {
331 $aInfo[$sKey] = $sVal;
338 * Takes an array of settings and return the DNS string. Key names can be
339 * 'database', 'hostspec', 'port', 'username', 'password' but aliases
340 * 'dbname', 'host' and 'user' are also supported.
345 public static function generateDSN($aInfo)
348 'pgsql:host=%s;port=%s;dbname=%s;user=%s;password=%s;',
349 $aInfo['host'] ?? $aInfo['hostspec'] ?? '',
350 $aInfo['port'] ?? '',
351 $aInfo['dbname'] ?? $aInfo['database'] ?? '',
352 $aInfo['user'] ?? '',
353 $aInfo['password'] ?? ''
355 $sDSN = preg_replace('/\b\w+=;/', '', $sDSN);
356 $sDSN = preg_replace('/;\Z/', '', $sDSN);