Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functions and parsing in select keyword #5

Merged
merged 1 commit into from
Nov 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion flashdb/core/query/exceptions/query_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ class ValidationError(Exception):


class EmptyValueError(ValidationError):
pass
pass
19 changes: 19 additions & 0 deletions flashdb/core/query/functions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from flashdb.core.query.functions.aggregate_functions import Avg, Count, Max, Min, Sum
from flashdb.core.query.functions.string_functions import Length, Lower, Trim, Upper
from flashdb.core.query.functions.time_functions import Now
from flashdb.core.query.functions.misc_functions import Distinct


VALID_FUNCTIONS = {
"avg": Avg,
"count": Count,
"distinct": Distinct,
"length": Length,
"lower": Lower,
"max": Max,
"min": Min,
"now": Now,
"sum": Sum,
"trim": Trim,
"upper": Upper
}
21 changes: 21 additions & 0 deletions flashdb/core/query/functions/aggregate_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from flashdb.core.query.functions.base import Function


class Avg(Function):
name = "avg"


class Count(Function):
name = "count"


class Max(Function):
name = "max"


class Min(Function):
name = "min"


class Sum(Function):
name = "sum"
9 changes: 9 additions & 0 deletions flashdb/core/query/functions/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from abc import ABC


class Function(ABC):

def __eq__(self, other):
if isinstance(other, self.__class__):
return getattr(other, "name") == getattr(self, "name")
return False
5 changes: 5 additions & 0 deletions flashdb/core/query/functions/misc_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from flashdb.core.query.functions.base import Function


class Distinct(Function):
name = "distinct"
18 changes: 18 additions & 0 deletions flashdb/core/query/functions/string_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from flashdb.core.query.functions.base import Function


class Length(Function):
name = "length"


class Lower(Function):
name = "lower"


class Trim(Function):
name = "trim"


class Upper(Function):
name = "upper"

5 changes: 5 additions & 0 deletions flashdb/core/query/functions/time_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from flashdb.core.query.functions.base import Function


class Now(Function):
name = "now"
57 changes: 44 additions & 13 deletions flashdb/core/query/keywords/select_keyword.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,65 @@
from typing import Set, List, Dict
from typing import List

from flashdb.core.query.exceptions.query_exception import ParseException, EmptyValueError, ValidationError
from flashdb.core.query.functions import VALID_FUNCTIONS
from flashdb.core.query.keywords.base import Keyword


class Column(object):

def __init__(self, name: str, alias: str = None, functions: List = None):
self.name = name
self.alias = alias
self.functions = functions

def __eq__(self, other):
if isinstance(other, self.__class__):
print(other.__dict__)
return other.__dict__ == self.__dict__
return False


class SelectKeyword(Keyword):

NAME = "name"
AS = "as"
FUNCTION = "apply_function"
FUNCTION_DELIMITER = '>'

def __init__(self, s_query: List):
self.data = s_query
self.columns = []

def validate(self) -> "Keyword":
for item in self.data:
keys = item.keys()
if 'name' not in keys:
raise ValidationError("[SELECT] `name` is mandatory")
if not item['name']:
raise EmptyValueError(f"[SELECT] Empty `name` is not allowed")
if 'as' in keys and not item['as']:
raise EmptyValueError(f"[SELECT] Empty `as` is not allowed")
# check if function exists
if self.NAME not in keys:
raise ValidationError(f"[SELECT] `{self.NAME}` is mandatory")
if not item[self.NAME]:
raise EmptyValueError(f"[SELECT] Empty `{self.NAME}` is not allowed")
for keyword in [self.AS, self.FUNCTION]:
if keyword in keys and not item[keyword]:
raise EmptyValueError(f"[SELECT] Empty `{keyword}` is not allowed")
return self

def parse(self):
if not self.data:
return
for item in self.data:
self.columns.append({
'name': item['name'],
'as': item.get('as', None),
'functions': None # create function with a split
})
self.columns.append(Column(
item[self.NAME],
item.get(self.AS, None),
self.parse_functions(item.get(self.FUNCTION, None))
))
return self

def parse_functions(self, raw_functions: str):
if raw_functions is None:
return None
split_functions = raw_functions.split(self.FUNCTION_DELIMITER)
if any(f not in VALID_FUNCTIONS for f in split_functions):
raise ParseException(f"[SELECT] {raw_functions} is invalid")
functions = list()
for f_name in split_functions:
functions.append(VALID_FUNCTIONS[f_name]())
return functions
1 change: 1 addition & 0 deletions install/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parameterized
42 changes: 31 additions & 11 deletions tests/core/query/keywords/test_select.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import unittest
from typing import Dict

from flashdb.core.query.exceptions.query_exception import EmptyValueError
from flashdb.core.query.keywords.select_keyword import SelectKeyword
from parameterized import parameterized

from flashdb.core.query.exceptions.query_exception import EmptyValueError, ParseException
from flashdb.core.query.functions import Length, Avg
from flashdb.core.query.keywords.select_keyword import SelectKeyword, Column


class TestSelectKeyword(unittest.TestCase):
Expand All @@ -14,18 +18,34 @@ def test_validate_success(self):
}
SelectKeyword([expected]).validate()

def test_validate_emptyName_throwException(self):
with self.assertRaises(EmptyValueError):
SelectKeyword([{
"name": ""
}]).validate()
@parameterized.expand([
({"name": ""}, EmptyValueError),
({"name": "myColumn", "as": ""}, EmptyValueError),
({"name": "myColumn", "apply_function": ""}, EmptyValueError),
])
def test_validate_emptyFieldInItem_throwException(self, item: Dict, expected_exception: Exception):
with self.assertRaises(expected_exception):
SelectKeyword([item]).validate()

def test_parse_success(self):
expected = Column("myColumn", "toto", [
Avg(), Length()
])
select = SelectKeyword([{
"name": "myColumn",
"as": "toto",
"apply_function": "avg>length"
}]).parse()
self.assertEqual(1, len(select.columns))
self.assertEqual(expected, select.columns[0])

def test_validate_emptyAs_throwException(self):
with self.assertRaises(EmptyValueError):
def test_parse_unknownFunction_throwException(self):
with self.assertRaises(ParseException):
SelectKeyword([{
"name": "myColumn",
"as": ""
}]).validate()
"as": "toto",
"apply_function": "dfdf"
}]).parse()


if __name__ == "__main__":
Expand Down