Coverage for tests / test_obscore.py: 15%

293 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-24 01:10 -0800

1# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28import os 

29import tempfile 

30import unittest 

31from abc import abstractmethod 

32from typing import cast 

33 

34import astropy.time 

35import sqlalchemy 

36 

37from lsst.daf.butler import ( 

38 CollectionType, 

39 Config, 

40 DataCoordinate, 

41 DatasetRef, 

42 DatasetType, 

43 StorageClassFactory, 

44) 

45from lsst.daf.butler.registry import RegistryConfig, _RegistryFactory 

46from lsst.daf.butler.registry.obscore import ( 

47 DatasetTypeConfig, 

48 ObsCoreConfig, 

49 ObsCoreLiveTableManager, 

50 ObsCoreSchema, 

51 RegionTypeWarning, 

52) 

53from lsst.daf.butler.registry.obscore._schema import _STATIC_COLUMNS 

54from lsst.daf.butler.registry.sql_registry import SqlRegistry 

55from lsst.daf.butler.tests.postgresql import setup_postgres_test_db 

56from lsst.daf.butler.tests.utils import TestCaseMixin, makeTestTempDir, removeTestTempDir 

57from lsst.sphgeom import Box, ConvexPolygon, LonLat, UnitVector3d 

58 

59TESTDIR = os.path.abspath(os.path.dirname(__file__)) 

60 

61 

62class ObsCoreTests(TestCaseMixin): 

63 """Base class for testing obscore manager functionality.""" 

64 

65 root: str 

66 

67 def make_registry( 

68 self, collections: list[str] | None = None, collection_type: str | None = None 

69 ) -> SqlRegistry: 

70 """Create new empty Registry.""" 

71 config = self.make_registry_config(collections, collection_type) 

72 registry = _RegistryFactory(config).create_from_config(butlerRoot=self.root) 

73 self.addCleanup(registry.close) 

74 self.initialize_registry(registry) 

75 return registry 

76 

77 @abstractmethod 

78 def make_registry_config( 

79 self, collections: list[str] | None = None, collection_type: str | None = None 

80 ) -> RegistryConfig: 

81 """Make Registry configuration.""" 

82 raise NotImplementedError() 

83 

84 def initialize_registry(self, registry: SqlRegistry) -> None: 

85 """Populate Registry with the things that we need for tests.""" 

86 registry.insertDimensionData("instrument", {"name": "DummyCam"}) 

87 registry.insertDimensionData( 

88 "physical_filter", {"instrument": "DummyCam", "name": "d-r", "band": "r"} 

89 ) 

90 registry.insertDimensionData("day_obs", {"instrument": "DummyCam", "id": 20200101}) 

91 for detector in (1, 2, 3, 4): 

92 registry.insertDimensionData( 

93 "detector", {"instrument": "DummyCam", "id": detector, "full_name": f"detector{detector}"} 

94 ) 

95 

96 for exposure in (1, 2, 3, 4): 

97 registry.insertDimensionData("group", {"instrument": "DummyCam", "name": f"group{exposure}"}) 

98 registry.insertDimensionData( 

99 "exposure", 

100 { 

101 "instrument": "DummyCam", 

102 "id": exposure, 

103 "obs_id": f"exposure{exposure}", 

104 "physical_filter": "d-r", 

105 "group": f"group{exposure}", 

106 "day_obs": 20200101, 

107 }, 

108 ) 

109 

110 registry.insertDimensionData("visit_system", {"instrument": "DummyCam", "id": 1, "name": "default"}) 

111 

112 for visit in (1, 2, 3, 4, 9): 

113 visit_start = astropy.time.Time(f"2020-01-01 08:0{visit}:00", scale="tai") 

114 visit_end = astropy.time.Time(f"2020-01-01 08:0{visit}:45", scale="tai") 

115 registry.insertDimensionData( 

116 "visit", 

117 { 

118 "instrument": "DummyCam", 

119 "id": visit, 

120 "name": f"visit{visit}", 

121 "physical_filter": "d-r", 

122 "datetime_begin": visit_start, 

123 "datetime_end": visit_end, 

124 "day_obs": 20200101, 

125 }, 

126 ) 

127 registry.insertDimensionData( 

128 "visit_system_membership", 

129 {"instrument": "DummyCam", "visit": visit, "visit_system": 1}, 

130 ) 

131 

132 # Only couple of exposures are linked to visits. 

