Coverage for python/lsst/daf/butler/registry/datasets/byDimensions/tables.py: 89%

114 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-13 01:31 -0800

1# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

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

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

7# for details of code ownership. 

8# 

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

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

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

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

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

14# 

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

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

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

18# (at your option) any later version. 

19# 

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

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

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

23# GNU General Public License for more details. 

24# 

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

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

27 

28from __future__ import annotations 

29 

30__all__ = ( 

31 "StaticDatasetTablesTuple", 

32 "addDatasetForeignKey", 

33 "makeCalibTableName", 

34 "makeCalibTableSpec", 

35 "makeStaticTableSpecs", 

36 "makeTagTableName", 

37 "makeTagTableSpec", 

38) 

39 

40from collections import namedtuple 

41from typing import Any, TypeAlias 

42 

43import sqlalchemy 

44 

45from lsst.utils.classes import immutable 

46 

47from .... import ddl 

48from ...._utilities.thread_safe_cache import ThreadSafeCache 

49from ....dimensions import DimensionGroup, DimensionUniverse, GovernorDimension, addDimensionForeignKey 

50from ....timespan_database_representation import TimespanDatabaseRepresentation 

51from ...interfaces import CollectionManager, Database, VersionTuple 

52 

53DATASET_TYPE_NAME_LENGTH = 128 

54 

55 

56class MissingDatabaseTableError(RuntimeError): 

57 """Exception raised when a table is not found in a database.""" 

58 

59 

60StaticDatasetTablesTuple = namedtuple( 

61 "StaticDatasetTablesTuple", 

62 [ 

63 "dataset_type", 

64 "dataset", 

65 ], 

66) 

67 

68 

69def addDatasetForeignKey( 

70 tableSpec: ddl.TableSpec, 

71 *, 

72 name: str = "dataset", 

73 onDelete: str | None = None, 

74 constraint: bool = True, 

75 **kwargs: Any, 

76) -> ddl.FieldSpec: 

77 """Add a foreign key column for datasets and (optionally) a constraint to 

78 a table. 

79 

80 This is an internal interface for the ``byDimensions`` package; external 

81 code should use `DatasetRecordStorageManager.addDatasetForeignKey` instead. 

82 

83 Parameters 

84 ---------- 

85 tableSpec : `ddl.TableSpec` 

86 Specification for the table that should reference the dataset 

87 table. Will be modified in place. 

88 name : `str`, optional 

89 A name to use for the prefix of the new field; the full name is 

90 ``{name}_id``. 

91 onDelete : `str`, optional 

92 One of "CASCADE" or "SET NULL", indicating what should happen to 

93 the referencing row if the collection row is deleted. `None` 

94 indicates that this should be an integrity error. 

95 constraint : `bool`, optional 

96 If `False` (`True` is default), add a field that can be joined to 

97 the dataset primary key, but do not add a foreign key constraint. 

98 **kwargs 

99 Additional keyword arguments are forwarded to the `ddl.FieldSpec` 

100 constructor (only the ``name`` and ``dtype`` arguments are 

101 otherwise provided). 

102 

103 Returns 

104 ------- 

105 idSpec : `ddl.FieldSpec` 

106 Specification for the ID field. 

107 """ 

108 idFieldSpec = ddl.FieldSpec(f"{name}_id", dtype=ddl.GUID, **kwargs) 

109 tableSpec.fields.add(idFieldSpec) 

110 if constraint: 

111 tableSpec.foreignKeys.append( 

112 ddl.ForeignKeySpec("dataset", source=(idFieldSpec.name,), target=("id",), onDelete=onDelete) 

113 ) 

114 return idFieldSpec 

115 

116 

117def makeStaticTableSpecs( 

118 collections: type[CollectionManager], 

119 universe: DimensionUniverse, 

120 schema_version: VersionTuple, 

121) -> StaticDatasetTablesTuple: 

122 """Construct all static tables used by the classes in this package. 

123 

124 Static tables are those that are present in all Registries and do not 

125 depend on what DatasetTypes have been registered. 

126 

127 Parameters 

128 ---------- 

129 collections : `CollectionManager` 

130 Manager object for the collections in this `Registry`. 

131 universe : `DimensionUniverse` 

132 Universe graph containing all dimensions known to this `Registry`. 

133 schema_version : `VersionTuple` 

134 The version of this schema. 

135 

136 Returns 

137 ------- 

138 specs : `StaticDatasetTablesTuple` 

139 A named tuple containing `ddl.TableSpec` instances. 

140 """ 

