Coverage for tests/test_metadata.py: 10%
146 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-19 01:33 -0800
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-19 01:33 -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 os
23import unittest
25import yaml
26from sqlalchemy import (
27 CheckConstraint,
28 Constraint,
29 ForeignKeyConstraint,
30 Index,
31 MetaData,
32 PrimaryKeyConstraint,
33 UniqueConstraint,
34 create_engine,
35)
37from felis import datamodel as dm
38from felis.datamodel import Schema
39from felis.db.utils import DatabaseContext
40from felis.metadata import MetaDataBuilder, get_datatype_with_variants
42TESTDIR = os.path.abspath(os.path.dirname(__file__))
43TEST_YAML = os.path.join(TESTDIR, "data", "sales.yaml")
46class MetaDataTestCase(unittest.TestCase):
47 """Test creation of SQLAlchemy metadata from a Felis schema."""
49 def setUp(self) -> None:
50 """Create an in-memory SQLite database and load the test data."""
51 self.engine = create_engine("sqlite://")
52 with open(TEST_YAML) as data:
53 self.yaml_data = yaml.safe_load(data)
55 def test_create_all(self) -> None:
56 """Create all tables in the schema using the metadata object and a
57 SQLite connection.
59 Check that the reflected metadata matches that created by the builder.
60 """
62 def _sorted_indexes(indexes: set[Index]) -> list[Index]:
63 """Return a sorted list of indexes."""
64 return sorted(indexes, key=lambda i: i.name)
66 def _sorted_constraints(constraints: set[Constraint]) -> list[Constraint]:
67 """Return a sorted list of constraints with the
68 `PrimaryKeyConstraint` objects filtered out.
69 """
70 return sorted(
71 [c for c in constraints if not isinstance(c, PrimaryKeyConstraint)], key=lambda c: c.name
72 )
74 schema = Schema.model_validate(self.yaml_data)
75 schema.name = "main"
76 builder = MetaDataBuilder(schema)
77 md = builder.build()
79 ctx = DatabaseContext(md, self.engine)
81 ctx.create_all()
83 md_db = MetaData()
84 md_db.reflect(self.engine.connect(), schema=schema.name)
86 self.assertEqual(md_db.tables.keys(), md.tables.keys())
88 for md_table_name in md.tables.keys():
89 md_table = md.tables[md_table_name]
90 md_db_table = md_db.tables[md_table_name]
91 self.assertEqual(md_table.columns.keys(), md_db_table.columns.keys())
92 for md_column_name in md_table.columns.keys():
93 md_column = md_table.columns[md_column_name]
94 md_db_column = md_db_table.columns[md_column_name]
95 self.assertEqual(type(md_column.type), type(md_db_column.type))
96 self.assertEqual(md_column.nullable, md_db_column.nullable)
97 self.assertEqual(md_column.primary_key, md_db_column.primary_key)
98 self.assertTrue(
99 (md_table.constraints and md_db_table.constraints)
100 or (not md_table.constraints and not md_table.constraints),
101 "Constraints not created correctly",
102 )
103 if md_table.constraints:
104 self.assertEqual(len(md_table.constraints), len(md_db_table.constraints))
105 md_constraints = _sorted_constraints(md_table.constraints)
106 md_db_constraints = _sorted_constraints(md_db_table.constraints)
107 for md_constraint, md_db_constraint in zip(md_constraints, md_db_constraints):
108 self.assertEqual(md_constraint.name, md_db_constraint.name)
109 self.assertEqual(md_constraint.deferrable, md_db_constraint.deferrable)
110 self.assertEqual(md_constraint.initially, md_db_constraint.initially)
111 self.assertEqual(
112 type(md_constraint), type(md_db_constraint), "Constraint types do not match"
113 )
114 if isinstance(md_constraint, ForeignKeyConstraint) and isinstance(
115 md_db_constraint, ForeignKeyConstraint
116 ):
117 md_fk: ForeignKeyConstraint = md_constraint
118 md_db_fk: ForeignKeyConstraint = md_db_constraint
119 self.assertEqual(md_fk.referred_table.name, md_db_fk.referred_table.name)
120 self.assertEqual(md_fk.column_keys, md_db_fk.column_keys)
121 self.assertEqual(md_fk.ondelete, md_db_fk.ondelete)
122 self.assertEqual(md_fk.onupdate, md_db_fk.onupdate)
123 elif isinstance(md_constraint, UniqueConstraint) and isinstance(
124 md_db_constraint, UniqueConstraint
125 ):
126 md_uniq: UniqueConstraint = md_constraint
127 md_db_uniq: UniqueConstraint = md_db_constraint
128 self.assertEqual(md_uniq.columns.keys(), md_db_uniq.columns.keys())
129 elif isinstance(md_constraint, CheckConstraint) and isinstance(
130 md_db_constraint, CheckConstraint
131 ):
132 md_check: CheckConstraint = md_constraint
133 md_db_check: CheckConstraint = md_db_constraint
134 self.assertEqual(str(md_check.sqltext), str(md_db_check.sqltext))
135 self.assertTrue(
136 (md_table.indexes and md_db_table.indexes) or (not md_table.indexes and not md_table.indexes),
137 "Indexes not created correctly",
138 )
139 if md_table.indexes:
140 md_indexes = _sorted_indexes(md_table.indexes)
141 md_db_indexes = _sorted_indexes(md_db_table.indexes)
142 self.assertEqual(len(md_indexes), len(md_db_indexes))
143 for md_index, md_db_index in zip(md_table.indexes, md_db_table.indexes):
144 self.assertEqual(md_index.name, md_db_index.name)
145 self.assertEqual(md_index.columns.keys(), md_db_index.columns.keys())
147 def test_builder(self) -> None:
148 """Test that the information in the metadata object created by the
149 builder matches the data in the Felis schema used to create it.
150 """
151 sch = Schema.model_validate(self.yaml_data)
152 bld = MetaDataBuilder(sch, apply_schema_to_metadata=False)
153 md = bld.build()
155 self.assertEqual(len(sch.tables), len(md.tables))
156 self.assertEqual([table.name for table in sch.tables], list(md.tables.keys()))
157 for table in sch.tables:
158 md_table = md.tables[table.name]
159 self.assertEqual(table.name, md_table.name)
160 self.assertEqual(len(table.columns), len(md_table.columns))
161 for column in table.columns:
162 md_table_column = md_table.columns[column.name]
163 datatype = get_datatype_with_variants(column)
164 self.assertEqual(type(datatype), type(md_table_column.type))
165 if column.nullable is not None:
166 self.assertEqual(column.nullable, md_table_column.nullable)
167 for constraint in table.constraints:
168 md_constraint = [mdc for mdc in md_table.constraints if mdc.name == constraint.name][0]
169 if isinstance(constraint, dm.ForeignKeyConstraint):
170 self.assertTrue(isinstance(md_constraint, ForeignKeyConstraint))
171 self.assertTrue(
172 sorted([sch[column_id].name for column_id in constraint.columns]),
173 sorted(md_constraint.columns.keys()),
174 )
175 elif isinstance(constraint, dm.UniqueConstraint):
176 self.assertEqual(
177 sorted([sch[column_id].name for column_id in constraint.columns]),
178 sorted(md_constraint.columns.keys()),
179 )
180 elif isinstance(constraint, dm.CheckConstraint):
181 self.assertEqual(constraint.expression, str(md_constraint.sqltext))
182 for index in table.indexes:
183 md_index = [mdi for mdi in md_table.indexes if mdi.name == index.name][0]
184 self.assertEqual(
185 sorted([sch[column_id].name for column_id in index.columns]),
186 sorted(md_index.columns.keys()),
187 )
188 if table.primary_key:
189 if isinstance(table.primary_key, str):
190 primary_keys = [sch[table.primary_key].name]
191 else:
192 primary_keys = [sch[pk].name for pk in table.primary_key]
193 for primary_key in primary_keys:
194 self.assertTrue(md_table.columns[primary_key].primary_key)
196 def test_timestamp(self) -> None:
197 """Test that the `timestamp` datatype is created correctly."""
198 for precision in [None, 6]:
199 col = dm.Column(
200 **{
201 "name": "timestamp_test",
202 "id": "#timestamp_test",
203 "datatype": "timestamp",
204 "precision": precision,
205 }
206 )
207 datatype = get_datatype_with_variants(col)
208 variant_dict = datatype._variant_mapping
209 self.assertTrue("mysql" in variant_dict)
210 self.assertTrue("postgresql" in variant_dict)
211 pg_timestamp = variant_dict["postgresql"]
212 self.assertEqual(pg_timestamp.timezone, False)
213 self.assertEqual(pg_timestamp.precision, precision)
214 mysql_timestamp = variant_dict["mysql"]
215 self.assertEqual(mysql_timestamp.timezone, False)
216 self.assertEqual(mysql_timestamp.fsp, precision)
218 def test_ignore_constraints(self) -> None:
219 """Test that constraints are not created when the
220 ``ignore_constraints`` flag is set on the metadata builder.
221 """
222 schema = Schema.model_validate(self.yaml_data)
223 schema.name = "main"
224 builder = MetaDataBuilder(schema, ignore_constraints=True)
225 md = builder.build()
226 for table in md.tables.values():
227 non_primary_key_constraints = [
228 c for c in table.constraints if not isinstance(c, PrimaryKeyConstraint)
229 ]
230 self.assertEqual(
231 len(non_primary_key_constraints),
232 0,
233 msg=f"Table {table.name} has non-primary key constraints defined",
234 )
236 def test_table_name_postfix(self) -> None:
237 """Test that table name postfixes are correctly applied."""
238 schema = Schema.model_validate(self.yaml_data)
239 schema.name = "main"
240 builder = MetaDataBuilder(schema, table_name_postfix="_test")
241 md = builder.build()
242 for table in md.tables.values():
243 self.assertTrue(table.name.endswith("_test"))
245 def test_fk_actions(self) -> None:
246 """Test that foreign key constraints with on delete and on update
247 actions are created correctly.
248 """
249 schema = Schema.model_validate(self.yaml_data)
250 schema.name = "main"
251 builder = MetaDataBuilder(schema)
252 md = builder.build()
254 for table in md.tables.values():
255 for constraint in table.constraints:
256 if isinstance(constraint, ForeignKeyConstraint):
257 self.assertIn(constraint.ondelete, ["SET NULL"])
258 self.assertIn(constraint.onupdate, ["CASCADE"])
261if __name__ == "__main__": 261 ↛ 262line 261 didn't jump to line 262 because the condition on line 261 was never true
262 unittest.main()