133 for visit in (1, 2): 

134 registry.insertDimensionData( 

135 "visit_definition", 

136 { 

137 "instrument": "DummyCam", 

138 "exposure": visit, 

139 "visit": visit, 

140 }, 

141 ) 

142 

143 # map visit and detector to region 

144 self.regions: dict[tuple[int, int], ConvexPolygon] = {} 

145 for visit in (1, 2, 3, 4): 

146 for detector in (1, 2, 3, 4): 

147 lon = visit * 90 - 88 

148 lat = detector * 2 - 5 

149 region = ConvexPolygon( 

150 [ 

151 UnitVector3d(LonLat.fromDegrees(lon - 1.0, lat - 1.0)), 

152 UnitVector3d(LonLat.fromDegrees(lon + 1.0, lat - 1.0)), 

153 UnitVector3d(LonLat.fromDegrees(lon + 1.0, lat + 1.0)), 

154 UnitVector3d(LonLat.fromDegrees(lon - 1.0, lat + 1.0)), 

155 ] 

156 ) 

157 registry.insertDimensionData( 

158 "visit_detector_region", 

159 { 

160 "instrument": "DummyCam", 

161 "visit": visit, 

162 "detector": detector, 

163 "region": region, 

164 }, 

165 ) 

166 self.regions[(visit, detector)] = region 

167 

168 # Visit 9 has non-polygon region 

169 for detector in (1, 2, 3, 4): 

170 lat = detector * 2 - 5 

171 region = Box.fromDegrees(17.0, lat - 1.0, 19.0, lat + 1.0) 

172 registry.insertDimensionData( 

173 "visit_detector_region", 

174 { 

175 "instrument": "DummyCam", 

176 "visit": 9, 

177 "detector": detector, 

178 "region": region, 

179 }, 

180 ) 

181 

182 # Add few dataset types 

183 storage_class_factory = StorageClassFactory() 

184 storage_class = storage_class_factory.getStorageClass("StructuredDataDict") 

185 

186 self.dataset_types: dict[str, DatasetType] = {} 

187 

188 dimensions = registry.dimensions.conform(["instrument", "physical_filter", "detector", "exposure"]) 

189 self.dataset_types["raw"] = DatasetType("raw", dimensions, storage_class) 

190 

191 dimensions = registry.dimensions.conform(["instrument", "physical_filter", "detector", "visit"]) 

192 self.dataset_types["calexp"] = DatasetType("calexp", dimensions, storage_class) 

193 

194 dimensions = registry.dimensions.conform(["instrument", "physical_filter", "detector", "visit"]) 

195 self.dataset_types["no_obscore"] = DatasetType("no_obscore", dimensions, storage_class) 

196 

197 dimensions = registry.dimensions.conform(["instrument", "physical_filter", "detector"]) 

198 self.dataset_types["calib"] = DatasetType("calib", dimensions, storage_class, isCalibration=True) 

199 

200 for dataset_type in self.dataset_types.values(): 

201 registry.registerDatasetType(dataset_type) 

202 

203 # Add few run collections. 

204 for run in (1, 2, 3, 4, 5, 6): 

205 registry.registerRun(f"run{run}") 

206 

207 # Add few chained collections, run6 is not in any chained collections. 

208 registry.registerCollection("chain12", CollectionType.CHAINED) 

209 registry.setCollectionChain("chain12", ("run1", "run2")) 

210 registry.registerCollection("chain34", CollectionType.CHAINED) 

211 registry.setCollectionChain("chain34", ("run3", "run4")) 

212 registry.registerCollection("chain-all", CollectionType.CHAINED) 

213 registry.setCollectionChain("chain-all", ("chain12", "chain34", "run5")) 

214 

215 # And a tagged collection 

216 registry.registerCollection("tagged", CollectionType.TAGGED) 

217 

218 def make_obscore_config( 

219 self, collections: list[str] | None = None, collection_type: str | None = None 

220 ) -> Config: 

221 """Make configuration for obscore manager.""" 

222 obscore_config = Config(os.path.join(TESTDIR, "config", "basic", "obscore.yaml")) 

223 if collections is not None: 

224 obscore_config["collections"] = collections 

225 if collection_type is not None: 

226 obscore_config["collection_type"] = collection_type 

227 return obscore_config 

228 

229 def _insert_dataset( 

230 self, registry: SqlRegistry, run: str, dataset_type: str, do_import: bool = False, **kwargs 

231 ) -> DatasetRef: 