141 ingest_date_type: type 

142 ingest_date_default: Any = None 

143 if schema_version.major > 1: 143 ↛ 146line 143 didn't jump to line 146 because the condition on line 143 was always true

144 ingest_date_type = ddl.AstropyTimeNsecTai 

145 else: 

146 ingest_date_type = sqlalchemy.TIMESTAMP 

147 # New code provides explicit values for ingest_data, but we keep 

148 # default just to be consistent with the existing schema. 

149 ingest_date_default = sqlalchemy.sql.func.now() 

150 

151 specs = StaticDatasetTablesTuple( 

152 dataset_type=ddl.TableSpec( 

153 fields=[ 

154 ddl.FieldSpec( 

155 name="id", 

156 dtype=sqlalchemy.BigInteger, 

157 autoincrement=True, 

158 primaryKey=True, 

159 doc=( 

160 "Autoincrement ID that uniquely identifies a dataset " 

161 "type in other tables. Python code outside the " 

162 "`Registry` class should never interact with this; " 

163 "its existence is considered an implementation detail." 

164 ), 

165 ), 

166 ddl.FieldSpec( 

167 name="name", 

168 dtype=sqlalchemy.String, 

169 length=DATASET_TYPE_NAME_LENGTH, 

170 nullable=False, 

171 doc="String name that uniquely identifies a dataset type.", 

172 ), 

173 ddl.FieldSpec( 

174 name="storage_class", 

175 dtype=sqlalchemy.String, 

176 length=64, 

177 nullable=False, 

178 doc=( 

179 "Name of the storage class associated with all " 

180 "datasets of this type. Storage classes are " 

181 "generally associated with a Python class, and are " 

182 "enumerated in butler configuration." 

183 ), 

184 ), 

185 ddl.FieldSpec( 

186 name="dimensions_key", 

187 dtype=sqlalchemy.BigInteger, 

188 nullable=False, 

189 doc="Unique key for the set of dimensions that identifies datasets of this type.", 

190 ), 

191 ddl.FieldSpec( 

192 name="tag_association_table", 

193 dtype=sqlalchemy.String, 

194 length=128, 

195 nullable=False, 

196 doc=( 

197 "Name of the table that holds associations between " 

198 "datasets of this type and most types of collections." 

199 ), 

200 ), 

201 ddl.FieldSpec( 

202 name="calibration_association_table", 

203 dtype=sqlalchemy.String, 

204 length=128, 

205 nullable=True, 

206 doc=( 

207 "Name of the table that holds associations between " 

208 "datasets of this type and CALIBRATION collections. " 

209 "NULL values indicate dataset types with " 

210 "isCalibration=False." 

211 ), 

212 ), 

213 ], 

214 unique=[("name",)], 

215 ), 

216 dataset=ddl.TableSpec( 

217 fields=[ 

218 ddl.FieldSpec( 

219 name="id", 

220 dtype=ddl.GUID, 

221 primaryKey=True, 

222 doc="A unique field used as the primary key for dataset.", 

223 ), 

224 ddl.FieldSpec( 

225 name="dataset_type_id", 

226 dtype=sqlalchemy.BigInteger, 

227 nullable=False, 

228 doc="Reference to the associated entry in the dataset_type table.", 

229 ), 

230 ddl.FieldSpec( 

231 name="ingest_date", 

232 dtype=ingest_date_type, 

233 default=ingest_date_default, 

234 nullable=False, 

235 doc="Time of dataset ingestion.", 

236 ), 

237 # Foreign key field/constraint to run added below. 

238 ], 

239 foreignKeys=[ 

240 ddl.ForeignKeySpec("dataset_type", source=("dataset_type_id",), target=("id",)), 

241 ], 

242 ), 

243 ) 

244 # Add foreign key fields programmatically. 

245 collections.addRunForeignKey(specs.dataset, onDelete="CASCADE", nullable=False) 

246 return specs 

247 

248 

