]> git.openstreetmap.org Git - nominatim.git/blob - lib-php/DB.php
63733cc793e815c43386d0c7efdf2112a74c7c1c
[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         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'");
53         } else {
54             $this->connection->exec("SET jit_above_cost TO -1");
55             $this->connection->exec("SET max_parallel_workers_per_gather TO 0");    
56         }
57         
58         $iMaxExecution = ini_get('max_execution_time');
59         if ($iMaxExecution > 0) {
60             $conn->setAttribute(\PDO::ATTR_TIMEOUT, $iMaxExecution); // seconds
61         }
62         
63         return true;
64     }
65
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')
69     {
70         $val = null;
71         try {
72             if (isset($aInputVars)) {
73                 $stmt = $this->connection->prepare($sSQL);
74                 $stmt->execute($aInputVars);
75             } else {
76                 $val = $this->connection->exec($sSQL);
77             }
78         } catch (\PDOException $e) {
79             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
80         }
81         return $val;
82     }
83
84     /**
85      * Executes query. Returns first row as array.
86      * Returns false if no result found.
87      *
88      * @param string  $sSQL
89      *
90      * @return array[]
91      */
92     public function getRow($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
93     {
94         try {
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);
99         }
100         return $row;
101     }
102
103     /**
104      * Executes query. Returns first value of first result.
105      * Returns false if no results found.
106      *
107      * @param string  $sSQL
108      *
109      * @return array[]
110      */
111     public function getOne($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
112     {
113         try {
114             $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
115             $row = $stmt->fetch(\PDO::FETCH_NUM);
116             if ($row === false) {
117                 return false;
118             }
119         } catch (\PDOException $e) {
120             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
121         }
122         return $row[0];
123     }
124
125     /**
126      * Executes query. Returns array of results (arrays).
127      * Returns empty array if no results found.
128      *
129      * @param string  $sSQL
130      *
131      * @return array[]
132      */
133     public function getAll($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
134     {
135         try {
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);
140         }
141         return $rows;
142     }
143
144     /**
145      * Executes query. Returns array of the first value of each result.
146      * Returns empty array if no results found.
147      *
148      * @param string  $sSQL
149      *
150      * @return array[]
151      */
152     public function getCol($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
153     {
154         $aVals = array();
155         try {
156             $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
157
158             while (($val = $stmt->fetchColumn(0)) !== false) { // returns first column or false
159                 $aVals[] = $val;
160             }
161         } catch (\PDOException $e) {
162             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
163         }
164         return $aVals;
165     }
166
167     /**
168      * Executes query. Returns associate array mapping first value to second value of each result.
169      * Returns empty array if no results found.
170      *
171      * @param string  $sSQL
172      *
173      * @return array[]
174      */
175     public function getAssoc($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
176     {
177         try {
178             $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
179
180             $aList = array();
181             while ($aRow = $stmt->fetch(\PDO::FETCH_NUM)) {
182                 $aList[$aRow[0]] = $aRow[1];
183             }
184         } catch (\PDOException $e) {
185             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
186         }
187         return $aList;
188     }
189
190     /**
191      * Executes query. Returns a PDO statement to iterate over.
192      *
193      * @param string  $sSQL
194      *
195      * @return PDOStatement
196      */
197     public function getQueryStatement($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
198     {
199         try {
200             if (isset($aInputVars)) {
201                 $stmt = $this->connection->prepare($sSQL);
202                 $stmt->execute($aInputVars);
203             } else {
204                 $stmt = $this->connection->query($sSQL);
205             }
206         } catch (\PDOException $e) {
207             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
208         }
209         return $stmt;
210     }
211
212     /**
213      * St. John's Way => 'St. John\'s Way'
214      *
215      * @param string  $sVal  Text to be quoted.
216      *
217      * @return string
218      */
219     public function getDBQuoted($sVal)
220     {
221         return $this->connection->quote($sVal);
222     }
223
224     /**
225      * Like getDBQuoted, but takes an array.
226      *
227      * @param array  $aVals  List of text to be quoted.
228      *
229      * @return array[]
230      */
231     public function getDBQuotedList($aVals)
232     {
233         return array_map(function ($sVal) {
234             return $this->getDBQuoted($sVal);
235         }, $aVals);
236     }
237
238     /**
239      * [1,2,'b'] => 'ARRAY[1,2,'b']''
240      *
241      * @param array  $aVals  List of text to be quoted.
242      *
243      * @return string
244      */
245     public function getArraySQL($a)
246     {
247         return 'ARRAY['.join(',', $a).']';
248     }
249
250     /**
251      * Check if a table exists in the database. Returns true if it does.
252      *
253      * @param string  $sTableName
254      *
255      * @return boolean
256      */
257     public function tableExists($sTableName)
258     {
259         $sSQL = 'SELECT count(*) FROM pg_tables WHERE tablename = :tablename';
260         return ($this->getOne($sSQL, array(':tablename' => $sTableName)) == 1);
261     }
262
263     /**
264      * Deletes a table. Returns true if deleted or didn't exist.
265      *
266      * @param string  $sTableName
267      *
268      * @return boolean
269      */
270     public function deleteTable($sTableName)
271     {
272         return $this->exec('DROP TABLE IF EXISTS '.$sTableName.' CASCADE') == 0;
273     }
274
275     /**
276      * Tries to connect to the database but on failure doesn't throw an exception.
277      *
278      * @return boolean
279      */
280     public function checkConnection()
281     {
282         $bExists = true;
283         try {
284             $this->connect(true);
285         } catch (\Nominatim\DatabaseError $e) {
286             $bExists = false;
287         }
288         return $bExists;
289     }
290
291     /**
292      * e.g. 9.6, 10, 11.2
293      *
294      * @return float
295      */
296     public function getPostgresVersion()
297     {
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]);
301     }
302
303     /**
304      * e.g. 2, 2.2
305      *
306      * @return float
307      */
308     public function getPostgisVersion()
309     {
310         $sVersionString = $this->getOne('select postgis_lib_version()');
311         preg_match('#^([0-9]+)[.]([0-9]+)[.]#', $sVersionString, $aMatches);
312         return (float) ($aMatches[1].'.'.$aMatches[2]);
313     }
314
315     /**
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.
319      *
320      * @return array[]
321      */
322     public static function parseDSN($sDSN)
323     {
324         // https://secure.php.net/manual/en/ref.pdo-pgsql.connection.php
325         $aInfo = array();
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') {
330                     $sKey = 'hostspec';
331                 } elseif ($sKey == 'dbname') {
332                     $sKey = 'database';
333                 } elseif ($sKey == 'user') {
334                     $sKey = 'username';
335                 }
336                 $aInfo[$sKey] = $sVal;
337             }
338         }
339         return $aInfo;
340     }
341
342     /**
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.
346      *
347      * @return string
348      *
349      */
350     public static function generateDSN($aInfo)
351     {
352         $sDSN = sprintf(
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'] ?? ''
359         );
360         $sDSN = preg_replace('/\b\w+=;/', '', $sDSN);
361         $sDSN = preg_replace('/;\Z/', '', $sDSN);
362
363         return $sDSN;
364     }
365 }