Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 program is free software: you can redistribute it and/or modify 

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

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

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

21 

22from contextlib import contextmanager, closing 

23import os 

24import os.path 

25import secrets 

26from typing import List 

27import unittest 

28 

29import sqlalchemy 

30 

31from lsst.utils import doImport 

32from lsst.daf.butler import DimensionUniverse, ddl 

33from lsst.daf.butler.registry import RegistryConfig 

34from lsst.daf.butler.registry.databases.oracle import OracleDatabase 

35from lsst.daf.butler.registry import Registry 

36from lsst.daf.butler.registry.tests import DatabaseTests, RegistryTests 

37 

38ENVVAR = "DAF_BUTLER_ORACLE_TEST_URI" 

39TEST_URI = os.environ.get(ENVVAR) 

40 

41 

42def cleanUpPrefixes(connection: sqlalchemy.engine.Connection, prefixes: List[str]): 

43 """Drop all tables and other schema entities that start with any of the 

44 given prefixes. 

45 """ 

46 commands = [] 

47 dbapi = connection.connection 

48 with closing(dbapi.cursor()) as cursor: 

49 for objectType, objectName in cursor.execute("SELECT object_type, object_name FROM user_objects"): 

50 if not any(objectName.lower().startswith(prefix) for prefix in prefixes): 

51 continue 

52 if objectType == "TABLE": 

53 commands.append(f'DROP TABLE "{objectName}" CASCADE CONSTRAINTS') 

54 elif objectType in ("VIEW", "PROCEDURE", "SEQUENCE"): 

55 commands.append(f'DROP {objectType} "{objectName}"') 

56 for command in commands: 

57 cursor.execute(command) 

58 

59 

60@unittest.skipUnless(TEST_URI is not None, f"{ENVVAR} environment variable not set.") 

61class OracleDatabaseTestCase(unittest.TestCase, DatabaseTests): 

62 

63 @classmethod 

64 def setUpClass(cls): 

65 # Create a single engine for all Database instances we create, to avoid 

66 # repeatedly spending time connecting. 

67 cls._connection = OracleDatabase.connect(TEST_URI) 

68 cls._prefixes = [] 

69 

70 @classmethod 

71 def tearDownClass(cls): 

72 cleanUpPrefixes(cls._connection, cls._prefixes) 

73 

74 def makeEmptyDatabase(self, origin: int = 0) -> OracleDatabase: 

75 prefix = f"test_{secrets.token_hex(8).lower()}_" 

76 self._prefixes.append(prefix) 

77 return OracleDatabase(origin=origin, connection=self._connection, prefix=prefix) 

78 

79 def getNewConnection(self, database: OracleDatabase, *, writeable: bool) -> OracleDatabase: 

80 return OracleDatabase(origin=database.origin, connection=self._connection, 

81 prefix=database.prefix, writeable=writeable) 

82 

83 @contextmanager 

84 def asReadOnly(self, database: OracleDatabase) -> OracleDatabase: 

85 yield self.getNewConnection(database, writeable=False) 

86 

87 def testNameShrinking(self): 

88 """Test that too-long names for database entities other than tables 

89 and columns (which we preserve, and just expect to fit) are shrunk. 

90 """ 

91 db = self.makeEmptyDatabase(origin=1) 

92 with db.declareStaticTables(create=True) as context: 

93 # Table and field names are each below the 128-char limit even when 

94 # accounting for the prefix, but their combination (which will 

95 # appear in sequences and constraints) is not. 

96 tableName = "a_table_with_a_very_very_very_very_very_very_very_very_long_72_char_name" 

97 fieldName1 = "a_column_with_a_very_very_very_very_very_very_very_very_long_73_char_name" 

98 fieldName2 = "another_column_with_a_very_very_very_very_very_very_very_very_long_79_char_name" 

99 context.addTable( 

100 tableName, 

101 ddl.TableSpec( 

102 fields=[ 

103 ddl.FieldSpec( 

104 fieldName1, 

105 dtype=sqlalchemy.BigInteger, 

106 autoincrement=True, 

107 primaryKey=True 

108 ), 

109 ddl.FieldSpec( 

110 fieldName2, 

111 dtype=sqlalchemy.String, 

112 length=16, 

113 nullable=False, 

114 ), 

115 ], 

116 unique={(fieldName2,)}, 

117 ) 

118 ) 

119 # Add another table, this time dynamically, with a foreign key to the 

120 # first table. 

121 db.ensureTableExists( 

122 tableName + "_b", 

123 ddl.TableSpec( 

124 fields=[ 

125 ddl.FieldSpec( 

126 fieldName1 + "_b", 

127 dtype=sqlalchemy.BigInteger, 

128 autoincrement=True, 

129 primaryKey=True 

130 ), 

131 ddl.FieldSpec( 

132 fieldName2 + "_b", 

133 dtype=sqlalchemy.String, 

134 length=16, 

135 nullable=False, 

136 ), 

137 ], 

138 foreignKeys=[ 

139 ddl.ForeignKeySpec(tableName, source=(fieldName2 + "_b",), target=(fieldName2,)), 

140 ] 

141 ) 

142 ) 

143 

144 

145@unittest.skipUnless(TEST_URI is not None, f"{ENVVAR} environment variable not set.") 

146class OracleRegistryTestCase(unittest.TestCase, RegistryTests): 

147 """Tests for `Registry` backed by an `Oracle` database. 

148 """ 

149 

150 @classmethod 

151 def setUpClass(cls): 

152 # Create a single engine for all Database instances we create, to avoid 

153 # repeatedly spending time connecting. 

154 cls._connection = OracleDatabase.connect(TEST_URI) 

155 cls._prefixes = [] 

156 

157 @classmethod 

158 def tearDownClass(cls): 

159 cleanUpPrefixes(cls._connection, cls._prefixes) 

160 

161 @classmethod 

162 def getDataDir(cls) -> str: 

163 return os.path.normpath(os.path.join(os.path.dirname(__file__), "data", "registry")) 

164 

165 def makeRegistry(self) -> Registry: 

166 prefix = f"test_{secrets.token_hex(8).lower()}_" 

167 self._prefixes.append(prefix) 

168 config = RegistryConfig() 

169 # Can't use Registry.fromConfig for these tests because we don't want 

170 # to reconnect to the server every single time. But we at least use 

171 # OracleDatabase.fromConnection rather than the constructor so 

172 # we can try to pass a prefix through via "+" in a namespace. 

173 database = OracleDatabase.fromConnection(connection=self._connection, origin=0, 

174 namespace=f"+{prefix}") 

175 opaque = doImport(config["managers", "opaque"]) 

176 dimensions = doImport(config["managers", "dimensions"]) 

177 collections = doImport(config["managers", "collections"]) 

178 return Registry(database=database, 

179 opaque=opaque, 

180 dimensions=dimensions, 

181 collections=collections, 

182 universe=DimensionUniverse(config), create=True) 

183 

184 

185if __name__ == "__main__": 185 ↛ 186line 185 didn't jump to line 186, because the condition on line 185 was never true

186 unittest.main()