2 Functions for bringing auxiliary data in the database up-to-date.
 
   7 from textwrap import dedent
 
   9 from psycopg2.extras import execute_values
 
  11 from ..db.utils import execute_file
 
  13 LOG = logging.getLogger()
 
  15 def update_postcodes(dsn, sql_dir):
 
  16     """ Recalculate postcode centroids and add, remove and update entries in the
 
  17         location_postcode table. `conn` is an opne connection to the database.
 
  19     execute_file(dsn, sql_dir / 'update-postcodes.sql')
 
  22 def recompute_word_counts(dsn, sql_dir):
 
  23     """ Compute the frequency of full-word search terms.
 
  25     execute_file(dsn, sql_dir / 'words_from_search_name.sql')
 
  28 def _add_address_level_rows_from_entry(rows, entry):
 
  29     """ Converts a single entry from the JSON format for address rank
 
  30         descriptions into a flat format suitable for inserting into a
 
  31         PostgreSQL table and adds these lines to `rows`.
 
  33     countries = entry.get('countries') or (None, )
 
  34     for key, values in entry['tags'].items():
 
  35         for value, ranks in values.items():
 
  36             if isinstance(ranks, list):
 
  37                 rank_search, rank_address = ranks
 
  39                 rank_search = rank_address = ranks
 
  42             for country in countries:
 
  43                 rows.append((country, key, value, rank_search, rank_address))
 
  45 def load_address_levels(conn, table, levels):
 
  46     """ Replace the `address_levels` table with the contents of `levels'.
 
  48         A new table is created any previously existing table is dropped.
 
  49         The table has the following columns:
 
  50             country, class, type, rank_search, rank_address
 
  54         _add_address_level_rows_from_entry(rows, entry)
 
  56     with conn.cursor() as cur:
 
  57         cur.execute('DROP TABLE IF EXISTS {}'.format(table))
 
  59         cur.execute("""CREATE TABLE {} (country_code varchar(2),
 
  63                                         rank_address SMALLINT)""".format(table))
 
  65         execute_values(cur, "INSERT INTO {} VALUES %s".format(table), rows)
 
  67         cur.execute('CREATE UNIQUE INDEX ON {} (country_code, class, type)'.format(table))
 
  71 def load_address_levels_from_file(conn, config_file):
 
  72     """ Replace the `address_levels` table with the contents of the config
 
  75     with config_file.open('r') as fdesc:
 
  76         load_address_levels(conn, 'address_levels', json.load(fdesc))
 
  78 PLPGSQL_BASE_MODULES = (
 
  87 PLPGSQL_TABLE_MODULES = (
 
  88     ('place', 'place_triggers.sql'),
 
  89     ('placex', 'placex_triggers.sql'),
 
  90     ('location_postcode', 'postcode_triggers.sql')
 
  93 def _get_standard_function_sql(conn, config, sql_dir, enable_diff_updates, enable_debug):
 
  94     """ Read all applicable SQLs containing PL/pgSQL functions, replace
 
  95         placefolders and execute them.
 
  97     sql_func_dir = sql_dir / 'functions'
 
 100     # Get the basic set of functions that is always imported.
 
 101     for sql_file in PLPGSQL_BASE_MODULES:
 
 102         with (sql_func_dir / sql_file).open('r') as fdesc:
 
 105     # Some files require the presence of a certain table
 
 106     for table, fname in PLPGSQL_TABLE_MODULES:
 
 107         if conn.table_exists(table):
 
 108             with (sql_func_dir / fname).open('r') as fdesc:
 
 111     # Replace placeholders.
 
 112     sql = sql.replace('{modulepath}',
 
 113                       config.DATABASE_MODULE_PATH or str((config.project_dir / 'module').resolve()))
 
 115     if enable_diff_updates:
 
 116         sql = sql.replace('RETURN NEW; -- %DIFFUPDATES%', '--')
 
 119         sql = sql.replace('--DEBUG:', '')
 
 121     if config.get_bool('LIMIT_REINDEXING'):
 
 122         sql = sql.replace('--LIMIT INDEXING:', '')
 
 124     if not config.get_bool('USE_US_TIGER_DATA'):
 
 125         sql = sql.replace('-- %NOTIGERDATA% ', '')
 
 127     if not config.get_bool('USE_AUX_LOCATION_DATA'):
 
 128         sql = sql.replace('-- %NOAUXDATA% ', '')
 
 130     reverse_only = 'false' if conn.table_exists('search_name') else 'true'
 
 132     return sql.replace('%REVERSE-ONLY%', reverse_only)
 
 135 def replace_partition_string(sql, partitions):
 
 136     """ Replace a partition template with the actual partition code.
 
 138     for match in re.findall('^-- start(.*?)^-- end', sql, re.M | re.S):
 
 140         for part in partitions:
 
 141             repl += match.replace('-partition-', str(part))
 
 142         sql = sql.replace(match, repl)
 
 146 def _get_partition_function_sql(conn, sql_dir):
 
 147     """ Create functions that work on partition tables.
 
 149     with conn.cursor() as cur:
 
 150         cur.execute('SELECT distinct partition FROM country_name')
 
 151         partitions = set([0])
 
 153             partitions.add(row[0])
 
 155     with (sql_dir / 'partition-functions.src.sql').open('r') as fdesc:
 
 158     return replace_partition_string(sql, sorted(partitions))
 
 160 def create_functions(conn, config, sql_dir,
 
 161                      enable_diff_updates=True, enable_debug=False):
 
 162     """ (Re)create the PL/pgSQL functions.
 
 164     sql = _get_standard_function_sql(conn, config, sql_dir,
 
 165                                      enable_diff_updates, enable_debug)
 
 166     sql += _get_partition_function_sql(conn, sql_dir)
 
 168     with conn.cursor() as cur:
 
 184 # constants needed by PHP scripts: PHP name, config name, type
 
 186     ('Database_DSN', 'DATABASE_DSN', str),
 
 187     ('Default_Language', 'DEFAULT_LANGUAGE', str),
 
 188     ('Log_DB', 'LOG_DB', bool),
 
 189     ('Log_File', 'LOG_FILE', str),
 
 190     ('Max_Word_Frequency', 'MAX_WORD_FREQUENCY', int),
 
 191     ('NoAccessControl', 'CORS_NOACCESSCONTROL', bool),
 
 192     ('Places_Max_ID_count', 'LOOKUP_MAX_COUNT', int),
 
 193     ('PolygonOutput_MaximumTypes', 'POLYGON_OUTPUT_MAX_TYPES', int),
 
 194     ('Search_BatchMode', 'SEARCH_BATCH_MODE', bool),
 
 195     ('Search_NameOnlySearchFrequencyThreshold', 'SEARCH_NAME_ONLY_THRESHOLD', str),
 
 196     ('Term_Normalization_Rules', 'TERM_NORMALIZATION', str),
 
 197     ('Use_Aux_Location_data', 'USE_AUX_LOCATION_DATA', bool),
 
 198     ('Use_US_Tiger_Data', 'USE_US_TIGER_DATA', bool),
 
 199     ('MapIcon_URL', 'MAPICON_URL', str),
 
 203 def import_wikipedia_articles(dsn, data_path, ignore_errors=False):
 
 204     """ Replaces the wikipedia importance tables with new data.
 
 205         The import is run in a single transaction so that the new data
 
 206         is replace seemlessly.
 
 208         Returns 0 if all was well and 1 if the importance file could not
 
 209         be found. Throws an exception if there was an error reading the file.
 
 211     datafile = data_path / 'wikimedia-importance.sql.gz'
 
 213     if not datafile.exists():
 
 217                   DROP TABLE IF EXISTS "wikipedia_article";
 
 218                   DROP TABLE IF EXISTS "wikipedia_redirect"
 
 221     execute_file(dsn, datafile, ignore_errors=ignore_errors,
 
 222                  pre_code=pre_code, post_code=post_code)
 
 227 def recompute_importance(conn):
 
 228     """ Recompute wikipedia links and importance for all entries in placex.
 
 229         This is a long-running operations that must not be executed in
 
 230         parallel with updates.
 
 232     with conn.cursor() as cur:
 
 233         cur.execute('ALTER TABLE placex DISABLE TRIGGER ALL')
 
 235             UPDATE placex SET (wikipedia, importance) =
 
 236                (SELECT wikipedia, importance
 
 237                 FROM compute_importance(extratags, country_code, osm_type, osm_id))
 
 240             UPDATE placex s SET wikipedia = d.wikipedia, importance = d.importance
 
 242              WHERE s.place_id = d.linked_place_id and d.wikipedia is not null
 
 243                    and (s.wikipedia is null or s.importance < d.importance);
 
 246         cur.execute('ALTER TABLE placex ENABLE TRIGGER ALL')
 
 250 def setup_website(basedir, phplib_dir, config):
 
 251     """ Create the website script stubs.
 
 253     if not basedir.exists():
 
 254         LOG.info('Creating website directory.')
 
 257     template = dedent("""\
 
 260                       @define('CONST_Debug', $_GET['debug'] ?? false);
 
 261                       @define('CONST_LibDir', '{}');
 
 263                       """.format(phplib_dir))
 
 265     for php_name, conf_name, var_type in PHP_CONST_DEFS:
 
 267             varout = 'true' if config.get_bool(conf_name) else 'false'
 
 268         elif var_type == int:
 
 269             varout = getattr(config, conf_name)
 
 270         elif not getattr(config, conf_name):
 
 273             varout = "'{}'".format(getattr(config, conf_name).replace("'", "\\'"))
 
 275         template += "@define('CONST_{}', {});\n".format(php_name, varout)
 
 277     template += "\nrequire_once('{}/website/{{}}');\n".format(phplib_dir)
 
 279     for script in WEBSITE_SCRIPTS:
 
 280         (basedir / script).write_text(template.format(script), 'utf-8')