3 namespace Nominatim\Setup;
5 require_once(CONST_LibDir.'/Shell.php');
13 protected $sIgnoreErrors;
14 protected $bEnableDiffUpdates;
15 protected $bEnableDebugStatements;
16 protected $bNoPartitions;
18 protected $oDB = null;
19 protected $oNominatimCmd;
21 public function __construct(array $aCMDResult)
23 // by default, use all but one processor, but never more than 15.
24 $this->iInstances = isset($aCMDResult['threads'])
25 ? $aCMDResult['threads']
26 : (min(16, getProcessorCount()) - 1);
28 if ($this->iInstances < 1) {
29 $this->iInstances = 1;
30 warn('resetting threads to '.$this->iInstances);
33 // parse database string
34 $this->aDSNInfo = \Nominatim\DB::parseDSN(getSetting('DATABASE_DSN'));
35 if (!isset($this->aDSNInfo['port'])) {
36 $this->aDSNInfo['port'] = 5432;
39 // setting member variables based on command line options stored in $aCMDResult
40 $this->bQuiet = isset($aCMDResult['quiet']) && $aCMDResult['quiet'];
41 $this->bVerbose = $aCMDResult['verbose'];
43 //setting default values which are not set by the update.php array
44 if (isset($aCMDResult['ignore-errors'])) {
45 $this->sIgnoreErrors = $aCMDResult['ignore-errors'];
47 $this->sIgnoreErrors = false;
49 if (isset($aCMDResult['enable-debug-statements'])) {
50 $this->bEnableDebugStatements = $aCMDResult['enable-debug-statements'];
52 $this->bEnableDebugStatements = false;
54 if (isset($aCMDResult['no-partitions'])) {
55 $this->bNoPartitions = $aCMDResult['no-partitions'];
57 $this->bNoPartitions = false;
59 if (isset($aCMDResult['enable-diff-updates'])) {
60 $this->bEnableDiffUpdates = $aCMDResult['enable-diff-updates'];
62 $this->bEnableDiffUpdates = false;
65 $this->bDrop = isset($aCMDResult['drop']) && $aCMDResult['drop'];
67 $this->oNominatimCmd = new \Nominatim\Shell(getSetting('NOMINATIM_TOOL'));
69 $this->oNominatimCmd->addParams('--quiet');
71 if ($this->bVerbose) {
72 $this->oNominatimCmd->addParams('--verbose');
76 public function createFunctions()
78 info('Create Functions');
80 // Try accessing the C module, so we know early if something is wrong
81 $this->checkModulePresence(); // raises exception on failure
83 $this->createSqlFunctions();
86 public function createTables($bReverseOnly = false)
88 info('Create Tables');
90 $sTemplate = file_get_contents(CONST_SqlDir.'/tables.sql');
91 $sTemplate = $this->replaceSqlPatterns($sTemplate);
93 $this->pgsqlRunScript($sTemplate, false);
96 $this->dropTable('search_name');
99 (clone($this->oNominatimCmd))->addParams('refresh', '--address-levels')->run();
102 public function createTableTriggers()
104 info('Create Tables');
106 $sTemplate = file_get_contents(CONST_SqlDir.'/table-triggers.sql');
107 $sTemplate = $this->replaceSqlPatterns($sTemplate);
109 $this->pgsqlRunScript($sTemplate, false);
112 public function createPartitionTables()
114 info('Create Partition Tables');
116 $sTemplate = file_get_contents(CONST_SqlDir.'/partition-tables.src.sql');
117 $sTemplate = $this->replaceSqlPatterns($sTemplate);
119 $this->pgsqlRunPartitionScript($sTemplate);
122 public function loadData($bDisableTokenPrecalc)
124 info('Drop old Data');
128 $oDB->exec('TRUNCATE word');
130 $oDB->exec('TRUNCATE placex');
132 $oDB->exec('TRUNCATE location_property_osmline');
134 $oDB->exec('TRUNCATE place_addressline');
136 $oDB->exec('TRUNCATE location_area');
138 if (!$this->dbReverseOnly()) {
139 $oDB->exec('TRUNCATE search_name');
142 $oDB->exec('TRUNCATE search_name_blank');
144 $oDB->exec('DROP SEQUENCE seq_place');
146 $oDB->exec('CREATE SEQUENCE seq_place start 100000');
149 $sSQL = 'select distinct partition from country_name';
150 $aPartitions = $oDB->getCol($sSQL);
152 if (!$this->bNoPartitions) $aPartitions[] = 0;
153 foreach ($aPartitions as $sPartition) {
154 $oDB->exec('TRUNCATE location_road_'.$sPartition);
158 // used by getorcreate_word_id to ignore frequent partial words
159 $sSQL = 'CREATE OR REPLACE FUNCTION get_maxwordfreq() RETURNS integer AS ';
160 $sSQL .= '$$ SELECT '.getSetting('MAX_WORD_FREQUENCY').' as maxwordfreq; $$ LANGUAGE SQL IMMUTABLE';
164 // pre-create the word list
165 if (!$bDisableTokenPrecalc) {
166 info('Loading word list');
167 $this->pgsqlRunScriptFile(CONST_DataDir.'/words.sql');
171 $sColumns = 'osm_type, osm_id, class, type, name, admin_level, address, extratags, geometry';
173 $aDBInstances = array();
174 $iLoadThreads = max(1, $this->iInstances - 1);
175 for ($i = 0; $i < $iLoadThreads; $i++) {
176 // https://secure.php.net/manual/en/function.pg-connect.php
177 $DSN = getSetting('DATABASE_DSN');
178 $DSN = preg_replace('/^pgsql:/', '', $DSN);
179 $DSN = preg_replace('/;/', ' ', $DSN);
180 $aDBInstances[$i] = pg_connect($DSN, PGSQL_CONNECT_FORCE_NEW);
181 pg_ping($aDBInstances[$i]);
184 for ($i = 0; $i < $iLoadThreads; $i++) {
185 $sSQL = "INSERT INTO placex ($sColumns) SELECT $sColumns FROM place WHERE osm_id % $iLoadThreads = $i";
186 $sSQL .= " and not (class='place' and type='houses' and osm_type='W'";
187 $sSQL .= " and ST_GeometryType(geometry) = 'ST_LineString')";
188 $sSQL .= ' and ST_IsValid(geometry)';
189 if ($this->bVerbose) echo "$sSQL\n";
190 if (!pg_send_query($aDBInstances[$i], $sSQL)) {
191 fail(pg_last_error($aDBInstances[$i]));
195 // last thread for interpolation lines
196 // https://secure.php.net/manual/en/function.pg-connect.php
197 $DSN = getSetting('DATABASE_DSN');
198 $DSN = preg_replace('/^pgsql:/', '', $DSN);
199 $DSN = preg_replace('/;/', ' ', $DSN);
200 $aDBInstances[$iLoadThreads] = pg_connect($DSN, PGSQL_CONNECT_FORCE_NEW);
201 pg_ping($aDBInstances[$iLoadThreads]);
202 $sSQL = 'insert into location_property_osmline';
203 $sSQL .= ' (osm_id, address, linegeo)';
204 $sSQL .= ' SELECT osm_id, address, geometry from place where ';
205 $sSQL .= "class='place' and type='houses' and osm_type='W' and ST_GeometryType(geometry) = 'ST_LineString'";
206 if ($this->bVerbose) echo "$sSQL\n";
207 if (!pg_send_query($aDBInstances[$iLoadThreads], $sSQL)) {
208 fail(pg_last_error($aDBInstances[$iLoadThreads]));
212 for ($i = 0; $i <= $iLoadThreads; $i++) {
213 while (($hPGresult = pg_get_result($aDBInstances[$i])) !== false) {
214 $resultStatus = pg_result_status($hPGresult);
215 // PGSQL_EMPTY_QUERY, PGSQL_COMMAND_OK, PGSQL_TUPLES_OK,
216 // PGSQL_COPY_OUT, PGSQL_COPY_IN, PGSQL_BAD_RESPONSE,
217 // PGSQL_NONFATAL_ERROR and PGSQL_FATAL_ERROR
218 // echo 'Query result ' . $i . ' is: ' . $resultStatus . "\n";
219 if ($resultStatus != PGSQL_COMMAND_OK && $resultStatus != PGSQL_TUPLES_OK) {
220 $resultError = pg_result_error($hPGresult);
221 echo '-- error text ' . $i . ': ' . $resultError . "\n";
227 fail('SQL errors loading placex and/or location_property_osmline tables');
230 for ($i = 0; $i < $this->iInstances; $i++) {
231 pg_close($aDBInstances[$i]);
235 info('Reanalysing database');
236 $this->pgsqlRunScript('ANALYSE');
238 $sDatabaseDate = getDatabaseDate($oDB);
239 $oDB->exec('TRUNCATE import_status');
240 if (!$sDatabaseDate) {
241 warn('could not determine database date.');
243 $sSQL = "INSERT INTO import_status (lastimportdate) VALUES('".$sDatabaseDate."')";
245 echo "Latest data imported from $sDatabaseDate.\n";
249 public function importTigerData($sTigerPath)
251 info('Import Tiger data');
253 $aFilenames = glob($sTigerPath.'/*.sql');
254 info('Found '.count($aFilenames).' SQL files in path '.$sTigerPath);
255 if (empty($aFilenames)) {
256 warn('Tiger data import selected but no files found in path '.$sTigerPath);
259 $sTemplate = file_get_contents(CONST_SqlDir.'/tiger_import_start.sql');
260 $sTemplate = $this->replaceSqlPatterns($sTemplate);
262 $this->pgsqlRunScript($sTemplate, false);
264 $aDBInstances = array();
265 for ($i = 0; $i < $this->iInstances; $i++) {
266 // https://secure.php.net/manual/en/function.pg-connect.php
267 $DSN = getSetting('DATABASE_DSN');
268 $DSN = preg_replace('/^pgsql:/', '', $DSN);
269 $DSN = preg_replace('/;/', ' ', $DSN);
270 $aDBInstances[$i] = pg_connect($DSN, PGSQL_CONNECT_FORCE_NEW | PGSQL_CONNECT_ASYNC);
271 pg_ping($aDBInstances[$i]);
274 foreach ($aFilenames as $sFile) {
276 $hFile = fopen($sFile, 'r');
277 $sSQL = fgets($hFile, 100000);
280 for ($i = 0; $i < $this->iInstances; $i++) {
281 if (!pg_connection_busy($aDBInstances[$i])) {
282 while (pg_get_result($aDBInstances[$i]));
283 $sSQL = fgets($hFile, 100000);
285 if (!pg_send_query($aDBInstances[$i], $sSQL)) fail(pg_last_error($aDBInstances[$i]));
287 if ($iLines == 1000) {
300 for ($i = 0; $i < $this->iInstances; $i++) {
301 if (pg_connection_busy($aDBInstances[$i])) $bAnyBusy = true;
308 for ($i = 0; $i < $this->iInstances; $i++) {
309 pg_close($aDBInstances[$i]);
312 info('Creating indexes on Tiger data');
313 $sTemplate = file_get_contents(CONST_SqlDir.'/tiger_import_finish.sql');
314 $sTemplate = $this->replaceSqlPatterns($sTemplate);
316 $this->pgsqlRunScript($sTemplate, false);
319 public function calculatePostcodes($bCMDResultAll)
321 info('Calculate Postcodes');
322 $this->pgsqlRunScriptFile(CONST_SqlDir.'/postcode_tables.sql');
324 $sPostcodeFilename = CONST_InstallDir.'/gb_postcode_data.sql.gz';
325 if (file_exists($sPostcodeFilename)) {
326 $this->pgsqlRunScriptFile($sPostcodeFilename);
328 warn('optional external GB postcode table file ('.$sPostcodeFilename.') not found. Skipping.');
331 $sPostcodeFilename = CONST_InstallDir.'/us_postcode_data.sql.gz';
332 if (file_exists($sPostcodeFilename)) {
333 $this->pgsqlRunScriptFile($sPostcodeFilename);
335 warn('optional external US postcode table file ('.$sPostcodeFilename.') not found. Skipping.');
339 $this->db()->exec('TRUNCATE location_postcode');
341 $sSQL = 'INSERT INTO location_postcode';
342 $sSQL .= ' (place_id, indexed_status, country_code, postcode, geometry) ';
343 $sSQL .= "SELECT nextval('seq_place'), 1, country_code,";
344 $sSQL .= " upper(trim (both ' ' from address->'postcode')) as pc,";
345 $sSQL .= ' ST_Centroid(ST_Collect(ST_Centroid(geometry)))';
346 $sSQL .= ' FROM placex';
347 $sSQL .= " WHERE address ? 'postcode' AND address->'postcode' NOT SIMILAR TO '%(,|;)%'";
348 $sSQL .= ' AND geometry IS NOT null';
349 $sSQL .= ' GROUP BY country_code, pc';
350 $this->db()->exec($sSQL);
352 // only add postcodes that are not yet available in OSM
353 $sSQL = 'INSERT INTO location_postcode';
354 $sSQL .= ' (place_id, indexed_status, country_code, postcode, geometry) ';
355 $sSQL .= "SELECT nextval('seq_place'), 1, 'us', postcode,";
356 $sSQL .= ' ST_SetSRID(ST_Point(x,y),4326)';
357 $sSQL .= ' FROM us_postcode WHERE postcode NOT IN';
358 $sSQL .= ' (SELECT postcode FROM location_postcode';
359 $sSQL .= " WHERE country_code = 'us')";
360 $this->db()->exec($sSQL);
362 // add missing postcodes for GB (if available)
363 $sSQL = 'INSERT INTO location_postcode';
364 $sSQL .= ' (place_id, indexed_status, country_code, postcode, geometry) ';
365 $sSQL .= "SELECT nextval('seq_place'), 1, 'gb', postcode, geometry";
366 $sSQL .= ' FROM gb_postcode WHERE postcode NOT IN';
367 $sSQL .= ' (SELECT postcode FROM location_postcode';
368 $sSQL .= " WHERE country_code = 'gb')";
369 $this->db()->exec($sSQL);
371 if (!$bCMDResultAll) {
372 $sSQL = "DELETE FROM word WHERE class='place' and type='postcode'";
373 $sSQL .= 'and word NOT IN (SELECT postcode FROM location_postcode)';
374 $this->db()->exec($sSQL);
377 $sSQL = 'SELECT count(getorcreate_postcode_id(v)) FROM ';
378 $sSQL .= '(SELECT distinct(postcode) as v FROM location_postcode) p';
379 $this->db()->exec($sSQL);
382 public function createSearchIndices()
384 info('Create Search indices');
386 $sSQL = 'SELECT relname FROM pg_class, pg_index ';
387 $sSQL .= 'WHERE pg_index.indisvalid = false AND pg_index.indexrelid = pg_class.oid';
388 $aInvalidIndices = $this->db()->getCol($sSQL);
390 foreach ($aInvalidIndices as $sIndexName) {
391 info("Cleaning up invalid index $sIndexName");
392 $this->db()->exec("DROP INDEX $sIndexName;");
395 $sTemplate = file_get_contents(CONST_SqlDir.'/indices.src.sql');
397 $sTemplate .= file_get_contents(CONST_SqlDir.'/indices_updates.src.sql');
399 if (!$this->dbReverseOnly()) {
400 $sTemplate .= file_get_contents(CONST_SqlDir.'/indices_search.src.sql');
402 $sTemplate = $this->replaceSqlPatterns($sTemplate);
404 $this->pgsqlRunScript($sTemplate);
407 public function createCountryNames()
409 info('Create search index for default country names');
411 $this->pgsqlRunScript("select getorcreate_country(make_standard_name('uk'), 'gb')");
412 $this->pgsqlRunScript("select getorcreate_country(make_standard_name('united states'), 'us')");
413 $this->pgsqlRunScript('select count(*) from (select getorcreate_country(make_standard_name(country_code), country_code) from country_name where country_code is not null) as x');
414 $this->pgsqlRunScript("select count(*) from (select getorcreate_country(make_standard_name(name->'name'), country_code) from country_name where name ? 'name') as x");
415 $sSQL = 'select count(*) from (select getorcreate_country(make_standard_name(v),'
416 .'country_code) from (select country_code, skeys(name) as k, svals(name) as v from country_name) x where k ';
417 $sLanguages = getSetting('LANGUAGES');
421 foreach (explode(',', $sLanguages) as $sLang) {
422 $sSQL .= $sDelim."'name:$sLang'";
427 // all include all simple name tags
428 $sSQL .= "like 'name:%'";
431 $this->pgsqlRunScript($sSQL);
435 * Return the connection to the database.
437 * @return Database object.
439 * Creates a new connection if none exists yet. Otherwise reuses the
440 * already established connection.
442 private function db()
444 if (is_null($this->oDB)) {
445 $this->oDB = new \Nominatim\DB();
446 $this->oDB->connect();
452 private function pgsqlRunScript($sScript, $bfatal = true)
462 private function createSqlFunctions()
464 $oCmd = (clone($this->oNominatimCmd))
465 ->addParams('refresh', '--functions');
467 if (!$this->bEnableDiffUpdates) {
468 $oCmd->addParams('--no-diff-updates');
471 if ($this->bEnableDebugStatements) {
472 $oCmd->addParams('--enable-debug-statements');
475 $oCmd->run(!$this->sIgnoreErrors);
478 private function pgsqlRunPartitionScript($sTemplate)
480 $sSQL = 'select distinct partition from country_name';
481 $aPartitions = $this->db()->getCol($sSQL);
482 if (!$this->bNoPartitions) $aPartitions[] = 0;
484 preg_match_all('#^-- start(.*?)^-- end#ms', $sTemplate, $aMatches, PREG_SET_ORDER);
485 foreach ($aMatches as $aMatch) {
487 foreach ($aPartitions as $sPartitionName) {
488 $sResult .= str_replace('-partition-', $sPartitionName, $aMatch[1]);
490 $sTemplate = str_replace($aMatch[0], $sResult, $sTemplate);
493 $this->pgsqlRunScript($sTemplate);
496 private function pgsqlRunScriptFile($sFilename)
498 if (!file_exists($sFilename)) fail('unable to find '.$sFilename);
500 $oCmd = (new \Nominatim\Shell('psql'))
501 ->addParams('--port', $this->aDSNInfo['port'])
502 ->addParams('--dbname', $this->aDSNInfo['database']);
504 if (!$this->bVerbose) {
505 $oCmd->addParams('--quiet');
507 if (isset($this->aDSNInfo['hostspec'])) {
508 $oCmd->addParams('--host', $this->aDSNInfo['hostspec']);
510 if (isset($this->aDSNInfo['username'])) {
511 $oCmd->addParams('--username', $this->aDSNInfo['username']);
513 if (isset($this->aDSNInfo['password'])) {
514 $oCmd->addEnvPair('PGPASSWORD', $this->aDSNInfo['password']);
517 if (preg_match('/\\.gz$/', $sFilename)) {
518 $aDescriptors = array(
519 0 => array('pipe', 'r'),
520 1 => array('pipe', 'w'),
521 2 => array('file', '/dev/null', 'a')
523 $oZcatCmd = new \Nominatim\Shell('zcat', $sFilename);
525 $hGzipProcess = proc_open($oZcatCmd->escapedCmd(), $aDescriptors, $ahGzipPipes);
526 if (!is_resource($hGzipProcess)) fail('unable to start zcat');
527 $aReadPipe = $ahGzipPipes[1];
528 fclose($ahGzipPipes[0]);
530 $oCmd->addParams('--file', $sFilename);
531 $aReadPipe = array('pipe', 'r');
533 $aDescriptors = array(
535 1 => array('pipe', 'w'),
536 2 => array('file', '/dev/null', 'a')
540 $hProcess = proc_open($oCmd->escapedCmd(), $aDescriptors, $ahPipes, null, $oCmd->aEnv);
541 if (!is_resource($hProcess)) fail('unable to start pgsql');
542 // TODO: error checking
543 while (!feof($ahPipes[1])) {
544 echo fread($ahPipes[1], 4096);
547 $iReturn = proc_close($hProcess);
549 fail("pgsql returned with error code ($iReturn)");
552 fclose($ahGzipPipes[1]);
553 proc_close($hGzipProcess);
557 private function replaceSqlPatterns($sSql)
559 $sSql = str_replace('{www-user}', getSetting('DATABASE_WEBUSER'), $sSql);
562 '{ts:address-data}' => getSetting('TABLESPACE_ADDRESS_DATA'),
563 '{ts:address-index}' => getSetting('TABLESPACE_ADDRESS_INDEX'),
564 '{ts:search-data}' => getSetting('TABLESPACE_SEARCH_DATA'),
565 '{ts:search-index}' => getSetting('TABLESPACE_SEARCH_INDEX'),
566 '{ts:aux-data}' => getSetting('TABLESPACE_AUX_DATA'),
567 '{ts:aux-index}' => getSetting('TABLESPACE_AUX_INDEX')
570 foreach ($aPatterns as $sPattern => $sTablespace) {
572 $sSql = str_replace($sPattern, 'TABLESPACE "'.$sTablespace.'"', $sSql);
574 $sSql = str_replace($sPattern, '', $sSql);
582 * Drop table with the given name if it exists.
584 * @param string $sName Name of table to remove.
588 private function dropTable($sName)
590 if ($this->bVerbose) echo "Dropping table $sName\n";
591 $this->db()->deleteTable($sName);
595 * Check if the database is in reverse-only mode.
597 * @return True if there is no search_name table and infrastructure.
599 private function dbReverseOnly()
601 return !($this->db()->tableExists('search_name'));
605 * Try accessing the C module, so we know early if something is wrong.
607 * Raises Nominatim\DatabaseError on failure
609 private function checkModulePresence()
611 $sModulePath = getSetting('DATABASE_MODULE_PATH', CONST_InstallDir.'/module');
612 $sSQL = "CREATE FUNCTION nominatim_test_import_func(text) RETURNS text AS '";
613 $sSQL .= $sModulePath . "/nominatim.so', 'transliteration' LANGUAGE c IMMUTABLE STRICT";
614 $sSQL .= ';DROP FUNCTION nominatim_test_import_func(text);';
616 $oDB = new \Nominatim\DB();
618 $oDB->exec($sSQL, null, 'Database server failed to load '.$sModulePath.'/nominatim.so module');