232 """Insert or import one dataset into a specified run collection.""" 

233 data_id = {"instrument": "DummyCam", "physical_filter": "d-r"} 

234 data_id.update(kwargs) 

235 coordinate = DataCoordinate.standardize(data_id, universe=registry.dimensions) 

236 if do_import: 

237 ds_type = self.dataset_types[dataset_type] 

238 ref = DatasetRef(ds_type, coordinate, run=run) 

239 [ref] = registry._importDatasets([ref]) 

240 else: 

241 [ref] = registry.insertDatasets(dataset_type, [data_id], run=run) 

242 return ref 

243 

244 def _insert_datasets(self, registry: SqlRegistry, do_import: bool = False) -> list[DatasetRef]: 

245 """Inset a small bunch of datasets into every run collection.""" 

246 return [ 

247 self._insert_dataset(registry, "run1", "raw", detector=1, exposure=1, do_import=do_import), 

248 self._insert_dataset(registry, "run2", "calexp", detector=2, visit=2, do_import=do_import), 

249 self._insert_dataset(registry, "run3", "raw", detector=3, exposure=3, do_import=do_import), 

250 self._insert_dataset(registry, "run4", "calexp", detector=4, visit=4, do_import=do_import), 

251 self._insert_dataset(registry, "run5", "calexp", detector=4, visit=4, do_import=do_import), 

252 # This dataset type is not configured, will not be in obscore. 

253 self._insert_dataset(registry, "run5", "no_obscore", detector=1, visit=1, do_import=do_import), 

254 self._insert_dataset(registry, "run6", "raw", detector=1, exposure=4, do_import=do_import), 

255 ] 

256 

257 def test_config_errors(self): 

258 """Test for handling various configuration problems.""" 

259 # This raises pydantic ValidationError, which wraps ValueError 

260 exception_re = "'collections' must have one element" 

261 with self.assertRaisesRegex(ValueError, exception_re): 

262 self.make_registry(None, "TAGGED") 

263 

264 with self.assertRaisesRegex(ValueError, exception_re): 

265 self.make_registry([], "TAGGED") 

266 

267 with self.assertRaisesRegex(ValueError, exception_re): 

268 self.make_registry(["run1", "run2"], "TAGGED") 

269 

270 # Invalid regex. 

271 with self.assertRaisesRegex(ValueError, "Failed to compile regex"): 

272 self.make_registry(["+run"], "RUN") 

273 

274 def test_schema(self): 

275 """Check how obscore schema is constructed""" 

276 config = ObsCoreConfig(obs_collection="", dataset_types={}, facility_name="FACILITY") 

277 schema = ObsCoreSchema(config, []) 

278 table_spec = schema.table_spec 

279 self.assertEqual(list(table_spec.fields.names), [col.name for col in _STATIC_COLUMNS]) 

280 

281 # extra columns from top-level config 

282 config = ObsCoreConfig( 

283 obs_collection="", 

284 extra_columns={"c1": 1, "c2": "string", "c3": {"template": "{calib_level}", "type": "float"}}, 

285 dataset_types={}, 

286 facility_name="FACILITY", 

287 ) 

288 schema = ObsCoreSchema(config, []) 

289 table_spec = schema.table_spec 

290 self.assertEqual( 

291 list(table_spec.fields.names), 

292 [col.name for col in _STATIC_COLUMNS] + ["c1", "c2", "c3"], 

293 ) 

294 self.assertEqual(table_spec.fields["c1"].dtype, sqlalchemy.BigInteger) 

295 self.assertEqual(table_spec.fields["c2"].dtype, sqlalchemy.String) 

296 self.assertEqual(table_spec.fields["c3"].dtype, sqlalchemy.Float) 

297 

298 # extra columns from per-dataset type configs 

299 config = ObsCoreConfig( 

300 obs_collection="", 

301 extra_columns={"c1": 1}, 

302 dataset_types={ 

303 "raw": DatasetTypeConfig( 

304 name="raw", 

305 dataproduct_type="image", 

306 calib_level=1, 

307 extra_columns={"c2": "string"}, 

308 ), 

309 "calexp": DatasetTypeConfig( 

310 dataproduct_type="image", 

311 calib_level=2, 

312 extra_columns={"c3": 1e10}, 

313 ), 

314 }, 

315 facility_name="FACILITY", 

316 ) 

317 schema = ObsCoreSchema(config, []) 