249def makeTagTableName(dimensionsKey: int) -> str: 

250 """Construct the name for a dynamic (DatasetType-dependent) tag table used 

251 by the classes in this package. 

252 

253 Parameters 

254 ---------- 

255 dimensionsKey : `int` 

256 Integer key used to save ``datasetType.dimensions`` to the database. 

257 

258 Returns 

259 ------- 

260 name : `str` 

261 Name for the table. 

262 """ 

263 return f"dataset_tags_{dimensionsKey:08d}" 

264 

265 

266def makeCalibTableName(dimensionsKey: int) -> str: 

267 """Construct the name for a dynamic (DatasetType-dependent) tag + validity 

268 range table used by the classes in this package. 

269 

270 Parameters 

271 ---------- 

272 dimensionsKey : `int` 

273 Integer key used to save ``datasetType.dimensions`` to the database. 

274 

275 Returns 

276 ------- 

277 name : `str` 

278 Name for the table. 

279 """ 

280 return f"dataset_calibs_{dimensionsKey:08d}" 

281 

282 

283def makeTagTableSpec( 

284 dimensions: DimensionGroup, collections: type[CollectionManager], *, constraints: bool = True 

285) -> ddl.TableSpec: 

286 """Construct the specification for a dynamic (DatasetType-dependent) tag 

287 table used by the classes in this package. 

288 

289 Parameters 

290 ---------- 

291 dimensions : `DimensionGroup` 

292 Dimensions of the dataset type. 

293 collections : `type` [ `CollectionManager` ] 

294 `CollectionManager` subclass that can be used to construct foreign keys 

295 to the run and/or collection tables. 

296 constraints : `bool`, optional 

297 If `False` (`True` is default), do not define foreign key constraints. 

298 

299 Returns 

300 ------- 

301 spec : `ddl.TableSpec` 

302 Specification for the table. 

303 """ 

304 tableSpec = ddl.TableSpec( 

305 fields=[ 

306 # Foreign key fields to dataset, collection, and usually dimension 

307 # tables added below. 

308 # The dataset_type_id field here would be redundant with the one 

309 # in the main monolithic dataset table, but we need it here for an 

310 # important unique constraint. 

311 ddl.FieldSpec("dataset_type_id", dtype=sqlalchemy.BigInteger, nullable=False), 

312 ] 

313 ) 

314 if constraints: 

315 tableSpec.foreignKeys.append( 

316 ddl.ForeignKeySpec("dataset_type", source=("dataset_type_id",), target=("id",)) 

317 ) 

318 # We'll also have a unique constraint on dataset type, collection, and data 

319 # ID. We only include the required part of the data ID, as that's 

320 # sufficient and saves us from worrying about nulls in the constraint. 

321 constraint = ["dataset_type_id"] 

322 # Add foreign key fields to dataset table (part of the primary key) 

323 addDatasetForeignKey(tableSpec, primaryKey=True, onDelete="CASCADE", constraint=constraints) 

324 # Add foreign key fields to collection table (part of the primary key and 

325 # the data ID unique constraint). 

326 collectionFieldSpec = collections.addCollectionForeignKey( 

327 tableSpec, primaryKey=True, onDelete="CASCADE", constraint=constraints 

328 ) 

329 constraint.append(collectionFieldSpec.name) 

330 # Add foreign key constraint to the collection_summary_dataset_type table. 

331 if constraints: 

332 tableSpec.foreignKeys.append( 

333 ddl.ForeignKeySpec( 

334 "collection_summary_dataset_type", 

335 source=(collectionFieldSpec.name, "dataset_type_id"), 

336 target=(collectionFieldSpec.name, "dataset_type_id"), 

337 ) 

338 ) 

339 for dimension_name in dimensions.required: 

340 dimension = dimensions.universe.dimensions[dimension_name] 

341 fieldSpec = addDimensionForeignKey( 

342 tableSpec, dimension=dimension, nullable=False, primaryKey=False, constraint=constraints 

343 ) 

344 constraint.append(fieldSpec.name) 

345 # If this is a governor dimension, add a foreign key constraint to the 

346 # collection_summary_<dimension> table. 

347 if isinstance(dimension, GovernorDimension) and constraints: 

