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