Coverage for python/lsst/daf/butler/registry/tests/_registry.py: 6%

1679 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-04 01:36 -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/>. 

27from __future__ import annotations 

28 

29from ... import ddl 

30 

31__all__ = ["RegistryTests"] 

32 

33import contextlib 

34import datetime 

35import itertools 

36import re 

37import time 

38import unittest 

39import uuid 

40from abc import ABC, abstractmethod 

41from collections import defaultdict, namedtuple 

42from collections.abc import Callable, Iterator 

43from concurrent.futures import ThreadPoolExecutor 

44from contextlib import ExitStack 

45from datetime import timedelta 

46from threading import Barrier 

47from typing import TypeVar 

48 

49import astropy.time 

50import sqlalchemy 

51 

52try: 

53 import numpy as np 

54except ImportError: 

55 np = None 

56 

57import lsst.sphgeom 

58 

59from ... import Butler 

60from ..._collection_type import CollectionType 

61from ..._dataset_association import DatasetAssociation 

62from ..._dataset_ref import DatasetIdFactory, DatasetIdGenEnum, DatasetRef 

63from ..._dataset_type import DatasetType 

64from ..._exceptions import ( 

65 CalibrationLookupError, 

66 CollectionTypeError, 

67 DataIdValueError, 

68 InconsistentDataIdError, 

69 InvalidQueryError, 

70 MissingCollectionError, 

71 MissingDatasetTypeError, 

72) 

73from ..._exceptions_legacy import DatasetTypeError 

74from ..._storage_class import StorageClass 

75from ..._timespan import Timespan 

76from ...dimensions import DataCoordinate, DataCoordinateSet, DimensionUniverse, SkyPixDimension 

77from ...direct_butler import DirectButler 

78from .._collection_summary import CollectionSummary 

79from .._config import RegistryConfig 

80from .._exceptions import ( 

81 ArgumentError, 

82 CollectionError, 

83 ConflictingDefinitionError, 

84 DatasetTypeExpressionError, 

85 NoDefaultCollectionError, 

86 OrphanedRecordError, 

87) 

88from ..interfaces import ButlerAttributeExistsError, ReadOnlyDatabaseError 

89from ..queries import ParentDatasetQueryResults 

90from ..sql_registry import SqlRegistry 

91 

92_T = TypeVar("_T") 

93 

94 

95class RegistryTests(ABC): 

96 """Generic tests for the `SqlRegistry` class that can be subclassed to 

97 generate tests for different configurations. 

98 """ 

99 

100 collectionsManager: str | None = None 

101 """Name of the collections manager class, if subclass provides value for 

102 this member then it overrides name specified in default configuration 

103 (`str`). 

104 """ 

105 

106 datasetsManager: str | dict[str, str] | None = None 

107 """Name or configuration dictionary of the datasets manager class, if 

108 subclass provides value for this member then it overrides name specified 

109 in default configuration (`str` or `dict`). 

110 """ 

111 

112 supportsCollectionRegex: bool = False 

113 """True if the registry class being tested supports regex searches for 

114 collections.""" 

115 

116 def makeRegistryConfig(self) -> RegistryConfig: 

117 """Create RegistryConfig used to create a registry. 

118 

119 This method should be called by a subclass from `makeRegistry`. 

120 Returned instance will be pre-configured based on the values of class 

121 members, and default-configured for all other parameters. Subclasses 

122 that need default configuration should just instantiate 

123 `RegistryConfig` directly. 

124 """ 

125 config = RegistryConfig() 

126 if self.collectionsManager: 

127 config["managers", "collections"] = self.collectionsManager 

128 if self.datasetsManager: 

129 config["managers", "datasets"] = self.datasetsManager 

130 return config 

131 

132 @abstractmethod 

133 def make_butler(self, registry_config: RegistryConfig | None = None) -> Butler: 

134 """Return the butler to be tested. 

135 

136 Parameters 

137 ---------- 

138 registry_config : `RegistryConfig`, optional 

139 Registry configuration used when instantiating the Butler. 

140 

141 Returns 

142 ------- 

143 butler : `~lsst.daf.butler.Butler` 

144 The butler with a registry to be tested. 

145 """ 

146 raise NotImplementedError() 

147 

148 def load_data(self, butler: Butler, *filenames: str) -> None: 

149 """Load registry test data from 

150 ``resource://lsst.daf.butler/tests/registry_data/<filename>``, 

151 which should be a YAML import/export file. 

152 

153 Parameters 

154 ---------- 

155 butler : `Butler` 

156 The butler to load into. 

157 *filenames : `str` 

158 The names of the files to load. 

159 """ 

160 for filename in filenames: 

161 butler.import_( 

162 filename=f"resource://lsst.daf.butler/tests/registry_data/{filename}", without_datastore=True 

163 ) 

164 

165 def checkQueryResults(self, results, expected): 

166 """Check that a query results object contains expected values. 

167 

168 Parameters 

169 ---------- 

170 results : `DataCoordinateQueryResults` or `DatasetQueryResults` 

171 A lazy-evaluation query results object. 

172 expected : `list` 

173 A list of `DataCoordinate` o `DatasetRef` objects that should be 

174 equal to results of the query, aside from ordering. 

175 """ 

176 self.assertCountEqual(list(results), expected) 

177 self.assertEqual(results.count(), len(expected)) 

178 if expected: 

179 self.assertTrue(results.any()) 

180 else: 

181 self.assertFalse(results.any()) 

182 

183 def testOpaque(self): 

184 """Tests for `SqlRegistry.registerOpaqueTable`, 

185 `SqlRegistry.insertOpaqueData`, `SqlRegistry.fetchOpaqueData`, and 

186 `SqlRegistry.deleteOpaqueData`. 

187 """ 

188 butler = self.make_butler() 

189 registry = butler._registry 

190 table = "opaque_table_for_testing" 

191 registry.registerOpaqueTable( 

192 table, 

193 spec=ddl.TableSpec( 

194 fields=[ 

195 ddl.FieldSpec("id", dtype=sqlalchemy.BigInteger, primaryKey=True), 

196 ddl.FieldSpec("name", dtype=sqlalchemy.String, length=16, nullable=False), 

197 ddl.FieldSpec("count", dtype=sqlalchemy.SmallInteger, nullable=True), 

198 ], 

199 ), 

200 ) 

201 rows = [ 

202 {"id": 1, "name": "one", "count": None}, 

203 {"id": 2, "name": "two", "count": 5}, 

204 {"id": 3, "name": "three", "count": 6}, 

205 ] 

206 registry.insertOpaqueData(table, *rows) 

207 self.assertCountEqual(rows, list(registry.fetchOpaqueData(table))) 

208 self.assertEqual(rows[0:1], list(registry.fetchOpaqueData(table, id=1))) 

209 self.assertEqual(rows[1:2], list(registry.fetchOpaqueData(table, name="two"))) 

210 self.assertEqual(rows[0:1], list(registry.fetchOpaqueData(table, id=(1, 3), name=("one", "two")))) 

211 self.assertEqual(rows, list(registry.fetchOpaqueData(table, id=(1, 2, 3)))) 

212 # Test very long IN clause which exceeds sqlite limit on number of 

213 # parameters. SQLite says the limit is 32k but it looks like it is 

214 # much higher. 

215 self.assertEqual(rows, list(registry.fetchOpaqueData(table, id=list(range(300_000))))) 

216 # Two IN clauses, each longer than 1k batch size, first with 

217 # duplicates, second has matching elements in different batches (after 

218 # sorting). 

219 self.assertEqual( 

220 rows[0:2], 

221 list( 

222 registry.fetchOpaqueData( 

223 table, 

224 id=list(range(1000)) + list(range(100, 0, -1)), 

225 name=["one"] + [f"q{i}" for i in range(2200)] + ["two"], 

226 ) 

227 ), 

228 ) 

229 self.assertEqual([], list(registry.fetchOpaqueData(table, id=1, name="two"))) 

230 registry.deleteOpaqueData(table, id=3) 

231 self.assertCountEqual(rows[:2], list(registry.fetchOpaqueData(table))) 

232 registry.deleteOpaqueData(table) 

233 self.assertEqual([], list(registry.fetchOpaqueData(table))) 

234 

235 def testDatasetType(self): 

236 """Tests for `SqlRegistry.registerDatasetType` and 

237 `SqlRegistry.getDatasetType`. 

238 """ 

239 butler = self.make_butler() 

240 registry = butler.registry 

241 # Check valid insert 

242 datasetTypeName = "test" 

243 storageClass = StorageClass("testDatasetType") 

244 registry.storageClasses.registerStorageClass(storageClass) 

245 dimensions = registry.dimensions.conform(("instrument", "visit")) 

246 differentDimensions = registry.dimensions.conform(("instrument", "patch")) 

247 inDatasetType = DatasetType(datasetTypeName, dimensions, storageClass) 

248 # Inserting for the first time should return True 

249 self.assertTrue(registry.registerDatasetType(inDatasetType)) 

250 outDatasetType1 = registry.getDatasetType(datasetTypeName) 

251 self.assertEqual(outDatasetType1, inDatasetType) 

252 

253 # Re-inserting should work 

254 self.assertFalse(registry.registerDatasetType(inDatasetType)) 

255 # Except when they are not identical 

256 with self.assertRaises(ConflictingDefinitionError): 

257 nonIdenticalDatasetType = DatasetType(datasetTypeName, differentDimensions, storageClass) 

258 registry.registerDatasetType(nonIdenticalDatasetType) 

259 

260 # Template can be None 

261 datasetTypeName = "testNoneTemplate" 

262 storageClass = StorageClass("testDatasetType2") 

263 registry.storageClasses.registerStorageClass(storageClass) 

264 dimensions = registry.dimensions.conform(("instrument", "visit")) 

265 inDatasetType = DatasetType(datasetTypeName, dimensions, storageClass) 

266 registry.registerDatasetType(inDatasetType) 

267 outDatasetType2 = registry.getDatasetType(datasetTypeName) 

268 self.assertEqual(outDatasetType2, inDatasetType) 

269 

270 allTypes = set(registry.queryDatasetTypes()) 

271 self.assertEqual(allTypes, {outDatasetType1, outDatasetType2}) 

272 

273 # Test some basic queryDatasetTypes functionality 

274 missing: list[str] = [] 

275 types = registry.queryDatasetTypes(["te*", "notarealdatasettype"], missing=missing) 

276 self.assertCountEqual([dt.name for dt in types], ["test", "testNoneTemplate"]) 

277 self.assertEqual(missing, ["notarealdatasettype"]) 

278 

279 # Trying to register a dataset type with different universe version or 

280 # namespace will raise. 

281 wrong_universes = (DimensionUniverse(version=-1), DimensionUniverse(namespace="🔭")) 

282 for universe in wrong_universes: 

283 storageClass = StorageClass("testDatasetType") 

284 dataset_type = DatasetType( 

285 "wrong_universe", ("instrument", "visit"), storageClass, universe=universe 

286 ) 

287 with self.assertRaisesRegex(ValueError, "Incompatible dimension universe versions"): 

288 registry.registerDatasetType(dataset_type) 

289 

290 def testDatasetTypeCache(self): 

291 """Test for dataset type cache update logic after a cache miss.""" 

292 butler1 = self.make_butler() 

293 butler2 = butler1.clone() 

294 self.load_data(butler1, "base.yaml") 

295 

296 # Trigger full cache load. 

297 butler2.get_dataset_type("flat") 

298 # Have an external process register a dataset type. 

299 butler1.registry.registerDatasetType( 

300 DatasetType("test_type", ["instrument"], "int", universe=butler1.dimensions) 

301 ) 

302 # Try to read the new dataset type -- this is a cache miss that 

303 # triggers fetch of a single dataset type. 

304 dt = butler2.get_dataset_type("test_type") 

305 self.assertEqual(dt.name, "test_type") 

306 self.assertEqual(list(dt.dimensions.names), ["instrument"]) 

307 # Read it again -- this time it should pull from the cache. 

308 dt = butler2.get_dataset_type("test_type") 

309 self.assertEqual(dt.name, "test_type") 

310 self.assertEqual(list(dt.dimensions.names), ["instrument"]) 

311 # Do a query that uses the dataset type's tags table. 

312 self.assertEqual( 

313 butler2.query_datasets("test_type", collections="*", find_first=False, explain=False), [] 

314 ) 

315 

316 def testDimensions(self): 

317 """Tests for `SqlRegistry.insertDimensionData`, 

318 `SqlRegistry.syncDimensionData`, and `SqlRegistry.expandDataId`. 

319 """ 

320 butler = self.make_butler() 

321 registry = butler.registry 

322 dimensionName = "instrument" 

323 dimension = registry.dimensions[dimensionName] 

324 dimensionValue = { 

325 "name": "DummyCam", 

326 "visit_max": 10, 

327 "visit_system": 0, 

328 "exposure_max": 10, 

329 "detector_max": 2, 

330 "class_name": "lsst.pipe.base.Instrument", 

331 } 

332 registry.insertDimensionData(dimensionName, dimensionValue) 

333 # Inserting the same value twice should fail 

334 with self.assertRaises(sqlalchemy.exc.IntegrityError): 

335 registry.insertDimensionData(dimensionName, dimensionValue) 

336 # expandDataId should retrieve the record we just inserted 

337 self.assertEqual( 

338 registry.expandDataId(instrument="DummyCam", dimensions=dimension.minimal_group) 

339 .records[dimensionName] 

340 .toDict(), 

341 dimensionValue, 

342 ) 

343 # expandDataId should raise if there is no record with the given ID. 

344 with self.assertRaises(DataIdValueError): 

345 registry.expandDataId({"instrument": "Unknown"}, dimensions=dimension.minimal_group) 

346 # band doesn't have a table; insert should fail. 

347 with self.assertRaises(TypeError): 

348 registry.insertDimensionData("band", {"band": "i"}) 

349 dimensionName2 = "physical_filter" 

350 dimension2 = registry.dimensions[dimensionName2] 

351 dimensionValue2 = {"name": "DummyCam_i", "band": "i"} 

352 # Missing required dependency ("instrument") should fail 

353 with self.assertRaises(KeyError): 

354 registry.insertDimensionData(dimensionName2, dimensionValue2) 

355 # Adding required dependency should fix the failure 

356 dimensionValue2["instrument"] = "DummyCam" 

357 registry.insertDimensionData(dimensionName2, dimensionValue2) 

358 # expandDataId should retrieve the record we just inserted. 

359 self.assertEqual( 

360 registry.expandDataId( 

361 instrument="DummyCam", physical_filter="DummyCam_i", dimensions=dimension2.minimal_group 

362 ) 

363 .records[dimensionName2] 

364 .toDict(), 

365 dimensionValue2, 

366 ) 

367 # Use syncDimensionData to insert a new record successfully. 

368 dimensionName3 = "detector" 

369 dimensionValue3 = { 

370 "instrument": "DummyCam", 

371 "id": 1, 

372 "full_name": "one", 

373 "name_in_raft": "zero", 

374 "purpose": "SCIENCE", 

375 } 

376 self.assertTrue(registry.syncDimensionData(dimensionName3, dimensionValue3)) 

377 # Sync that again. Note that one field ("raft") is NULL, and that 

378 # should be okay. 

379 self.assertFalse(registry.syncDimensionData(dimensionName3, dimensionValue3)) 

380 # Now try that sync with the same primary key but a different value. 

381 # This should fail. 

382 with self.assertRaises(ConflictingDefinitionError): 

383 registry.syncDimensionData( 

384 dimensionName3, 

385 { 

386 "instrument": "DummyCam", 

387 "id": 1, 

388 "full_name": "one", 

389 "name_in_raft": "four", 

390 "purpose": "SCIENCE", 

391 }, 

392 ) 

393 

394 @unittest.skipIf(np is None, "numpy not available.") 

395 def testNumpyDataId(self): 

396 """Test that we can use a numpy int in a dataId.""" 

397 butler = self.make_butler() 

398 registry = butler.registry 

399 dimensionEntries = [ 

400 ("instrument", {"instrument": "DummyCam"}), 

401 ("physical_filter", {"instrument": "DummyCam", "name": "d-r", "band": "R"}), 

402 ("day_obs", {"instrument": "DummyCam", "id": 20250101}), 

403 # Using an np.int64 here fails unless Records.fromDict is also 

404 # patched to look for numbers.Integral 

405 ( 

406 "visit", 

407 { 

408 "instrument": "DummyCam", 

409 "id": 42, 

410 "name": "fortytwo", 

411 "physical_filter": "d-r", 

412 "day_obs": 20250101, 

413 }, 

414 ), 

415 ] 

416 for args in dimensionEntries: 

417 registry.insertDimensionData(*args) 

418 

419 # Try a normal integer and something that looks like an int but 

420 # is not. 

421 for visit_id in (42, np.int64(42)): 

422 with self.subTest(visit_id=visit_id, id_type=type(visit_id).__name__): 

423 expanded = registry.expandDataId({"instrument": "DummyCam", "visit": visit_id}) 

424 self.assertEqual(expanded["visit"], int(visit_id)) 

425 self.assertIsInstance(expanded["visit"], int) 

426 

427 def testDataIdRelationships(self): 

428 """Test that `SqlRegistry.expandDataId` raises an exception when the 

429 given keys are inconsistent. 

430 """ 

431 butler = self.make_butler() 

432 self.load_data(butler, "base.yaml") 

433 registry = butler.registry 

434 # Insert a few more dimension records for the next test. 

435 registry.insertDimensionData( 

436 "day_obs", 

437 {"instrument": "Cam1", "id": 20250101}, 

438 ) 

439 registry.insertDimensionData( 

440 "group", 

441 {"instrument": "Cam1", "name": "group1"}, 

442 ) 

443 registry.insertDimensionData( 

444 "exposure", 

445 { 

446 "instrument": "Cam1", 

447 "id": 1, 

448 "obs_id": "one", 

449 "physical_filter": "Cam1-G", 

450 "group": "group1", 

451 "day_obs": 20250101, 

452 }, 

453 ) 

454 registry.insertDimensionData( 

455 "group", 

456 {"instrument": "Cam1", "name": "group2"}, 

457 ) 

458 registry.insertDimensionData( 

459 "exposure", 

460 { 

461 "instrument": "Cam1", 

462 "id": 2, 

463 "obs_id": "two", 

464 "physical_filter": "Cam1-G", 

465 "group": "group2", 

466 "day_obs": 20250101, 

467 }, 

468 ) 

469 registry.insertDimensionData( 

470 "visit_system", 

471 {"instrument": "Cam1", "id": 0, "name": "one-to-one"}, 

472 ) 

473 registry.insertDimensionData( 

474 "visit", 

475 {"instrument": "Cam1", "id": 1, "name": "one", "physical_filter": "Cam1-G", "day_obs": 20250101}, 

476 ) 

477 registry.insertDimensionData( 

478 "visit_definition", 

479 {"instrument": "Cam1", "visit": 1, "exposure": 1}, 

480 ) 

481 with self.assertRaises(InconsistentDataIdError): 

482 registry.expandDataId( 

483 {"instrument": "Cam1", "visit": 1, "exposure": 2}, 

484 ) 

485 

486 def testDataset(self): 

487 """Basic tests for `SqlRegistry.insertDatasets`, 

488 `SqlRegistry.getDataset`, and `SqlRegistry.removeDatasets`. 

489 """ 

490 butler = self.make_butler() 

491 registry = butler.registry 

492 self.load_data(butler, "base.yaml") 

493 run = "tésτ" 

494 registry.registerRun(run) 

495 datasetType = registry.getDatasetType("bias") 

496 dataId = {"instrument": "Cam1", "detector": 2} 

497 (ref,) = registry.insertDatasets(datasetType, dataIds=[dataId], run=run) 

498 outRef = registry.getDataset(ref.id) 

499 self.assertIsNotNone(ref.id) 

500 self.assertEqual(ref, outRef) 

501 with self.assertRaises(ConflictingDefinitionError): 

502 registry.insertDatasets(datasetType, dataIds=[dataId], run=run) 

503 registry.removeDatasets([ref]) 

504 self.assertIsNone(registry.findDataset(datasetType, dataId, collections=[run])) 

505 

506 def test_fetch_run_dataset_ids(self): 

507 butler = self.make_butler() 

508 registry = butler._registry 

509 self.load_data(butler, "base.yaml", "datasets.yaml") 

510 dataset_ids = registry._fetch_run_dataset_ids("imported_r") 

511 self.assertEqual(len(dataset_ids), 7) 

512 refs = butler.query_all_datasets("imported_r") 

513 self.assertCountEqual(dataset_ids, [ref.id for ref in refs]) 

514 

515 def testFindDataset(self): 

516 """Tests for `SqlRegistry.findDataset`.""" 

517 butler = self.make_butler() 

518 registry = butler.registry 

519 self.load_data(butler, "base.yaml") 

520 run = "tésτ" 

521 datasetType = registry.getDatasetType("bias") 

522 dataId = {"instrument": "Cam1", "detector": 4} 

523 registry.registerRun(run) 

524 (inputRef,) = registry.insertDatasets(datasetType, dataIds=[dataId], run=run) 

525 outputRef = registry.findDataset(datasetType, dataId, collections=[run]) 

526 self.assertEqual(outputRef, inputRef) 

527 # Check that retrieval with invalid dataId raises 

528 with self.assertRaises(LookupError): 

529 dataId = {"instrument": "Cam1"} # no detector 

530 registry.findDataset(datasetType, dataId, collections=run) 

531 # Check that different dataIds match to different datasets 

532 dataId1 = {"instrument": "Cam1", "detector": 1} 

533 (inputRef1,) = registry.insertDatasets(datasetType, dataIds=[dataId1], run=run) 

534 dataId2 = {"instrument": "Cam1", "detector": 2} 

535 (inputRef2,) = registry.insertDatasets(datasetType, dataIds=[dataId2], run=run) 

536 self.assertEqual(registry.findDataset(datasetType, dataId1, collections=run), inputRef1) 

537 self.assertEqual(registry.findDataset(datasetType, dataId2, collections=run), inputRef2) 

538 self.assertNotEqual(registry.findDataset(datasetType, dataId1, collections=run), inputRef2) 

539 self.assertNotEqual(registry.findDataset(datasetType, dataId2, collections=run), inputRef1) 

540 # Check that requesting a non-existing dataId returns None 

541 nonExistingDataId = {"instrument": "Cam1", "detector": 3} 

542 self.assertIsNone(registry.findDataset(datasetType, nonExistingDataId, collections=run)) 

543 # Search more than one collection, in which two have the right 

544 # dataset type and another does not. 

545 registry.registerRun("empty") 

546 self.load_data(butler, "datasets.yaml") 

547 bias1 = registry.findDataset("bias", instrument="Cam1", detector=2, collections=["imported_g"]) 

548 self.assertIsNotNone(bias1) 

549 bias2 = registry.findDataset("bias", instrument="Cam1", detector=2, collections=["imported_r"]) 

550 self.assertIsNotNone(bias2) 

551 self.assertEqual( 

552 bias1, 

553 registry.findDataset( 

554 "bias", instrument="Cam1", detector=2, collections=["empty", "imported_g", "imported_r"] 

555 ), 

556 ) 

557 self.assertEqual( 

558 bias2, 

559 registry.findDataset( 

560 "bias", instrument="Cam1", detector=2, collections=["empty", "imported_r", "imported_g"] 

561 ), 

562 ) 

563 # If the input data ID was an expanded DataCoordinate with records, 

564 # then the output ref has records, too. 

565 expanded_id = registry.expandDataId({"instrument": "Cam1", "detector": 2}) 

566 expanded_ref = registry.findDataset("bias", expanded_id, collections=["imported_r"]) 

567 self.assertTrue(expanded_ref.dataId.hasRecords()) 

568 # Search more than one collection, with one of them a CALIBRATION 

569 # collection. 

570 registry.registerCollection("Cam1/calib", CollectionType.CALIBRATION) 

571 timespan = Timespan( 

572 begin=astropy.time.Time("2020-01-01T01:00:00", format="isot", scale="tai"), 

573 end=astropy.time.Time("2020-01-01T02:00:00", format="isot", scale="tai"), 

574 ) 

575 registry.certify("Cam1/calib", [bias2], timespan=timespan) 

576 self.assertEqual( 

577 bias1, 

578 registry.findDataset( 

579 "bias", 

580 instrument="Cam1", 

581 detector=2, 

582 collections=["empty", "imported_g", "Cam1/calib"], 

583 timespan=timespan, 

584 ), 

585 ) 

586 self.assertEqual( 

587 bias1, 

588 registry.findDataset( 

589 "bias", 

590 instrument="Cam1", 

591 detector=2, 

592 # Calibration dataset type, with no calibration collection, but 

593 # a timespan was provided. 

594 collections=["imported_g"], 

595 timespan=timespan, 

596 ), 

597 ) 