348 tableSpec.foreignKeys.append( 

349 ddl.ForeignKeySpec( 

350 f"collection_summary_{dimension.name}", 

351 source=(collectionFieldSpec.name, fieldSpec.name), 

352 target=(collectionFieldSpec.name, fieldSpec.name), 

353 ) 

354 ) 

355 # Actually add the unique constraint. 

356 tableSpec.unique.add(tuple(constraint)) 

357 return tableSpec 

358 

359 

360def makeCalibTableSpec( 

361 dimensions: DimensionGroup, 

362 collections: type[CollectionManager], 

363 TimespanReprClass: type[TimespanDatabaseRepresentation], 

364) -> ddl.TableSpec: 

365 """Construct the specification for a dynamic (DatasetType-dependent) tag + 

366 validity range table used by the classes in this package. 

367 

368 Parameters 

369 ---------- 

370 dimensions : `DimensionGroup` 

371 Dimensions of the dataset type. 

372 collections : `type` [ `CollectionManager` ] 

373 `CollectionManager` subclass that can be used to construct foreign keys 

374 to the run and/or collection tables. 

375 TimespanReprClass : `type` of `TimespanDatabaseRepresentation` 

376 The Python type to use to represent a timespan. 

377 

378 Returns 

379 ------- 

380 spec : `ddl.TableSpec` 

381 Specification for the table. 

382 """ 

383 tableSpec = ddl.TableSpec( 

384 fields=[ 

385 # This table has no natural primary key, compound or otherwise, so 

386 # we add an autoincrement key. We may use this field a bit 

387 # internally, but its presence is an implementation detail and it 

388 # shouldn't appear as a foreign key in any other tables. 

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

390 # Foreign key fields to dataset, collection, and usually dimension 

391 # tables added below. The dataset_type_id field here is redundant 

392 # with the one in the main monolithic dataset table, but this bit 

393 # of denormalization lets us define what should be a much more 

394 # useful index. 

395 ddl.FieldSpec("dataset_type_id", dtype=sqlalchemy.BigInteger, nullable=False), 

396 ], 

397 foreignKeys=[ 

398 ddl.ForeignKeySpec("dataset_type", source=("dataset_type_id",), target=("id",)), 

399 ], 

400 ) 

401 # Record fields that should go in the temporal lookup index/constraint, 

402 # starting with the dataset type. 

403 index: list[str | type[TimespanDatabaseRepresentation]] = ["dataset_type_id"] 

404 # Add foreign key fields to dataset table (not part of the temporal 

405 # lookup/constraint). 

406 addDatasetForeignKey(tableSpec, nullable=False, onDelete="CASCADE") 

407 # Add foreign key fields to collection table (part of the temporal lookup 

408 # index/constraint). 

409 collectionFieldSpec = collections.addCollectionForeignKey(tableSpec, nullable=False, onDelete="CASCADE") 

410 index.append(collectionFieldSpec.name) 

411 # Add foreign key constraint to the collection_summary_dataset_type table. 

412 tableSpec.foreignKeys.append( 

413 ddl.ForeignKeySpec( 

414 "collection_summary_dataset_type", 

415 source=(collectionFieldSpec.name, "dataset_type_id"), 

416 target=(collectionFieldSpec.name, "dataset_type_id"), 

417 ) 

418 ) 

419 # Add dimension fields (part of the temporal lookup index.constraint). 

420 for dimension_name in dimensions.required: 

421 dimension = dimensions.universe.dimensions[dimension_name] 

422 fieldSpec = addDimensionForeignKey(tableSpec, dimension=dimension, nullable=False, primaryKey=False) 

423 index.append(fieldSpec.name) 

424 # If this is a governor dimension, add a foreign key constraint to the 

425 # collection_summary_<dimension> table. 

426 if isinstance(dimension, GovernorDimension): 

427 tableSpec.foreignKeys.append( 

428 ddl.ForeignKeySpec( 

429 f"collection_summary_{dimension.name}", 

430 source=(collectionFieldSpec.name, fieldSpec.name), 

431 target=(collectionFieldSpec.name, fieldSpec.name), 

432 ) 

433 ) 

434 # Add validity-range field(s) (part of the temporal lookup 

435 # index/constraint). 

436 tsFieldSpecs = TimespanReprClass.makeFieldSpecs(nullable=False) 

