2 # This file is part of Nominatim. (https://nominatim.org)
4 # Copyright (C) 2026 by the Nominatim developer community.
5 # For a full list of authors see the git log.
7 Tests for running the indexing.
11 import pytest_asyncio # noqa
12 from psycopg import sql as pysql
14 from nominatim_db.indexer import indexer
15 from nominatim_db.tokenizer import factory
19 @pytest.fixture(autouse=True)
20 def setup(self, temp_db_conn, project_env, tokenizer_mock,
21 placex_table, postcode_table, osmline_table):
22 self.conn = temp_db_conn
23 temp_db_conn.execute("""
24 CREATE OR REPLACE FUNCTION date_update() RETURNS TRIGGER AS $$
26 IF NEW.indexed_status = 0 and OLD.indexed_status != 0 THEN
27 NEW.indexed_date = now();
30 END; $$ LANGUAGE plpgsql;
32 DROP TYPE IF EXISTS prepare_update_info CASCADE;
33 CREATE TYPE prepare_update_info AS (
36 rank_address SMALLINT,
40 linked_place_id BIGINT
42 CREATE OR REPLACE FUNCTION placex_indexing_prepare(p placex,
43 OUT result prepare_update_info) AS $$
45 result.address := p.address;
46 result.name := p.name;
47 result.class := p.class;
48 result.type := p.type;
49 result.country_code := p.country_code;
50 result.rank_address := p.rank_address;
51 END; $$ LANGUAGE plpgsql STABLE;
53 CREATE OR REPLACE FUNCTION get_interpolation_address(in_address HSTORE, wayid BIGINT)
54 RETURNS HSTORE AS $$ SELECT in_address $$ LANGUAGE sql STABLE;
57 for table in ('placex', 'location_property_osmline', 'location_postcodes'):
58 temp_db_conn.execute("""CREATE TRIGGER {0}_update BEFORE UPDATE ON {0}
59 FOR EACH ROW EXECUTE PROCEDURE date_update()
62 self.tokenizer = factory.create_tokenizer(project_env)
64 def scalar(self, query):
65 with self.conn.cursor() as cur:
67 return cur.fetchone()[0]
69 def placex_unindexed(self):
70 return self.scalar('SELECT count(*) from placex where indexed_status > 0')
72 def osmline_unindexed(self):
73 return self.scalar("""SELECT count(*) from location_property_osmline
74 WHERE indexed_status > 0""")
76 @pytest.mark.parametrize("threads", [1, 15])
78 async def test_index_all_by_rank(self, def_config, threads, placex_row, osmline_row):
79 for rank in range(31):
80 placex_row(rank_address=rank, rank_search=rank, indexed_status=1)
83 assert self.placex_unindexed() == 31
84 assert self.osmline_unindexed() == 1
86 idx = indexer.Indexer(def_config, self.tokenizer, threads)
87 await idx.index_by_rank(0, 30)
89 assert self.placex_unindexed() == 0
90 assert self.osmline_unindexed() == 0
92 assert self.scalar("""SELECT count(*) from placex
93 WHERE indexed_status = 0 and indexed_date is null""") == 0
94 # ranks come in order of rank address
95 assert self.scalar("""
96 SELECT count(*) FROM placex p WHERE rank_address > 0
97 AND indexed_date >= (SELECT min(indexed_date) FROM placex o
98 WHERE p.rank_address < o.rank_address)""") == 0
99 # placex address ranked objects come before interpolations
101 """SELECT count(*) FROM placex WHERE rank_address > 0
103 (SELECT min(indexed_date) FROM location_property_osmline)""") == 0
104 # rank 0 comes after all other placex objects
106 """SELECT count(*) FROM placex WHERE rank_address > 0
108 (SELECT min(indexed_date) FROM placex WHERE rank_address = 0)""") == 0
110 @pytest.mark.parametrize("threads", [1, 15])
112 async def test_index_partial_without_30(self, def_config, threads, placex_row, osmline_row):
113 for rank in range(31):
114 placex_row(rank_address=rank, rank_search=rank, indexed_status=1)
117 assert self.placex_unindexed() == 31
118 assert self.osmline_unindexed() == 1
120 idx = indexer.Indexer(def_config, self.tokenizer, threads)
121 await idx.index_by_rank(4, 15)
123 assert self.placex_unindexed() == 19
124 assert self.osmline_unindexed() == 1
126 assert self.scalar("""
127 SELECT count(*) FROM placex
128 WHERE indexed_status = 0 AND not rank_address between 4 and 15""") == 0
130 @pytest.mark.parametrize("threads", [1, 15])
132 async def test_index_partial_with_30(self, def_config, threads, placex_row, osmline_row):
133 for rank in range(31):
134 placex_row(rank_address=rank, rank_search=rank, indexed_status=1)
137 assert self.placex_unindexed() == 31
138 assert self.osmline_unindexed() == 1
140 idx = indexer.Indexer(def_config, self.tokenizer, threads)
141 await idx.index_by_rank(28, 30)
143 assert self.placex_unindexed() == 28
144 assert self.osmline_unindexed() == 0
146 assert self.scalar("""
147 SELECT count(*) FROM placex
148 WHERE indexed_status = 0 AND rank_address between 0 and 27""") == 0
150 @pytest.mark.parametrize("threads", [1, 15])
152 async def test_index_boundaries(self, def_config, threads, placex_row, osmline_row):
153 bnd_cat = pysql.SQL("ARRAY['osm.boundary.administrative']::ltree[]")
154 for rank in range(4, 10):
155 placex_row(cls='boundary', typ='administrative',
157 rank_address=rank, rank_search=rank, indexed_status=1)
158 for rank in range(31):
159 placex_row(rank_address=rank, rank_search=rank, indexed_status=1)
162 assert self.placex_unindexed() == 37
163 assert self.osmline_unindexed() == 1
165 idx = indexer.Indexer(def_config, self.tokenizer, threads)
166 await idx.index_boundaries()
168 assert self.placex_unindexed() == 31
169 assert self.osmline_unindexed() == 1
171 assert self.scalar("""
172 SELECT count(*) FROM placex
173 WHERE indexed_status = 0 AND class != 'boundary'""") == 0
175 @pytest.mark.parametrize("threads", [1, 15])
177 async def test_index_postcodes(self, def_config, threads, postcode_row):
178 for postcode in range(1000):
179 postcode_row(country='de', postcode=postcode)
180 for postcode in range(32000, 33000):
181 postcode_row(country='us', postcode=postcode)
183 idx = indexer.Indexer(def_config, self.tokenizer, threads)
184 await idx.index_postcodes()
186 assert self.scalar("""SELECT count(*) FROM location_postcodes
187 WHERE indexed_status != 0""") == 0
189 @pytest.mark.parametrize("analyse", [True, False])
191 async def test_index_full(self, def_config, analyse, placex_row, osmline_row, postcode_row):
192 bnd_cat = pysql.SQL("ARRAY['osm.boundary.administrative']::ltree[]")
193 for rank in range(4, 10):
194 placex_row(cls='boundary', typ='administrative',
196 rank_address=rank, rank_search=rank, indexed_status=1)
197 for rank in range(31):
198 placex_row(rank_address=rank, rank_search=rank, indexed_status=1)
200 for postcode in range(1000):
201 postcode_row(country='de', postcode=postcode)
203 idx = indexer.Indexer(def_config, self.tokenizer, 4)
204 await idx.index_full(analyse=analyse)
206 assert self.placex_unindexed() == 0
207 assert self.osmline_unindexed() == 0
208 assert self.scalar("""SELECT count(*) FROM location_postcodes
209 WHERE indexed_status != 0""") == 0