diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 937d4dd..77c9ad2 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -22,7 +22,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pylint psycopg2 + pip install pylint psycopg2 packaging - name: Analysing the code with pylint run: | pylint $(git ls-files '*.py') diff --git a/setup.py b/setup.py index 1e8ffb5..1eb5cdb 100644 --- a/setup.py +++ b/setup.py @@ -69,7 +69,8 @@ def get_version(rel_path: str) -> str: "Programming Language :: Python :: 3 :: Only", ], install_requires=[ - 'psycopg2', + 'psycopg2', + 'packaging', ], entry_points={ "console_scripts": [ diff --git a/src/doctor/__init__.py b/src/doctor/__init__.py index d0c9487..8de5707 100644 --- a/src/doctor/__init__.py +++ b/src/doctor/__init__.py @@ -21,6 +21,7 @@ from textwrap import dedent, fill from os.path import dirname, basename, isfile, join from abc import ABC +from packaging.version import parse import psycopg2 @@ -54,14 +55,31 @@ class Rule(ABC): optional field. If no field is given, the value of the "message" field will be used. + dependencies: Dictionary with dependencies on packages and what + versions that are required. + """ + def get_versions(self, conn): + """Get extension versions.""" + with conn.cursor() as cursor: + cursor.execute("SELECT extname, extversion FROM pg_extension WHERE extname IN %s", + (tuple(self.dependencies.keys()),)) # pylint: disable=E1101 + return {row["extname"]: row["extversion"] for row in cursor} + + def execute(self, conn, text): """Execute rule and return one string for each mismatching object.""" - cursor = conn.cursor() - cursor.execute(self.query) # pylint: disable=E1101 - return [text.format(**kwrds) for kwrds in cursor] - + with conn.cursor() as cursor: + # Check that all dependencies are met. If not, we do not + # execute the rule. + if hasattr(self, 'dependencies'): + versions = self.get_versions(conn) + for ext,req in self.dependencies.items(): # pylint: disable=E1101 + if ext not in versions or parse(req) > parse(versions[ext]): + return [] + cursor.execute(self.query) # pylint: disable=E1101 + return [text.format(**kwrds) for kwrds in cursor] def register(cls): """Register a rule.""" diff --git a/src/doctor/rules/compression.py b/src/doctor/rules/compression.py index 379319f..8eb0bcf 100644 --- a/src/doctor/rules/compression.py +++ b/src/doctor/rules/compression.py @@ -14,8 +14,6 @@ """Rules for compressed hypertables.""" -from dataclasses import dataclass - import doctor LINEAR_QUERY = """ @@ -30,7 +28,6 @@ """ @doctor.register -@dataclass class LinearSegmentby(doctor.Rule): """Detect segmentby column for compressed table.""" @@ -44,6 +41,9 @@ class LinearSegmentby(doctor.Rule): " with the number of rows of the table." ) + dependencies: dict = { + 'timescaledb': '1.0' + } POINTLESS_QUERY = """ SELECT format('%I.%I', schema_name, table_name)::regclass AS relation, s.attname @@ -57,7 +57,6 @@ class LinearSegmentby(doctor.Rule): """ @doctor.register -@dataclass class PointlessSegmentBy(doctor.Rule): """Detect pointless segmentby column in compressed table.""" @@ -69,3 +68,6 @@ class PointlessSegmentBy(doctor.Rule): "Column '{attname}' in hypertable '{relation}' as segment-by column is pointless" " since it contains a single value." ) + dependencies: dict = { + 'timescaledb': '1.0' + } diff --git a/src/doctor/rules/hypertable.py b/src/doctor/rules/hypertable.py index 6b73192..ce932e2 100644 --- a/src/doctor/rules/hypertable.py +++ b/src/doctor/rules/hypertable.py @@ -14,8 +14,6 @@ """Rules for hypertables.""" -from dataclasses import dataclass - import doctor CANDIDATE_QUERY = """ @@ -49,7 +47,6 @@ """ @doctor.register -@dataclass class HypertableCandidate(doctor.Rule): """Detect candidate hypertable.""" @@ -72,9 +69,11 @@ class HypertableCandidate(doctor.Rule): """ @doctor.register -@dataclass class ChunkPermissions(doctor.Rule): """Detect bad chunk permissions.""" query: str = PERMISSION_QUERY message: str = "Chunk '{chunk}' have different permissions from hypertable '{hypertable}'." + dependencies: dict = { + 'timescaledb': '1.0' + }