598 self.assertEqual( 

599 bias2, 

600 registry.findDataset( 

601 "bias", 

602 instrument="Cam1", 

603 detector=2, 

604 collections=["empty", "Cam1/calib", "imported_g"], 

605 timespan=timespan, 

606 ), 

607 ) 

608 # If we try to search those same collections without a timespan, it 

609 # should still work, since the CALIBRATION collection is ignored. 

610 self.assertEqual( 

611 bias1, 

612 registry.findDataset( 

613 "bias", instrument="Cam1", detector=2, collections=["empty", "imported_g", "Cam1/calib"] 

614 ), 

615 ) 

616 self.assertEqual( 

617 bias1, 

618 registry.findDataset( 

619 "bias", instrument="Cam1", detector=2, collections=["empty", "Cam1/calib", "imported_g"] 

620 ), 

621 ) 

622 self.assertIsNone( 

623 registry.findDataset("bias", instrument="Cam1", detector=2, collections=["Cam1/calib"]) 

624 ) 

625 # Test non-calibration dataset type. 

626 registry.registerDatasetType( 

627 DatasetType("noncalibration", ["instrument", "detector"], "int", universe=butler.dimensions) 

628 ) 

629 (non_calibration_ref,) = registry.insertDatasets("noncalibration", dataIds=[dataId2], run=run) 

630 self.assertIsNone( 

631 registry.findDataset("noncalibration", instrument="Cam1", detector=2, collections=["imported_g"]) 

632 ) 

633 self.assertEqual( 

634 non_calibration_ref, 

635 registry.findDataset("noncalibration", instrument="Cam1", detector=2, collections=[run]), 

636 ) 

637 # Timespan parameter is ignored for non-calibration dataset types. 

638 self.assertIsNone( 

639 registry.findDataset( 

640 "noncalibration", instrument="Cam1", detector=2, collections=["imported_g"], timespan=timespan 

641 ) 

642 ) 

643 self.assertEqual( 

644 non_calibration_ref, 

645 registry.findDataset( 

646 "noncalibration", instrument="Cam1", detector=2, collections=[run], timespan=timespan 

647 ), 

648 ) 

649 self.assertEqual( 

650 non_calibration_ref, 

651 registry.findDataset( 

652 "noncalibration", 

653 instrument="Cam1", 

654 detector=2, 

655 collections=["Cam1/calib", run], 

656 timespan=timespan, 

657 ), 

658 ) 

659 # Add a dataset type whose dimension group involves an "implied" 

660 # dimension. ("physical_filter" implies "band".) 

661 registry.registerDatasetType( 

662 DatasetType( 

663 "dt_with_implied", 

664 [ 

665 "instrument", 

666 "physical_filter", 

667 ], 

668 "int", 

669 universe=butler.dimensions, 

670 ) 

671 ) 

672 data_id = {"instrument": "Cam1", "physical_filter": "Cam1-G"} 

673 (implied_ref,) = registry.insertDatasets("dt_with_implied", dataIds=[data_id], run=run) 

674 found_ref = registry.findDataset("dt_with_implied", data_id, collections=[run]) 

675 self.assertEqual(implied_ref, found_ref) 

676 # The "full" data ID with implied values is looked up, even though we 

677 # provided only the "required" values. 

678 self.assertTrue(found_ref.dataId.hasFull()) 

679 # The search ignores excess data ID values beyond the 'required' set. 

680 # This is not the correct band value for this physical_filter, but 

681 # the mismatch is ignored. 

682 self.assertEqual( 

683 implied_ref, 

684 registry.findDataset( 

685 "dt_with_implied", 

686 {"instrument": "Cam1", "physical_filter": "Cam1-G", "band": "r"}, 

687 collections=[run], 

688 ), 

689 ) 

690 # Correct band value, wrong physical_filter. 

691 self.assertIsNone( 

692 registry.findDataset( 

693 "dt_with_implied", 

694 {"instrument": "Cam1", "physical_filter": "Cam1-R1", "band": "g"}, 

695 collections=[run], 

696 ), 

697 ) 

698 

699 def testRemoveDatasetTypeSuccess(self): 

700 """Test that SqlRegistry.removeDatasetType works when there are no 

701 datasets of that type present. 

702 """ 

703 butler = self.make_butler() 

704 registry = butler.registry 

705 self.load_data(butler, "base.yaml") 

706 registry.removeDatasetType("flat") 

707 with self.assertRaises(MissingDatasetTypeError): 

708 registry.getDatasetType("flat") 

709 

710 def testRemoveDatasetTypeFailure(self): 

711 """Test that SqlRegistry.removeDatasetType raises when there are 

712 datasets of that type present or if the dataset type is for a 

713 component. 

714 """ 

715 butler = self.make_butler() 

716 registry = butler.registry 

717 self.load_data(butler, "base.yaml", "datasets.yaml") 

718 with self.assertRaises(OrphanedRecordError): 

719 registry.removeDatasetType("flat") 

720 with self.assertRaises(DatasetTypeError): 

721 registry.removeDatasetType(DatasetType.nameWithComponent("flat", "image")) 

722 

723 def testImportDatasetsUUID(self): 

724 """Test for `SqlRegistry._importDatasets` with UUID dataset ID.""" 

725 if isinstance(self.datasetsManager, str): 

726 if not self.datasetsManager.endswith(".ByDimensionsDatasetRecordStorageManagerUUID"): 

727 self.skipTest(f"Unexpected dataset manager {self.datasetsManager}") 

728 elif isinstance(self.datasetsManager, dict) and not self.datasetsManager["cls"].endswith( 

729 ".ByDimensionsDatasetRecordStorageManagerUUID" 

730 ): 

731 self.skipTest(f"Unexpected dataset manager {self.datasetsManager['cls']}") 

732 

733 butler = self.make_butler() 

734 registry = butler.registry 

735 self.load_data(butler, "base.yaml") 

736 for run in range(6): 

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

738 datasetTypeBias = registry.getDatasetType("bias") 

739 datasetTypeFlat = registry.getDatasetType("flat") 

740 dataIdBias1 = {"instrument": "Cam1", "detector": 1} 

741 dataIdBias2 = {"instrument": "Cam1", "detector": 2} 

742 dataIdFlat1 = {"instrument": "Cam1", "detector": 1, "physical_filter": "Cam1-G", "band": "g"} 

743 

744 ref = DatasetRef(datasetTypeBias, dataIdBias1, run="run0") 

745 (ref1,) = registry._importDatasets([ref], assume_new=True) 

746 # UUID is used without change 

747 self.assertEqual(ref.id, ref1.id) 

748 

749 # Inserting this ref with assume_new=True should fail, since this 

750 # dataset exists. 

751 with self.assertRaises(ConflictingDefinitionError): 

752 registry._importDatasets([ref], assume_new=True) 

753 

754 # All different failure modes 

755 refs = ( 

756 # Importing same DatasetRef with different dataset ID is an error 

757 DatasetRef(datasetTypeBias, dataIdBias1, run="run0"), 

758 # Same DatasetId but different DataId 

759 DatasetRef(datasetTypeBias, dataIdBias2, id=ref1.id, run="run0"), 

760 DatasetRef(datasetTypeFlat, dataIdFlat1, id=ref1.id, run="run0"), 

761 # Same DatasetRef and DatasetId but different run 

762 DatasetRef(datasetTypeBias, dataIdBias1, id=ref1.id, run="run1"), 

763 ) 

764 for ref in refs: 

765 with self.assertRaises(ConflictingDefinitionError): 

766 registry._importDatasets([ref]) 

767 

768 # Test for non-unique IDs, they can be re-imported multiple times. 

769 for run, idGenMode in ((2, DatasetIdGenEnum.DATAID_TYPE), (4, DatasetIdGenEnum.DATAID_TYPE_RUN)): 

770 with self.subTest(idGenMode=idGenMode): 

771 # Make dataset ref with reproducible dataset ID. 

772 ref = DatasetRef(datasetTypeBias, dataIdBias1, run=f"run{run}", id_generation_mode=idGenMode) 

773 (ref1,) = registry._importDatasets([ref]) 

774 self.assertIsInstance(ref1.id, uuid.UUID) 

775 self.assertEqual(ref1.id.version, 5) 

776 self.assertEqual(ref1.id, ref.id) 

777 

778 # Importing it again is OK 

779 (ref2,) = registry._importDatasets([ref1]) 

780 self.assertEqual(ref2.id, ref1.id) 

781 

782 # Cannot import to different run with the same ID 

783 ref = DatasetRef(datasetTypeBias, dataIdBias1, id=ref1.id, run=f"run{run + 1}") 

784 with self.assertRaises(ConflictingDefinitionError): 

785 registry._importDatasets([ref]) 

786 

787 ref = DatasetRef( 

788 datasetTypeBias, dataIdBias1, run=f"run{run + 1}", id_generation_mode=idGenMode 

789 ) 

790 if idGenMode is DatasetIdGenEnum.DATAID_TYPE: 

791 # Cannot import same DATAID_TYPE ref into a new run 

792 with self.assertRaises(ConflictingDefinitionError): 

793 (ref2,) = registry._importDatasets([ref]) 

794 else: 

795 # DATAID_TYPE_RUN ref can be imported into a new run 

796 (ref2,) = registry._importDatasets([ref]) 

797 

798 def testComponentLookups(self): 

799 """Test searching for component datasets via their parents. 

800 

801 Components can no longer be found by registry. This test checks 

802 that this now fails. 

803 """ 

804 butler = self.make_butler() 

805 registry = butler.registry 

806 self.load_data(butler, "base.yaml", "datasets.yaml") 

807 # Test getting the child dataset type (which does still exist in the 

808 # Registry), and check for consistency with 

809 # DatasetRef.makeComponentRef. 

810 collection = "imported_g" 

811 parentType = registry.getDatasetType("bias") 

812 childType = registry.getDatasetType("bias.wcs") 

813 parentRefResolved = registry.findDataset( 

814 parentType, collections=collection, instrument="Cam1", detector=1 

815 ) 

816 self.assertIsInstance(parentRefResolved, DatasetRef) 

817 self.assertEqual(childType, parentRefResolved.makeComponentRef("wcs").datasetType) 

818 # Search for a single dataset with findDataset. 

819 with self.assertRaises(DatasetTypeError): 

820 registry.findDataset("bias.wcs", collections=collection, dataId=parentRefResolved.dataId) 

821 

822 def testCollections(self): 

823 """Tests for registry methods that manage collections.""" 

824 butler = self.make_butler() 

825 registry = butler.registry 

826 other_registry = butler.clone().registry 

827 self.load_data(butler, "base.yaml", "datasets.yaml") 

828 run1 = "imported_g" 

829 run2 = "imported_r" 

830 # Test setting a collection docstring after it has been created. 

831 registry.setCollectionDocumentation(run1, "doc for run1") 

832 self.assertEqual(registry.getCollectionDocumentation(run1), "doc for run1") 

833 registry.setCollectionDocumentation(run1, None) 

834 self.assertIsNone(registry.getCollectionDocumentation(run1)) 

835 datasetType = "bias" 

836 # Find some datasets via their run's collection. 

837 dataId1 = {"instrument": "Cam1", "detector": 1} 

838 ref1 = registry.findDataset(datasetType, dataId1, collections=run1) 

839 self.assertIsNotNone(ref1) 

840 dataId2 = {"instrument": "Cam1", "detector": 2} 

841 ref2 = registry.findDataset(datasetType, dataId2, collections=run1) 

842 self.assertIsNotNone(ref2) 

843 # Associate those into a new collection, then look for them there. 

844 tag1 = "tag1" 

845 registry.registerCollection(tag1, type=CollectionType.TAGGED, doc="doc for tag1") 

846 # Check that we can query for old and new collections by type. 

847 self.assertEqual(set(registry.queryCollections(collectionTypes=CollectionType.RUN)), {run1, run2}) 

848 self.assertEqual( 

849 set(registry.queryCollections(collectionTypes={CollectionType.TAGGED, CollectionType.RUN})), 

850 {tag1, run1, run2}, 

851 ) 

852 self.assertEqual(registry.getCollectionDocumentation(tag1), "doc for tag1") 

853 registry.associate(tag1, [ref1, ref2]) 

854 self.assertEqual(registry.findDataset(datasetType, dataId1, collections=tag1), ref1) 

855 self.assertEqual(registry.findDataset(datasetType, dataId2, collections=tag1), ref2) 

856 # Disassociate one and verify that we can't it there anymore... 

857 registry.disassociate(tag1, [ref1]) 

858 self.assertIsNone(registry.findDataset(datasetType, dataId1, collections=tag1)) 

859 # ...but we can still find ref2 in tag1, and ref1 in the run. 

860 self.assertEqual(registry.findDataset(datasetType, dataId1, collections=run1), ref1) 

861 self.assertEqual(registry.findDataset(datasetType, dataId2, collections=tag1), ref2) 

862 collections = set(registry.queryCollections()) 

863 self.assertEqual(collections, {run1, run2, tag1}) 

864 # Associate both refs into tag1 again; ref2 is already there, but that 

865 # should be a harmless no-op. 

866 registry.associate(tag1, [ref1, ref2]) 

867 self.assertEqual(registry.findDataset(datasetType, dataId1, collections=tag1), ref1) 

868 self.assertEqual(registry.findDataset(datasetType, dataId2, collections=tag1), ref2) 

869 # Get a different dataset (from a different run) that has the same 

870 # dataset type and data ID as ref2. 

871 ref2b = registry.findDataset(datasetType, dataId2, collections=run2) 

872 self.assertNotEqual(ref2, ref2b) 

873 # Attempting to associate that into tag1 should be an error. 

874 with self.assertRaises(ConflictingDefinitionError): 

875 registry.associate(tag1, [ref2b]) 

876 # That error shouldn't have messed up what we had before. 

877 self.assertEqual(registry.findDataset(datasetType, dataId1, collections=tag1), ref1) 

878 self.assertEqual(registry.findDataset(datasetType, dataId2, collections=tag1), ref2) 

879 # Attempt to associate the conflicting dataset again, this time with 

880 # a dataset that isn't in the collection and won't cause a conflict. 

881 # Should also fail without modifying anything. 

882 dataId3 = {"instrument": "Cam1", "detector": 3} 

883 ref3 = registry.findDataset(datasetType, dataId3, collections=run1) 

884 with self.assertRaises(ConflictingDefinitionError): 

885 registry.associate(tag1, [ref3, ref2b]) 

886 self.assertEqual(registry.findDataset(datasetType, dataId1, collections=tag1), ref1) 

887 self.assertEqual(registry.findDataset(datasetType, dataId2, collections=tag1), ref2) 

888 self.assertIsNone(registry.findDataset(datasetType, dataId3, collections=tag1)) 

889 # Register a chained collection that searches [tag1, run2] 

890 chain1 = "chain1" 

891 registry.registerCollection(chain1, type=CollectionType.CHAINED) 

892 self.assertIs(registry.getCollectionType(chain1), CollectionType.CHAINED) 

893 # Chained collection exists, but has no collections in it. 

894 self.assertFalse(registry.getCollectionChain(chain1)) 

895 # If we query for all collections, we should get the chained collection 

896 # if we don't ask to flatten it (i.e. yield only its children) or if we 

897 # explicitly ask to include it too. 

898 self.assertEqual(set(registry.queryCollections(flattenChains=False)), {tag1, run1, run2, chain1}) 

899 self.assertEqual(set(registry.queryCollections(flattenChains=True)), {tag1, run1, run2}) 

900 self.assertEqual( 

901 set(registry.queryCollections(flattenChains=True, includeChains=True)), {tag1, run1, run2, chain1} 

902 ) 

903 # Attempt to set its child collections to something circular; that 

904 # should fail. 

905 with self.assertRaises(ValueError): 

906 registry.setCollectionChain(chain1, [tag1, chain1]) 

907 # Add the child collections. 

908 registry.setCollectionChain(chain1, [tag1, run2]) 

909 self.assertEqual(list(registry.getCollectionChain(chain1)), [tag1, run2]) 

910 self.assertEqual(registry.getCollectionParentChains(tag1), {chain1}) 

911 self.assertEqual(registry.getCollectionParentChains(run2), {chain1}) 

912 # Refresh the other registry that points to the same repo, and make 

913 # sure it can see the things we've done (note that this does require 

914 # an explicit refresh(); that's the documented behavior, because 

915 # caching is ~impossible otherwise). 

916 if other_registry is not None: 

917 other_registry.refresh() 

918 self.assertEqual(list(other_registry.getCollectionChain(chain1)), [tag1, run2]) 

919 self.assertEqual(other_registry.getCollectionParentChains(tag1), {chain1}) 

920 self.assertEqual(other_registry.getCollectionParentChains(run2), {chain1}) 

921 # Searching for dataId1 or dataId2 in the chain should return ref1 and 

922 # ref2, because both are in tag1. 

923 self.assertEqual(registry.findDataset(datasetType, dataId1, collections=chain1), ref1) 

924 self.assertEqual(registry.findDataset(datasetType, dataId2, collections=chain1), ref2) 

925 # Now disassociate ref2 from tag1. The search (for bias) with 

926 # dataId2 in chain1 should then: 

927 # 1. not find it in tag1 

928 # 2. find a different dataset in run2 

929 registry.disassociate(tag1, [ref2]) 

930 ref2b = registry.findDataset(datasetType, dataId2, collections=chain1) 

931 self.assertNotEqual(ref2b, ref2) 

932 self.assertEqual(ref2b, registry.findDataset(datasetType, dataId2, collections=run2)) 

933 # Define a new chain so we can test recursive chains. 

934 chain2 = "chain2" 

935 registry.registerCollection(chain2, type=CollectionType.CHAINED) 

936 registry.setCollectionChain(chain2, [run2, chain1]) 

937 self.assertEqual(registry.getCollectionParentChains(chain1), {chain2}) 

938 self.assertEqual(registry.getCollectionParentChains(run2), {chain1, chain2}) 

939 

940 if self.supportsCollectionRegex: 

941 # Query for collections matching a regex. 

942 with self.assertWarns(FutureWarning): 

943 self.assertCountEqual( 

944 list(registry.queryCollections(re.compile("imported_."), flattenChains=False)), 

945 ["imported_r", "imported_g"], 

946 ) 

947 # Query for collections matching a regex or an explicit str. 

948 with self.assertWarns(FutureWarning): 

949 self.assertCountEqual( 

950 list( 

951 registry.queryCollections([re.compile("imported_."), "chain1"], flattenChains=False) 

952 ), 

953 ["imported_r", "imported_g", "chain1"], 

954 ) 

955 # Same queries as the regex ones above, but using globs instead of 

956 # regex. 

957 self.assertCountEqual( 

958 list(registry.queryCollections("imported_*", flattenChains=False)), 

959 ["imported_r", "imported_g"], 

960 ) 

961 # Query for collections matching a regex or an explicit str. 

962 self.assertCountEqual( 

963 list(registry.queryCollections(["imported_*", "chain1"], flattenChains=False)), 

964 ["imported_r", "imported_g", "chain1"], 

965 ) 

966 # Query for collection matching chain names, by flattening it should 

967 # only return non-chain names. 

968 self.assertCountEqual(list(registry.queryCollections("chain?", flattenChains=True)), [tag1, run2]) 

969 # Query for collection matching chain name, by flattening and 

970 # asking to include chains it should return everything. 

971 self.assertCountEqual( 

972 list(registry.queryCollections("chain*", flattenChains=True, includeChains=True)), 

973 [tag1, run2, chain1, chain2], 

974 ) 

975 # Order of children in chained collections is preserved. 

976 self.assertEqual(list(registry.queryCollections("chain1", flattenChains=True)), [tag1, run2]) 

977 self.assertEqual(list(registry.queryCollections("cha*2", flattenChains=True)), [run2, tag1]) 

978 self.assertEqual( 

979 list(registry.queryCollections("chain1", flattenChains=True, includeChains=True)), 

980 [chain1, tag1, run2], 

981 ) 

982 self.assertEqual( 

983 list(registry.queryCollections("chain2", flattenChains=True, includeChains=True)), 

984 [chain2, run2, chain1, tag1], 

985 ) 

986 

987 # Search for bias with dataId1 should find it via tag1 in chain2, 

988 # recursing, because is not in run1. 

989 self.assertIsNone(registry.findDataset(datasetType, dataId1, collections=run2)) 

990 self.assertEqual(registry.findDataset(datasetType, dataId1, collections=chain2), ref1) 

991 # Search for bias with dataId2 should find it in run2 (ref2b). 

992 self.assertEqual(registry.findDataset(datasetType, dataId2, collections=chain2), ref2b) 

993 # Search for a flat that is in run2. That should not be found 

994 # at the front of chain2, because of the restriction to bias 

995 # on run2 there, but it should be found in at the end of chain1. 

996 dataId4 = {"instrument": "Cam1", "detector": 3, "physical_filter": "Cam1-R2"} 

997 ref4 = registry.findDataset("flat", dataId4, collections=run2) 

998 self.assertIsNotNone(ref4) 

999 self.assertEqual(ref4, registry.findDataset("flat", dataId4, collections=chain2)) 

1000 # Deleting a collection that's part of a CHAINED collection is not 

1001 # allowed, and is exception-safe. 

1002 with self.assertRaises(sqlalchemy.exc.IntegrityError): 

1003 registry.removeCollection(run2) 

1004 self.assertEqual(registry.getCollectionType(run2), CollectionType.RUN) 

1005 with self.assertRaises(sqlalchemy.exc.IntegrityError): 

1006 registry.removeCollection(chain1) 

1007 self.assertEqual(registry.getCollectionType(chain1), CollectionType.CHAINED) 

1008 # Actually remove chain2, test that it's gone by asking for its type. 

1009 registry.removeCollection(chain2) 

1010 with self.assertRaises(MissingCollectionError): 

1011 registry.getCollectionType(chain2) 

1012 # Actually remove run2 and chain1, which should work now. 

1013 registry.removeCollection(chain1) 

1014 registry.removeCollection(run2) 

1015 with self.assertRaises(MissingCollectionError): 

1016 registry.getCollectionType(run2) 

1017 with self.assertRaises(MissingCollectionError): 

1018 registry.getCollectionType(chain1) 

1019 # Remove tag1 as well, just to test that we can remove TAGGED 

1020 # collections. 

1021 registry.removeCollection(tag1) 

1022 with self.assertRaises(MissingCollectionError): 

1023 registry.getCollectionType(tag1) 

1024 

1025 def test_collection_clearing(self) -> None: 

1026 """Test that we can delete TAGGED and CALIBRATION collections without 

1027 manually removing all associated datasets first. 

1028 """ 

1029 butler = self.make_butler() 

1030 self.load_data(butler, "base.yaml", "datasets.yaml") 

1031 

1032 # This brings in datasets of two different types, with the same 

1033 # dimension group. 

1034 original_datasets = tuple(butler.query_all_datasets("imported_r", instrument="Cam1", detector=2)) 

1035 self.assertEqual(len(original_datasets), 2) 

1036 

1037 # Test tagged collections. 

1038 butler.collections.register("tag1", CollectionType.TAGGED) 

1039 butler.collections.register("tag2", CollectionType.TAGGED) 

1040 butler.registry.associate("tag1", original_datasets) 

1041 butler.registry.associate("tag2", original_datasets) 

1042 butler.collections.x_remove("tag1") 

1043 with self.assertRaises(MissingCollectionError): 

1044 butler.collections.get_info("tag1") 

1045 # Make sure there was no collateral damage -- tag2 should still be 

1046 # intact. 

1047 self.assertEqual(set(butler.query_all_datasets("tag2")), set(original_datasets)) 

1048 

1049 # Test calibration collections. 

1050 butler.collections.register("calib1", CollectionType.CALIBRATION) 

1051 butler.collections.register("calib2", CollectionType.CALIBRATION) 

1052 butler.registry.certify("calib1", original_datasets, Timespan(None, None)) 

1053 butler.registry.certify("calib2", original_datasets, Timespan(None, None)) 

1054 butler.collections.x_remove("calib1") 

1055 with self.assertRaises(MissingCollectionError): 

1056 butler.collections.get_info("calib1") 

1057 # Make sure there was no collateral damage -- calib2 should still be 

1058 # intact. 

1059 self.assertEqual(set(butler.query_all_datasets("calib2")), set(original_datasets)) 

1060 

1061 def testCollectionChainCaching(self): 

1062 butler = self.make_butler() 

1063 registry = butler.registry 

1064 with registry.caching_context(): 

1065 registry.registerCollection("a") 

1066 registry.registerCollection("chain", CollectionType.CHAINED) 

1067 # There used to be a caching bug (DM-43750) that would throw an 

1068 # exception if you modified a collection chain for a collection 

1069 # that was already in the cache. 

1070 registry.setCollectionChain("chain", ["a"]) 

1071 self.assertEqual(list(registry.getCollectionChain("chain")), ["a"]) 

1072 

1073 def testCollectionChainFlatten(self): 

1074 """Test that `SqlRegistry.setCollectionChain` obeys its 'flatten' 

1075 option. 

1076 """ 

