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