Skip to content

Commit

Permalink
Feature/implement tests (#1)
Browse files Browse the repository at this point in the history
* Setup new project structure using setup.cfg

* Add Static typing, linting and test libraries

- Lint all files using flake8

* Disable VS Code linting

* Improve robustness of query functions

* Add query function tests

* Add full code coverage to tests

* Linting

* Setup Github actions

* Change commands in tox.ini

* Update README
  • Loading branch information
leonardomathon authored Mar 8, 2022
1 parent 73ea3aa commit b65f120
Show file tree
Hide file tree
Showing 19 changed files with 251 additions and 49 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Tests

on:
- push
- pull_request

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ['3.6', '3.7', '3.8', '3.9']

steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Test with tox
run: tox
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"python.linting.enabled": true,
"python.linting.enabled": false,
"python.linting.pylintEnabled": true
}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

---

![Tests](https://github.com/mCodingLLC/SlapThatLikeButton-TestingStarterProject/actions/workflows/tests.yml/badge.svg)

## About SQLEyes

Sqleyes is a CLI tool for analyzing simple, raw SQL queries for common sql anti-patterns.
Expand Down
1 change: 0 additions & 1 deletion VERSION

This file was deleted.

23 changes: 23 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[build-system]
requires = ["setuptools>=42.0", "wheel"]
build-backend = "setuptools.build_meta"

[tool.pytest.ini_options]
addopts = "--cov=sqleyes"
testpaths = [
"tests",
]

[tool.mypy]
mypy_path = "."
check_untyped_defs = true
disallow_any_generics = true
ignore_missing_imports = true
no_implicit_optional = true
show_error_codes = true
strict_equality = true
warn_redundant_casts = true
warn_return_any = true
warn_unreachable = true
warn_unused_configs = true
no_implicit_reexport = true
5 changes: 5 additions & 0 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
flake8==4.0.1
tox==3.24.5
pytest==7.0.1
pytest-cov==3.0.0
mypy===0.931
47 changes: 47 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[metadata]
name = sqleyes
version = 0.1.0
description = A CLI in Python that analyzes raw SQL queries for common anti-patterns
long_description_content_type = text/markdown
long_description = file: README.md
keywords = SQL, anti-patterns, analysis
author = Leonardo Mathon
author_email = [email protected]
home_page = leonardomathon.nl
license = MIT
license_file = LICENSE
requires_dist = setuptools
platforms = unix, linux, osx, cygwin, win32
classifiers =
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
License :: OSI Approved :: MIT License
Operating System :: OS Independent

[options]
packages =
sqleyes
install_requires =
sqlparse>=0.4.2
python_requires = >=3.6
package_dir =
=.
zip_safe = no

[options.extras_require]
testing =
pytest>=7.0
pytest-cov>=3.0
mypy>=0.910
flake8>=4.0
tox>=3.24

[options.package_data]
sqleyes = py.typed

[flake8]
max-line-length = 80
34 changes: 3 additions & 31 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,4 @@
from setuptools import setup, find_packages
from setuptools import setup

def read_requirements(file):
with open(file) as f:
return f.read().splitlines()

def read_file(file):
with open(file) as f:
return f.read()

long_description = read_file("README.md")
version = read_file("VERSION")
requirements = read_requirements("requirements.txt")

setup(
name = 'sqleyes',
version = version,
author = 'Leonardo Mathon',
author_email = '[email protected]',
url = 'leonardomathon.nl',
description = 'A CLI in Python that analyzes raw SQL queries for common anti-patterns',
long_description_content_type = "text/markdown", # If this causes a warning, upgrade your setuptools package
long_description = long_description,
license = "MIT license",
packages = find_packages(exclude=["test"]), # Don't include test directory in binary distribution
install_requires = requirements,
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
] # Update these accordingly
)
if __name__ == "__main__":
setup()
1 change: 1 addition & 0 deletions sqleyes/detector/abstract_ap.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Abstract anti-pattern detector class"""
from abc import ABC, abstractmethod


class APDetector(ABC):
"""
This is a class for detecting anti-patterns.
Expand Down
9 changes: 5 additions & 4 deletions sqleyes/detector/ambiguous_groups_ap.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
from sqleyes.detector.abstract_ap import APDetector
from sqleyes.detector.detector_output import DetectorOutput
from sqleyes.utils.query_functions import (check_single_value_rule,
get_columns_from_group_by_statement,
get_columns_from_select_statement)
get_columns_from_group_by_statement,
get_columns_from_select_statement)


class AmbiguousGroupsAPDetector(APDetector):
type = "Ambiguous Groups"
Expand All @@ -21,7 +22,7 @@ def check(self):

# Get columns in SELECT & GROUP BY statement
select_columns = get_columns_from_select_statement(self.query)
group_columns = get_columns_from_group_by_statement(self.query)
group_columns = get_columns_from_group_by_statement(self.query)

# Get columns which are in select_columns but not in group_columns
remaining_columns = list(set(select_columns) - set(group_columns))
Expand All @@ -31,7 +32,7 @@ def check(self):

if not single_values:
return DetectorOutput(detector_type=self.detector_type,
type=self.type)
type=self.type)

return None

Expand Down
8 changes: 5 additions & 3 deletions sqleyes/detector/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from sqleyes.detector.fear_of_the_unknown_ap import FearOfTheUnknownApDetector
from sqleyes.detector.implicit_columns_ap import ImplicitColumnsAPDetector