437 for fieldSpec in tsFieldSpecs: 

438 tableSpec.fields.add(fieldSpec) 

439 if TimespanReprClass.hasExclusionConstraint(): 439 ↛ 444line 439 didn't jump to line 444 because the condition on line 439 was never true

440 # This database's timespan representation can define a database-level 

441 # constraint that prevents overlapping validity ranges for entries with 

442 # the same DatasetType, collection, and data ID. 

443 # This also creates an index. 

444 index.append(TimespanReprClass) 

445 tableSpec.exclusion.add(tuple(index)) 

446 else: 

447 # No database-level constraint possible. We'll have to simulate that 

448 # in our DatasetRecordStorage.certify() implementation, and just create 

449 # a regular index here in the hope that helps with lookups. 

450 index.extend(fieldSpec.name for fieldSpec in tsFieldSpecs) 

451 tableSpec.indexes.add(ddl.IndexSpec(*index)) # type: ignore 

452 return tableSpec 

453 

454 

455TableCache: TypeAlias = ThreadSafeCache[str, sqlalchemy.Table] 

456 

457 

458@immutable 

459class DynamicTables: 

460 """A struct that holds the "dynamic" tables common to dataset types that 

461 share the same dimensions. 

462 

463 Objects of this class may be shared between multiple threads, so it must be 

464 immutable to prevent concurrency issues. 

465 

466 Parameters 

467 ---------- 

468 dimensions : `DimensionGroup` 

469 Dimensions of the dataset types that use these tables. 

470 dimensions_key : `int` 

471 Integer key used to persist this dimension group in the database and 

472 name the associated tables. 

473 tags_name : `str` 

474 Name of the "tags" table that associates datasets with data IDs in 

475 RUN and TAGGED collections. 

476 calibs_name : `str` or `None` 

477 Name of the "calibs" table that associates datasets with data IDs and 

478 timespans in CALIBRATION collections. This is `None` if none of the 

479 dataset types (or at least none of those seen by this client) are 

480 calibrations. 

481 """ 

482 

483 def __init__( 

484 self, dimensions: DimensionGroup, dimensions_key: int, tags_name: str, calibs_name: str | None 

485 ): 

486 self._dimensions = dimensions 

487 self.dimensions_key = dimensions_key 

488 self.tags_name = tags_name 

489 self.calibs_name = calibs_name 

490 

491 def copy(self, calibs_name: str) -> DynamicTables: 

492 return DynamicTables(self._dimensions, self.dimensions_key, self.tags_name, calibs_name) 

493 

494 @classmethod 

495 def from_dimensions_key( 

496 cls, dimensions: DimensionGroup, dimensions_key: int, is_calibration: bool 

497 ) -> DynamicTables: 

498 """Construct with table names generated from the dimension key. 

499 

500 Parameters 

501 ---------- 

502 dimensions : `DimensionGroup` 

503 Dimensions of the dataset types that use these tables. 

504 dimensions_key : `int` 

505 Integer key used to persist this dimension group in the database 

506 and name the associated tables. 

507 is_calibration : `bool` 

508 Whether any of the dataset types that use these tables are 

509 calibrations. 

510 

511 Returns 

512 ------- 

513 dynamic_tables : `DynamicTables` 

514 Struct that holds tables for a group of dataset types. 

515 """ 

516 return cls( 

517 dimensions, 

518 dimensions_key=dimensions_key, 

519 tags_name=makeTagTableName(dimensions_key), 

520 calibs_name=makeCalibTableName(dimensions_key) if is_calibration else None, 

521 ) 

522 

523 def create(self, db: Database, collections: type[CollectionManager], cache: TableCache) -> None: 

524 """Create the tables if they don't already exist. 

525 

526 Parameters 

527 ---------- 

528 db : `Database` 

529 Database interface. 

530 collections : `type` [ `CollectionManager` ] 

531 Manager class for collections; used to create foreign key columns 

532 for collections. 

533 cache : `DynamicTablesCache` 

534 Cache used to store sqlalchemy Table objects. 

535 """ 

536 if cache.get(self.tags_name) is None: 

537 cache.set_or_get( 

538 self.tags_name, 

539 db.ensureTableExists( 

540 self.tags_name, 

541 makeTagTableSpec(self._dimensions, collections), 

542 ), 

543 ) 

