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("UPDATE pg_settings SET setting = -1 WHERE name = 'jit_above_cost'");
 
  51         $conn->exec("UPDATE pg_settings SET setting = 0 WHERE name = 'max_parallel_workers_per_gather'");
 
  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);