1077 butler = self.make_butler() 

1078 registry = butler.registry 

1079 registry.registerCollection("inner", CollectionType.CHAINED) 

1080 registry.registerCollection("innermost", CollectionType.RUN) 

1081 registry.setCollectionChain("inner", ["innermost"]) 

1082 registry.registerCollection("outer", CollectionType.CHAINED) 

1083 registry.setCollectionChain("outer", ["inner"], flatten=False) 

1084 self.assertEqual(list(registry.getCollectionChain("outer")), ["inner"]) 

1085 registry.setCollectionChain("outer", ["inner"], flatten=True) 

1086 self.assertEqual(list(registry.getCollectionChain("outer")), ["innermost"]) 

1087 

1088 def testCollectionChainPrependConcurrency(self): 

1089 """Verify that locking via database row locks is working as 

1090 expected. 

1091 """ 

1092 

1093 def blocked_thread_func(butler: Butler): 

1094 # This call will become blocked after it has decided on positions 

1095 # for the new children in the collection chain, but before 

1096 # inserting them. 

1097 butler.collections.prepend_chain("chain", ["a"]) 

1098 

1099 def unblocked_thread_func(butler: Butler): 

1100 butler.collections.prepend_chain("chain", ["b"]) 

1101 

1102 registry = self._do_collection_concurrency_test(blocked_thread_func, unblocked_thread_func) 

1103 

1104 # blocked_thread_func should have finished first, inserting "a". 

1105 # unblocked_thread_func should have finished second, prepending "b". 

1106 self.assertEqual(("b", "a"), registry.getCollectionChain("chain")) 

1107 

1108 def testCollectionChainReplaceConcurrency(self): 

1109 """Verify that locking via database row locks is working as 

1110 expected. 

1111 """ 

1112 

1113 def blocked_thread_func(butler: Butler): 

1114 # This call will become blocked after deleting children, but before 

1115 # inserting new ones. 

1116 butler.collections.redefine_chain("chain", ["a"]) 

1117 

1118 def unblocked_thread_func(butler: Butler): 

1119 butler.collections.redefine_chain("chain", ["b"]) 

1120 

1121 registry = self._do_collection_concurrency_test(blocked_thread_func, unblocked_thread_func) 

1122 

1123 # blocked_thread_func should have finished first. 

1124 # unblocked_thread_func should have finished second, overwriting the 

1125 # chain with "b". 

1126 self.assertEqual(("b",), registry.getCollectionChain("chain")) 

1127 

1128 def testCollectionChainRemoveConcurrency(self): 

1129 def blocked_thread_func(butler: Butler): 

1130 # This call will become blocked after taking the lock, but before 

1131 # deleting the children. 

1132 butler.collections.remove_from_chain("chain", ["b"]) 

1133 

1134 def unblocked_thread_func(butler: Butler): 

1135 butler.collections.redefine_chain("chain", ["b", "a"]) 

1136 

1137 registry = self._do_collection_concurrency_test(blocked_thread_func, unblocked_thread_func) 

1138 

1139 # blocked_thread_func should have finished first, removing "b". 

1140 # unblocked_thread_func should have finished second, putting "b" back. 

1141 self.assertEqual(("b", "a"), registry.getCollectionChain("chain")) 

1142 

1143 def _do_collection_concurrency_test( 

1144 self, blocked_thread_func: Callable[[Butler]], unblocked_thread_func: Callable[[Butler]] 

1145 ) -> SqlRegistry: 

1146 # This function: 

1147 # 1. Sets up two registries pointing at the same database. 

1148 # 2. Start running 'blocked_thread_func' in a background thread, 

1149 # arranging for it to become blocked during a critical section in 

1150 # the collections manager. 

1151 # 3. Wait for 'blocked_thread_func' to reach the critical section 

1152 # 4. Start running 'unblocked_thread_func'. 

1153 # 5. Allow both functions to run to completion. 

1154 

1155 # Set up two registries pointing to the same DB 

1156 butler1 = self.make_butler() 

1157 butler2 = butler1.clone() 

1158 registry1 = butler1._registry 

1159 assert isinstance(registry1, SqlRegistry) 

1160 registry2 = butler2._registry 

1161 

1162 with contextlib.suppress(AttributeError): 

1163 if ":memory:" in str(registry2._db): 

1164 raise unittest.SkipTest("Testing concurrency requires two connections to the same DB.") 

1165 

1166 registry1.registerCollection("chain", CollectionType.CHAINED) 

1167 for collection in ["a", "b"]: 

1168 registry1.registerCollection(collection) 

1169 

1170 # Arrange for registry1 to block during its critical section, allowing 

1171 # us to detect this and control when it becomes unblocked. 

1172 enter_barrier = Barrier(2, timeout=60) 

1173 exit_barrier = Barrier(2, timeout=60) 

1174 

1175 def wait_for_barrier(): 

1176 enter_barrier.wait() 

1177 exit_barrier.wait() 

1178 

1179 registry1._managers.collections._block_for_concurrency_test = wait_for_barrier 

1180 

1181 with ThreadPoolExecutor(max_workers=1) as exec1: 

1182 with ThreadPoolExecutor(max_workers=1) as exec2: 

1183 future1 = exec1.submit(blocked_thread_func, butler1) 

1184 enter_barrier.wait() 

1185 

1186 # At this point registry 1 has entered the critical section and 

1187 # is waiting for us to release it. Start the other thread. 

1188 future2 = exec2.submit(unblocked_thread_func, butler2) 

1189 # thread2 should block inside a database call, but we have no 

1190 # way to detect when it is in this state. 

1191 time.sleep(0.200) 

1192 

1193 # Let the threads run to completion. 

1194 exit_barrier.wait() 

1195 future1.result() 

1196 future2.result() 

1197 

1198 return registry1 

1199 

1200 def testBasicTransaction(self): 

1201 """Test that all operations within a single transaction block are 

1202 rolled back if an exception propagates out of the block. 

1203 """ 

1204 butler = self.make_butler() 

1205 registry = butler.registry 

1206 storageClass = StorageClass("testDatasetType") 

1207 registry.storageClasses.registerStorageClass(storageClass) 

1208 with registry.transaction(): 

1209 registry.insertDimensionData("instrument", {"name": "Cam1", "class_name": "A"}) 

1210 with self.assertRaises(ValueError): 

1211 with registry.transaction(): 

1212 registry.insertDimensionData("instrument", {"name": "Cam2"}) 

1213 raise ValueError("Oops, something went wrong") 

1214 # Cam1 should exist 

1215 self.assertEqual(registry.expandDataId(instrument="Cam1").records["instrument"].class_name, "A") 

1216 # But Cam2 and Cam3 should both not exist 

1217 with self.assertRaises(DataIdValueError): 

1218 registry.expandDataId(instrument="Cam2") 

1219 with self.assertRaises(DataIdValueError): 

1220 registry.expandDataId(instrument="Cam3") 

1221 

1222 def testNestedTransaction(self): 

1223 """Test that operations within a transaction block are not rolled back 

1224 if an exception propagates out of an inner transaction block and is 

1225 then caught. 

1226 """ 

1227 butler = self.make_butler() 

1228 registry = butler.registry 

1229 dimension = registry.dimensions["instrument"] 

1230 dataId1 = {"instrument": "DummyCam"} 

1231 dataId2 = {"instrument": "DummyCam2"} 

1232 checkpointReached = False 

1233 with registry.transaction(): 

1234 # This should be added and (ultimately) committed. 

1235 registry.insertDimensionData(dimension, dataId1) 

1236 with self.assertRaises(sqlalchemy.exc.IntegrityError): 

1237 with registry.transaction(savepoint=True): 

1238 # This does not conflict, and should succeed (but not 

1239 # be committed). 

1240 registry.insertDimensionData(dimension, dataId2) 

1241 checkpointReached = True 

1242 # This should conflict and raise, triggering a rollback 

1243 # of the previous insertion within the same transaction 

1244 # context, but not the original insertion in the outer 

1245 # block. 

1246 registry.insertDimensionData(dimension, dataId1) 

1247 self.assertTrue(checkpointReached) 

1248 self.assertIsNotNone(registry.expandDataId(dataId1, dimensions=dimension.minimal_group)) 

1249 with self.assertRaises(DataIdValueError): 

1250 registry.expandDataId(dataId2, dimensions=dimension.minimal_group) 

1251 

1252 def testInstrumentDimensions(self): 

1253 """Test queries involving only instrument dimensions, with no joins to 

1254 skymap. 

1255 """ 

1256 butler = self.make_butler() 

1257 registry = butler.registry 

1258 

1259 # need a bunch of dimensions and datasets for test 

1260 registry.insertDimensionData( 

1261 "instrument", dict(name="DummyCam", visit_max=25, exposure_max=300, detector_max=6) 

1262 ) 

1263 registry.insertDimensionData("day_obs", dict(instrument="DummyCam", id=20250101)) 

1264 registry.insertDimensionData( 

1265 "physical_filter", 

1266 dict(instrument="DummyCam", name="dummy_r", band="r"), 

1267 dict(instrument="DummyCam", name="dummy_i", band="i"), 

1268 ) 

1269 registry.insertDimensionData( 

1270 "detector", *[dict(instrument="DummyCam", id=i, full_name=str(i)) for i in range(1, 6)] 

1271 ) 

1272 registry.insertDimensionData( 

1273 "visit", 

1274 dict(instrument="DummyCam", id=10, name="ten", physical_filter="dummy_i", day_obs=20250101), 

1275 dict(instrument="DummyCam", id=11, name="eleven", physical_filter="dummy_r", day_obs=20250101), 

1276 dict(instrument="DummyCam", id=20, name="twelve", physical_filter="dummy_r", day_obs=20250101), 

1277 ) 

1278 registry.insertDimensionData( 

1279 "group", 

1280 dict(instrument="DummyCam", name="ten"), 

1281 dict(instrument="DummyCam", name="eleven"), 

1282 dict(instrument="DummyCam", name="twelve"), 

1283 ) 

1284 for i in range(1, 6): 

1285 registry.insertDimensionData( 

1286 "visit_detector_region", 

1287 dict(instrument="DummyCam", visit=10, detector=i), 

1288 dict(instrument="DummyCam", visit=11, detector=i), 

1289 dict(instrument="DummyCam", visit=20, detector=i), 

1290 ) 

1291 registry.insertDimensionData( 

1292 "exposure", 

1293 dict( 

1294 instrument="DummyCam", 

1295 id=100, 

1296 obs_id="100", 

1297 physical_filter="dummy_i", 

1298 group="ten", 

1299 day_obs=20250101, 

1300 ), 

1301 dict( 

1302 instrument="DummyCam", 

1303 id=101, 

1304 obs_id="101", 

1305 physical_filter="dummy_i", 

1306 group="ten", 

1307 day_obs=20250101, 

1308 ), 

1309 dict( 

1310 instrument="DummyCam", 

1311 id=110, 

1312 obs_id="110", 

1313 physical_filter="dummy_r", 

1314 group="eleven", 

1315 day_obs=20250101, 

1316 ), 

1317 dict( 

1318 instrument="DummyCam", 

1319 id=111, 

1320 obs_id="111", 

1321 physical_filter="dummy_r", 

1322 group="eleven", 

1323 day_obs=20250101, 

1324 ), 

1325 dict( 

1326 instrument="DummyCam", 

1327 id=200, 

1328 obs_id="200", 

1329 physical_filter="dummy_r", 

1330 group="twelve", 

1331 day_obs=20250101, 

1332 ), 

1333 dict( 

1334 instrument="DummyCam", 

1335 id=201, 

1336 obs_id="201", 

1337 physical_filter="dummy_r", 

1338 group="twelve", 

1339 day_obs=20250101, 

1340 ), 

1341 ) 

1342 registry.insertDimensionData( 

1343 "visit_definition", 

1344 dict(instrument="DummyCam", exposure=100, visit=10), 

1345 dict(instrument="DummyCam", exposure=101, visit=10), 

1346 dict(instrument="DummyCam", exposure=110, visit=11), 

1347 dict(instrument="DummyCam", exposure=111, visit=11), 

1348 dict(instrument="DummyCam", exposure=200, visit=20), 

1349 dict(instrument="DummyCam", exposure=201, visit=20), 

1350 ) 

1351 # dataset types 

1352 run1 = "test1_r" 

1353 run2 = "test2_r" 

1354 tagged2 = "test2_t" 

1355 registry.registerRun(run1) 

1356 registry.registerRun(run2) 

1357 registry.registerCollection(tagged2) 

1358 storageClass = StorageClass("testDataset") 

1359 registry.storageClasses.registerStorageClass(storageClass) 

1360 rawType = DatasetType( 

1361 name="RAW", 

1362 dimensions=registry.dimensions.conform(("instrument", "exposure", "detector")), 

1363 storageClass=storageClass, 

1364 ) 

1365 registry.registerDatasetType(rawType) 

1366 calexpType = DatasetType( 

1367 name="CALEXP", 

1368 dimensions=registry.dimensions.conform(("instrument", "visit", "detector")), 

1369 storageClass=storageClass, 

1370 ) 

1371 registry.registerDatasetType(calexpType) 

1372 

1373 # add pre-existing datasets 

1374 for exposure in (100, 101, 110, 111): 

1375 for detector in (1, 2, 3): 

1376 # note that only 3 of 5 detectors have datasets 

1377 dataId = dict(instrument="DummyCam", exposure=exposure, detector=detector) 

1378 (ref,) = registry.insertDatasets(rawType, dataIds=[dataId], run=run1) 

1379 # exposures 100 and 101 appear in both run1 and tagged2. 

1380 # 100 has different datasets in the different collections 

1381 # 101 has the same dataset in both collections. 

1382 if exposure == 100: 

1383 (ref,) = registry.insertDatasets(rawType, dataIds=[dataId], run=run2) 

1384 if exposure in (100, 101): 

1385 registry.associate(tagged2, [ref]) 

1386 # Add pre-existing datasets to tagged2. 

1387 for exposure in (200, 201): 

1388 for detector in (3, 4, 5): 

1389 # note that only 3 of 5 detectors have datasets 

1390 dataId = dict(instrument="DummyCam", exposure=exposure, detector=detector) 

1391 (ref,) = registry.insertDatasets(rawType, dataIds=[dataId], run=run2) 

1392 registry.associate(tagged2, [ref]) 

1393 

1394 dimensions = registry.dimensions.conform(rawType.dimensions.required | calexpType.dimensions.required) 

1395 # Test that single dim string works as well as list of str 

1396 rows = registry.queryDataIds("visit", datasets=rawType, collections=run1).expanded().toSet() 

1397 rowsI = registry.queryDataIds(["visit"], datasets=rawType, collections=run1).expanded().toSet() 

1398 self.assertEqual(rows, rowsI) 

1399 # with empty expression 

1400 rows = registry.queryDataIds(dimensions, datasets=rawType, collections=run1).expanded().toSet() 

1401 self.assertEqual(len(rows), 4 * 3) # 4 exposures times 3 detectors 

1402 self.assertCountEqual({dataId["exposure"] for dataId in rows}, (100, 101, 110, 111)) 

1403 self.assertCountEqual({dataId["visit"] for dataId in rows}, (10, 11)) 

1404 self.assertCountEqual({dataId["detector"] for dataId in rows}, (1, 2, 3)) 

1405 

1406 # second collection 

1407 rows = registry.queryDataIds(dimensions, datasets=rawType, collections=tagged2).toSet() 

1408 self.assertEqual(len(rows), 4 * 3) # 4 exposures times 3 detectors 

1409 for dataId in rows: 

1410 self.assertCountEqual(dataId.dimensions.required, ("instrument", "detector", "exposure", "visit")) 

1411 self.assertCountEqual({dataId["exposure"] for dataId in rows}, (100, 101, 200, 201)) 

1412 self.assertCountEqual({dataId["visit"] for dataId in rows}, (10, 20)) 

1413 self.assertCountEqual({dataId["detector"] for dataId in rows}, (1, 2, 3, 4, 5)) 

1414 

1415 # with two input datasets 

1416 rows = registry.queryDataIds(dimensions, datasets=rawType, collections=[run1, tagged2]).toSet() 

1417 self.assertEqual(len(set(rows)), 6 * 3) # 6 exposures times 3 detectors; set needed to de-dupe 

1418 for dataId in rows: 

1419 self.assertCountEqual(dataId.dimensions.required, ("instrument", "detector", "exposure", "visit")) 

1420 self.assertCountEqual({dataId["exposure"] for dataId in rows}, (100, 101, 110, 111, 200, 201)) 

1421 self.assertCountEqual({dataId["visit"] for dataId in rows}, (10, 11, 20)) 

1422 self.assertCountEqual({dataId["detector"] for dataId in rows}, (1, 2, 3, 4, 5)) 

1423 

1424 # limit to single visit 

1425 rows = registry.queryDataIds( 

1426 dimensions, datasets=rawType, collections=run1, where="visit = 10", instrument="DummyCam" 

1427 ).toSet() 

1428 self.assertEqual(len(rows), 2 * 3) # 2 exposures times 3 detectors 

1429 self.assertCountEqual({dataId["exposure"] for dataId in rows}, (100, 101)) 

1430 self.assertCountEqual({dataId["visit"] for dataId in rows}, (10,)) 

1431 self.assertCountEqual({dataId["detector"] for dataId in rows}, (1, 2, 3)) 

1432 

1433 # more limiting expression, using link names instead of Table.column 

1434 rows = registry.queryDataIds( 

1435 dimensions, 

1436 datasets=rawType, 

1437 collections=run1, 

1438 where="visit = 10 and detector > 1 and 'DummyCam'=instrument", 

1439 ).toSet() 

1440 self.assertEqual(len(rows), 2 * 2) # 2 exposures times 2 detectors 

1441 self.assertCountEqual({dataId["exposure"] for dataId in rows}, (100, 101)) 

1442 self.assertCountEqual({dataId["visit"] for dataId in rows}, (10,)) 

1443 self.assertCountEqual({dataId["detector"] for dataId in rows}, (2, 3)) 

1444 

1445 # queryDataIds with only one of `datasets` and `collections` is an 

1446 # error. 

1447 with self.assertRaises(CollectionError): 

1448 registry.queryDataIds(dimensions, datasets=rawType) 

1449 with self.assertRaises(ArgumentError): 

1450 registry.queryDataIds(dimensions, collections=run1) 

1451 

1452 # expression excludes everything 

1453 rows = registry.queryDataIds( 

1454 dimensions, datasets=rawType, collections=run1, where="visit > 1000", instrument="DummyCam" 

1455 ).toSet() 

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

1457 

1458 # Selecting by physical_filter, this is not in the dimensions, but it 

1459 # is a part of the full expression so it should work too. 

1460 rows = registry.queryDataIds( 

1461 dimensions, 

1462 datasets=rawType, 

1463 collections=run1, 

1464 where="physical_filter = 'dummy_r'", 

1465 instrument="DummyCam", 

1466 ).toSet() 

1467 self.assertEqual(len(rows), 2 * 3) # 2 exposures times 3 detectors 

1468 self.assertCountEqual({dataId["exposure"] for dataId in rows}, (110, 111)) 

1469 self.assertCountEqual({dataId["visit"] for dataId in rows}, (11,)) 

1470 self.assertCountEqual({dataId["detector"] for dataId in rows}, (1, 2, 3)) 

1471 

1472 def testSkyMapDimensions(self): 

1473 """Tests involving only skymap dimensions, no joins to instrument.""" 

1474 butler = self.make_butler() 

1475 registry = butler.registry 

1476 

1477 # need a bunch of dimensions and datasets for test, we want 

1478 # "band" in the test so also have to add physical_filter 

1479 # dimensions 

1480 registry.insertDimensionData("instrument", dict(instrument="DummyCam")) 

1481 registry.insertDimensionData( 

1482 "physical_filter", 

1483 dict(instrument="DummyCam", name="dummy_r", band="r"), 

1484 dict(instrument="DummyCam", name="dummy_i", band="i"), 

1485 ) 

1486 registry.insertDimensionData("skymap", dict(name="DummyMap", hash=b"sha!")) 

1487 for tract in range(10): 

1488 registry.insertDimensionData("tract", dict(skymap="DummyMap", id=tract)) 

1489 registry.insertDimensionData( 

1490 "patch", 

1491 *[dict(skymap="DummyMap", tract=tract, id=patch, cell_x=0, cell_y=0) for patch in range(10)], 

1492 ) 

1493 

1494 # dataset types 

1495 run = "tésτ" 

1496 registry.registerRun(run) 

1497 storageClass = StorageClass("testDataset") 

1498 registry.storageClasses.registerStorageClass(storageClass) 

1499 calexpType = DatasetType( 

1500 name="deepCoadd_calexp", 

1501 dimensions=registry.dimensions.conform(("skymap", "tract", "patch", "band")), 

1502 storageClass=storageClass, 

1503 ) 

1504 registry.registerDatasetType(calexpType) 

1505 mergeType = DatasetType( 

1506 name="deepCoadd_mergeDet", 

1507 dimensions=registry.dimensions.conform(("skymap", "tract", "patch")), 

1508 storageClass=storageClass, 

1509 ) 

1510 registry.registerDatasetType(mergeType) 

1511 measType = DatasetType( 

1512 name="deepCoadd_meas", 

1513 dimensions=registry.dimensions.conform(("skymap", "tract", "patch", "band")), 

1514 storageClass=storageClass, 

1515 ) 

1516 registry.registerDatasetType(measType) 

1517 

1518 dimensions = registry.dimensions.conform( 

1519 calexpType.dimensions.required | mergeType.dimensions.required | measType.dimensions.required 

1520 ) 

1521 

1522 # add pre-existing datasets 

1523 for tract in (1, 3, 5): 

1524 for patch in (2, 4, 6, 7): 

1525 dataId = dict(skymap="DummyMap", tract=tract, patch=patch) 

1526 registry.insertDatasets(mergeType, dataIds=[dataId], run=run) 

1527 for aFilter in ("i", "r"): 

1528 dataId = dict(skymap="DummyMap", tract=tract, patch=patch, band=aFilter) 

1529 registry.insertDatasets(calexpType, dataIds=[dataId], run=run) 

1530 

1531 # with empty expression 

1532 rows = registry.queryDataIds(dimensions, datasets=[calexpType, mergeType], collections=run).toSet() 

1533 self.assertEqual(len(rows), 3 * 4 * 2) # 4 tracts x 4 patches x 2 filters 

1534 for dataId in rows: 

1535 self.assertCountEqual(dataId.dimensions.required, ("skymap", "tract", "patch", "band")) 

1536 self.assertCountEqual({dataId["tract"] for dataId in rows}, (1, 3, 5)) 

1537 self.assertCountEqual({dataId["patch"] for dataId in rows}, (2, 4, 6, 7)) 

1538 self.assertCountEqual({dataId["band"] for dataId in rows}, ("i", "r")) 

1539 

1540 # limit to 2 tracts and 2 patches 

1541 rows = registry.queryDataIds( 

1542 dimensions, 

1543 datasets=[calexpType, mergeType], 

1544 collections=run, 

1545 where="tract IN (1, 5) AND patch IN (2, 7)", 

1546 skymap="DummyMap", 

1547 ).toSet() 

1548 self.assertEqual(len(rows), 2 * 2 * 2) # 2 tracts x 2 patches x 2 filters 

1549 self.assertCountEqual({dataId["tract"] for dataId in rows}, (1, 5)) 

1550 self.assertCountEqual({dataId["patch"] for dataId in rows}, (2, 7)) 

1551 self.assertCountEqual({dataId["band"] for dataId in rows}, ("i", "r")) 

1552 

1553 # limit to single filter 

1554 rows = registry.queryDataIds( 

1555 dimensions, datasets=[calexpType, mergeType], collections=run, where="band = 'i'" 

1556 ).toSet() 

1557 self.assertEqual(len(rows), 3 * 4 * 1) # 4 tracts x 4 patches x 2 filters 

1558 self.assertCountEqual({dataId["tract"] for dataId in rows}, (1, 3, 5)) 

1559 self.assertCountEqual({dataId["patch"] for dataId in rows}, (2, 4, 6, 7)) 

1560 self.assertCountEqual({dataId["band"] for dataId in rows}, ("i",)) 

1561 

1562 def do_query(): 

1563 return registry.queryDataIds( 

1564 dimensions, datasets=[calexpType, mergeType], collections=run, where="skymap = 'Mars'" 

1565 ).toSet() 

1566 

1567 self.assertEqual(len(do_query()), 0) 

1568 

1569 def testSpatialJoin(self): 

1570 """Test queries that involve spatial overlap joins.""" 

1571 butler = self.make_butler() 

1572 registry = butler.registry 

1573 self.load_data(butler, "base.yaml", "spatial.yaml") 

1574 

1575 # Dictionary of spatial DatabaseDimensionElements, keyed by the name of 

1576 # the TopologicalFamily they belong to. We'll relate all elements in 