544 

545 if self.calibs_name is not None and cache.get(self.calibs_name) is None: 

546 cache.set_or_get( 

547 self.calibs_name, 

548 db.ensureTableExists( 

549 self.calibs_name, 

550 makeCalibTableSpec(self._dimensions, collections, db.getTimespanRepresentation()), 

551 ), 

552 ) 

553 

554 def add_calibs( 

555 self, db: Database, collections: type[CollectionManager], cache: TableCache 

556 ) -> DynamicTables: 

557 """Create a calibs table for a dataset type whose dimensions already 

558 have a tags table. 

559 

560 Parameters 

561 ---------- 

562 db : `Database` 

563 Database interface. 

564 collections : `type` [ `CollectionManager` ] 

565 Manager class for collections; used to create foreign key columns 

566 for collections. 

567 cache : `DynamicTablesCache` 

568 Cache used to store sqlalchemy Table objects. 

569 """ 

570 calibs_name = makeCalibTableName(self.dimensions_key) 

571 cache.set_or_get( 

572 calibs_name, 

573 db.ensureTableExists( 

574 calibs_name, 

575 makeCalibTableSpec(self._dimensions, collections, db.getTimespanRepresentation()), 

576 ), 

577 ) 

578 

579 return self.copy(calibs_name=calibs_name) 

580 

581 def tags(self, db: Database, collections: type[CollectionManager], cache: TableCache) -> sqlalchemy.Table: 

582 """Return the "tags" table that associates datasets with data IDs in 

583 TAGGED and RUN collections. 

584 

585 This method caches its result the first time it is called (and assumes 

586 the arguments it is given never change). 

587 

588 Parameters 

589 ---------- 

590 db : `Database` 

591 Database interface. 

592 collections : `type` [ `CollectionManager` ] 

593 Manager class for collections; used to create foreign key columns 

594 for collections. 

595 cache : `DynamicTablesCache` 

596 Cache used to store sqlalchemy Table objects. 

597 

598 Returns 

599 ------- 

600 table : `sqlalchemy.Table` 

601 SQLAlchemy table object. 

602 """ 

603 table = cache.get(self.tags_name) 

604 if table is not None: 

605 return table 

606 

607 spec = makeTagTableSpec(self._dimensions, collections) 

608 table = db.getExistingTable(self.tags_name, spec) 

609 if table is None: 609 ↛ 610line 609 didn't jump to line 610 because the condition on line 609 was never true

610 raise MissingDatabaseTableError(f"Table {self.tags_name!r} is missing from database schema.") 

611 return cache.set_or_get(self.tags_name, table) 

612 

613 def calibs( 

614 self, db: Database, collections: type[CollectionManager], cache: TableCache 

615 ) -> sqlalchemy.Table: 

616 """Return the "calibs" table that associates datasets with data IDs and 

617 timespans in CALIBRATION collections. 

618 

619 This method caches its result the first time it is called (and assumes 

620 the arguments it is given never change). It may only be called if the 

621 dataset type is calibration. 

622 

623 Parameters 

624 ---------- 

625 db : `Database` 

626 Database interface. 

627 collections : `type` [ `CollectionManager` ] 

628 Manager class for collections; used to create foreign key columns 

629 for collections. 

630 cache : `DynamicTablesCache` 

631 Cache used to store sqlalchemy Table objects. 

632 

633 Returns 

634 ------- 

635 table : `sqlalchemy.Table` 

636 SQLAlchemy table object. 

637 """ 

638 assert self.calibs_name is not None, ( 

639 "Dataset type should be checked to be calibration by calling code." 

640 ) 

641 table = cache.get(self.calibs_name) 

642 if table is not None: 642 ↛ 645line 642 didn't jump to line 645 because the condition on line 642 was always true

643 return table 

644 

645 spec = makeCalibTableSpec(self._dimensions, collections, db.getTimespanRepresentation()) 

646 table = db.getExistingTable(self.calibs_name, spec) 

647 if table is None: 

648 raise MissingDatabaseTableError(f"Table {self.calibs_name!r} is missing from database schema.") 

649 return cache.set_or_get(self.calibs_name, table)