318 table_spec = schema.table_spec 

319 self.assertEqual( 

320 list(table_spec.fields.names), 

321 [col.name for col in _STATIC_COLUMNS] + ["c1", "c2", "c3"], 

322 ) 

323 self.assertEqual(table_spec.fields["c1"].dtype, sqlalchemy.BigInteger) 

324 self.assertEqual(table_spec.fields["c2"].dtype, sqlalchemy.String) 

325 self.assertEqual(table_spec.fields["c3"].dtype, sqlalchemy.Float) 

326 

327 # Columns with the same names as in static list in configs, types 

328 # are not overriden. 

329 config = ObsCoreConfig( 

330 version=0, 

331 obs_collection="", 

332 extra_columns={"t_xel": 1e10}, 

333 dataset_types={ 

334 "raw": DatasetTypeConfig( 

335 dataproduct_type="image", 

336 calib_level=1, 

337 extra_columns={"target_name": 1}, 

338 ), 

339 "calexp": DatasetTypeConfig( 

340 dataproduct_type="image", 

341 calib_level=2, 

342 extra_columns={"em_xel": "string"}, 

343 ), 

344 }, 

345 facility_name="FACILITY", 

346 ) 

347 schema = ObsCoreSchema(config, []) 

348 table_spec = schema.table_spec 

349 self.assertEqual(list(table_spec.fields.names), [col.name for col in _STATIC_COLUMNS]) 

350 self.assertEqual(table_spec.fields["t_xel"].dtype, sqlalchemy.Integer) 

351 self.assertEqual(table_spec.fields["target_name"].dtype, sqlalchemy.String) 

352 self.assertEqual(table_spec.fields["em_xel"].dtype, sqlalchemy.Integer) 

353 

354 def test_insert_existing_collection(self): 

355 """Test insert and import registry methods, with various restrictions 

356 on collection names. 

357 """ 

358 # First item is collections, second item is expected record count. 

359 test_data = ( 

360 (None, 6), 

361 (["run1", "run2"], 2), 

362 (["run[34]"], 2), 

363 (["[rR]un[^6]"], 5), 

364 ) 

365 

366 for collections, count in test_data: 

367 for do_import in (False, True): 

368 registry = self.make_registry(collections) 

369 obscore = registry.obsCoreTableManager 

370 assert obscore is not None 

371 self._insert_datasets(registry, do_import) 

372 

373 with obscore.query() as result: 

374 rows = list(result) 

375 self.assertEqual(len(rows), count) 

376 

377 # Also check `query` method with COUNT(*) 

378 with obscore.query([sqlalchemy.sql.func.count()]) as result: 

379 scalar = result.scalar_one() 

380 self.assertEqual(scalar, count) 

381 

382 def test_drop_datasets(self): 

383 """Test for dropping datasets after obscore insert.""" 

384 collections = None 

385 registry = self.make_registry(collections) 

386 obscore = registry.obsCoreTableManager 

387 assert obscore is not None 

388 refs = self._insert_datasets(registry) 

389 

390 with obscore.query() as result: 

391 rows = list(result) 

392 self.assertEqual(len(rows), 6) 

393 

394 # drop single dataset 

395 registry.removeDatasets(ref for ref in refs if ref.run == "run1") 

396 with obscore.query() as result: 

397 rows = list(result) 

398 self.assertEqual(len(rows), 5) 

399 

400 # drop whole run collection 

401 registry.removeCollection("run6") 

402 with obscore.query() as result: 

403 rows = list(result) 

404 self.assertEqual(len(rows), 4) 

405 

406 def test_associate(self): 

407 """Test for associating datasets to TAGGED collection.""" 

408 collections = ["tagged"] 

409 registry = self.make_registry(collections, "TAGGED") 

410 obscore = registry.obsCoreTableManager 

411 assert obscore is not None 

412 refs = self._insert_datasets(registry) 

413 

414 with obscore.query() as result: 

415 rows = list(result) 

416 self.assertEqual(len(rows), 0) 

417 

418 # Associate datasets that are already in obscore, changes nothing. 

419 registry.associate("tagged", (ref for ref in refs if ref.run == "run1")) 

420 with obscore.query() as result: 

421 rows = list(result) 

422 self.assertEqual(len(rows), 1) 

423 

424 # Associate datasets that are not in obscore 

425 registry.associate("tagged", (ref for ref in refs if ref.run == "run3")) 

