Coverage for tests / test_database_context.py: 31%
120 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 08:37 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 08:37 +0000
1# This file is part of felis.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://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 <https://www.gnu.org/licenses/>.
22import os
23import unittest
25import sqlalchemy
27try:
28 from testing.postgresql import Postgresql # type: ignore
29except ImportError:
30 Postgresql = None
32from felis.datamodel import Schema
33from felis.db.database_context import (
34 DatabaseContext,
35 DatabaseContextError,
36 DatabaseContextFactory,
37 MockContext,
38 MySQLContext,
39 PostgreSQLContext,
40 create_database_context,
41)
42from felis.metadata import MetaDataBuilder
44TEST_DIR = os.path.abspath(os.path.dirname(__file__))
45TEST_YAML = os.path.join(TEST_DIR, "data", "sales.yaml")
48class BaseDatabaseContextTest:
49 """Base tests of database context."""
51 def setUp(self) -> None:
52 """Set up the test case."""
53 self._schema = Schema.from_uri(TEST_YAML)
54 self._metadata = MetaDataBuilder(self._schema).build()
55 self._engine_url: str
56 self._dialect_name: str
58 def _create_database_context(self) -> DatabaseContext:
59 return create_database_context(self._engine_url, self._metadata)
61 def test_database_context(self):
62 """Test database context with SQLite."""
63 with self._create_database_context() as db_ctx:
64 self.assertIsNotNone(db_ctx)
65 self.assertIsNotNone(db_ctx.metadata)
66 if not isinstance(db_ctx, MockContext):
67 # Only a non-mock connection has a valid engine object.
68 self.assertIsNotNone(db_ctx.engine)
69 self.assertEqual(db_ctx.engine.dialect.name, self._dialect_name)
71 # Drop first in case it exists from a previous test
72 db_ctx.drop()
74 # Initialize the database
75 db_ctx.initialize()
77 # Create all tables
78 db_ctx.create_all()
80 # Drop indexes
81 db_ctx.drop_indexes()
83 # Create indexes
84 db_ctx.create_indexes()
86 # Determine schema prefix (none if schema name is not set)
87 if db_ctx.metadata.schema:
88 schema_prefix = f"{db_ctx.metadata.schema}."
89 else:
90 schema_prefix = ""
92 # Execute a simple query from a string
93 db_ctx.execute(f"SELECT * FROM {schema_prefix}customers")
95 # Execute a simple query from a TextClause
96 db_ctx.execute(sqlalchemy.text(f"SELECT * FROM {schema_prefix}orders"))
98 # Execute a query that raises an error, except for MockContext
99 if not isinstance(db_ctx, MockContext):
100 with self.assertRaises(DatabaseContextError):
101 db_ctx.execute("SELECT * FROM non_existent_table")
103 # Drop all tables
104 db_ctx.drop()
106 def test_initialize_is_idempotent(self):
107 """Test that attempting to create a schema that already exists does not
108 raise an error.
109 """
110 with self._create_database_context() as db_ctx:
111 # Ensure database does not exist by first dropping
112 db_ctx.drop()
114 # Initialize the database
115 db_ctx.initialize()
117 # Second initialization should not raise an error.
118 db_ctx.initialize()
120 # Clean up
121 db_ctx.drop()
123 def test_drop_is_idempotent(self):
124 """Test that dropping a schema that does not exist does not raise an
125 error.
126 """
127 with self._create_database_context() as db_ctx:
128 # Initialize the database
129 db_ctx.initialize()
131 # Drop the schema
132 db_ctx.drop()
134 # Second drop should not raise an error.
135 db_ctx.drop()
138class SQLiteTestCase(BaseDatabaseContextTest, unittest.TestCase):
139 """Tests of database context using SQLite dialect."""
141 def setUp(self) -> None:
142 """Set up the test case."""
143 super().setUp()
144 self._dialect_name = "sqlite"
145 self._engine_url = "sqlite:///:memory:"
148@unittest.skipIf(Postgresql is None, "testing.postgresql is not installed")
149class PostgreSQLTestCase(BaseDatabaseContextTest, unittest.TestCase):
150 """Tests of database context using PostgreSQL dialect."""
152 def setUp(self) -> None:
153 """Set up the test case."""
154 super().setUp()
155 self._dialect_name = "postgresql"
156 self._postgres = Postgresql()
157 self._engine_url = self._postgres.url()
159 def test_missing_schema_name(self):
160 """Test that a missing schema name raises an error when using
161 a Postgres context.
162 """
163 metadata = MetaDataBuilder(self._schema, apply_schema_to_metadata=False).build()
164 with self.assertRaises(DatabaseContextError):
165 PostgreSQLContext(sqlalchemy.engine.make_url(self._engine_url), metadata)
168class MySQLTestCase(BaseDatabaseContextTest, unittest.TestCase):
169 """Tests of MySQL database context."""
171 # Environment variable name for MySQL engine URL
172 _env_name = "_FELIS_MYSQL_ENGINE_URL"
174 def setUp(self) -> None:
175 super().setUp()
176 try:
177 mysql_engine_url = os.environ[f"{self._env_name}"]
178 except KeyError:
179 raise unittest.SkipTest(f"{self._env_name} is not set in the environment; skipping MySQL tests.")
180 if mysql_engine_url is None or mysql_engine_url == "":
181 raise ValueError(f"Value of {self._env_name} from environment is invalid")
182 self._dialect_name = "mysql"
183 self._engine_url = mysql_engine_url
185 def test_missing_schema_name(self):
186 """Test that a missing schema name raises an error when using
187 a MySQL context.
188 """
189 metadata = MetaDataBuilder(self._schema, apply_schema_to_metadata=False).build()
190 with self.assertRaises(DatabaseContextError):
191 MySQLContext(sqlalchemy.engine.make_url(self._engine_url), metadata)
194class MockTestCase(BaseDatabaseContextTest, unittest.TestCase):
195 """Tests of mock database context."""
197 def setUp(self) -> None:
198 super().setUp()
199 # This URL should result in a mock connection being setup.
200 self._engine_url = "sqlite://"
202 def test_mock_connection_engine_error(self):
203 """Test that attempting to access the engine of a mock context throws
204 an error.
205 """
206 db_ctx = create_database_context(self._engine_url, self._metadata)
207 with self.assertRaises(DatabaseContextError):
208 _ = db_ctx.engine
211class DatabaseContextTestCase(unittest.TestCase):
212 """Test that a mismatch between the engine and database context correctly
213 throws an error.
214 """
216 def test_create_with_bad_url(self):
217 """Test that using a SQLite engine with a Postgres context correctly
218 throws an error.
219 """
220 with self.assertRaises(DatabaseContextError):
221 engine = sqlalchemy.engine.make_url("sqlite:///:memory:")
222 PostgreSQLContext(engine, sqlalchemy.MetaData(schema="test_schema"))
224 def test_create_with_bad_dialect(self):
225 """Test that attempting to create a database context with an
226 unsupported dialect raises an error.
227 """
228 engine_url = "oracle+cx_oracle://user:password@host:1521/service_name"
229 metadata = sqlalchemy.MetaData()
230 with self.assertRaises(DatabaseContextError):
231 create_database_context(engine_url, metadata)
234class SupportedDialectsTestCase(unittest.TestCase):
235 """Test supported dialects."""
237 def test_supported_dialects(self) -> None:
238 """Test that supported dialects are correctly reported."""
239 supported_dialects = DatabaseContextFactory.get_supported_dialects()
240 self.assertIn("sqlite", supported_dialects)
241 self.assertIn("postgresql", supported_dialects)
242 self.assertIn("mysql", supported_dialects)
244 def test_supported_dialect_with_driver(self) -> None:
245 """Test that supported dialects with drivers are correctly
246 normalized.
247 """
248 supported_dialects = DatabaseContextFactory.get_supported_dialects()
249 # Test that postgresql+psycopg2 normalizes to postgresql
250 self.assertIn("postgresql", supported_dialects)
251 # Test that mysql+mysqlconnector normalizes to mysql
252 self.assertIn("mysql", supported_dialects)
254 def test_unsupported_dialect(self) -> None:
255 """Test that an unsupported dialect raises an error."""
256 metadata = sqlalchemy.MetaData()
257 with self.assertRaises(DatabaseContextError):
258 create_database_context("oracle+cx_oracle://user:pass@host/db", metadata)
259 with self.assertRaises(DatabaseContextError):
260 create_database_context("oracle://user:pass@host/db", metadata)
263if __name__ == "__main__": 263 ↛ 264line 263 didn't jump to line 264 because the condition on line 263 was never true
264 unittest.main()