class Detector:
"""
This is a Detector class that is responsible for detecting errors
Expand All @@ -16,7 +17,7 @@ class Detector:

def __init__(self, query: str):
self.query = query
self.anti_pattern_list = []
self.anti_pattern_list: List[DetectorOutput] = []

def run(self) -> List[DetectorOutput]:
"""
Expand All @@ -30,10 +31,11 @@ def run(self) -> List[DetectorOutput]:
self.anti_pattern_list.append(ap_implicit_col)

ap_fear_of_the_unknown = FearOfTheUnknownApDetector(query=self.query) \
.check()
.check()
self.anti_pattern_list.append(ap_fear_of_the_unknown)

ap_ambiguous_groups = AmbiguousGroupsAPDetector(query=self.query).check()
ap_ambiguous_groups = AmbiguousGroupsAPDetector(query=self.query) \
.check()
self.anti_pattern_list.append(ap_ambiguous_groups)

return [ap for ap in self.anti_pattern_list if ap is not None]
5 changes: 3 additions & 2 deletions sqleyes/detector/detector_output.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Detector Ouput class specifying general output format"""
import json


class DetectorOutput:
"""
This class represents the output of a detector.
Expand All @@ -15,8 +16,8 @@ def __init__(self, detector_type: str, type: str):

def __create_dictionary(self):
return {
"type": self.type,
"detector_type": self.detector_type
"type": self.type,
"detector_type": self.detector_type
}

def __repr__(self):
Expand Down
3 changes: 2 additions & 1 deletion sqleyes/detector/fear_of_the_unknown_ap.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from sqleyes.detector.abstract_ap import APDetector
from sqleyes.detector.detector_output import DetectorOutput


class FearOfTheUnknownApDetector(APDetector):
type = "Fear of the Unknown"

Expand All @@ -16,6 +17,6 @@ def check(self):
for pattern in patterns:
if pattern.search(self.query):
return DetectorOutput(detector_type=self.detector_type,
type=self.type)
type=self.type)

return None
5 changes: 4 additions & 1 deletion sqleyes/detector/implicit_columns_ap.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from sqleyes.detector.abstract_ap import APDetector
from sqleyes.detector.detector_output import DetectorOutput


class ImplicitColumnsAPDetector(APDetector):

type = "Implict Columns"

def __init__(self, query):
Expand All @@ -13,6 +15,7 @@ def check(self):
pattern = re.compile("(SELECT\\s+\\*)", re.IGNORECASE)

if pattern.search(self.query):
return DetectorOutput(detector_type=self.detector_type, type=self.type)
return DetectorOutput(detector_type=self.detector_type,
type=self.type)

return None
4 changes: 3 additions & 1 deletion sqleyes/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import argparse
from sqleyes.detector.detector import Detector


def run(query: str):
"""
This function bootstraps the detector and runs it
Expand All @@ -15,12 +16,13 @@ def run(query: str):
"""
return Detector(query).run()


# Setup argument parser
parser = argparse.ArgumentParser(
description="Analyze raw SQL queries for anti-patterns")

parser.add_argument('-q', '--query', metavar="", type=str, required=True,
help="A raw SQL query to analyze")
help="A raw SQL query to analyze")

args = parser.parse_args()

Expand Down
Empty file added sqleyes/py.typed
Empty file.
11 changes: 7 additions & 4 deletions sqleyes/utils/query_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from sqleyes.utils.query_keywords import SQL_FUNCTIONS


def get_columns_from_select_statement(query: str) -> List[str]:
"""
This function takes a query string as input and returns a list of columns
Expand All @@ -18,7 +19,7 @@ def get_columns_from_select_statement(query: str) -> List[str]:
columns (list): A list of columns selected in the SELECT statement.
"""
columns = re.findall(r'SELECT (.*?) FROM', query,
flags=re.DOTALL | re.IGNORECASE)
flags=re.DOTALL | re.IGNORECASE)

if len(columns) == 0:
return []
Expand All @@ -27,6 +28,7 @@ def get_columns_from_select_statement(query: str) -> List[str]:
columns = [column.strip() for column in columns]
return columns


def get_columns_from_group_by_statement(query: str) -> List[str]:
"""
This function takes a query string as input and returns a list of columns
Expand All @@ -46,7 +48,7 @@ def get_columns_from_group_by_statement(query: str) -> List[str]:
break

# Query has no GROUP BY statement
if i == len(tokens):
if i == len(tokens) - 1:
return []

# Find possible index of next keyword
Expand All @@ -56,7 +58,7 @@ def get_columns_from_group_by_statement(query: str) -> List[str]:

# Get column names
group_columns = []
for item in tokens[i : j + 1]:
for item in tokens[i:j + 1]:
if isinstance(item, sqlparse.sql.IdentifierList):
for identifier in item.get_identifiers():
group_columns.append(identifier.get_name())
Expand All @@ -65,6 +67,7 @@ def get_columns_from_group_by_statement(query: str) -> List[str]:

return group_columns


def check_single_value_rule(columns: List[str]) -> bool:
"""
This function checks if the columns in the list break the single-value
Expand All @@ -84,7 +87,7 @@ def check_single_value_rule(columns: List[str]) -> bool:
single_value = True

for function in SQL_FUNCTIONS:
if column.find(function) != -1:
if re.search(function, column, re.IGNORECASE):
single_value = True

if not single_value:
Expand Down
Loading

0 comments on commit b65f120

Please sign in to comment.