1577 # each family to all of the elements in each other family. 

1578 families = defaultdict(set) 

1579 # Dictionary of {element.name: {dataId: region}}. 

1580 regions = {} 

1581 for element in registry.dimensions.database_elements: 

1582 if element.spatial is not None: 

1583 families[element.spatial.name].add(element) 

1584 regions[element.name] = { 

1585 record.dataId: record.region for record in registry.queryDimensionRecords(element) 

1586 } 

1587 

1588 # If this check fails, it's not necessarily a problem - it may just be 

1589 # a reasonable change to the default dimension definitions - but the 

1590 # test below depends on there being more than one family to do anything 

1591 # useful. 

1592 self.assertEqual(len(families), 2) 

1593 

1594 # Overlap DatabaseDimensionElements with each other. 

1595 for family1, family2 in itertools.combinations(families, 2): 

1596 for element1, element2 in itertools.product(families[family1], families[family2]): 

1597 dimensions = element1.minimal_group | element2.minimal_group 

1598 # Construct expected set of overlapping data IDs via a 

1599 # brute-force comparison of the regions we've already fetched. 

1600 expected = { 

1601 DataCoordinate.standardize( 

1602 {**dataId1.required, **dataId2.required}, dimensions=dimensions 

1603 ) 

1604 for (dataId1, region1), (dataId2, region2) in itertools.product( 

1605 regions[element1.name].items(), regions[element2.name].items() 

1606 ) 

1607 if not region1.isDisjointFrom(region2) 

1608 } 

1609 self.assertGreater(len(expected), 2, msg="Test that we aren't just comparing empty sets.") 

1610 queried = set(registry.queryDataIds(dimensions)) 

1611 self.assertEqual(expected, queried) 

1612 

1613 # Overlap each DatabaseDimensionElement with the commonSkyPix system. 

1614 commonSkyPix = registry.dimensions.commonSkyPix 

1615 for elementName, these_regions in regions.items(): 

1616 dimensions = registry.dimensions[elementName].minimal_group | commonSkyPix.minimal_group 

1617 expected = set() 

1618 for dataId, region in these_regions.items(): 

1619 for begin, end in commonSkyPix.pixelization.envelope(region): 

1620 expected.update( 

1621 DataCoordinate.standardize( 

1622 {commonSkyPix.name: index, **dataId.required}, dimensions=dimensions 

1623 ) 

1624 for index in range(begin, end) 

1625 ) 

1626 self.assertGreater(len(expected), 2, msg="Test that we aren't just comparing empty sets.") 

1627 queried = set(registry.queryDataIds(dimensions)) 

1628 self.assertEqual(expected, queried) 

1629 

1630 def testAbstractQuery(self): 

1631 """Test that we can run a query that just lists the known 

1632 bands. This is tricky because band is 

1633 backed by a query against physical_filter. 

1634 """ 

1635 butler = self.make_butler() 

1636 registry = butler.registry 

1637 registry.insertDimensionData("instrument", dict(name="DummyCam")) 

1638 registry.insertDimensionData( 

1639 "physical_filter", 

1640 dict(instrument="DummyCam", name="dummy_i", band="i"), 

1641 dict(instrument="DummyCam", name="dummy_i2", band="i"), 

1642 dict(instrument="DummyCam", name="dummy_r", band="r"), 

1643 ) 

1644 rows = registry.queryDataIds(["band"]).toSet() 

1645 self.assertCountEqual( 

1646 rows, 

1647 [ 

1648 DataCoordinate.standardize(band="i", universe=registry.dimensions), 

1649 DataCoordinate.standardize(band="r", universe=registry.dimensions), 

1650 ], 

1651 ) 

1652 

1653 def testAttributeManager(self): 

1654 """Test basic functionality of attribute manager.""" 

1655 # number of attributes with schema versions in a fresh database, 

1656 # 6 managers with 2 records per manager, plus config for dimensions 

1657 VERSION_COUNT = 6 * 2 + 1 

1658 

1659 butler = self.make_butler() 

1660 registry = butler._registry 

1661 attributes = registry._managers.attributes 

1662 

1663 # check what get() returns for non-existing key 

1664 self.assertIsNone(attributes.get("attr")) 

1665 self.assertEqual(attributes.get("attr", ""), "") 

1666 self.assertEqual(attributes.get("attr", "Value"), "Value") 

1667 self.assertEqual(len(list(attributes.items())), VERSION_COUNT) 

1668 

1669 # cannot store empty key or value 

1670 with self.assertRaises(ValueError): 

1671 attributes.set("", "value") 

1672 with self.assertRaises(ValueError): 

1673 attributes.set("attr", "") 

1674 

1675 # set value of non-existing key 

1676 attributes.set("attr", "value") 

1677 self.assertEqual(len(list(attributes.items())), VERSION_COUNT + 1) 

1678 self.assertEqual(attributes.get("attr"), "value") 

1679 

1680 # update value of existing key 

1681 with self.assertRaises(ButlerAttributeExistsError): 

1682 attributes.set("attr", "value2") 

1683 

1684 attributes.set("attr", "value2", force=True) 

1685 self.assertEqual(len(list(attributes.items())), VERSION_COUNT + 1) 

1686 self.assertEqual(attributes.get("attr"), "value2") 

1687 

1688 # delete existing key 

1689 self.assertTrue(attributes.delete("attr")) 

1690 self.assertEqual(len(list(attributes.items())), VERSION_COUNT) 

1691 

1692 # delete non-existing key 

1693 self.assertFalse(attributes.delete("non-attr")) 

1694 

1695 # store bunch of keys and get the list back 

1696 data = [ 

1697 ("version.core", "1.2.3"), 

1698 ("version.dimensions", "3.2.1"), 

1699 ("config.managers.opaque", "ByNameOpaqueTableStorageManager"), 

1700 ] 

1701 for key, value in data: 

1702 attributes.set(key, value) 

1703 items = dict(attributes.items()) 

1704 for key, value in data: 

1705 self.assertEqual(items[key], value) 

1706 

1707 def testQueryDatasetsDeduplication(self): 

1708 """Test that the findFirst option to queryDatasets selects datasets 

1709 from collections in the order given". 

1710 """ 

1711 butler = self.make_butler() 

1712 registry = butler.registry 

1713 self.load_data(butler, "base.yaml", "datasets.yaml") 

1714 self.assertCountEqual( 

1715 list(registry.queryDatasets("bias", collections=["imported_g", "imported_r"])), 

1716 [ 

1717 registry.findDataset("bias", instrument="Cam1", detector=1, collections="imported_g"), 

1718 registry.findDataset("bias", instrument="Cam1", detector=2, collections="imported_g"), 

1719 registry.findDataset("bias", instrument="Cam1", detector=3, collections="imported_g"), 

1720 registry.findDataset("bias", instrument="Cam1", detector=2, collections="imported_r"), 

1721 registry.findDataset("bias", instrument="Cam1", detector=3, collections="imported_r"), 

1722 registry.findDataset("bias", instrument="Cam1", detector=4, collections="imported_r"), 

1723 ], 

1724 ) 

1725 self.assertCountEqual( 

1726 list(registry.queryDatasets("bias", collections=["imported_g", "imported_r"], findFirst=True)), 

1727 [ 

1728 registry.findDataset("bias", instrument="Cam1", detector=1, collections="imported_g"), 

1729 registry.findDataset("bias", instrument="Cam1", detector=2, collections="imported_g"), 

1730 registry.findDataset("bias", instrument="Cam1", detector=3, collections="imported_g"), 

1731 registry.findDataset("bias", instrument="Cam1", detector=4, collections="imported_r"), 

1732 ], 

1733 ) 

1734 self.assertCountEqual( 

1735 list(registry.queryDatasets("bias", collections=["imported_r", "imported_g"], findFirst=True)), 

1736 [ 

1737 registry.findDataset("bias", instrument="Cam1", detector=1, collections="imported_g"), 

1738 registry.findDataset("bias", instrument="Cam1", detector=2, collections="imported_r"), 

1739 registry.findDataset("bias", instrument="Cam1", detector=3, collections="imported_r"), 

1740 registry.findDataset("bias", instrument="Cam1", detector=4, collections="imported_r"), 

1741 ], 

1742 ) 

1743 

1744 with self.assertRaises(TypeError): 

1745 # Collection wildcards not allowed in find-first searches because 

1746 # they do not guarantee the ordering of collections. 

1747 registry.queryDatasets("bias", collections="imported_*", findFirst=True) 

1748 

1749 def testQueryDatasetsExtraDimensions(self): 

1750 butler = self.make_butler() 

1751 registry = butler.registry 

1752 self.load_data(butler, "base.yaml", "datasets.yaml") 

1753 # Bias dataset type does not include physical filter. By adding 

1754 # "physical_filter" to dimensions, we are effectively searching here 

1755 # for bias datasets with an instrument that has a specific filter 

1756 # available, even though that filter has nothing to do with the bias 

1757 # datasets we are finding. 

1758 self.assertEqual( 

1759 0, 

1760 registry.queryDatasets( 

1761 "bias", 

1762 collections=..., 

1763 dimensions=["physical_filter"], 

1764 dataId={ 

1765 "instrument": "Cam1", 

1766 "band": "not_a_real_band", 

1767 }, 

1768 ).count(), 

1769 ) 

1770 self.assertEqual( 

1771 6, 

1772 len( 

1773 set( 

1774 registry.queryDatasets( 

1775 "bias", 

1776 collections=..., 

1777 dimensions=["physical_filter"], 

1778 dataId={ 

1779 "instrument": "Cam1", 

1780 "band": "r", 

1781 }, 

1782 ) 

1783 ) 

1784 ), 

1785 ) 

1786 

1787 def testQueryResults(self): 

1788 """Test querying for data IDs and then manipulating the QueryResults 

1789 object returned to perform other queries. 

1790 """ 

1791 butler = self.make_butler() 

1792 registry = butler.registry 

1793 self.load_data(butler, "base.yaml", "datasets.yaml") 

1794 bias = registry.getDatasetType("bias") 

1795 flat = registry.getDatasetType("flat") 

1796 # Obtain expected results from methods other than those we're testing 

1797 # here. That includes: 

1798 # - the dimensions of the data IDs we want to query: 

1799 expected_dimensions = registry.dimensions.conform(["detector", "physical_filter"]) 

1800 # - the dimensions of some other data IDs we'll extract from that: 

1801 expected_subset_dimensions = registry.dimensions.conform(["detector"]) 

1802 # - the data IDs we expect to obtain from the first queries: 

1803 expectedDataIds = DataCoordinateSet( 

1804 { 

1805 DataCoordinate.standardize( 

1806 instrument="Cam1", detector=d, physical_filter=p, universe=registry.dimensions 

1807 ) 

1808 for d, p in itertools.product({1, 2, 3}, {"Cam1-G", "Cam1-R1", "Cam1-R2"}) 

1809 }, 

1810 dimensions=expected_dimensions, 

1811 hasFull=False, 

1812 hasRecords=False, 

1813 ) 

1814 # - the flat datasets we expect to find from those data IDs, in just 

1815 # one collection (so deduplication is irrelevant): 

1816 expectedFlats = [ 

1817 registry.findDataset( 

1818 flat, instrument="Cam1", detector=1, physical_filter="Cam1-R1", collections="imported_r" 

1819 ), 

1820 registry.findDataset( 

1821 flat, instrument="Cam1", detector=2, physical_filter="Cam1-R1", collections="imported_r" 

1822 ), 

1823 registry.findDataset( 

1824 flat, instrument="Cam1", detector=3, physical_filter="Cam1-R2", collections="imported_r" 

1825 ), 

1826 ] 

1827 # - the data IDs we expect to extract from that: 

1828 expectedSubsetDataIds = expectedDataIds.subset(expected_subset_dimensions) 

1829 # - the bias datasets we expect to find from those data IDs, after we 

1830 # subset-out the physical_filter dimension, both with duplicates: 

1831 expectedAllBiases = [ 

1832 registry.findDataset(bias, instrument="Cam1", detector=1, collections="imported_g"), 

1833 registry.findDataset(bias, instrument="Cam1", detector=2, collections="imported_g"), 

1834 registry.findDataset(bias, instrument="Cam1", detector=3, collections="imported_g"), 

1835 registry.findDataset(bias, instrument="Cam1", detector=2, collections="imported_r"), 

1836 registry.findDataset(bias, instrument="Cam1", detector=3, collections="imported_r"), 

1837 ] 

1838 # - ...and without duplicates: 

1839 expectedDeduplicatedBiases = [ 

1840 registry.findDataset(bias, instrument="Cam1", detector=1, collections="imported_g"), 

1841 registry.findDataset(bias, instrument="Cam1", detector=2, collections="imported_r"), 

1842 registry.findDataset(bias, instrument="Cam1", detector=3, collections="imported_r"), 

1843 ] 

1844 # Test against those expected results, using a "lazy" query for the 

1845 # data IDs (which re-executes that query each time we use it to do 

1846 # something new). 

1847 dataIds = registry.queryDataIds( 

1848 ["detector", "physical_filter"], 

1849 where="detector.purpose = 'SCIENCE'", # this rejects detector=4 

1850 instrument="Cam1", 

1851 ) 

1852 self.assertEqual(dataIds.dimensions, expected_dimensions) 

1853 self.assertEqual(dataIds.toSet(), expectedDataIds) 

1854 self.assertCountEqual( 

1855 list( 

1856 dataIds.findDatasets( 

1857 flat, 

1858 collections=["imported_r"], 

1859 ) 

1860 ), 

1861 expectedFlats, 

1862 ) 

1863 subsetDataIds = dataIds.subset(expected_subset_dimensions, unique=True) 

1864 self.assertEqual(subsetDataIds.dimensions, expected_subset_dimensions) 

1865 self.assertEqual(subsetDataIds.toSet(), expectedSubsetDataIds) 

1866 self.assertCountEqual( 

1867 list(subsetDataIds.findDatasets(bias, collections=["imported_r", "imported_g"], findFirst=False)), 

1868 expectedAllBiases, 

1869 ) 

1870 self.assertCountEqual( 

1871 list(subsetDataIds.findDatasets(bias, collections=["imported_r", "imported_g"], findFirst=True)), 

1872 expectedDeduplicatedBiases, 

1873 ) 

1874 

1875 # Searching for a dataset with dimensions we had projected away 

1876 # restores those dimensions. 

1877 self.assertCountEqual( 

1878 list(subsetDataIds.findDatasets("flat", collections=["imported_r"], findFirst=True)), 

1879 expectedFlats, 

1880 ) 

1881 

1882 # Use a named dataset type that does not exist and a dataset type 

1883 # object that does not exist. 

1884 unknown_type = DatasetType("not_known", dimensions=bias.dimensions, storageClass="Exposure") 

1885 

1886 # Test both string name and dataset type object. 

1887 test_type: str | DatasetType 

1888 for test_type, test_type_name in ( 

1889 (unknown_type, unknown_type.name), 

1890 (unknown_type.name, unknown_type.name), 

1891 ): 

1892 with self.assertRaisesRegex(DatasetTypeError, expected_regex=test_type_name): 

1893 list( 

1894 subsetDataIds.findDatasets( 

1895 test_type, collections=["imported_r", "imported_g"], findFirst=True 

1896 ) 

1897 ) 

1898 

1899 # Materialize the data ID subset query, but not the dataset queries. 

1900 with subsetDataIds.materialize() as subsetDataIds: 

1901 self.assertEqual(subsetDataIds.dimensions, expected_subset_dimensions) 

1902 self.assertEqual(subsetDataIds.toSet(), expectedSubsetDataIds) 

1903 self.assertCountEqual( 

1904 list( 

1905 subsetDataIds.findDatasets( 

1906 bias, collections=["imported_r", "imported_g"], findFirst=False 

1907 ) 

1908 ), 

1909 expectedAllBiases, 

1910 ) 

1911 self.assertCountEqual( 

1912 list( 

1913 subsetDataIds.findDatasets(bias, collections=["imported_r", "imported_g"], findFirst=True) 

1914 ), 

1915 expectedDeduplicatedBiases, 

1916 ) 

1917 # Materialize the original query, but none of the follow-up queries. 

1918 with dataIds.materialize() as dataIds: 

1919 self.assertEqual(dataIds.dimensions, expected_dimensions) 

1920 self.assertEqual(dataIds.toSet(), expectedDataIds) 

1921 self.assertCountEqual( 

1922 list( 

1923 dataIds.findDatasets( 

1924 flat, 

1925 collections=["imported_r"], 

1926 ) 

1927 ), 

1928 expectedFlats, 

1929 ) 

1930 subsetDataIds = dataIds.subset(expected_subset_dimensions, unique=True) 

1931 self.assertEqual(subsetDataIds.dimensions, expected_subset_dimensions) 

1932 self.assertEqual(subsetDataIds.toSet(), expectedSubsetDataIds) 

1933 self.assertCountEqual( 

1934 list( 

1935 subsetDataIds.findDatasets( 

1936 bias, collections=["imported_r", "imported_g"], findFirst=False 

1937 ) 

1938 ), 

1939 expectedAllBiases, 

1940 ) 

1941 self.assertCountEqual( 

1942 list( 

1943 subsetDataIds.findDatasets(bias, collections=["imported_r", "imported_g"], findFirst=True) 

1944 ), 

1945 expectedDeduplicatedBiases, 

1946 ) 

1947 # Materialize the subset data ID query, but not the dataset 

1948 # queries. 

1949 with subsetDataIds.materialize() as subsetDataIds: 

1950 self.assertEqual(subsetDataIds.dimensions, expected_subset_dimensions) 

1951 self.assertEqual(subsetDataIds.toSet(), expectedSubsetDataIds) 

1952 self.assertCountEqual( 

1953 list( 

1954 subsetDataIds.findDatasets( 

1955 bias, collections=["imported_r", "imported_g"], findFirst=False 

1956 ) 

1957 ), 

1958 expectedAllBiases, 

1959 ) 

1960 self.assertCountEqual( 

1961 list( 

1962 subsetDataIds.findDatasets( 

1963 bias, collections=["imported_r", "imported_g"], findFirst=True 

1964 ) 

1965 ), 

1966 expectedDeduplicatedBiases, 

1967 ) 

1968 

1969 def testStorageClassPropagation(self): 

1970 """Test that queries for datasets respect the storage class passed in 

1971 as part of a full dataset type. 

1972 """ 

1973 butler = self.make_butler() 

1974 registry = butler.registry 

1975 self.load_data(butler, "base.yaml") 

1976 dataset_type_in_registry = DatasetType( 

1977 "tbl", dimensions=["instrument"], storageClass="Packages", universe=registry.dimensions 

1978 ) 

1979 registry.registerDatasetType(dataset_type_in_registry) 

1980 run = "run1" 

1981 registry.registerRun(run) 

1982 (inserted_ref,) = registry.insertDatasets( 

1983 dataset_type_in_registry, [registry.expandDataId(instrument="Cam1")], run=run 

1984 ) 

1985 self.assertEqual(inserted_ref.datasetType, dataset_type_in_registry) 

1986 query_dataset_type = DatasetType( 

1987 "tbl", dimensions=["instrument"], storageClass="StructuredDataDict", universe=registry.dimensions 

1988 ) 

1989 self.assertNotEqual(dataset_type_in_registry, query_dataset_type) 

1990 query_datasets_result = registry.queryDatasets(query_dataset_type, collections=[run]) 

1991 self.assertEqual(query_datasets_result.parentDatasetType, query_dataset_type) # type: ignore 

1992 (query_datasets_ref,) = query_datasets_result 

1993 self.assertEqual(query_datasets_ref.datasetType, query_dataset_type) 

1994 query_data_ids_find_datasets_result = registry.queryDataIds(["instrument"]).findDatasets( 

1995 query_dataset_type, collections=[run] 

1996 ) 

1997 self.assertEqual(query_data_ids_find_datasets_result.parentDatasetType, query_dataset_type) 

1998 (query_data_ids_find_datasets_ref,) = query_data_ids_find_datasets_result 

1999 self.assertEqual(query_data_ids_find_datasets_ref.datasetType, query_dataset_type) 

2000 query_dataset_types_result = registry.queryDatasetTypes(query_dataset_type) 

2001 self.assertEqual(list(query_dataset_types_result), [query_dataset_type]) 

2002 find_dataset_ref = registry.findDataset(query_dataset_type, instrument="Cam1", collections=[run]) 

2003 self.assertEqual(find_dataset_ref.datasetType, query_dataset_type) 

2004 

2005 def testEmptyDimensionsQueries(self): 

2006 """Test Query and QueryResults objects in the case where there are no 

2007 dimensions. 

2008 """ 

2009 # Set up test data: one dataset type, two runs, one dataset in each. 

2010 butler = self.make_butler() 

2011 registry = butler.registry 

2012 self.load_data(butler, "base.yaml") 

2013 schema = DatasetType("schema", dimensions=registry.dimensions.empty, storageClass="Catalog") 

2014 registry.registerDatasetType(schema) 

2015 dataId = DataCoordinate.make_empty(registry.dimensions) 

2016 run1 = "run1" 

2017 run2 = "run2" 

2018 registry.registerRun(run1) 

2019 registry.registerRun(run2) 

2020 (dataset1,) = registry.insertDatasets(schema, dataIds=[dataId], run=run1) 

2021 (dataset2,) = registry.insertDatasets(schema, dataIds=[dataId], run=run2) 

2022 # Query directly for both of the datasets, and each one, one at a time. 

2023 self.checkQueryResults( 

2024 registry.queryDatasets(schema, collections=[run1, run2], findFirst=False), [dataset1, dataset2] 

2025 ) 

2026 self.checkQueryResults( 

2027 registry.queryDatasets(schema, collections=[run1, run2], findFirst=True), 

2028 [dataset1], 

2029 ) 

2030 self.checkQueryResults( 

2031 registry.queryDatasets(schema, collections=[run2, run1], findFirst=True), 

2032 [dataset2], 

2033 ) 

2034 # Query for data IDs with no dimensions. 

2035 dataIds = registry.queryDataIds([]) 

2036 self.checkQueryResults(dataIds, [dataId]) 

2037 # Use queried data IDs to find the datasets. 

2038 self.checkQueryResults( 

2039 dataIds.findDatasets(schema, collections=[run1, run2], findFirst=False), 

2040 [dataset1, dataset2], 

2041 ) 

2042 self.checkQueryResults( 

2043 dataIds.findDatasets(schema, collections=[run1, run2], findFirst=True), 

2044 [dataset1], 

2045 ) 

2046 self.checkQueryResults( 

2047 dataIds.findDatasets(schema, collections=[run2, run1], findFirst=True), 

2048 [dataset2], 

2049 ) 

2050 # Now materialize the data ID query results and repeat those tests. 

2051 with dataIds.materialize() as dataIds: 

2052 self.checkQueryResults(dataIds, [dataId]) 

2053 self.checkQueryResults( 

2054 dataIds.findDatasets(schema, collections=[run1, run2], findFirst=True), 

2055 [dataset1], 

2056 ) 

2057 self.checkQueryResults( 

2058 dataIds.findDatasets(schema, collections=[run2, run1], findFirst=True), 

2059 [dataset2], 

2060 ) 

2061 # Query for non-empty data IDs, then subset that to get the empty one. 

2062 # Repeat the above tests starting from that. 

2063 dataIds = registry.queryDataIds(["instrument"]).subset(registry.dimensions.empty, unique=True) 

2064 self.checkQueryResults(dataIds, [dataId]) 

2065 self.checkQueryResults( 

2066 dataIds.findDatasets(schema, collections=[run1, run2], findFirst=False), 

2067 [dataset1, dataset2], 

2068 ) 

2069 self.checkQueryResults( 

2070 dataIds.findDatasets(schema, collections=[run1, run2], findFirst=True), 

2071 [dataset1], 

2072 ) 

2073 self.checkQueryResults( 

2074 dataIds.findDatasets(schema, collections=[run2, run1], findFirst=True), 

2075 [dataset2], 

2076 ) 

2077 with dataIds.materialize() as dataIds: 

2078 self.checkQueryResults(dataIds, [dataId]) 

2079 self.checkQueryResults( 

2080 dataIds.findDatasets(schema, collections=[run1, run2], findFirst=False), 

2081 [dataset1, dataset2], 

2082 ) 

2083 self.checkQueryResults( 

2084 dataIds.findDatasets(schema, collections=[run1, run2], findFirst=True), 

2085 [dataset1], 

2086 ) 

2087 self.checkQueryResults( 

2088 dataIds.findDatasets(schema, collections=[run2, run1], findFirst=True), 

2089 [dataset2], 

2090 ) 

2091 # Query for non-empty data IDs, then materialize, then subset to get 

2092 # the empty one. Repeat again. 

2093 with registry.queryDataIds(["instrument"]).materialize() as nonEmptyDataIds: 

2094 dataIds = nonEmptyDataIds.subset(registry.dimensions.empty, unique=True) 

