Coverage for tests/test_tap_schema_postgres.py: 29%
82 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-07 01:49 -0800
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-07 01:49 -0800
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 gc
23import os
24import unittest
26from sqlalchemy.engine import create_engine
27from sqlalchemy.exc import SQLAlchemyError
28from sqlalchemy.schema import CreateSchema
30from felis.datamodel import Schema
31from felis.db.utils import DatabaseContext
32from felis.metadata import MetaDataBuilder
33from felis.tap_schema import DataLoader, TableManager
35try:
36 from testing.postgresql import Postgresql
37except ImportError:
38 Postgresql = None
40TEST_DIR = os.path.abspath(os.path.dirname(__file__))
41TEST_SALES = os.path.join(TEST_DIR, "data", "sales.yaml")
42TEST_TAP_SCHEMA_NONSTD = os.path.join(TEST_DIR, "data", "test_tap_schema_nonstandard.yaml")
45class TestTapSchemaPostgresql(unittest.TestCase):
46 """Test TAP_SCHEMA for PostgreSQL"""
48 def setUp(self) -> None:
49 """Set up a local PostgreSQL database and a test schema."""
50 # Skip the test if the testing.postgresql package is not installed.
51 if not Postgresql:
52 self.skipTest("testing.postgresql not installed")
54 # Start a PostgreSQL database for testing.
55 self.postgresql = Postgresql()
56 url = self.postgresql.url()
57 self.engine = create_engine(url)
59 # Setup a test schema.
60 self.test_schema = Schema.from_uri(TEST_SALES)
62 def test_create_metadata(self) -> None:
63 """Test loading of data into a PostgreSQL TAP_SCHEMA database created
64 by the `~felis.tap_schema.TableManager`.
65 """
66 try:
67 # Create the TAP_SCHEMA database.
68 mgr = TableManager()
69 mgr.initialize_database(self.engine)
71 # Load the test data into the database.
72 loader = DataLoader(self.test_schema, mgr, self.engine, 1)
73 loader.load()
74 finally:
75 # Drop the schema.
76 DatabaseContext(metadata=mgr.metadata, engine=self.engine).drop()
78 def test_reflect_database(self) -> None:
79 """Test reflecting an existing PostgreSQL TAP_SCHEMA database into a
80 `~felis.tap_schema.TableManager`.
81 """
82 try:
83 # Build the TAP_SCHEMA database independently of the TableManager.
84 schema = TableManager.load_schema_resource()
85 md = MetaDataBuilder(schema).build()
86 with self.engine.connect() as conn:
87 trans = conn.begin()
88 try:
89 print(f"Creating schema '{schema.name}'")
90 conn.execute(CreateSchema(schema.name, if_not_exists=False))
91 trans.commit()
92 except SQLAlchemyError as e:
93 trans.rollback()
94 self.fail(f"Failed to create schema: {e}")
95 try:
96 print(f"Creating tables in schema: {md.schema}")
97 md.create_all(self.engine)
98 except SQLAlchemyError as e:
99 self.fail(f"Failed to create database: {e}")
101 # Reflect the existing database into a TableManager.
102 mgr = TableManager(engine=self.engine)
103 self.assertIsNotNone(mgr.metadata)
104 self.assertGreater(len(mgr.metadata.tables), 0)
105 table_names = set(
106 [table_name.replace(f"{schema.name}.", "") for table_name in mgr.metadata.tables.keys()]
107 )
108 self.assertEqual(table_names, set(TableManager.get_table_names_std()))
110 # See if test data can be loaded successfully using the existing
111 # database.
112 loader = DataLoader(self.test_schema, mgr, self.engine, 1)
113 loader.load()
114 finally:
115 # Drop the schema.
116 DatabaseContext(metadata=mgr.metadata, engine=self.engine).drop()
118 def test_nonstandard_names(self) -> None:
119 """Test the TAP table manager class with non-standard names for the
120 schema and columns, which are present in the test YAML file used
121 to create the TAP_SCHEMA database.
122 """
123 try:
124 with open(TEST_TAP_SCHEMA_NONSTD) as file:
125 sch = Schema.from_stream(file, context={"id_generation": True})
126 md = MetaDataBuilder(sch).build()
127 ctx = DatabaseContext(md, self.engine)
128 ctx.initialize()
129 ctx.create_all()
131 postfix = "11"
132 mgr = TableManager(engine=self.engine, table_name_postfix=postfix, schema_name=sch.name)
133 for table_name in mgr.get_table_names_std():
134 table = mgr[table_name]
135 self.assertEqual(table.name, f"{table_name}{postfix}".replace(f"{sch.name}", ""))
136 finally:
137 if ctx:
138 ctx.drop()
140 def test_bad_engine(self) -> None:
141 """Test the TableManager class with an invalid engine."""
142 bad_engine = create_engine("postgresql+psycopg2://fake_user:fake_password@fake_host:5555")
143 with self.assertRaises(SQLAlchemyError):
144 TableManager(engine=bad_engine)
146 def tearDown(self) -> None:
147 """Tear down the test case."""
148 gc.collect()
149 self.engine.dispose()
152if __name__ == "__main__": 152 ↛ 153line 152 didn't jump to line 153 because the condition on line 152 was never true
153 unittest.main()