426 with obscore.query() as result: 

427 rows = list(result) 

428 self.assertEqual(len(rows), 2) 

429 

430 # Disassociate them 

431 registry.disassociate("tagged", (ref for ref in refs if ref.run == "run3")) 

432 with obscore.query() as result: 

433 rows = list(result) 

434 self.assertEqual(len(rows), 1) 

435 

436 # Non-associated dataset, should be OK and not throw. 

437 registry.disassociate("tagged", (ref for ref in refs if ref.run == "run2")) 

438 with obscore.query() as result: 

439 rows = list(result) 

440 self.assertEqual(len(rows), 1) 

441 

442 registry.disassociate("tagged", (ref for ref in refs if ref.run == "run1")) 

443 with obscore.query() as result: 

444 rows = list(result) 

445 self.assertEqual(len(rows), 0) 

446 

447 def test_region_type_warning(self) -> None: 

448 """Test that non-polygon region generates one or more warnings.""" 

449 collections = None 

450 registry = self.make_registry(collections) 

451 

452 with self.assertWarns(RegionTypeWarning) as cm: 

453 self._insert_dataset(registry, "run2", "calexp", detector=2, visit=9) 

454 self.assertRegex( 

455 str(cm.warning), 

456 "Unexpected region type: .*lsst.sphgeom._sphgeom.Box.*", 

457 ) 

458 

459 def test_update_exposure_region(self) -> None: 

460 """Test for update_exposure_regions method.""" 

461 registry = self.make_registry(["run1"]) 

462 obscore = registry.obsCoreTableManager 

463 assert obscore is not None 

464 

465 # Exposure 4 is not associated with any visit. 

466 for detector in (1, 2, 3, 4): 

467 self._insert_dataset(registry, "run1", "raw", detector=detector, exposure=4) 

468 

469 # All spatial columns should be None. 

470 with obscore.query() as result: 

471 rows = list(result) 

472 self.assertEqual(len(rows), 4) 

473 for row in rows: 

474 self.assertIsNone(row.s_ra) 

475 self.assertIsNone(row.s_dec) 

476 self.assertIsNone(row.s_region) 

477 

478 # Assign Region from visit 4. 

479 count = obscore.update_exposure_regions( 

480 "DummyCam", [(4, 1, self.regions[(4, 1)]), (4, 2, self.regions[(4, 2)])] 

481 ) 

482 self.assertEqual(count, 2) 

483 

484 with obscore.query(["s_ra", "s_dec", "s_region", "lsst_detector", "facility_name"]) as result: 

485 rows = list(result) 

486 self.assertEqual(len(rows), 4) 

487 for row in rows: 

488 if row.lsst_detector in (1, 2): 

489 self.assertIsNotNone(row.s_ra) 

490 self.assertIsNotNone(row.s_dec) 

491 self.assertIsNotNone(row.s_region) 

492 else: 

493 self.assertIsNone(row.s_ra) 

494 self.assertIsNone(row.s_dec) 

495 self.assertIsNone(row.s_region) 

496 self.assertEqual(row.facility_name, "derived_facility") 

497 

498 

499class SQLiteObsCoreTest(ObsCoreTests, unittest.TestCase): 

500 """Unit test for obscore with SQLite backend.""" 

501 

502 def setUp(self): 

503 self.root = makeTestTempDir(TESTDIR) 

504 

505 def tearDown(self): 

506 removeTestTempDir(self.root) 

507 

508 def make_registry_config( 

509 self, collections: list[str] | None = None, collection_type: str | None = None 

510 ) -> RegistryConfig: 

511 # docstring inherited from a base class 

512 _, filename = tempfile.mkstemp(dir=self.root, suffix=".sqlite3") 

513 config = RegistryConfig() 

514 config["db"] = f"sqlite:///{filename}" 

515 config["managers", "obscore"] = { 

516 "cls": "lsst.daf.butler.registry.obscore.ObsCoreLiveTableManager", 

517 "config": self.make_obscore_config(collections, collection_type), 

518 } 

519 return config 

520 

521 

522class ClonedSqliteObscoreTest(SQLiteObsCoreTest, unittest.TestCase): 

523 """Unit test for obscore manager created via clone()""" 

524 

525 def make_registry( 

526 self, collections: list[str] | None = None, collection_type: str | None = None 

527 ) -> SqlRegistry: 

528 """Create new empty Registry.""" 