2095 self.checkQueryResults(dataIds, [dataId]) 

2096 self.checkQueryResults( 

2097 dataIds.findDatasets(schema, collections=[run1, run2], findFirst=False), 

2098 [dataset1, dataset2], 

2099 ) 

2100 self.checkQueryResults( 

2101 dataIds.findDatasets(schema, collections=[run1, run2], findFirst=True), 

2102 [dataset1], 

2103 ) 

2104 self.checkQueryResults( 

2105 dataIds.findDatasets(schema, collections=[run2, run1], findFirst=True), 

2106 [dataset2], 

2107 ) 

2108 with dataIds.materialize() as dataIds: 

2109 self.checkQueryResults(dataIds, [dataId]) 

2110 self.checkQueryResults( 

2111 dataIds.findDatasets(schema, collections=[run1, run2], findFirst=False), 

2112 [dataset1, dataset2], 

2113 ) 

2114 self.checkQueryResults( 

2115 dataIds.findDatasets(schema, collections=[run1, run2], findFirst=True), 

2116 [dataset1], 

2117 ) 

2118 self.checkQueryResults( 

2119 dataIds.findDatasets(schema, collections=[run2, run1], findFirst=True), 

2120 [dataset2], 

2121 ) 

2122 # Query for non-empty data IDs with a constraint on an empty-data-ID 

2123 # dataset that exists. 

2124 dataIds = registry.queryDataIds(["instrument"], datasets="schema", collections=...) 

2125 self.checkQueryResults( 

2126 dataIds.subset(unique=True), 

2127 [DataCoordinate.standardize(instrument="Cam1", universe=registry.dimensions)], 

2128 ) 

2129 # Again query for non-empty data IDs with a constraint on empty-data-ID 

2130 # datasets, but when the datasets don't exist. We delete the existing 

2131 # dataset and query just that collection rather than creating a new 

2132 # empty collection because this is a bit less likely for our build-time 

2133 # logic to shortcut-out (via the collection summaries), and such a 

2134 # shortcut would make this test a bit more trivial than we'd like. 

2135 registry.removeDatasets([dataset2]) 

2136 dataIds = registry.queryDataIds(["instrument"], datasets="schema", collections=run2) 

2137 self.checkQueryResults(dataIds, []) 

2138 

2139 def testDimensionDataModifications(self): 

2140 """Test that modifying dimension records via: 

2141 syncDimensionData(..., update=True) and 

2142 insertDimensionData(..., replace=True) works as expected, even in the 

2143 presence of datasets using those dimensions and spatial overlap 

2144 relationships. 

2145 """ 

2146 

2147 def _unpack_range_set(ranges: lsst.sphgeom.RangeSet) -> Iterator[int]: 

2148 """Unpack a sphgeom.RangeSet into the integers it contains.""" 

2149 for begin, end in ranges: 

2150 yield from range(begin, end) 

2151 

2152 def _range_set_hull( 

2153 ranges: lsst.sphgeom.RangeSet, 

2154 pixelization: lsst.sphgeom.HtmPixelization, 

2155 ) -> lsst.sphgeom.ConvexPolygon: 

2156 """Create a ConvexPolygon hull of the region defined by a set of 

2157 HTM pixelization index ranges. 

2158 """ 

2159 points = [] 

2160 for index in _unpack_range_set(ranges): 

2161 points.extend(pixelization.triangle(index).getVertices()) 

2162 return lsst.sphgeom.ConvexPolygon(points) 

2163 

2164 # Use HTM to set up an initial parent region (one arbitrary trixel) 

2165 # and four child regions (the trixels within the parent at the next 

2166 # level. We'll use the parent as a tract/visit region and the children 

2167 # as its patch/visit_detector regions. 

2168 butler = self.make_butler() 

2169 registry = butler.registry 

2170 htm6 = registry.dimensions.skypix["htm"][6].pixelization 

2171 commonSkyPix = registry.dimensions.commonSkyPix.pixelization 

2172 index = 12288 

2173 child_ranges_small = lsst.sphgeom.RangeSet(index).scaled(4) 

2174 assert htm6.universe().contains(child_ranges_small) 

2175 child_regions_small = [htm6.triangle(i) for i in _unpack_range_set(child_ranges_small)] 

2176 parent_region_small = lsst.sphgeom.ConvexPolygon( 

2177 list(itertools.chain.from_iterable(c.getVertices() for c in child_regions_small)) 

2178 ) 

2179 assert all(parent_region_small.contains(c) for c in child_regions_small) 

2180 # Make a larger version of each child region, defined to be the set of 

2181 # htm6 trixels that overlap the original's bounding circle. Make a new 

2182 # parent that's the convex hull of the new children. 

2183 child_regions_large = [ 

2184 _range_set_hull(htm6.envelope(c.getBoundingCircle()), htm6) for c in child_regions_small 

2185 ] 

2186 assert all( 

2187 large.contains(small) 

2188 for large, small in zip(child_regions_large, child_regions_small, strict=True) 

2189 ) 

2190 parent_region_large = lsst.sphgeom.ConvexPolygon( 

2191 list(itertools.chain.from_iterable(c.getVertices() for c in child_regions_large)) 

2192 ) 

2193 assert all(parent_region_large.contains(c) for c in child_regions_large) 

2194 assert parent_region_large.contains(parent_region_small) 

2195 assert not parent_region_small.contains(parent_region_large) 

2196 assert not all(parent_region_small.contains(c) for c in child_regions_large) 

2197 # Find some commonSkyPix indices that overlap the large regions but not 

2198 # overlap the small regions. We use commonSkyPix here to make sure the 

2199 # real tests later involve what's in the database, not just post-query 

2200 # filtering of regions. 

2201 child_difference_indices = [] 

2202 for large, small in zip(child_regions_large, child_regions_small, strict=True): 

2203 difference = list(_unpack_range_set(commonSkyPix.envelope(large) - commonSkyPix.envelope(small))) 

2204 assert difference, "if this is empty, we can't test anything useful with these regions" 

2205 assert all( 

2206 not commonSkyPix.triangle(d).isDisjointFrom(large) 

2207 and commonSkyPix.triangle(d).isDisjointFrom(small) 

2208 for d in difference 

2209 ) 

2210 child_difference_indices.append(difference) 

2211 parent_difference_indices = list( 

2212 _unpack_range_set( 

2213 commonSkyPix.envelope(parent_region_large) - commonSkyPix.envelope(parent_region_small) 

2214 ) 

2215 ) 

2216 assert parent_difference_indices, "if this is empty, we can't test anything useful with these regions" 

2217 assert all( 

2218 ( 

2219 not commonSkyPix.triangle(d).isDisjointFrom(parent_region_large) 

2220 and commonSkyPix.triangle(d).isDisjointFrom(parent_region_small) 

2221 ) 

2222 for d in parent_difference_indices 

2223 ) 

2224 # Now that we've finally got those regions, we'll insert the large ones 

2225 # as tract/patch dimension records. 

2226 skymap_name = "testing_v1" 

2227 registry.insertDimensionData( 

2228 "skymap", 

2229 { 

2230 "name": skymap_name, 

2231 "hash": bytes([42]), 

2232 "tract_max": 1, 

2233 "patch_nx_max": 2, 

2234 "patch_ny_max": 2, 

2235 }, 

2236 ) 

2237 registry.insertDimensionData("tract", {"skymap": skymap_name, "id": 0, "region": parent_region_large}) 

