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