529 original = super().make_registry(collections, collection_type) 

530 copy = original.copy() 

531 self.addCleanup(copy.close) 

532 return copy 

533 

534 

535class PostgresObsCoreTest(ObsCoreTests, unittest.TestCase): 

536 """Unit test for obscore with PostgreSQL backend.""" 

537 

538 @classmethod 

539 def setUpClass(cls): 

540 # Create the postgres test server. 

541 cls.postgresql = cls.enterClassContext(setup_postgres_test_db()) 

542 super().setUpClass() 

543 

544 def setUp(self): 

545 self.root = makeTestTempDir(TESTDIR) 

546 

547 def tearDown(self): 

548 removeTestTempDir(self.root) 

549 

550 def make_registry_config( 

551 self, collections: list[str] | None = None, collection_type: str | None = None 

552 ) -> RegistryConfig: 

553 # docstring inherited from a base class 

554 config = RegistryConfig() 

555 self.postgresql.patch_registry_config(config) 

556 

557 config["managers", "obscore"] = { 

558 "cls": "lsst.daf.butler.registry.obscore.ObsCoreLiveTableManager", 

559 "config": self.make_obscore_config(collections, collection_type), 

560 } 

561 return config 

562 

563 

564class PostgresPgSphereObsCoreTest(PostgresObsCoreTest): 

565 """Unit test for obscore with PostgreSQL backend and pgsphere plugin.""" 

566 

567 @classmethod 

568 def setUpClass(cls): 

569 super().setUpClass() 

570 with cls.postgresql.begin() as connection: 

571 try: 

572 connection.execute(sqlalchemy.text("CREATE EXTENSION pg_sphere")) 

573 except sqlalchemy.exc.DatabaseError as exc: 

574 raise unittest.SkipTest(f"pg_sphere extension does not exist: {exc}") from None 

575 

576 def make_obscore_config( 

577 self, collections: list[str] | None = None, collection_type: str | None = None 

578 ) -> Config: 

579 """Make configuration for obscore manager.""" 

580 obscore_config = super().make_obscore_config(collections, collection_type) 

581 obscore_config["spatial_plugins"] = { 

582 "pgsphere": { 

583 "cls": "lsst.daf.butler.registry.obscore.pgsphere.PgSphereObsCorePlugin", 

584 "config": { 

585 "region_column": "pgs_region", 

586 "position_column": "pgs_center", 

587 }, 

588 } 

589 } 

590 return obscore_config 

591 

592 def test_spatial(self): 

593 """Test that pgsphere plugin fills spatial columns.""" 

594 collections = None 

595 registry = self.make_registry(collections) 

596 obscore = registry.obsCoreTableManager 

597 assert obscore is not None 

598 self._insert_datasets(registry) 

599 

600 # select everything 

601 with obscore.query() as result: 

602 rows = list(result) 

603 self.assertEqual(len(rows), 6) 

604 

605 db = registry._db 

606 assert registry.obsCoreTableManager is not None 

607 table = cast(ObsCoreLiveTableManager, registry.obsCoreTableManager).table 

608 

609 # It's not easy to generate spatial queries in sqlalchemy, use plain 

610 # text queries for testing. 

611 

612 # position matching visit=1, there is a single dataset 

613 query = f"SELECT * FROM {table.key} WHERE pgs_center <-> '(2d,0d)'::spoint < .1" 

614 with db.query(sqlalchemy.text(query)) as results: 

615 self.assertEqual(len(list(results)), 1) 

616 

617 # position matching visit=4, there are two datasets 

618 query = f"SELECT * FROM {table.key} WHERE pgs_center <-> '(272d,0d)'::spoint < .1" 

619 with db.query(sqlalchemy.text(query)) as results: 

620 self.assertEqual(len(list(results)), 2) 

621 

622 # position matching visit=1, there is a single dataset 

623 query = f"SELECT * FROM {table.key} WHERE '(2d,-3d)'::spoint @ pgs_region" 

624 with db.query(sqlalchemy.text(query)) as results: 

625 self.assertEqual(len(list(results)), 1) 

626 

627 # position matching visit=4, there are two datasets 

628 query = f"SELECT * FROM {table.key} WHERE '(272d,3d)'::spoint @ pgs_region" 

629 with db.query(sqlalchemy.text(query)) as results: 

630 self.assertEqual(len(list(results)), 2) 

631 

632 

633if __name__ == "__main__": 

634 unittest.main()