diff --git a/src/doctor/rules/index.py b/src/doctor/rules/index.py index 85eb873..7d967cb 100644 --- a/src/doctor/rules/index.py +++ b/src/doctor/rules/index.py @@ -38,3 +38,22 @@ class UnusedIndex(doctor.Rule): detail: str = "Index {indexrelname} is not used and occupied {index_size}." hint: str = ("Since the index '{indexrelname}' on table '{relation}' is not used," " you can remove it.") + +DUPLICATE_INDEX_QUERY = """ +SELECT indrelid::regclass as relation, + a.indexrelid::regclass as index1, + b.indexrelid::regclass as index2 + FROM pg_index a JOIN pg_index b USING (indrelid, indkey) + WHERE a.indexrelid != b.indexrelid; +""" + +@doctor.register +@dataclass +class DuplicateIndex(doctor.Rule): + """Find duplicate indexes.""" + + query: str = DUPLICATE_INDEX_QUERY + message: str = "index '{index1}' and '{index2}' seems to be duplicates" + detail: str = ("Index '{index1}' and '{index2}' are on the same relation " + "'{relation}' and has the same keys.") + hint: str = "You might want to remove one of the indexes to save space." diff --git a/src/doctor/rules/index_test.py b/src/doctor/rules/index_test.py new file mode 100644 index 0000000..67e729b --- /dev/null +++ b/src/doctor/rules/index_test.py @@ -0,0 +1,49 @@ +# Copyright 2023 Timescale, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for index checking rules.""" + +from timescaledb import Table + +from doctor.unittest import PostgreSQLTestCase +from doctor.rules.index import DuplicateIndex + +class TestIndexRules(PostgreSQLTestCase): + """Test index checking rules.""" + + def setUp(self): + """Set up unit tests for index checking rules.""" + table = Table("with_duplicate_index", { + "one": "int", + "two": "int", + }) + table.create(self.connection) + + with self.connection.cursor() as cursor: + cursor.execute("CREATE INDEX index_one ON with_duplicate_index(one,two)") + cursor.execute("CREATE INDEX index_two ON with_duplicate_index(one,two)") + self.connection.commit() + + def tearDown(self): + """Tear down unit tests for index checking rules.""" + with self.connection.cursor() as cursor: + cursor.execute("DROP TABLE with_duplicate_index") + self.connection.commit() + + def test_duplicate(self): + """Test rule for detecting duplicate index.""" + messages = [] + messages.extend(self.run_rule(DuplicateIndex())) + self.assertIn(DuplicateIndex.message.format(index1="index_one", index2="index_two"), + messages)