]> git.openstreetmap.org Git - nominatim.git/blob - lib/DB.php
add symphony dotenv to prerequisites list
[nominatim.git] / lib / 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) $conn->setAttribute(\PDO::ATTR_TIMEOUT, $iMaxExecution); // seconds
43
44         $this->connection = $conn;
45         return true;
46     }
47
48     // returns the number of rows that were modified or deleted by the SQL
49     // statement. If no rows were affected returns 0.
50     public function exec($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
51     {
52         $val = null;
53         try {
54             if (isset($aInputVars)) {
55                 $stmt = $this->connection->prepare($sSQL);
56                 $stmt->execute($aInputVars);
57             } else {
58                 $val = $this->connection->exec($sSQL);
59             }
60         } catch (\PDOException $e) {
61             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
62         }
63         return $val;
64     }
65
66     /**
67      * Executes query. Returns first row as array.
68      * Returns false if no result found.
69      *
70      * @param string  $sSQL
71      *
72      * @return array[]
73      */
74     public function getRow($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
75     {
76         try {
77             $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
78             $row = $stmt->fetch();
79         } catch (\PDOException $e) {
80             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
81         }
82         return $row;
83     }
84
85     /**
86      * Executes query. Returns first value of first result.
87      * Returns false if no results found.
88      *
89      * @param string  $sSQL
90      *
91      * @return array[]
92      */
93     public function getOne($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
94     {
95         try {
96             $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
97             $row = $stmt->fetch(\PDO::FETCH_NUM);
98             if ($row === false) return false;
99         } catch (\PDOException $e) {
100             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
101         }
102         return $row[0];
103     }
104
105     /**
106      * Executes query. Returns array of results (arrays).
107      * Returns empty array if no results found.
108      *
109      * @param string  $sSQL
110      *
111      * @return array[]
112      */
113     public function getAll($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
114     {
115         try {
116             $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
117             $rows = $stmt->fetchAll();
118         } catch (\PDOException $e) {
119             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
120         }
121         return $rows;
122     }
123
124     /**
125      * Executes query. Returns array of the first value of each result.
126      * Returns empty array if no results found.
127      *
128      * @param string  $sSQL
129      *
130      * @return array[]
131      */
132     public function getCol($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
133     {
134         $aVals = array();
135         try {
136             $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
137
138             while (($val = $stmt->fetchColumn(0)) !== false) { // returns first column or false
139                 $aVals[] = $val;
140             }
141         } catch (\PDOException $e) {
142             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
143         }
144         return $aVals;
145     }
146
147     /**
148      * Executes query. Returns associate array mapping first value to second value of each result.
149      * Returns empty array if no results found.
150      *
151      * @param string  $sSQL
152      *
153      * @return array[]
154      */
155     public function getAssoc($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
156     {
157         try {
158             $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage);
159
160             $aList = array();
161             while ($aRow = $stmt->fetch(\PDO::FETCH_NUM)) {
162                 $aList[$aRow[0]] = $aRow[1];
163             }
164         } catch (\PDOException $e) {
165             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
166         }
167         return $aList;
168     }
169
170     /**
171      * Executes query. Returns a PDO statement to iterate over.
172      *
173      * @param string  $sSQL
174      *
175      * @return PDOStatement
176      */
177     public function getQueryStatement($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed')
178     {
179         try {
180             if (isset($aInputVars)) {
181                 $stmt = $this->connection->prepare($sSQL);
182                 $stmt->execute($aInputVars);
183             } else {
184                 $stmt = $this->connection->query($sSQL);
185             }
186         } catch (\PDOException $e) {
187             throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL);
188         }
189         return $stmt;
190     }
191
192     /**
193      * St. John's Way => 'St. John\'s Way'
194      *
195      * @param string  $sVal  Text to be quoted.
196      *
197      * @return string
198      */
199     public function getDBQuoted($sVal)
200     {
201         return $this->connection->quote($sVal);
202     }
203
204     /**
205      * Like getDBQuoted, but takes an array.
206      *
207      * @param array  $aVals  List of text to be quoted.
208      *
209      * @return array[]
210      */
211     public function getDBQuotedList($aVals)
212     {
213         return array_map(function ($sVal) {
214             return $this->getDBQuoted($sVal);
215         }, $aVals);
216     }
217
218     /**
219      * [1,2,'b'] => 'ARRAY[1,2,'b']''
220      *
221      * @param array  $aVals  List of text to be quoted.
222      *
223      * @return string
224      */
225     public function getArraySQL($a)
226     {
227         return 'ARRAY['.join(',', $a).']';
228     }
229
230     /**
231      * Check if a table exists in the database. Returns true if it does.
232      *
233      * @param string  $sTableName
234      *
235      * @return boolean
236      */
237     public function tableExists($sTableName)
238     {
239         $sSQL = 'SELECT count(*) FROM pg_tables WHERE tablename = :tablename';
240         return ($this->getOne($sSQL, array(':tablename' => $sTableName)) == 1);
241     }
242
243     /**
244     * Returns a list of table names in the database
245     *
246     * @return array[]
247     */
248     public function getListOfTables()
249     {
250         return $this->getCol("SELECT tablename FROM pg_tables WHERE schemaname='public'");
251     }
252
253     /**
254      * Deletes a table. Returns true if deleted or didn't exist.
255      *
256      * @param string  $sTableName
257      *
258      * @return boolean
259      */
260     public function deleteTable($sTableName)
261     {
262         return $this->exec('DROP TABLE IF EXISTS '.$sTableName.' CASCADE') == 0;
263     }
264
265     /**
266     * Check if an index exists in the database. Optional filtered by tablename
267     *
268     * @param string  $sTableName
269     *
270     * @return boolean
271     */
272     public function indexExists($sIndexName, $sTableName = null)
273     {
274         return in_array($sIndexName, $this->getListOfIndices($sTableName));
275     }
276
277     /**
278     * Returns a list of index names in the database, optional filtered by tablename
279     *
280     * @param string  $sTableName
281     *
282     * @return array
283     */
284     public function getListOfIndices($sTableName = null)
285     {
286         //  table_name            | index_name                      | column_name
287         // -----------------------+---------------------------------+--------------
288         //  country_name          | idx_country_name_country_code   | country_code
289         //  country_osm_grid      | idx_country_osm_grid_geometry   | geometry
290         //  import_polygon_delete | idx_import_polygon_delete_osmid | osm_id
291         //  import_polygon_delete | idx_import_polygon_delete_osmid | osm_type
292         //  import_polygon_error  | idx_import_polygon_error_osmid  | osm_id
293         //  import_polygon_error  | idx_import_polygon_error_osmid  | osm_type
294         $sSql = <<< END
295 SELECT
296     t.relname as table_name,
297     i.relname as index_name,
298     a.attname as column_name
299 FROM
300     pg_class t,
301     pg_class i,
302     pg_index ix,
303     pg_attribute a
304 WHERE
305     t.oid = ix.indrelid
306     and i.oid = ix.indexrelid
307     and a.attrelid = t.oid
308     and a.attnum = ANY(ix.indkey)
309     and t.relkind = 'r'
310     and i.relname NOT LIKE 'pg_%'
311     FILTERS
312  ORDER BY
313     t.relname,
314     i.relname,
315     a.attname
316 END;
317
318         $aRows = null;
319         if ($sTableName) {
320             $sSql = str_replace('FILTERS', 'and t.relname = :tablename', $sSql);
321             $aRows = $this->getAll($sSql, array(':tablename' => $sTableName));
322         } else {
323             $sSql = str_replace('FILTERS', '', $sSql);
324             $aRows = $this->getAll($sSql);
325         }
326
327         $aIndexNames = array_unique(array_map(function ($aRow) {
328             return $aRow['index_name'];
329         }, $aRows));
330         sort($aIndexNames);
331
332         return $aIndexNames;
333     }
334
335     /**
336      * Tries to connect to the database but on failure doesn't throw an exception.
337      *
338      * @return boolean
339      */
340     public function checkConnection()
341     {
342         $bExists = true;
343         try {
344             $this->connect(true);
345         } catch (\Nominatim\DatabaseError $e) {
346             $bExists = false;
347         }
348         return $bExists;
349     }
350
351     /**
352      * e.g. 9.6, 10, 11.2
353      *
354      * @return float
355      */
356     public function getPostgresVersion()
357     {
358         $sVersionString = $this->getOne('SHOW server_version_num');
359         preg_match('#([0-9]?[0-9])([0-9][0-9])[0-9][0-9]#', $sVersionString, $aMatches);
360         return (float) ($aMatches[1].'.'.$aMatches[2]);
361     }
362
363     /**
364      * e.g. 2, 2.2
365      *
366      * @return float
367      */
368     public function getPostgisVersion()
369     {
370         $sVersionString = $this->getOne('select postgis_lib_version()');
371         preg_match('#^([0-9]+)[.]([0-9]+)[.]#', $sVersionString, $aMatches);
372         return (float) ($aMatches[1].'.'.$aMatches[2]);
373     }
374
375     /**
376      * Returns an associate array of postgresql database connection settings. Keys can
377      * be 'database', 'hostspec', 'port', 'username', 'password'.
378      * Returns empty array on failure, thus check if at least 'database' is set.
379      *
380      * @return array[]
381      */
382     public static function parseDSN($sDSN)
383     {
384         // https://secure.php.net/manual/en/ref.pdo-pgsql.connection.php
385         $aInfo = array();
386         if (preg_match('/^pgsql:(.+)$/', $sDSN, $aMatches)) {
387             foreach (explode(';', $aMatches[1]) as $sKeyVal) {
388                 list($sKey, $sVal) = explode('=', $sKeyVal, 2);
389                 if ($sKey == 'host') $sKey = 'hostspec';
390                 if ($sKey == 'dbname') $sKey = 'database';
391                 if ($sKey == 'user') $sKey = 'username';
392                 $aInfo[$sKey] = $sVal;
393             }
394         }
395         return $aInfo;
396     }
397
398     /**
399      * Takes an array of settings and return the DNS string. Key names can be
400      * 'database', 'hostspec', 'port', 'username', 'password' but aliases
401      * 'dbname', 'host' and 'user' are also supported.
402      *
403      * @return string
404      *
405      */
406     public static function generateDSN($aInfo)
407     {
408         $sDSN = sprintf(
409             'pgsql:host=%s;port=%s;dbname=%s;user=%s;password=%s;',
410             $aInfo['host'] ?? $aInfo['hostspec'] ?? '',
411             $aInfo['port'] ?? '',
412             $aInfo['dbname'] ?? $aInfo['database'] ?? '',
413             $aInfo['user'] ?? '',
414             $aInfo['password'] ?? ''
415         );
416         $sDSN = preg_replace('/\b\w+=;/', '', $sDSN);
417         $sDSN = preg_replace('/;\Z/', '', $sDSN);
418
419         return $sDSN;
420     }
421 }