]> git.openstreetmap.org Git - nominatim.git/blob - lib-php/DB.php
work around strange query planning behaviour
[nominatim.git] / lib-php / DB.php
1 <?php
2 /**
3  * SPDX-License-Identifier: GPL-2.0-only
4  *
5  * This file is part of Nominatim. (https://nominatim.org)
6  *
7  * Copyright (C) 2022 by the Nominatim developer community.
8  * For a full list of authors see the git log.
9  */
10
11 namespace Nominatim;
12
13 require_once(CONST_LibDir.'/DatabaseError.php');
14
15 /**
16  * Uses PDO to access the database specified in the CONST_Database_DSN
17  * setting.
18  */
19 class DB
20 {
21     protected $connection;
22
23     public function __construct($sDSN = null)
24     {
25         $this->sDSN = $sDSN ?? getSetting('DATABASE_DSN');
26     }
27
28     public function connect($bNew = false, $bPersistent = true)
29     {
30         if (isset($this->connection) && !$bNew) {
31             return true;
32         }
33         $aConnOptions = array(
34                          \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_EXCEPTION,
35                          \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
36                          \PDO::ATTR_PERSISTENT         => $bPersistent
37         );
38
39         // https://secure.php.net/manual/en/ref.pdo-pgsql.connection.php
40         try {
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());
45         }
46
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');
53         }
54         
55         $iMaxExecution = ini_get('max_execution_time');
56         if ($iMaxExecution > 0) {
57             $this->connection->setAttribute(\PDO::ATTR_TIMEOUT, $iMaxExecution); // seconds
58         }
59
60         return true;
61     }
62
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')
66     {
67         $val = null;
68         try {
69             if (isset($aInputVars)) {
70                 $stmt = $this->connection->prepare($sSQL);
71                 $stmt->execute($aInputVars);
72             } else {
73                 $val = $this->connection->exec($sSQL);
74             }
75         } catch (\PDOException $e) {
76             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
77         }
78         return $val;
79     }
80
81     /**
82      * Executes query. Returns first row as array.
83      * Returns false if no result found.
84      *
85      * @param string  $sSQL
86      *
87      * @return array[]
88      */
89     public function getRow($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
90     {
91         try {
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);
96         }
97         return $row;
98     }
99
100     /**
101      * Executes query. Returns first value of first result.
102      * Returns false if no results found.
103      *
104      * @param string  $sSQL
105      *
106      * @return array[]
107      */
108     public function getOne($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
109     {
110         try {
111             $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
112             $row = $stmt->fetch(\PDO::FETCH_NUM);
113             if ($row === false) {
114                 return false;
115             }
116         } catch (\PDOException $e) {
117             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
118         }
119         return $row[0];
120     }
121
122     /**
123      * Executes query. Returns array of results (arrays).
124      * Returns empty array if no results found.
125      *
126      * @param string  $sSQL
127      *
128      * @return array[]
129      */
130     public function getAll($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
131     {
132         try {
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);
137         }
138         return $rows;
139     }
140
141     /**
142      * Executes query. Returns array of the first value of each result.
143      * Returns empty array if no results found.
144      *
145      * @param string  $sSQL
146      *
147      * @return array[]
148      */
149     public function getCol($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
150     {
151         $aVals = array();
152         try {
153             $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
154
155             while (($val = $stmt->fetchColumn(0)) !== false) { // returns first column or false
156                 $aVals[] = $val;
157             }
158         } catch (\PDOException $e) {
159             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
160         }
161         return $aVals;
162     }
163
164     /**
165      * Executes query. Returns associate array mapping first value to second value of each result.
166      * Returns empty array if no results found.
167      *
168      * @param string  $sSQL
169      *
170      * @return array[]
171      */
172     public function getAssoc($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
173     {
174         try {
175             $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
176
177             $aList = array();
178             while ($aRow = $stmt->fetch(\PDO::FETCH_NUM)) {
179                 $aList[$aRow[0]] = $aRow[1];
180             }
181         } catch (\PDOException $e) {
182             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
183         }
184         return $aList;
185     }
186
187     /**
188      * Executes query. Returns a PDO statement to iterate over.
189      *
190      * @param string  $sSQL
191      *
192      * @return PDOStatement
193      */
194     public function getQueryStatement($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
195     {
196         try {
197             if (isset($aInputVars)) {
198                 $stmt = $this->connection->prepare($sSQL);
199                 $stmt->execute($aInputVars);
200             } else {
201                 $stmt = $this->connection->query($sSQL);
202             }
203         } catch (\PDOException $e) {
204             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
205         }
206         return $stmt;
207     }
208
209     /**
210      * St. John's Way => 'St. John\'s Way'
211      *
212      * @param string  $sVal  Text to be quoted.
213      *
214      * @return string
215      */
216     public function getDBQuoted($sVal)
217     {
218         return $this->connection->quote($sVal);
219     }
220
221     /**
222      * Like getDBQuoted, but takes an array.
223      *
224      * @param array  $aVals  List of text to be quoted.
225      *
226      * @return array[]
227      */
228     public function getDBQuotedList($aVals)
229     {
230         return array_map(function ($sVal) {
231             return $this->getDBQuoted($sVal);
232         }, $aVals);
233     }
234
235     /**
236      * [1,2,'b'] => 'ARRAY[1,2,'b']''
237      *
238      * @param array  $aVals  List of text to be quoted.
239      *
240      * @return string
241      */
242     public function getArraySQL($a)
243     {
244         return 'ARRAY['.join(',', $a).']';
245     }
246
247     /**
248      * Check if a table exists in the database. Returns true if it does.
249      *
250      * @param string  $sTableName
251      *
252      * @return boolean
253      */
254     public function tableExists($sTableName)
255     {
256         $sSQL = 'SELECT count(*) FROM pg_tables WHERE tablename = :tablename';
257         return ($this->getOne($sSQL, array(':tablename' => $sTableName)) == 1);
258     }
259
260     /**
261      * Deletes a table. Returns true if deleted or didn't exist.
262      *
263      * @param string  $sTableName
264      *
265      * @return boolean
266      */
267     public function deleteTable($sTableName)
268     {
269         return $this->exec('DROP TABLE IF EXISTS '.$sTableName.' CASCADE') == 0;
270     }
271
272     /**
273      * Tries to connect to the database but on failure doesn't throw an exception.
274      *
275      * @return boolean
276      */
277     public function checkConnection()
278     {
279         $bExists = true;
280         try {
281             $this->connect(true);
282         } catch (\Nominatim\DatabaseError $e) {
283             $bExists = false;
284         }
285         return $bExists;
286     }
287
288     /**
289      * e.g. 9.6, 10, 11.2
290      *
291      * @return float
292      */
293     public function getPostgresVersion()
294     {
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]);
298     }
299
300     /**
301      * e.g. 2, 2.2
302      *
303      * @return float
304      */
305     public function getPostgisVersion()
306     {
307         $sVersionString = $this->getOne('select postgis_lib_version()');
308         preg_match('#^([0-9]+)[.]([0-9]+)[.]#', $sVersionString, $aMatches);
309         return (float) ($aMatches[1].'.'.$aMatches[2]);
310     }
311
312     /**
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.
316      *
317      * @return array[]
318      */
319     public static function parseDSN($sDSN)
320     {
321         // https://secure.php.net/manual/en/ref.pdo-pgsql.connection.php
322         $aInfo = array();
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') {
327                     $sKey = 'hostspec';
328                 } elseif ($sKey == 'dbname') {
329                     $sKey = 'database';
330                 } elseif ($sKey == 'user') {
331                     $sKey = 'username';
332                 }
333                 $aInfo[$sKey] = $sVal;
334             }
335         }
336         return $aInfo;
337     }
338
339     /**
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.
343      *
344      * @return string
345      *
346      */
347     public static function generateDSN($aInfo)
348     {
349         $sDSN = sprintf(
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'] ?? ''
356         );
357         $sDSN = preg_replace('/\b\w+=;/', '', $sDSN);
358         $sDSN = preg_replace('/;\Z/', '', $sDSN);
359
360         return $sDSN;
361     }
362 }