2238 registry.insertDimensionData( 

2239 "patch", 

2240 *[ 

2241 {"skymap": skymap_name, "tract": 0, "id": n, "cell_x": n % 2, "cell_y": n // 2, "region": c} 

2242 for n, c in enumerate(child_regions_large) 

2243 ], 

2244 ) 

2245 # Add at dataset that uses these dimensions to make sure that modifying 

2246 # them doesn't disrupt foreign keys (need to make sure DB doesn't 

2247 # implement insert with replace=True as delete-then-insert). 

2248 dataset_type = DatasetType( 

2249 "coadd", 

2250 dimensions=["tract", "patch"], 

2251 universe=registry.dimensions, 

2252 storageClass="Exposure", 

2253 ) 

2254 registry.registerDatasetType(dataset_type) 

2255 registry.registerCollection("the_run", CollectionType.RUN) 

2256 registry.insertDatasets( 

2257 dataset_type, 

2258 [{"skymap": skymap_name, "tract": 0, "patch": 2}], 

2259 run="the_run", 

2260 ) 

2261 # Query for tracts and patches that overlap some "difference" htm9 

2262 # pixels; there should be overlaps, because the database has 

2263 # the "large" suite of regions. 

2264 self.assertEqual( 

2265 {0}, 

2266 { 

2267 data_id["tract"] 

2268 for data_id in registry.queryDataIds( 

2269 ["tract"], 

2270 skymap=skymap_name, 

2271 dataId={registry.dimensions.commonSkyPix.name: parent_difference_indices[0]}, 

2272 ) 

2273 }, 

2274 ) 

2275 for patch_id, patch_difference_indices in enumerate(child_difference_indices): 

2276 self.assertIn( 

2277 patch_id, 

2278 { 

2279 data_id["patch"] 

2280 for data_id in registry.queryDataIds( 

2281 ["patch"], 

2282 skymap=skymap_name, 

2283 dataId={registry.dimensions.commonSkyPix.name: patch_difference_indices[0]}, 

2284 ) 

2285 }, 

2286 ) 

2287 # Use sync to update the tract region and insert to update the regions 

2288 # of the patches, to the "small" suite. 

2289 updated = registry.syncDimensionData( 

2290 "tract", 

2291 {"skymap": skymap_name, "id": 0, "region": parent_region_small}, 

2292 update=True, 

2293 ) 

2294 self.assertEqual(updated, {"region": parent_region_large}) 

2295 registry.insertDimensionData( 

2296 "patch", 

2297 *[ 

2298 {"skymap": skymap_name, "tract": 0, "id": n, "cell_x": n % 2, "cell_y": n // 2, "region": c} 

2299 for n, c in enumerate(child_regions_small) 

2300 ], 

2301 replace=True, 

2302 ) 

2303 # Query again; there now should be no such overlaps, because the 

2304 # database has the "small" suite of regions. 

2305 self.assertFalse( 

2306 set( 

2307 registry.queryDataIds( 

2308 ["tract"], 

2309 skymap=skymap_name, 

2310 dataId={registry.dimensions.commonSkyPix.name: parent_difference_indices[0]}, 

2311 ) 

2312 ) 

2313 ) 

2314 for patch_id, patch_difference_indices in enumerate(child_difference_indices): 

2315 self.assertNotIn( 

2316 patch_id, 

2317 { 

2318 data_id["patch"] 

2319 for data_id in registry.queryDataIds( 

2320 ["patch"], 

2321 skymap=skymap_name, 

2322 dataId={registry.dimensions.commonSkyPix.name: patch_difference_indices[0]}, 

2323 ) 

2324 }, 

2325 ) 

2326 # Update back to the large regions and query one more time. 

2327 updated = registry.syncDimensionData( 

2328 "tract", 

2329 {"skymap": skymap_name, "id": 0, "region": parent_region_large}, 

2330 update=True, 

2331 ) 

2332 self.assertEqual(updated, {"region": parent_region_small}) 

2333 registry.insertDimensionData( 

2334 "patch", 

2335 *[ 

2336 {"skymap": skymap_name, "tract": 0, "id": n, "cell_x": n % 2, "cell_y": n // 2, "region": c} 

2337 for n, c in enumerate(child_regions_large) 

2338 ], 

2339 replace=True, 

2340 ) 

2341 self.assertEqual( 

2342 {0}, 

2343 { 

2344 data_id["tract"] 

2345 for data_id in registry.queryDataIds( 

2346 ["tract"], 

2347 skymap=skymap_name, 

2348 dataId={registry.dimensions.commonSkyPix.name: parent_difference_indices[0]}, 

2349 ) 

2350 }, 

2351 ) 

2352 for patch_id, patch_difference_indices in enumerate(child_difference_indices): 

2353 self.assertIn( 

2354 patch_id, 

2355 { 

2356 data_id["patch"] 

2357 for data_id in registry.queryDataIds( 

2358 ["patch"], 

2359 skymap=skymap_name, 

2360 dataId={registry.dimensions.commonSkyPix.name: patch_difference_indices[0]}, 

2361 ) 

2362 }, 

2363 ) 

2364 

2365 def testCalibrationCollections(self): 

2366 """Test operations on `~CollectionType.CALIBRATION` collections, 

2367 including `SqlRegistry.certify`, `SqlRegistry.decertify`, 

2368 `SqlRegistry.findDataset`, and 

2369 `DataCoordinateQueryResults.findRelatedDatasets`. 

2370 """ 

2371 # Setup - make a Registry, fill it with some datasets in 

2372 # non-calibration collections. 

2373 butler = self.make_butler() 

2374 registry = butler.registry 

2375 self.load_data(butler, "base.yaml", "datasets.yaml") 

2376 # Set up some timestamps. 

2377 t1 = astropy.time.Time("2020-01-01T01:00:00", format="isot", scale="tai") 

2378 t2 = astropy.time.Time("2020-01-01T02:00:00", format="isot", scale="tai") 

2379 t3 = astropy.time.Time("2020-01-01T03:00:00", format="isot", scale="tai") 

2380 t4 = astropy.time.Time("2020-01-01T04:00:00", format="isot", scale="tai") 

2381 t5 = astropy.time.Time("2020-01-01T05:00:00", format="isot", scale="tai") 

2382 allTimespans = [ 

2383 Timespan(a, b) for a, b in itertools.combinations([None, t1, t2, t3, t4, t5, None], r=2) 

2384 ] 

2385 # Insert some exposure records with timespans between each sequential 

2386 # pair of those. 

2387 registry.insertDimensionData( 

2388 "day_obs", {"instrument": "Cam1", "id": 20200101, "timespan": Timespan(t1, t5)} 

2389 ) 

2390 registry.insertDimensionData( 

2391 "group", 

2392 {"instrument": "Cam1", "name": "group0"}, 

2393 {"instrument": "Cam1", "name": "group1"}, 

2394 {"instrument": "Cam1", "name": "group2"}, 

2395 {"instrument": "Cam1", "name": "group3"}, 

2396 ) 

2397 registry.insertDimensionData( 

2398 "exposure", 

2399 { 

2400 "instrument": "Cam1", 

2401 "id": 0, 

2402 "group": "group0", 

2403 "obs_id": "zero", 

2404 "physical_filter": "Cam1-G", 

2405 "day_obs": 20200101, 

2406 "timespan": Timespan(t1, t2), 

2407 }, 

2408 { 

2409 "instrument": "Cam1", 

2410 "id": 1, 

2411 "group": "group1", 

2412 "obs_id": "one", 

2413 "physical_filter": "Cam1-G", 

2414 "day_obs": 20200101, 

2415 "timespan": Timespan(t2, t3), 

2416 }, 

2417 { 

2418 "instrument": "Cam1", 

2419 "id": 2, 

2420 "group": "group2", 

2421 "obs_id": "two", 

2422 "physical_filter": "Cam1-G", 

2423 "day_obs": 20200101, 

2424 "timespan": Timespan(t3, t4), 

2425 }, 

2426 { 

2427 "instrument": "Cam1", 

2428 "id": 3, 

2429 "group": "group3", 

2430 "obs_id": "three", 

2431 "physical_filter": "Cam1-G", 

2432 "day_obs": 20200101, 

2433 "timespan": Timespan(t4, t5), 

2434 }, 

2435 ) 

2436 # Get references to some datasets. 

2437 bias2a = registry.findDataset("bias", instrument="Cam1", detector=2, collections="imported_g") 

2438 bias3a = registry.findDataset("bias", instrument="Cam1", detector=3, collections="imported_g") 

2439 bias2b = registry.findDataset("bias", instrument="Cam1", detector=2, collections="imported_r") 

2440 bias3b = registry.findDataset("bias", instrument="Cam1", detector=3, collections="imported_r") 

2441 # Register the main calibration collection we'll be working with. 

2442 collection = "Cam1/calibs/default" 

2443 registry.registerCollection(collection, type=CollectionType.CALIBRATION) 

2444 # Cannot associate into a calibration collection (no timespan). 

2445 with self.assertRaises(CollectionTypeError): 

2446 registry.associate(collection, [bias2a]) 

2447 # Certify 2a dataset with [t2, t4) validity. 

2448 registry.certify(collection, [bias2a], Timespan(begin=t2, end=t4)) 

2449 # Test that we can query for this dataset via the new collection, both 

2450 # on its own and with a RUN collection. 

2451 self.assertEqual( 

2452 set(registry.queryDatasets("bias", findFirst=False, collections=collection)), 

2453 {bias2a}, 

2454 ) 

2455 self.assertEqual( 

2456 set(registry.queryDatasets("bias", findFirst=False, collections=[collection, "imported_r"])), 

2457 { 

2458 bias2a, 

2459 bias2b, 

2460 bias3b, 

2461 registry.findDataset("bias", instrument="Cam1", detector=4, collections="imported_r"), 

2462 }, 

2463 ) 

2464 self.assertEqual( 

2465 set(registry.queryDataIds("detector", datasets="bias", collections=collection)), 

2466 {registry.expandDataId(instrument="Cam1", detector=2)}, 

2467 ) 

2468 self.assertEqual( 

2469 set(registry.queryDataIds("detector", datasets="bias", collections=[collection, "imported_r"])), 

2470 { 

2471 registry.expandDataId(instrument="Cam1", detector=2), 

2472 registry.expandDataId(instrument="Cam1", detector=3), 

2473 registry.expandDataId(instrument="Cam1", detector=4), 

2474 }, 

2475 ) 

2476 self.assertEqual( 

2477 set( 

2478 registry.queryDataIds(["exposure", "detector"]).findRelatedDatasets( 

2479 "bias", findFirst=True, collections=[collection] 

2480 ) 

2481 ), 

2482 { 

2483 (registry.expandDataId(instrument="Cam1", detector=2, exposure=1), bias2a), 

2484 (registry.expandDataId(instrument="Cam1", detector=2, exposure=2), bias2a), 

2485 }, 

2486 ) 

2487 

2488 # We should not be able to certify 2b with anything overlapping that 

2489 # window. 

2490 with self.assertRaises(ConflictingDefinitionError): 

2491 registry.certify(collection, [bias2b], Timespan(begin=None, end=t3)) 

2492 with self.assertRaises(ConflictingDefinitionError): 

2493 registry.certify(collection, [bias2b], Timespan(begin=None, end=t5)) 

2494 with self.assertRaises(ConflictingDefinitionError): 

2495 registry.certify(collection, [bias2b], Timespan(begin=t1, end=t3)) 

2496 with self.assertRaises(ConflictingDefinitionError): 

2497 registry.certify(collection, [bias2b], Timespan(begin=t1, end=t5)) 

2498 with self.assertRaises(ConflictingDefinitionError): 

2499 registry.certify(collection, [bias2b], Timespan(begin=t1, end=None)) 

2500 with self.assertRaises(ConflictingDefinitionError): 

2501 registry.certify(collection, [bias2b], Timespan(begin=t2, end=t3)) 

2502 with self.assertRaises(ConflictingDefinitionError): 

2503 registry.certify(collection, [bias2b], Timespan(begin=t2, end=t5)) 

2504 with self.assertRaises(ConflictingDefinitionError): 

2505 registry.certify(collection, [bias2b], Timespan(begin=t2, end=None)) 

2506 # We should be able to certify 3a with a range overlapping that window, 

2507 # because it's for a different detector. 

2508 # We'll certify 3a over [t1, t3). 

2509 registry.certify(collection, [bias3a], Timespan(begin=t1, end=t3)) 

2510 # Now we'll certify 2b and 3b together over [t4, ∞). 

2511 registry.certify(collection, [bias2b, bias3b], Timespan(begin=t4, end=None)) 

2512 

2513 # Fetch all associations and check that they are what we expect. 

2514 self.assertCountEqual( 

2515 list( 

2516 registry.queryDatasetAssociations( 

2517 "bias", 

2518 collections=[collection, "imported_g", "imported_r"], 

2519 ) 

2520 ), 

2521 [ 

2522 DatasetAssociation( 

2523 ref=registry.findDataset("bias", instrument="Cam1", detector=1, collections="imported_g"), 

2524 collection="imported_g", 

2525 timespan=None, 

2526 ), 

2527 DatasetAssociation( 

2528 ref=registry.findDataset("bias", instrument="Cam1", detector=4, collections="imported_r"), 

2529 collection="imported_r", 

2530 timespan=None, 

2531 ), 

2532 DatasetAssociation(ref=bias2a, collection="imported_g", timespan=None), 

2533 DatasetAssociation(ref=bias3a, collection="imported_g", timespan=None), 

2534 DatasetAssociation(ref=bias2b, collection="imported_r", timespan=None), 

2535 DatasetAssociation(ref=bias3b, collection="imported_r", timespan=None), 

2536 DatasetAssociation(ref=bias2a, collection=collection, timespan=Timespan(begin=t2, end=t4)), 

2537 DatasetAssociation(ref=bias3a, collection=collection, timespan=Timespan(begin=t1, end=t3)), 

2538 DatasetAssociation(ref=bias2b, collection=collection, timespan=Timespan(begin=t4, end=None)), 

2539 DatasetAssociation(ref=bias3b, collection=collection, timespan=Timespan(begin=t4, end=None)), 

2540 ], 

2541 ) 

2542 

2543 class Ambiguous: 

2544 """Tag class to denote lookups that should be ambiguous.""" 

2545 

2546 pass 

2547 

2548 def _assertLookup( 

2549 detector: int, timespan: Timespan, expected: DatasetRef | type[Ambiguous] | None 

2550 ) -> None: 

2551 """Local function that asserts that a bias lookup returns the given 

2552 expected result. 

2553 """ 

2554 if expected is Ambiguous: 

2555 with self.assertRaises((DatasetTypeError, LookupError)): 

2556 registry.findDataset( 

2557 "bias", 

2558 collections=collection, 

2559 instrument="Cam1", 

2560 detector=detector, 

2561 timespan=timespan, 

2562 ) 

2563 else: 

2564 self.assertEqual( 

2565 expected, 

2566 registry.findDataset( 

2567 "bias", 

2568 collections=collection, 

2569 instrument="Cam1", 

2570 detector=detector, 

2571 timespan=timespan, 

2572 ), 

2573 ) 

2574 

2575 # Systematically test lookups against expected results. 

2576 _assertLookup(detector=2, timespan=Timespan(None, t1), expected=None) 

2577 _assertLookup(detector=2, timespan=Timespan(None, t2), expected=None) 

2578 _assertLookup(detector=2, timespan=Timespan(None, t3), expected=bias2a) 

2579 _assertLookup(detector=2, timespan=Timespan(None, t4), expected=bias2a) 

2580 _assertLookup(detector=2, timespan=Timespan(None, t5), expected=Ambiguous) 

2581 _assertLookup(detector=2, timespan=Timespan(None, None), expected=Ambiguous) 

2582 _assertLookup(detector=2, timespan=Timespan(t1, t2), expected=None) 

2583 _assertLookup(detector=2, timespan=Timespan(t1, t3), expected=bias2a) 

2584 _assertLookup(detector=2, timespan=Timespan(t1, t4), expected=bias2a) 

2585 _assertLookup(detector=2, timespan=Timespan(t1, t5), expected=Ambiguous) 

2586 _assertLookup(detector=2, timespan=Timespan(t1, None), expected=Ambiguous) 

2587 _assertLookup(detector=2, timespan=Timespan(t2, t3), expected=bias2a) 

2588 _assertLookup(detector=2, timespan=Timespan(t2, t4), expected=bias2a) 

2589 _assertLookup(detector=2, timespan=Timespan(t2, t5), expected=Ambiguous) 

2590 _assertLookup(detector=2, timespan=Timespan(t2, None), expected=Ambiguous) 

2591 _assertLookup(detector=2, timespan=Timespan(t3, t4), expected=bias2a) 

2592 _assertLookup(detector=2, timespan=Timespan(t3, t5), expected=Ambiguous) 

2593 _assertLookup(detector=2, timespan=Timespan(t3, None), expected=Ambiguous) 

2594 _assertLookup(detector=2, timespan=Timespan(t4, t5), expected=bias2b) 

2595 _assertLookup(detector=2, timespan=Timespan(t4, None), expected=bias2b) 

2596 _assertLookup(detector=2, timespan=Timespan(t5, None), expected=bias2b) 

2597 _assertLookup(detector=3, timespan=Timespan(None, t1), expected=None) 

2598 _assertLookup(detector=3, timespan=Timespan(None, t2), expected=bias3a) 

2599 _assertLookup(detector=3, timespan=Timespan(None, t3), expected=bias3a) 

2600 _assertLookup(detector=3, timespan=Timespan(None, t4), expected=bias3a) 

2601 _assertLookup(detector=3, timespan=Timespan(None, t5), expected=Ambiguous) 

2602 _assertLookup(detector=3, timespan=Timespan(None, None), expected=Ambiguous) 

2603 _assertLookup(detector=3, timespan=Timespan(t1, t2), expected=bias3a) 

2604 _assertLookup(detector=3, timespan=Timespan(t1, t3), expected=bias3a) 

2605 _assertLookup(detector=3, timespan=Timespan(t1, t4), expected=bias3a) 

2606 _assertLookup(detector=3, timespan=Timespan(t1, t5), expected=Ambiguous) 

2607 _assertLookup(detector=3, timespan=Timespan(t1, None), expected=Ambiguous) 

2608 _assertLookup(detector=3, timespan=Timespan(t2, t3), expected=bias3a) 

2609 _assertLookup(detector=3, timespan=Timespan(t2, t4), expected=bias3a) 

2610 _assertLookup(detector=3, timespan=Timespan(t2, t5), expected=Ambiguous) 

2611 _assertLookup(detector=3, timespan=Timespan(t2, None), expected=Ambiguous) 

2612 _assertLookup(detector=3, timespan=Timespan(t3, t4), expected=None) 

2613 _assertLookup(detector=3, timespan=Timespan(t3, t5), expected=bias3b) 

2614 _assertLookup(detector=3, timespan=Timespan(t3, None), expected=bias3b) 

2615 _assertLookup(detector=3, timespan=Timespan(t4, t5), expected=bias3b) 

2616 _assertLookup(detector=3, timespan=Timespan(t4, None), expected=bias3b) 

2617 _assertLookup(detector=3, timespan=Timespan(t5, None), expected=bias3b) 

2618 

2619 # Test lookups via temporal joins to exposures. 

2620 self.assertEqual( 

2621 set( 

2622 registry.queryDataIds( 

2623 ["exposure", "detector"], instrument="Cam1", detector=2 

2624 ).findRelatedDatasets("bias", collections=[collection]) 

2625 ), 

2626 { 

2627 (registry.expandDataId(instrument="Cam1", exposure=1, detector=2), bias2a), 

2628 (registry.expandDataId(instrument="Cam1", exposure=2, detector=2), bias2a), 

2629 (registry.expandDataId(instrument="Cam1", exposure=3, detector=2), bias2b), 

2630 }, 

2631 ) 

2632 self.assertEqual( 

2633 set( 

2634 registry.queryDataIds( 

2635 ["exposure", "detector"], instrument="Cam1", detector=3 

2636 ).findRelatedDatasets("bias", collections=[collection]) 

2637 ), 

2638 { 

2639 (registry.expandDataId(instrument="Cam1", exposure=0, detector=3), bias3a), 

2640 (registry.expandDataId(instrument="Cam1", exposure=1, detector=3), bias3a), 

2641 (registry.expandDataId(instrument="Cam1", exposure=3, detector=3), bias3b), 

2642 }, 

2643 ) 

2644 

2645 # Decertify [t3, t5) for all data IDs, and do test lookups again. 

2646 # This should truncate bias2a to [t2, t3), leave bias3a unchanged at 

2647 # [t1, t3), and truncate bias2b and bias3b to [t5, ∞). 

2648 registry.decertify(collection=collection, datasetType="bias", timespan=Timespan(t3, t5)) 

2649 _assertLookup(detector=2, timespan=Timespan(None, t1), expected=None) 

2650 _assertLookup(detector=2, timespan=Timespan(None, t2), expected=None) 

2651 _assertLookup(detector=2, timespan=Timespan(None, t3), expected=bias2a) 

2652 _assertLookup(detector=2, timespan=Timespan(None, t4), expected=bias2a) 

2653 _assertLookup(detector=2, timespan=Timespan(None, t5), expected=bias2a) 

2654 _assertLookup(detector=2, timespan=Timespan(None, None), expected=Ambiguous) 

2655 _assertLookup(detector=2, timespan=Timespan(t1, t2), expected=None) 

2656 _assertLookup(detector=2, timespan=Timespan(t1, t3), expected=bias2a) 

2657 _assertLookup(detector=2, timespan=Timespan(t1, t4), expected=bias2a) 

2658 _assertLookup(detector=2, timespan=Timespan(t1, t5), expected=bias2a) 

2659 _assertLookup(detector=2, timespan=Timespan(t1, None), expected=Ambiguous) 

2660 _assertLookup(detector=2, timespan=Timespan(t2, t3), expected=bias2a) 

2661 _assertLookup(detector=2, timespan=Timespan(t2, t4), expected=bias2a) 

2662 _assertLookup(detector=2, timespan=Timespan(t2, t5), expected=bias2a) 

2663 _assertLookup(detector=2, timespan=Timespan(t2, None), expected=Ambiguous) 

2664 _assertLookup(detector=2, timespan=Timespan(t3, t4), expected=None) 

2665 _assertLookup(detector=2, timespan=Timespan(t3, t5), expected=None) 

2666 _assertLookup(detector=2, timespan=Timespan(t3, None), expected=bias2b) 

2667 _assertLookup(detector=2, timespan=Timespan(t4, t5), expected=None) 

2668 _assertLookup(detector=2, timespan=Timespan(t4, None), expected=bias2b) 

2669 _assertLookup(detector=2, timespan=Timespan(t5, None), expected=bias2b) 

2670 _assertLookup(detector=3, timespan=Timespan(None, t1), expected=None) 

2671 _assertLookup(detector=3, timespan=Timespan(None, t2), expected=bias3a) 

2672 _assertLookup(detector=3, timespan=Timespan(None, t3), expected=bias3a) 

2673 _assertLookup(detector=3, timespan=Timespan(None, t4), expected=bias3a) 

2674 _assertLookup(detector=3, timespan=Timespan(None, t5), expected=bias3a) 

2675 _assertLookup(detector=3, timespan=Timespan(None, None), expected=Ambiguous) 

2676 _assertLookup(detector=3, timespan=Timespan(t1, t2), expected=bias3a) 

2677 _assertLookup(detector=3, timespan=Timespan(t1, t3), expected=bias3a) 

2678 _assertLookup(detector=3, timespan=Timespan(t1, t4), expected=bias3a) 

2679 _assertLookup(detector=3, timespan=Timespan(t1, t5), expected=bias3a) 

2680 _assertLookup(detector=3, timespan=Timespan(t1, None), expected=Ambiguous) 

2681 _assertLookup(detector=3, timespan=Timespan(t2, t3), expected=bias3a) 

2682 _assertLookup(detector=3, timespan=Timespan(t2, t4), expected=bias3a) 

2683 _assertLookup(detector=3, timespan=Timespan(t2, t5), expected=bias3a) 

2684 _assertLookup(detector=3, timespan=Timespan(t2, None), expected=Ambiguous) 

2685 _assertLookup(detector=3, timespan=Timespan(t3, t4), expected=None) 

2686 _assertLookup(detector=3, timespan=Timespan(t3, t5), expected=None) 

2687 _assertLookup(detector=3, timespan=Timespan(t3, None), expected=bias3b) 

2688 _assertLookup(detector=3, timespan=Timespan(t4, t5), expected=None) 

2689 _assertLookup(detector=3, timespan=Timespan(t4, None), expected=bias3b) 

2690 _assertLookup(detector=3, timespan=Timespan(t5, None), expected=bias3b) 

2691 

2692 # Decertify everything, this time with explicit data IDs, then check 

2693 # that no lookups succeed. 

2694 registry.decertify( 

2695 collection, 

2696 "bias", 

2697 Timespan(None, None), 

2698 dataIds=[ 

2699 dict(instrument="Cam1", detector=2), 

2700 dict(instrument="Cam1", detector=3), 

2701 ], 

2702 ) 

2703 for detector in (2, 3): 

2704 for timespan in allTimespans: 

2705 _assertLookup(detector=detector, timespan=timespan, expected=None) 

2706 # Certify bias2a and bias3a over (-∞, ∞), check that all lookups return 

2707 # those. 

2708 registry.certify( 

2709 collection, 

2710 [bias2a, bias3a], 

2711 Timespan(None, None), 

2712 ) 

2713 for timespan in allTimespans: 

2714 _assertLookup(detector=2, timespan=timespan, expected=bias2a) 

2715 _assertLookup(detector=3, timespan=timespan, expected=bias3a) 

2716 # Decertify just bias2 over [t2, t4). 

2717 # This should split a single certification row into two (and leave the 

2718 # other existing row, for bias3a, alone). 

2719 registry.decertify( 

2720 collection, "bias", Timespan(t2, t4), dataIds=[dict(instrument="Cam1", detector=2)] 

2721 ) 

2722 for timespan in allTimespans: 

2723 _assertLookup(detector=3, timespan=timespan, expected=bias3a) 

2724 overlapsBefore = timespan.overlaps(Timespan(None, t2)) 

2725 overlapsAfter = timespan.overlaps(Timespan(t4, None)) 

2726 if overlapsBefore and overlapsAfter: 

2727 expected = Ambiguous 

2728 elif overlapsBefore or overlapsAfter: 

2729 expected = bias2a 

2730 else: 

2731 expected = None 

2732 _assertLookup(detector=2, timespan=timespan, expected=expected) 

2733 

2734 def testSkipCalibs(self): 

2735 """Test how queries handle skipping of calibration collections.""" 

2736 butler = self.make_butler() 

2737 registry = butler.registry 

2738 self.load_data(butler, "base.yaml", "datasets.yaml") 

2739 

2740 coll_calib = "Cam1/calibs/default" 

2741 registry.registerCollection(coll_calib, type=CollectionType.CALIBRATION) 

2742 

2743 # Add all biases to the calibration collection. 

2744 # Without this, the logic that prunes dataset subqueries based on 

2745 # datasetType-collection summary information will fire before the logic 

2746 # we want to test below. This is a good thing (it avoids the dreaded 

2747 # NotImplementedError a bit more often) everywhere but here. 

2748 registry.certify(coll_calib, registry.queryDatasets("bias", collections=...), Timespan(None, None)) 

2749 

2750 coll_list = [coll_calib, "imported_g", "imported_r"] 

2751 chain = "Cam1/chain" 

2752 registry.registerCollection(chain, type=CollectionType.CHAINED) 

2753 registry.setCollectionChain(chain, coll_list) 

2754 

2755 # Lookup is ambiguous due to multiple datasets with the same data ID 

2756 # in the calibration collection. 

2757 with self.assertRaises(CalibrationLookupError): 

2758 list(registry.queryDatasets("bias", collections=coll_list, findFirst=True)) 

2759 

2760 # chain will skip 

2761 datasets = list(registry.queryDatasets("bias", collections=chain)) 

2762 self.assertGreater(len(datasets), 0) 

2763 

2764 dataIds = list(registry.queryDataIds(["instrument", "detector"], datasets="bias", collections=chain)) 

2765 self.assertGreater(len(dataIds), 0) 

2766 

2767 # glob will skip too 

2768 datasets = list(registry.queryDatasets("bias", collections="*d*")) 

2769 self.assertGreater(len(datasets), 0) 

2770 

2771 # regular expression will skip too 

2772 if self.supportsCollectionRegex: 

2773 pattern = re.compile(".*") 

2774 with self.assertWarns(FutureWarning): 

2775 datasets = list(registry.queryDatasets("bias", collections=pattern)) 

2776 self.assertGreater(len(datasets), 0) 

2777 

2778 # ellipsis should work as usual 

2779 datasets = list(registry.queryDatasets("bias", collections=...)) 

2780 self.assertGreater(len(datasets), 0) 

2781 

2782 # New query system correctly determines that this search is 

2783 # ambiguous, because there are multiple datasets with the same 

2784 # {instrument=Cam1, detector=2} data ID in the calibration 

2785 # collection at the beginning of the chain. 

2786 with self.assertRaises(CalibrationLookupError): 

2787 datasets = list(registry.queryDatasets("bias", collections=chain, findFirst=True)) 

2788 

2789 def testIngestTimeQuery(self): 

2790 butler = self.make_butler() 

2791 registry = butler.registry 

2792 dt0 = datetime.datetime.now(datetime.UTC) 

2793 self.load_data(butler, "base.yaml", "datasets.yaml") 

2794 dt1 = datetime.datetime.now(datetime.UTC) 

2795 

2796 datasets = list(registry.queryDatasets(..., collections=...)) 

2797 len0 = len(datasets) 

2798 self.assertGreater(len0, 0) 

2799 

2800 for where in ("ingest_date > T'2000-01-01'", "T'2000-01-01' < ingest_date"): 

2801 datasets = list(registry.queryDatasets(..., collections=..., where=where)) 

2802 len1 = len(datasets) 

2803 self.assertEqual(len0, len1) 

2804 

2805 # no one will ever use this piece of software in 30 years 

2806 for where in ("ingest_date > T'2050-01-01'", "T'2050-01-01' < ingest_date"): 

2807 datasets = list(registry.queryDatasets(..., collections=..., where=where)) 

2808 len2 = len(datasets) 

2809 self.assertEqual(len2, 0) 

2810 

2811 # Check more exact timing to make sure there is no 37 seconds offset 

2812 # (after fixing DM-30124). SQLite time precision is 1 second, make 

2813 # sure that we don't test with higher precision. 

2814 tests = [ 

2815 # format: (timestamp, operator, expected_len) 

2816 (dt0 - timedelta(seconds=1), ">", len0), 

2817 (dt0 - timedelta(seconds=1), "<", 0), 

2818 (dt1 + timedelta(seconds=1), "<", len0), 

2819 (dt1 + timedelta(seconds=1), ">", 0), 

2820 ] 

2821 for dt, op, expect_len in tests: 

2822 dt_str = dt.isoformat(sep=" ") 

2823 

2824 where = f"ingest_date {op} T'{dt_str}'" 

2825 datasets = list(registry.queryDatasets(..., collections=..., where=where)) 

2826 self.assertEqual(len(datasets), expect_len) 

2827 

2828 # same with bind using datetime or astropy Time 

2829 where = f"ingest_date {op} :ingest_time" 

2830 datasets = list( 

2831 registry.queryDatasets(..., collections=..., where=where, bind={"ingest_time": dt}) 

2832 ) 

2833 self.assertEqual(len(datasets), expect_len) 

2834 

2835 dt_astropy = astropy.time.Time(dt, format="datetime") 

2836 datasets = list( 

2837 registry.queryDatasets(..., collections=..., where=where, bind={"ingest_time": dt_astropy}) 

2838 ) 

2839 self.assertEqual(len(datasets), expect_len) 

2840 

2841 def testTimespanQueries(self): 

2842 """Test query expressions involving timespans.""" 

2843 butler = self.make_butler() 

2844 registry = butler.registry 

2845 self.load_data(butler, "ci_hsc-subset.yaml") 

2846 # All exposures in the database; mapping from ID to timespan. 

2847 visits = {record.id: record.timespan for record in registry.queryDimensionRecords("visit")} 

2848 # Just those IDs, sorted (which is also temporal sorting, because HSC 

2849 # exposure IDs are monotonically increasing). 

2850 ids = sorted(visits.keys()) 

2851 self.assertEqual(len(ids), 11) 

2852 # Pick some quasi-random indexes into `ids` to play with. 

2853 i1 = int(len(ids) * 0.1) 

2854 i2 = int(len(ids) * 0.3) 

2855 i3 = int(len(ids) * 0.6) 

2856 i4 = int(len(ids) * 0.8) 

2857 # Extract some times from those: just before the beginning of i1 (which 

2858 # should be after the end of the exposure before), exactly the 

2859 # beginning of i2, just after the beginning of i3 (and before its end), 

2860 # and the exact end of i4. 

2861 t1 = visits[ids[i1]].begin - astropy.time.TimeDelta(1.0, format="sec") 

2862 self.assertGreater(t1, visits[ids[i1 - 1]].end) 

2863 t2 = visits[ids[i2]].begin 

2864 t3 = visits[ids[i3]].begin + astropy.time.TimeDelta(1.0, format="sec") 

2865 self.assertLess(t3, visits[ids[i3]].end) 

2866 t4 = visits[ids[i4]].end 

2867 # Make sure those are actually in order. 

2868 self.assertEqual([t1, t2, t3, t4], sorted([t4, t3, t2, t1])) 

2869 

2870 bind = { 

2871 "t1": t1, 

2872 "t2": t2, 

2873 "t3": t3, 

2874 "t4": t4, 

2875 "ts23": Timespan(t2, t3), 

2876 } 

2877 

2878 def query(where): 

2879 """Return results as a sorted, deduplicated list of visit IDs. 

2880 

2881 Parameters 

2882 ---------- 

2883 where : `str` 

2884 The WHERE clause for the query. 

2885 """ 

2886 return sorted( 

2887 { 

2888 dataId["visit"] 

2889 for dataId in registry.queryDataIds("visit", instrument="HSC", bind=bind, where=where) 

2890 } 

2891 ) 

2892 

2893 # Try a bunch of timespan queries, mixing up the bounds themselves, 

2894 # where they appear in the expression, and how we get the timespan into 

2895 # the expression. 

2896 

2897 # t1 is before the start of i1, so this should not include i1. 

2898 self.assertEqual(ids[:i1], query("visit.timespan OVERLAPS (null, :t1)")) 

2899 # t2 is exactly at the start of i2, but ends are exclusive, so these 

2900 # should not include i2. 

2901 self.assertEqual(ids[i1:i2], query("(:t1, :t2) OVERLAPS visit.timespan")) 

2902 # t3 is in the middle of i3, so this should include i3. 

2903 self.assertEqual(ids[i2 : i3 + 1], query("visit.timespan OVERLAPS :ts23")) 

2904 # This one should not include t3 by the same reasoning. 

2905 # t4 is exactly at the end of i4, so this should include i4. 

2906 self.assertEqual(ids[i3 : i4 + 1], query(f"visit.timespan OVERLAPS (T'{t3.tai.isot}/tai', :t4)")) 

2907 # i4's upper bound of t4 is exclusive so this should not include t4. 

2908 self.assertEqual(ids[i4 + 1 :], query("visit.timespan OVERLAPS (:t4, NULL)")) 

2909 

2910 # Now some timespan vs. time scalar queries. 

2911 self.assertEqual(ids[i3 : i3 + 1], query("visit.timespan OVERLAPS :t3")) 

2912 self.assertEqual(ids[i3 : i3 + 1], query(f"T'{t3.tai.isot}/tai' OVERLAPS visit.timespan")) 

2913 

2914 # Empty timespans should not overlap anything. 

2915 self.assertEqual([], query("visit.timespan OVERLAPS (:t3, :t2)")) 

2916 

2917 # Make sure that expanded data IDs include the timespans. 

2918 results = list( 

2919 registry.queryDataIds(["visit"], dataId={"instrument": "HSC", "visit": ids[1]}).expanded() 

2920 ) 

2921 self.assertEqual(len(results), 1) 

2922 visit_timespan = visits[ids[1]] 

2923 self.assertEqual(results[0].timespan, visit_timespan) 

2924 visit_record = results[0].records["visit"] 

2925 assert visit_record is not None 

2926 self.assertEqual(visit_record.timespan, visit_timespan) 

2927 day_obs_record = results[0].records["day_obs"] 

2928 assert day_obs_record is not None 

2929 self.assertEqual(day_obs_record.id, 20130617) 

2930 self.assertEqual( 

2931 day_obs_record.timespan, 

2932 Timespan( 

2933 astropy.time.Time("2013-06-17T00:00:00", scale="tai"), 

2934 astropy.time.Time("2013-06-18T00:00:00", scale="tai"), 

2935 ), 

2936 ) 

2937 

2938 def testCollectionSummaries(self): 

2939 """Test recording and retrieval of collection summaries.""" 

2940 self.maxDiff = None 

2941 butler = self.make_butler() 

2942 registry = butler.registry 

2943 # Importing datasets from yaml should go through the code path where 

2944 # we update collection summaries as we insert datasets. 

2945 self.load_data(butler, "base.yaml", "datasets.yaml") 

2946 flat = registry.getDatasetType("flat") 

2947 expected1 = CollectionSummary() 

2948 expected1.dataset_types.add(registry.getDatasetType("bias")) 

2949 expected1.add_data_ids( 

2950 flat, [DataCoordinate.standardize(instrument="Cam1", universe=registry.dimensions)] 

2951 ) 

2952 self.assertEqual(registry.getCollectionSummary("imported_g"), expected1) 

2953 self.assertEqual(registry.getCollectionSummary("imported_r"), expected1) 

2954 # Create a chained collection with both of the imported runs; the 

2955 # summary should be the same, because it's a union with itself. 

2956 chain = "chain" 

2957 registry.registerCollection(chain, CollectionType.CHAINED) 

2958 registry.setCollectionChain(chain, ["imported_r", "imported_g"]) 

2959 self.assertEqual(registry.getCollectionSummary(chain), expected1) 

2960 # Associate flats only into a tagged collection and a calibration 

2961 # collection to check summaries of those. 

2962 tag = "tag" 

2963 registry.registerCollection(tag, CollectionType.TAGGED) 

2964 registry.associate(tag, registry.queryDatasets(flat, collections="imported_g")) 

2965 calibs = "calibs" 

2966 registry.registerCollection(calibs, CollectionType.CALIBRATION) 

2967 registry.certify( 

2968 calibs, registry.queryDatasets(flat, collections="imported_g"), timespan=Timespan(None, None) 

2969 ) 

2970 expected2 = expected1.copy() 

2971 expected2.dataset_types.discard("bias") 

2972 self.assertEqual(registry.getCollectionSummary(tag), expected2) 

2973 self.assertEqual(registry.getCollectionSummary(calibs), expected2) 

2974 # Explicitly calling SqlRegistry.refresh() should load those same 

2975 # summaries, via a totally different code path. 

2976 registry.refresh() 

2977 self.assertEqual(registry.getCollectionSummary("imported_g"), expected1) 

2978 self.assertEqual(registry.getCollectionSummary("imported_r"), expected1) 

2979 self.assertEqual(registry.getCollectionSummary(tag), expected2) 

2980 self.assertEqual(registry.getCollectionSummary(calibs), expected2) 

2981 

2982 def testBindInQueryDatasets(self): 

2983 """Test that the bind parameter is correctly forwarded in 

2984 queryDatasets recursion. 

2985 """ 

2986 butler = self.make_butler() 

2987 registry = butler.registry 

2988 # Importing datasets from yaml should go through the code path where 

2989 # we update collection summaries as we insert datasets. 

2990 self.load_data(butler, "base.yaml", "datasets.yaml") 

2991 self.assertEqual( 

2992 set(registry.queryDatasets("flat", band="r", collections=...)), 

2993 set( 

2994 registry.queryDatasets("flat", where="band=:my_band", bind={"my_band": "r"}, collections=...) 

2995 ), 

2996 ) 

2997 

2998 def testQueryIntRangeExpressions(self): 

2999 """Test integer range expressions in ``where`` arguments. 

3000 

3001 Note that our expressions use inclusive stop values, unlike Python's. 

3002 """ 

3003 butler = self.make_butler() 

3004 registry = butler.registry 

3005 self.load_data(butler, "base.yaml") 

3006 self.assertEqual( 

3007 set(registry.queryDataIds(["detector"], instrument="Cam1", where="detector IN (1..2)")), 

3008 {registry.expandDataId(instrument="Cam1", detector=n) for n in [1, 2]}, 

3009 ) 

3010 self.assertEqual( 

3011 set(registry.queryDataIds(["detector"], instrument="Cam1", where="detector IN (1..4:2)")), 

3012 {registry.expandDataId(instrument="Cam1", detector=n) for n in [1, 3]}, 

3013 ) 

3014 self.assertEqual( 

3015 set(registry.queryDataIds(["detector"], instrument="Cam1", where="detector IN (2..4:2)")), 

3016 {registry.expandDataId(instrument="Cam1", detector=n) for n in [2, 4]}, 

3017 ) 

3018 

3019 def testQueryResultSummaries(self): 

3020 """Test summary methods like `count`, `any`, and `explain_no_results` 

3021 on `DataCoordinateQueryResults` and `DatasetQueryResults`. 

3022 """ 

3023 butler = self.make_butler() 

3024 registry = butler.registry 

3025 self.load_data(butler, "base.yaml", "datasets.yaml", "spatial.yaml") 

3026 # Default test dataset has two collections, each with both flats and 

3027 # biases. Add a new collection with only biases. 

3028 registry.registerCollection("biases", CollectionType.TAGGED) 

3029 registry.associate("biases", registry.queryDatasets("bias", collections=["imported_g"])) 

3030 # First query yields two results, and involves no postprocessing. 

3031 query1 = registry.queryDataIds(["physical_filter"], band="r") 

3032 self.assertTrue(query1.any(execute=False, exact=False)) 

3033 self.assertTrue(query1.any(execute=True, exact=False)) 

3034 self.assertTrue(query1.any(execute=True, exact=True)) 

3035 self.assertEqual(query1.count(exact=False), 2) 

3036 self.assertEqual(query1.count(exact=True), 2) 

3037 self.assertFalse(list(query1.explain_no_results())) 

3038 # Second query should yield no results, which we should see when 

3039 # we attempt to expand the data ID. 

3040 query2 = registry.queryDataIds(["physical_filter"], band="h") 

3041 # There's no execute=False, exact=False test here because the behavior 

3042 # not something we want to guarantee in this case (and exact=False 

3043 # says either answer is legal). 

3044 self.assertFalse(query2.any(execute=True, exact=False)) 

3045 self.assertFalse(query2.any(execute=True, exact=True)) 

3046 self.assertEqual(query2.count(exact=False), 0) 

3047 self.assertEqual(query2.count(exact=True), 0) 

3048 # These queries yield no results due to various problems that can be 

3049 # spotted prior to execution, yielding helpful diagnostics. 

3050 base_query = registry.queryDataIds(["detector", "physical_filter"]) 

3051 queries_and_snippets = [ 

3052 ( 

3053 # Dataset type name doesn't match any existing dataset types. 

3054 registry.queryDatasets("nonexistent", collections=...), 

3055 ["nonexistent"], 

3056 ), 

3057 ( 

3058 # Dataset type object isn't registered. 

3059 registry.queryDatasets( 

3060 DatasetType( 

3061 "nonexistent", 

3062 dimensions=["instrument"], 

3063 universe=registry.dimensions, 

3064 storageClass="Image", 

3065 ), 

3066 collections=..., 

3067 ), 

3068 ["nonexistent"], 

3069 ), 

3070 ( 

3071 # No datasets of this type in this collection. 

3072 registry.queryDatasets("flat", collections=["biases"]), 

3073 ["flat", "biases"], 

3074 ), 

3075 ( 

3076 # No datasets of this type in this collection. 

3077 base_query.findDatasets("flat", collections=["biases"]), 

3078 ["flat", "biases"], 

3079 ), 

3080 ( 

3081 # No collections matching at all. 

3082 registry.queryDatasets("flat", collections="potato*"), 

3083 ["potato"], 

3084 ), 

3085 ] 

3086 with self.assertRaises(MissingDatasetTypeError): 

3087 # Dataset type name doesn't match any existing dataset types. 

3088 list(registry.queryDataIds(["detector"], datasets=["nonexistent"], collections=...)) 

3089 with self.assertRaises(MissingDatasetTypeError): 

3090 # Dataset type name doesn't match any existing dataset types. 

3091 registry.queryDimensionRecords("detector", datasets=["nonexistent"], collections=...).any() 

3092 with self.assertRaises(DatasetTypeExpressionError): 

3093 # queryDimensionRecords does not allow dataset type wildcards. 

3094 registry.queryDimensionRecords("detector", datasets=["f*"], collections=...).any() 

3095 for query, snippets in queries_and_snippets: 

3096 self.assertFalse(query.any(execute=False, exact=False)) 

3097 self.assertFalse(query.any(execute=True, exact=False)) 

3098 self.assertFalse(query.any(execute=True, exact=True)) 

3099 self.assertEqual(query.count(exact=False), 0) 

3100 self.assertEqual(query.count(exact=True), 0) 

3101 messages = list(query.explain_no_results()) 

3102 self.assertTrue(messages) 

3103 # Want all expected snippets to appear in at least one message. 

3104 self.assertTrue( 

3105 any( 

3106 all(snippet in message for snippet in snippets) for message in query.explain_no_results() 

3107 ), 

3108 messages, 

3109 ) 

3110 

3111 # Wildcards on dataset types are not permitted in queryDataIds. 

3112 with self.assertRaises(DatasetTypeExpressionError): 

3113 registry.queryDataIds(["detector"], datasets=re.compile("^nonexistent$"), collections=...) 

3114 

3115 # This query yields four overlaps in the database, but one is filtered 

3116 # out in postprocessing. The count queries aren't accurate because 

3117 # they don't account for duplication that happens due to an internal 

3118 # join against commonSkyPix. 

3119 query3 = registry.queryDataIds(["visit", "tract"], instrument="Cam1", skymap="SkyMap1") 

3120 self.assertEqual( 

3121 { 

3122 DataCoordinate.standardize( 

3123 instrument="Cam1", 

3124 skymap="SkyMap1", 

3125 visit=v, 

3126 tract=t, 

3127 universe=registry.dimensions, 

3128 ) 

3129 for v, t in [(1, 0), (2, 0), (2, 1)] 

3130 }, 

3131 set(query3), 

3132 ) 

3133 self.assertTrue(query3.any(execute=False, exact=False)) 

3134 self.assertTrue(query3.any(execute=True, exact=False)) 

3135 self.assertTrue(query3.any(execute=True, exact=True)) 

3136 self.assertGreaterEqual(query3.count(exact=False), 3) 

3137 self.assertGreaterEqual(query3.count(exact=True, discard=True), 3) 

3138 self.assertFalse(list(query3.explain_no_results())) 

3139 # This query yields overlaps in the database, but all are filtered 

3140 # out in postprocessing. The count queries again aren't very useful. 

3141 # We have to use `where=` here to avoid an optimization that 

3142 # (currently) skips the spatial postprocess-filtering because it 

3143 # recognizes that no spatial join is necessary. That's not ideal, but 

3144 # fixing it is out of scope for this ticket. 

3145 query4 = registry.queryDataIds( 

3146 ["visit", "tract"], 

3147 instrument="Cam1", 

3148 skymap="SkyMap1", 

3149 where="visit=1 AND detector=1 AND tract=0 AND patch=4", 

3150 ) 

3151 self.assertFalse(set(query4)) 

3152 self.assertTrue(query4.any(execute=False, exact=False)) 

3153 self.assertTrue(query4.any(execute=True, exact=False)) 

3154 self.assertFalse(query4.any(execute=True, exact=True)) 

3155 self.assertGreaterEqual(query4.count(exact=False), 1) 

3156 self.assertEqual(query4.count(exact=True, discard=True), 0) 

3157 # This query should yield results from one dataset type but not the 

3158 # other, which is not registered. 

3159 query5 = registry.queryDatasets(["bias", "nonexistent"], collections=["biases"]) 

3160 self.assertTrue(set(query5)) 

3161 self.assertTrue(query5.any(execute=False, exact=False)) 

3162 self.assertTrue(query5.any(execute=True, exact=False)) 

3163 self.assertTrue(query5.any(execute=True, exact=True)) 

3164 self.assertGreaterEqual(query5.count(exact=False), 1) 

3165 self.assertGreaterEqual(query5.count(exact=True), 1) 

3166 # This query applies a selection that yields no results, fully in the 

3167 # database. Explaining why it fails involves traversing the relation 

3168 # tree and running a LIMIT 1 query at each level that has the potential 

3169 # to remove rows. 

3170 query6 = registry.queryDimensionRecords( 

3171 "detector", where="detector.purpose = 'no-purpose'", instrument="Cam1" 

3172 ) 

3173 self.assertEqual(query6.count(exact=True), 0) 

3174 self.assertFalse(query6.any()) 

3175 

3176 def testQueryDataIdsExpressionError(self): 

3177 """Test error checking of 'where' expressions in queryDataIds.""" 

3178 butler = self.make_butler() 

3179 registry = butler.registry 

3180 self.load_data(butler, "base.yaml") 

3181 bind = {"time": astropy.time.Time("2020-01-01T01:00:00", format="isot", scale="tai")} 

3182 # The diagnostics raised are slightly different between the old query 

3183 # system (ValueError, first error string) and the new query system 

3184 # (InvalidQueryError, second error string). 

3185 with self.assertRaisesRegex( 

3186 (LookupError, InvalidQueryError), 

3187 r"(No dimension element with name 'foo' in 'foo\.bar'\.)|(Unrecognized identifier 'foo.bar')", 

3188 ): 

3189 list(registry.queryDataIds(["detector"], where="foo.bar = 12")) 

3190 with self.assertRaisesRegex( 

3191 (LookupError, InvalidQueryError), 

3192 "(Dimension element name cannot be inferred in this context.)" 

3193 "|(Unrecognized identifier 'timespan')", 

3194 ): 

3195 list(registry.queryDataIds(["detector"], where="timespan.end < :time", bind=bind)) 

3196 

3197 def testQueryDataIdsOrderBy(self): 

3198 """Test order_by and limit on result returned by queryDataIds().""" 

3199 butler = self.make_butler() 

3200 registry = butler.registry 

3201 self.load_data(butler, "base.yaml", "datasets.yaml", "spatial.yaml") 

3202 

3203 def do_query(dimensions=("visit", "tract"), datasets=None, collections=None): 

3204 return registry.queryDataIds( 

3205 dimensions, datasets=datasets, collections=collections, instrument="Cam1", skymap="SkyMap1" 

3206 ) 

3207 

3208 Test = namedtuple( 

3209 "testQueryDataIdsOrderByTest", 

3210 ("order_by", "keys", "result", "limit", "datasets", "collections"), 

3211 defaults=(None, None, None), 

3212 ) 

3213 

3214 test_data = [ 

3215 Test("tract,visit", "tract,visit", ((0, 1), (0, 2), (1, 2))), 

3216 Test("-tract,visit", "tract,visit", ((1, 2), (0, 1), (0, 2))), 

3217 Test("tract,-visit", "tract,visit", ((0, 2), (0, 1), (1, 2))), 

3218 Test("-tract,-visit", "tract,visit", ((1, 2), (0, 2), (0, 1))), 

3219 Test("tract.id,visit.id", "tract,visit", ((0, 1),), limit=(1,)), 

3220 Test("-tract,-visit", "tract,visit", ((1, 2),), limit=(1,)), 

3221 Test("tract,visit.exposure_time", "tract,visit", ((0, 2), (0, 1), (1, 2))), 

3222 Test("-tract,-visit.exposure_time", "tract,visit", ((1, 2), (0, 1), (0, 2))), 

3223 Test("tract,-exposure_time", "tract,visit", ((0, 1), (0, 2), (1, 2))), 

3224 Test("tract,visit.name", "tract,visit", ((0, 1), (0, 2), (1, 2))), 

3225 Test( 

3226 "tract,-visit.timespan.begin,visit.timespan.end", 

3227 "tract,visit", 

3228 ((0, 2), (0, 1), (1, 2)), 

3229 ), 

3230 Test("visit.day_obs,exposure.day_obs", "visit,exposure", ()), 

3231 Test("visit.timespan.begin,-exposure.timespan.begin", "visit,exposure", ()), 

3232 Test( 

3233 "tract,detector", 

3234 "tract,detector", 

3235 ((0, 1), (0, 2), (0, 3), (0, 4), (1, 1), (1, 2), (1, 3), (1, 4)), 

3236 datasets="flat", 

3237 collections="imported_r", 

3238 ), 

3239 Test( 

3240 "tract,detector.full_name", 

3241 "tract,detector", 

3242 ((0, 1), (0, 2), (0, 3), (0, 4), (1, 1), (1, 2), (1, 3), (1, 4)), 

3243 datasets="flat", 

3244 collections="imported_r", 

3245 ), 

3246 Test( 

3247 "tract,detector.raft,detector.name_in_raft", 

3248 "tract,detector", 

3249 ((0, 1), (0, 2), (0, 3), (0, 4), (1, 1), (1, 2), (1, 3), (1, 4)), 

3250 datasets="flat", 

3251 collections="imported_r", 

3252 ), 

3253 ] 

3254 

3255 for test in test_data: 

3256 with self.subTest(test=test): 

3257 order_by = test.order_by.split(",") 

3258 keys = test.keys.split(",") 

3259 query = do_query(keys, test.datasets, test.collections).order_by(*order_by) 

3260 if test.limit is not None: 

3261 query = query.limit(*test.limit) 

3262 dataIds = tuple(tuple(dataId[k] for k in keys) for dataId in query) 

3263 self.assertEqual(dataIds, test.result) 

3264 

3265 # and materialize 

3266 query = do_query(keys).order_by(*order_by) 

3267 if test.limit is not None: 

3268 query = query.limit(*test.limit) 

3269 

3270 # Test exceptions for errors in a name. 

3271 # Many of these raise slightly different diagnostics in the old query 

3272 # system (ValueError, first error string) than the new query system 

3273 # (InvalidQueryError, second error string). 

3274 for order_by in ("", "-"): 

3275 with self.assertRaisesRegex((ValueError, InvalidQueryError), "Empty dimension name in ORDER BY"): 

3276 list(do_query().order_by(order_by)) 

3277 

3278 for order_by in ("undimension.name", "-undimension.name"): 

3279 with self.assertRaisesRegex( 

3280 (ValueError, InvalidQueryError), 

3281 "(Unknown dimension element 'undimension')|(Unrecognized identifier 'undimension.name')", 

3282 ): 

3283 list(do_query().order_by(order_by)) 

3284 

3285 for order_by in ("attract", "-attract"): 

3286 with self.assertRaisesRegex( 

3287 (ValueError, InvalidQueryError), 

3288 "(Metadata 'attract' cannot be found in any dimension)|(Unrecognized identifier 'attract')", 

3289 ): 

3290 list(do_query().order_by(order_by)) 

3291 

3292 with self.assertRaisesRegex( 

3293 (ValueError, InvalidQueryError), 

3294 "(Metadata 'exposure_time' exists in more than one dimension)" 

3295 "|(Ambiguous identifier 'exposure_time' matches multiple fields)", 

3296 ): 

3297 list(do_query(("exposure", "visit")).order_by("exposure_time")) 

3298 

3299 with self.assertRaisesRegex( 

3300 (ValueError, InvalidQueryError), 

3301 r"(Timespan exists in more than one dimension element \(day_obs, exposure, visit\); " 

3302 r"qualify timespan with specific dimension name\.)|" 

3303 r"(Ambiguous identifier 'timespan' matches multiple fields)", 

3304 ): 

3305 list(do_query(("exposure", "visit")).order_by("timespan.begin")) 

3306 

3307 with self.assertRaisesRegex( 

3308 (ValueError, InvalidQueryError), 

3309 "(Cannot find any temporal dimension element for 'timespan.begin')" 

3310 "|(Unrecognized identifier 'timespan')", 

3311 ): 

3312 list(do_query("tract").order_by("timespan.begin")) 

3313 

3314 with self.assertRaisesRegex( 

3315 (ValueError, InvalidQueryError), 

3316 "(Cannot use 'timespan.begin' with non-temporal element)" 

3317 "|(Unrecognized field 'timespan' for tract)", 

3318 ): 

3319 list(do_query("tract").order_by("tract.timespan.begin")) 

3320 

3321 with self.assertRaisesRegex( 

3322 (ValueError, InvalidQueryError), 

3323 "(Field 'name' does not exist in 'tract')|(Unrecognized field 'name' for tract.)", 

3324 ): 

3325 list(do_query("tract").order_by("tract.name")) 

3326 

3327 with self.assertRaisesRegex( 

3328 (ValueError, InvalidQueryError), 

3329 r"(Unknown dimension element 'timestamp'; perhaps you meant 'timespan.begin'\?)" 

3330 r"|(Unrecognized identifier 'timestamp.begin')", 

3331 ): 

3332 list(do_query("visit").order_by("timestamp.begin")) 

3333 

3334 def testQueryDataIdsGovernorExceptions(self): 

3335 """Test exceptions raised by queryDataIds() for incorrect governors.""" 

3336 butler = self.make_butler() 

3337 registry = butler.registry 

3338 self.load_data(butler, "base.yaml", "datasets.yaml", "spatial.yaml") 

3339 

3340 def do_query(dimensions, dataId=None, where="", bind=None, **kwargs): 

3341 return registry.queryDataIds(dimensions, dataId=dataId, where=where, bind=bind, **kwargs) 

3342 

3343 Test = namedtuple( 

3344 "testQueryDataIdExceptionsTest", 

3345 ("dimensions", "dataId", "where", "bind", "kwargs", "exception", "count"), 

3346 defaults=(None, None, None, {}, None, 0), 

3347 ) 

3348 

3349 test_data = ( 

3350 Test("tract,visit", count=3), 

3351 Test("tract,visit", kwargs={"instrument": "Cam1", "skymap": "SkyMap1"}, count=3), 

3352 Test("tract,visit", kwargs={"instrument": "Cam2", "skymap": "SkyMap1"}, count=0), 

3353 Test("tract,visit", dataId={"instrument": "Cam1", "skymap": "SkyMap1"}, count=3), 

3354 Test("tract,visit", dataId={"instrument": "Cam1", "skymap": "SkyMap2"}, count=0), 

3355 Test("tract,visit", where="instrument='Cam1' AND skymap='SkyMap1'", count=3), 

3356 Test("tract,visit", where="instrument='Cam1' AND skymap='SkyMap5'", count=0), 

3357 Test( 

3358 "tract,visit", 

3359 where="instrument=:cam AND skymap=:map", 

3360 bind={"cam": "Cam1", "map": "SkyMap1"}, 

3361 count=3, 

3362 ), 

3363 Test( 

3364 "tract,visit", 

3365 where="instrument=:cam AND skymap=:map", 

3366 bind={"cam": "Cam", "map": "SkyMap"}, 

3367 count=0, 

3368 ), 

3369 ) 

3370 

3371 for test in test_data: 

3372 print(test) 

3373 dimensions = test.dimensions.split(",") 

3374 if test.exception: 

3375 with self.assertRaises(test.exception): 

3376 with ExitStack() as stack: 

3377 if test.exception == DataIdValueError: 

3378 stack.enter_context(self.assertWarns(FutureWarning)) 

3379 do_query(dimensions, test.dataId, test.where, bind=test.bind, **test.kwargs).count() 

3380 else: 

3381 query = do_query(dimensions, test.dataId, test.where, bind=test.bind, **test.kwargs) 

3382 print(list(query)) 

3383 self.assertEqual(query.count(discard=True), test.count) 

3384 

3385 # and materialize 

3386 if test.exception: 

3387 with self.assertRaises(test.exception): 

3388 with ExitStack() as stack: 

3389 if test.exception == DataIdValueError: 

3390 stack.enter_context(self.assertWarns(FutureWarning)) 

3391 query = do_query(dimensions, test.dataId, test.where, bind=test.bind, **test.kwargs) 

3392 with query.materialize() as materialized: 

3393 materialized.count(discard=True) 

3394 else: 

3395 query = do_query(dimensions, test.dataId, test.where, bind=test.bind, **test.kwargs) 

3396 with query.materialize() as materialized: 

3397 self.assertEqual(materialized.count(discard=True), test.count) 

3398 

3399 def testQueryDimensionRecordsOrderBy(self): 

3400 """Test order_by and limit on result returned by 

3401 queryDimensionRecords(). 

3402 """ 

3403 butler = self.make_butler() 

3404 registry = butler.registry 

3405 self.load_data(butler, "base.yaml", "datasets.yaml", "spatial.yaml") 

3406 

3407 def do_query(element, datasets=None, collections=None): 

3408 return registry.queryDimensionRecords( 

3409 element, instrument="Cam1", datasets=datasets, collections=collections 

3410 ) 

3411 

3412 query = do_query("detector") 

3413 self.assertEqual(len(list(query)), 4) 

3414 

3415 Test = namedtuple( 

3416 "testQueryDataIdsOrderByTest", 

3417 ("element", "order_by", "result", "limit", "datasets", "collections"), 

3418 defaults=(None, None, None), 

3419 ) 

3420 

3421 test_data = [ 

3422 Test("detector", "detector", (1, 2, 3, 4)), 

3423 Test("detector", "-detector", (4, 3, 2, 1)), 

3424 Test("detector", "raft,-name_in_raft", (2, 1, 4, 3)), 

3425 Test("detector", "-detector.purpose", (4,), limit=(1,)), 

3426 Test("visit", "visit", (1, 2)), 

3427 Test("visit", "-visit.id", (2, 1)), 

3428 Test("visit", "zenith_angle", (1, 2)), 

3429 Test("visit", "-visit.name", (2, 1)), 

3430 Test("visit", "day_obs,-visit.timespan.begin", (2, 1)), 

3431 ] 

3432 

3433 def do_test(test: Test): 

3434 order_by = test.order_by.split(",") 

3435 query = do_query(test.element).order_by(*order_by) 

3436 if test.limit is not None: 

3437 query = query.limit(*test.limit) 

3438 dataIds = tuple(rec.id for rec in query) 

3439 self.assertEqual(dataIds, test.result) 

3440 

3441 for test in test_data: 

3442 do_test(test) 

3443 

3444 # errors in a name 

3445 for order_by in ("", "-"): 

3446 with self.assertRaisesRegex( 

3447 (ValueError, InvalidQueryError), 

3448 "(Empty dimension name in ORDER BY)|(Unrecognized identifier)", 

3449 ): 

3450 list(do_query("detector").order_by(order_by)) 

3451 

3452 for order_by in ("undimension.name", "-undimension.name"): 

3453 with self.assertRaisesRegex( 

3454 (ValueError, InvalidQueryError), 

3455 "(Element name mismatch: 'undimension')|(Unrecognized identifier)", 

3456 ): 

3457 list(do_query("detector").order_by(order_by)) 

3458 

3459 for order_by in ("attract", "-attract"): 

3460 with self.assertRaisesRegex( 

3461 (ValueError, InvalidQueryError), 

3462 "(Field 'attract' does not exist in 'detector'.)|(Unrecognized identifier)", 

3463 ): 

3464 list(do_query("detector").order_by(order_by)) 

3465 

3466 for order_by in ("timestamp.begin", "-timestamp.begin"): 

3467 with self.assertRaisesRegex( 

3468 (ValueError, InvalidQueryError), 

3469 r"(Element name mismatch: 'timestamp' instead of 'visit'; " 

3470 r"perhaps you meant 'timespan.begin'\?)" 

3471 r"|(Unrecognized identifier)", 

3472 ): 

3473 list(do_query("visit").order_by(order_by)) 

3474 

3475 def testQueryDimensionRecordsExceptions(self): 

3476 """Test exceptions raised by queryDimensionRecords().""" 

3477 butler = self.make_butler() 

3478 registry = butler.registry 

3479 self.load_data(butler, "base.yaml", "datasets.yaml", "spatial.yaml") 

3480 

3481 result = registry.queryDimensionRecords("detector") 

3482 self.assertEqual(result.count(), 4) 

3483 result = registry.queryDimensionRecords("detector", instrument="Cam1") 

3484 self.assertEqual(result.count(), 4) 

3485 result = registry.queryDimensionRecords("detector", dataId={"instrument": "Cam1"}) 

3486 self.assertEqual(result.count(), 4) 

3487 

3488 # Test that values specified in kwargs override those specified in 

3489 # dataId. 

3490 result = registry.queryDimensionRecords( 

3491 "detector", dataId={"instrument": "NotCam1"}, instrument="Cam1" 

3492 ) 

3493 self.assertEqual(result.count(), 4) 

3494 

3495 result = registry.queryDimensionRecords("detector", where="instrument='Cam1'") 

3496 self.assertEqual(result.count(), 4) 

3497 result = registry.queryDimensionRecords("detector", where="instrument=:instr", bind={"instr": "Cam1"}) 

3498 self.assertTrue(result.any()) 

3499 self.assertEqual(result.count(), 4) 

3500 

3501 def testDatasetConstrainedDimensionRecordQueries(self): 

3502 """Test that queryDimensionRecords works even when given a dataset 

3503 constraint whose dimensions extend beyond the requested dimension 

3504 element's. 

3505 """ 

3506 butler = self.make_butler() 

3507 registry = butler.registry 

3508 self.load_data(butler, "base.yaml", "datasets.yaml") 

3509 # Query for physical_filter dimension records, using a dataset that 

3510 # has both physical_filter and dataset dimensions. 

3511 records = registry.queryDimensionRecords( 

3512 "physical_filter", 

3513 datasets=["flat"], 

3514 collections="imported_r", 

3515 ) 

3516 self.assertEqual({record.name for record in records}, {"Cam1-R1", "Cam1-R2"}) 

3517 # Trying to constrain by all dataset types is an error. 

3518 with self.assertRaises(TypeError): 

3519 list(registry.queryDimensionRecords("physical_filter", datasets=..., collections="imported_r")) 

3520 

3521 def testSkyPixDatasetQueries(self): 

3522 """Test that we can build queries involving skypix dimensions as long 

3523 as a dataset type that uses those dimensions is included. 

3524 """ 

3525 butler = self.make_butler() 

3526 registry = butler.registry 

3527 self.load_data(butler, "base.yaml") 

3528 dataset_type = DatasetType( 

3529 "a", dimensions=["htm7", "instrument"], universe=registry.dimensions, storageClass="int" 

3530 ) 

3531 registry.registerDatasetType(dataset_type) 

3532 run = "r" 

3533 registry.registerRun(run) 

3534 # First try queries where there are no datasets; the concern is whether 

3535 # we can even build and execute these queries without raising, even 

3536 # when "doomed" query shortcuts are in play. 

3537 self.assertFalse( 

3538 list(registry.queryDataIds(["htm7", "instrument"], datasets=dataset_type, collections=run)) 

3539 ) 

3540 self.assertFalse(list(registry.queryDatasets(dataset_type, collections=run))) 

3541 # Now add a dataset and see that we can get it back. 

3542 htm7 = registry.dimensions.skypix["htm"][7].pixelization 

3543 data_id = registry.expandDataId(instrument="Cam1", htm7=htm7.universe()[0][0]) 

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

3545 self.assertEqual( 

3546 set(registry.queryDataIds(["htm7", "instrument"], datasets=dataset_type, collections=run)), 

3547 {data_id}, 

3548 ) 

3549 self.assertEqual(set(registry.queryDatasets(dataset_type, collections=run)), {ref}) 

3550 

3551 def testDatasetIdFactory(self): 

3552 """Simple test for DatasetIdFactory, mostly to catch potential changes 

3553 in its API. 

3554 """ 

3555 butler = self.make_butler() 

3556 registry = butler.registry 

3557 factory = DatasetIdFactory() 

3558 dataset_type = DatasetType( 

3559 "datasetType", 

3560 dimensions=["detector", "instrument"], 

3561 universe=registry.dimensions, 

3562 storageClass="int", 

3563 ) 

3564 run = "run" 

3565 data_id = DataCoordinate.standardize( 

3566 instrument="Cam1", detector=1, dimensions=dataset_type.dimensions 

3567 ) 

3568 

3569 datasetId = factory.makeDatasetId(run, dataset_type, data_id, DatasetIdGenEnum.UNIQUE) 

3570 self.assertIsInstance(datasetId, uuid.UUID) 

3571 self.assertEqual(datasetId.version, 7) 

3572 

3573 datasetId = factory.makeDatasetId(run, dataset_type, data_id, DatasetIdGenEnum.DATAID_TYPE) 

3574 self.assertIsInstance(datasetId, uuid.UUID) 

3575 self.assertEqual(datasetId.version, 5) 

3576 

3577 datasetId = factory.makeDatasetId(run, dataset_type, data_id, DatasetIdGenEnum.DATAID_TYPE_RUN) 

3578 self.assertIsInstance(datasetId, uuid.UUID) 

3579 self.assertEqual(datasetId.version, 5) 

3580 

3581 def testExposureQueries(self): 

3582 """Test query methods using arguments sourced from the exposure log 

3583 service. 

3584 

3585 The most complete test dataset currently available to daf_butler tests 

3586 is ci_hsc-subset.yaml export , but that does not have 'exposure' 

3587 dimension records. So in this test we need to translate queries that 

3588 originally used the exposure dimension to use the (very similar) visit 

3589 dimension instead. 

3590 """ 

3591 butler = self.make_butler() 

3592 registry = butler.registry 

3593 self.load_data(butler, "ci_hsc-subset.yaml") 

3594 self.assertEqual( 

3595 [ 

3596 record.id 

3597 for record in registry.queryDimensionRecords("visit", instrument="HSC") 

3598 .order_by("visit") 

3599 .limit(5) 

3600 ], 

3601 [903334, 903336, 903338, 903342, 903344], 

3602 ) 

3603 self.assertEqual( 

3604 [ 

3605 data_id["visit"] 

3606 for data_id in registry.queryDataIds(["visit"], instrument="HSC").order_by("visit").limit(5) 

3607 ], 

3608 [903334, 903336, 903338, 903342, 903344], 

3609 ) 

3610 self.assertEqual( 

3611 [ 

3612 record.id 

3613 for record in registry.queryDimensionRecords("detector", instrument="HSC") 

3614 .order_by("full_name") 

3615 .limit(5) 

3616 ], 

3617 [25, 24, 23, 22, 18], 

3618 ) 

3619 self.assertEqual( 

3620 [ 

3621 data_id["detector"] 

3622 for data_id in registry.queryDataIds(["detector"], instrument="HSC") 

3623 .order_by("full_name") 

3624 .limit(5) 

3625 ], 

3626 [25, 24, 23, 22, 18], 

3627 ) 

3628 

3629 def test_long_query_names(self) -> None: 

3630 """Test that queries involving very long names are handled correctly. 

3631 

3632 This is especially important for PostgreSQL, which truncates symbols 

3633 longer than 64 chars, but it's worth testing for all DBs. 

3634 """ 

3635 butler = self.make_butler() 

3636 registry = butler.registry 

3637 name = "abcd" * 17 

3638 registry.registerDatasetType( 

3639 DatasetType( 

3640 name, 

3641 dimensions=(), 

3642 storageClass="Exposure", 

3643 universe=registry.dimensions, 

3644 ) 

3645 ) 

3646 # Need to search more than one collection actually containing a 

3647 # matching dataset to avoid optimizations that sidestep bugs due to 

3648 # truncation by making findFirst=True a no-op. 

3649 run1 = "run1" 

3650 registry.registerRun(run1) 

3651 run2 = "run2" 

3652 registry.registerRun(run2) 

3653 (ref1,) = registry.insertDatasets(name, [DataCoordinate.make_empty(registry.dimensions)], run1) 

3654 registry.insertDatasets(name, [DataCoordinate.make_empty(registry.dimensions)], run2) 

3655 self.assertEqual( 

3656 set(registry.queryDatasets(name, collections=[run1, run2], findFirst=True)), 

3657 {ref1}, 

3658 ) 

3659 

3660 def test_skypix_constraint_queries(self) -> None: 

3661 """Test queries spatially constrained by a skypix data ID.""" 

3662 butler = self.make_butler() 

3663 registry = butler.registry 

3664 self.load_data(butler, "base.yaml", "spatial.yaml") 

3665 patch_regions = { 

3666 (data_id["tract"], data_id["patch"]): data_id.region 

3667 for data_id in registry.queryDataIds(["patch"]).expanded() 

3668 } 

3669 skypix_dimension: SkyPixDimension = registry.dimensions["htm11"] 

3670 # This check ensures the test doesn't become trivial due to a config 

3671 # change; if it does, just pick a different HTML level. 

3672 self.assertNotEqual(skypix_dimension, registry.dimensions.commonSkyPix) 

3673 # Gather all skypix IDs that definitely overlap at least one of these 

3674 # patches. 

3675 relevant_skypix_ids = lsst.sphgeom.RangeSet() 

3676 for patch_region in patch_regions.values(): 

3677 relevant_skypix_ids |= skypix_dimension.pixelization.interior(patch_region) 

3678 # Look for a "nontrivial" skypix_id that overlaps at least one patch 

3679 # and does not overlap at least one other patch. 

3680 for skypix_id in itertools.chain.from_iterable( 

3681 range(begin, end) for begin, end in relevant_skypix_ids 

3682 ): 

3683 skypix_region = skypix_dimension.pixelization.pixel(skypix_id) 

3684 overlapping_patches = { 

3685 patch_key 

3686 for patch_key, patch_region in patch_regions.items() 

3687 if not patch_region.isDisjointFrom(skypix_region) 

3688 } 

3689 if overlapping_patches and overlapping_patches != patch_regions.keys(): 

3690 break 

3691 else: 

3692 raise RuntimeError("Could not find usable skypix ID for this dimension configuration.") 

3693 # Test that a three-way join that includes the common skypix system in 

3694 # the dimensions doesn't generate redundant join terms in the query. 

3695 with self.assertRaises(InvalidQueryError): 

3696 set( 

3697 registry.queryDataIds( 

3698 ["tract", "visit", "htm7"], skymap="SkyMap1", instrument="Cam1" 

3699 ).expanded() 

3700 ) 

3701 

3702 def test_spatial_constraint_queries(self) -> None: 

3703 """Test queries in which one spatial dimension in the constraint (data 

3704 ID or ``where`` string) constrains a different spatial dimension in the 

3705 query result columns. 

3706 """ 

3707 butler = self.make_butler() 

3708 registry = butler.registry 

3709 self.load_data(butler, "base.yaml", "spatial.yaml") 

3710 patch_regions = { 

3711 (data_id["tract"], data_id["patch"]): data_id.region 

3712 for data_id in registry.queryDataIds(["patch"]).expanded() 

3713 } 

3714 observation_regions = { 

3715 (data_id["visit"], data_id["detector"]): data_id.region 

3716 for data_id in registry.queryDataIds(["visit", "detector"]).expanded() 

3717 } 

3718 all_combos = { 

3719 (patch_key, observation_key) 

3720 for patch_key, observation_key in itertools.product(patch_regions, observation_regions) 

3721 } 

3722 overlapping_combos = { 

3723 (patch_key, observation_key) 

3724 for patch_key, observation_key in all_combos 

3725 if not patch_regions[patch_key].isDisjointFrom(observation_regions[observation_key]) 

3726 } 

3727 # Check a direct spatial join with no constraint first. 

3728 self.assertEqual( 

3729 { 

3730 ((data_id["tract"], data_id["patch"]), (data_id["visit"], data_id["detector"])) 

3731 for data_id in registry.queryDataIds(["patch", "visit", "detector"]) 

3732 }, 

3733 overlapping_combos, 

3734 ) 

3735 overlaps_by_patch: defaultdict[tuple[int, int], set[tuple[str, str]]] = defaultdict(set) 

3736 overlaps_by_observation: defaultdict[tuple[int, int], set[tuple[str, str]]] = defaultdict(set) 

3737 for patch_key, observation_key in overlapping_combos: 

3738 overlaps_by_patch[patch_key].add(observation_key) 

3739 overlaps_by_observation[observation_key].add(patch_key) 

3740 # Find patches and observations that overlap at least one of the other 

3741 # but not all of the other. 

3742 nontrivial_patch = next( 

3743 iter( 

3744 patch_key 

3745 for patch_key, observation_keys in overlaps_by_patch.items() 

3746 if observation_keys and observation_keys != observation_regions.keys() 

3747 ) 

3748 ) 

3749 nontrivial_observation = next( 

3750 iter( 

3751 observation_key 

3752 for observation_key, patch_keys in overlaps_by_observation.items() 

3753 if patch_keys and patch_keys != patch_regions.keys() 

3754 ) 

3755 ) 

3756 # Use the nontrivial patches and observations as constraints on the 

3757 # other dimensions in various ways, first via a 'where' expression. 

3758 # It's better in general to us 'bind' instead of f-strings, but these 

3759 # all integers so there are no quoting concerns. 

3760 self.assertEqual( 

3761 { 

3762 (data_id["visit"], data_id["detector"]) 

3763 for data_id in registry.queryDataIds( 

3764 ["visit", "detector"], 

3765 where=f"tract={nontrivial_patch[0]} AND patch={nontrivial_patch[1]}", 

3766 skymap="SkyMap1", 

3767 ) 

3768 }, 

3769 overlaps_by_patch[nontrivial_patch], 

3770 ) 

3771 self.assertEqual( 

3772 { 

3773 (data_id["tract"], data_id["patch"]) 

3774 for data_id in registry.queryDataIds( 

3775 ["patch"], 

3776 where=f"visit={nontrivial_observation[0]} AND detector={nontrivial_observation[1]}", 

3777 instrument="Cam1", 

3778 ) 

3779 }, 

3780 overlaps_by_observation[nontrivial_observation], 

3781 ) 

3782 # and then via the dataId argument. 

3783 self.assertEqual( 

3784 { 

3785 (data_id["visit"], data_id["detector"]) 

3786 for data_id in registry.queryDataIds( 

3787 ["visit", "detector"], 

3788 dataId={ 

3789 "tract": nontrivial_patch[0], 

3790 "patch": nontrivial_patch[1], 

3791 }, 

3792 skymap="SkyMap1", 

3793 ) 

3794 }, 

3795 overlaps_by_patch[nontrivial_patch], 

3796 ) 

3797 self.assertEqual( 

3798 { 

3799 (data_id["tract"], data_id["patch"]) 

3800 for data_id in registry.queryDataIds( 

3801 ["patch"], 

3802 dataId={ 

3803 "visit": nontrivial_observation[0], 

3804 "detector": nontrivial_observation[1], 

3805 }, 

3806 instrument="Cam1", 

3807 ) 

3808 }, 

3809 overlaps_by_observation[nontrivial_observation], 

3810 ) 

3811 

3812 def test_query_empty_collections(self) -> None: 

3813 """Test for registry query methods with empty collections. The methods 

3814 should return empty result set (or None when applicable) and provide 

3815 "doomed" diagnostics. 

3816 """ 

3817 butler = self.make_butler() 

3818 registry = butler.registry 

3819 self.load_data(butler, "base.yaml", "datasets.yaml") 

3820 

3821 # Tests for registry.findDataset() 

3822 with self.assertRaises(NoDefaultCollectionError): 

3823 registry.findDataset("bias", instrument="Cam1", detector=1) 

3824 self.assertIsNotNone(registry.findDataset("bias", instrument="Cam1", detector=1, collections=...)) 

3825 self.assertIsNone(registry.findDataset("bias", instrument="Cam1", detector=1, collections=[])) 

3826 

3827 # Tests for registry.queryDatasets() 

3828 with self.assertRaises(NoDefaultCollectionError): 

3829 registry.queryDatasets("bias") 

3830 self.assertTrue(list(registry.queryDatasets("bias", collections=...))) 

3831 

3832 result = registry.queryDatasets("bias", collections=[]) 

3833 self.assertEqual(len(list(result)), 0) 

3834 messages = list(result.explain_no_results()) 

3835 self.assertTrue(messages) 

3836 self.assertTrue(any("because collection list is empty" in message for message in messages)) 

3837 

3838 # Tests for registry.queryDataIds() 

3839 with self.assertRaises(NoDefaultCollectionError): 

3840 registry.queryDataIds("detector", datasets="bias") 

3841 self.assertTrue(list(registry.queryDataIds("detector", datasets="bias", collections=...))) 

3842 

3843 result = registry.queryDataIds("detector", datasets="bias", collections=[]) 

3844 self.assertEqual(len(list(result)), 0) 

3845 messages = list(result.explain_no_results()) 

3846 self.assertTrue(messages) 

3847 self.assertTrue(any("because collection list is empty" in message for message in messages)) 

3848 

3849 # Tests for registry.queryDimensionRecords() 

3850 with self.assertRaises(NoDefaultCollectionError): 

3851 registry.queryDimensionRecords("detector", datasets="bias") 

3852 self.assertTrue(list(registry.queryDimensionRecords("detector", datasets="bias", collections=...))) 

3853 

3854 result = registry.queryDimensionRecords("detector", datasets="bias", collections=[]) 

3855 self.assertEqual(len(list(result)), 0) 

3856 messages = list(result.explain_no_results()) 

3857 self.assertTrue(messages) 

3858 self.assertTrue(any("because collection list is empty" in message for message in messages)) 

3859 

3860 def test_dataset_followup_spatial_joins(self) -> None: 

3861 """Test queryDataIds(...).findRelatedDatasets(...) where a spatial join 

3862 is involved. 

3863 """ 

3864 butler = self.make_butler() 

3865 registry = butler.registry 

3866 self.load_data(butler, "base.yaml", "spatial.yaml") 

3867 pvi_dataset_type = DatasetType( 

3868 "pvi", {"visit", "detector"}, storageClass="StructuredDataDict", universe=registry.dimensions 

3869 ) 

3870 registry.registerDatasetType(pvi_dataset_type) 

3871 collection = "datasets" 

3872 registry.registerRun(collection) 

3873 (pvi1,) = registry.insertDatasets( 

3874 pvi_dataset_type, [{"instrument": "Cam1", "visit": 1, "detector": 1}], run=collection 

3875 ) 

3876 (pvi2,) = registry.insertDatasets( 

3877 pvi_dataset_type, [{"instrument": "Cam1", "visit": 1, "detector": 2}], run=collection 

3878 ) 

3879 (pvi3,) = registry.insertDatasets( 

3880 pvi_dataset_type, [{"instrument": "Cam1", "visit": 1, "detector": 3}], run=collection 

3881 ) 

3882 self.assertEqual( 

3883 set( 

3884 registry.queryDataIds(["patch"], skymap="SkyMap1", tract=0) 

3885 .expanded() 

3886 .findRelatedDatasets("pvi", [collection]) 

3887 ), 

3888 { 

3889 (registry.expandDataId(skymap="SkyMap1", tract=0, patch=0), pvi1), 

3890 (registry.expandDataId(skymap="SkyMap1", tract=0, patch=0), pvi2), 

3891 (registry.expandDataId(skymap="SkyMap1", tract=0, patch=1), pvi2), 

3892 (registry.expandDataId(skymap="SkyMap1", tract=0, patch=2), pvi1), 

3893 (registry.expandDataId(skymap="SkyMap1", tract=0, patch=2), pvi2), 

3894 (registry.expandDataId(skymap="SkyMap1", tract=0, patch=2), pvi3), 

3895 (registry.expandDataId(skymap="SkyMap1", tract=0, patch=3), pvi2), 

3896 (registry.expandDataId(skymap="SkyMap1", tract=0, patch=4), pvi3), 

3897 }, 

3898 ) 

3899 

3900 def test_expanded_data_id_queries(self) -> None: 

3901 """Tests for basic functionality of expanded() on queryDataIds and 

3902 queryDatasets. 

3903 """ 

3904 butler = self.make_butler() 

3905 registry = butler.registry 

3906 self.load_data(butler, "base.yaml", "spatial.yaml", "datasets.yaml") 

3907 

3908 result_obj = ( 

3909 registry.queryDataIds(["visit"], where="instrument = 'Cam1' and (visit.id = 1 or visit.id = 2)") 

3910 .expanded() 

3911 .order_by("visit.id") 

3912 ) 

3913 self.assertTrue(result_obj.hasRecords()) 

3914 visits = list(result_obj) 

3915 self.assertEqual(len(visits), 2) 

3916 

3917 self.assertEqual(visits[0]["visit"], 1) 

3918 self.assertEqual(visits[1]["visit"], 2) 

3919 self.assertEqual(visits[0].records["visit"].exposure_time, 60.0) 

3920 self.assertEqual(visits[1].records["visit"].exposure_time, 45.0) 

3921 # physical_filter is a "cacheable" dimension, so its records are loaded 

3922 # from local cache rather than being part of the DB rows. 

3923 self.assertEqual(visits[0].records["physical_filter"].band, "g") 

3924 self.assertEqual(visits[1].records["physical_filter"].band, "r") 

3925 

3926 # Make sure that we can fetch nulls in dimension records 

3927 registry.insertDimensionData( 

3928 "detector", 

3929 { 

3930 "instrument": "Cam1", 

3931 "id": 5, 

3932 "raft": "Z", 

3933 "name_in_raft": "z", 

3934 "full_name": "Zz", 

3935 "purpose": None, 

3936 }, 

3937 ) 

3938 detectors = list( 

3939 registry.queryDataIds("detector", dataId={"instrument": "Cam1", "detector": 5}).expanded() 

3940 ) 

3941 self.assertIsNone(detectors[0].records["detector"].purpose) 

3942 

3943 datasets_query = registry.queryDatasets( 

3944 "flat", collections="imported_g", where="instrument = 'Cam1' and detector <= 3" 

3945 ).expanded() 

3946 datasets = list(datasets_query) 

3947 datasets.sort(key=lambda ref: ref.dataId["detector"]) 

3948 self.assertEqual(len(datasets), 2) 

3949 self.assertEqual(datasets[0].id, uuid.UUID("60c8a65c-7290-4c38-b1de-e3b1cdcf872d")) 

3950 self.assertEqual(datasets[1].id, uuid.UUID("84239e7f-c41f-46d5-97b9-a27976b98ceb")) 

3951 # All of the dimensions for flat are "cached" dimensions. 

3952 self.assertEqual(datasets[0].dataId.records["detector"].full_name, "Ab") 

3953 self.assertEqual(datasets[1].dataId.records["detector"].full_name, "Ba") 

3954 self.assertEqual(datasets[0].dataId.records["instrument"].visit_system, 1) 

3955 assert isinstance(datasets_query, ParentDatasetQueryResults) 

3956 data_ids = list(datasets_query.dataIds) 

3957 data_ids.sort(key=lambda data_id: data_id["detector"]) 

3958 self.assertEqual(len(data_ids), 2) 

3959 self.assertEqual(data_ids[0].records["detector"].full_name, "Ab") 

3960 self.assertEqual(data_ids[1].records["detector"].full_name, "Ba") 

3961 self.assertEqual(data_ids[0].records["instrument"].visit_system, 1) 

3962 

3963 # None of the datasets in the test data include any uncached 

3964 # dimensions, so we have to set one up. 

3965 registry.registerDatasetType(DatasetType("test", ["visit"], "int", universe=registry.dimensions)) 

3966 registry.insertDatasets("test", [{"instrument": "Cam1", "visit": 1}], run="imported_g") 

3967 ref = list(registry.queryDatasets("test", collections="imported_g").expanded())[0] 

3968 self.assertEqual(ref.dataId.records["visit"].zenith_angle, 5.0) 

3969 self.assertEqual(ref.dataId.records["physical_filter"].band, "g") 

3970 self.assertEqual( 

3971 ref.dataId.timespan, 

3972 Timespan( 

3973 begin=astropy.time.Time("2021-09-09 03:00:00.000000000", scale="tai"), 

3974 end=astropy.time.Time("2021-09-09 03:01:00.000000000", scale="tai"), 

3975 ), 

3976 ) 

3977 

3978 def test_collection_summary(self) -> None: 

3979 """Test for collection summary methods.""" 

3980 butler = self.make_butler() 

3981 registry = butler.registry 

3982 self.load_data(butler, "base.yaml", "datasets.yaml", "spatial.yaml") 

3983 

3984 # Add one more dataset type, just for its existence to trigger a bug 

3985 # in `associate` (DM-44311). 

3986 test_dataset_type = DatasetType("test", ["tract", "patch"], "int", universe=registry.dimensions) 

3987 registry.registerDatasetType(test_dataset_type) 

3988 

3989 # Check for what has been imported. 

3990 summary = registry.getCollectionSummary("imported_g") 

3991 self.assertEqual(summary.dataset_types.names, {"bias", "flat"}) 

3992 self.assertEqual(summary.governors, {"instrument": {"Cam1"}}) 

3993 

3994 # Make a tagged collection and associate some datasets. 

3995 tagged_coll = "tagged" 

3996 registry.registerCollection(tagged_coll, CollectionType.TAGGED) 

3997 refsets = registry.queryDatasets(..., collections=["imported_g"]).byParentDatasetType() 

3998 for refs in refsets: 

3999 registry.associate(tagged_coll, refs) 

4000 

4001 # Summary has to have the same dataset types. 

4002 summary = registry.getCollectionSummary(tagged_coll) 

4003 self.assertEqual(summary.dataset_types.names, {"bias", "flat"}) 

4004 self.assertEqual(summary.governors, {"instrument": {"Cam1"}}) 

4005 

4006 # Remove all datasets from the tagged collection. 

4007 refs = list(registry.queryDatasets(..., collections=[tagged_coll])) 

4008 registry.disassociate(tagged_coll, refs) 

4009 

4010 # Summaries should not have changed. 

4011 summary = registry.getCollectionSummary(tagged_coll) 

4012 self.assertEqual(summary.dataset_types.names, {"bias", "flat"}) 

4013 self.assertEqual(summary.governors, {"instrument": {"Cam1"}}) 

4014 

4015 # Cleanup summaries. 

4016 registry.refresh_collection_summaries() 

4017 summary = registry.getCollectionSummary(tagged_coll) 

4018 self.assertFalse(summary.dataset_types.names) 

4019 # We do not clean governor summaries yet, but because how the query is 

4020 # run, it returns empty governors when collection is missing from 

4021 # summaries. 

4022 self.assertFalse(summary.governors) 

4023 

4024 # Add dataset with different governor, this is to test that governors 

4025 # are not actually cleaned. 

4026 refs = registry.insertDatasets("test", [{"skymap": "SkyMap1", "tract": 0, "patch": 0}], "imported_g") 

4027 registry.associate(tagged_coll, refs) 

4028 summary = registry.getCollectionSummary(tagged_coll) 

4029 self.assertEqual(summary.dataset_types.names, {"test"}) 

4030 # Note that instrument governor resurrects here, even though there are 

4031 # no datasets left with that governor. 

4032 self.assertEqual(summary.governors, {"instrument": {"Cam1"}, "skymap": {"SkyMap1"}}) 

4033 

4034 def test_temp_table_config(self) -> None: 

4035 config = self.makeRegistryConfig() 

4036 config["temporary_tables"] = False 

4037 self.assertEqual(config.areTemporaryTablesAllowed, False) 

4038 butler = self.make_butler(config) 

4039 if not isinstance(butler, DirectButler): 

4040 raise unittest.SkipTest("Test only makes sense for registry with direct database connection.") 

4041 self.assertEqual(butler._registry._db.supports_temporary_tables, False) 

4042 with self.assertRaisesRegex(ReadOnlyDatabaseError, "temporary tables"): 

4043 with butler._registry._db.temporary_table(...): 

4044 pass