Repository: kayak/pypika (python)

Issue:
Compensate missing functions or dialects with raw sql
We're starting to use PyPika with Redshift in my work, and we're facing some troubles as I already saw some people facing here.

The problem is that as PyPika tries to be a generic query builder (as it should be), but as we're using it with Redshift, we're facing some differences in function-wise. 

E.g.:

```
Query.from_('table').select(CurDate())
```
will return
```
SELECT CURRENT_DATE() FROM table
```

Redshift doesn't understand this CURRENT_DATE(). The solution that we're using now extends `Function` and create the `RawFunction`, a function that we can do something like this:

```
Query.from_('table').select(RawFunction('current_date'))
```
and will return
```
SELECT current_date FROM table
```

I understand that with it we can open margin for errors, but also we can give to the user more flexibility to the reserved functions for each type of database.

I'm opening a PR with my solution. I hope that we can discuss it to get into an agreement.

Also, I'm trying to implement a way to use the same function for different dialects, respecting its syntax. But I think it's a faster way to solve some problems.

Pull request description:
Custom Function implementation
As some dialects don't understand the current functions, it's a way to
use some reserved functions in all dialects without the need to change
the core for each.

This works as a Factory, it gets the function name and arguments and
return a callable that can be used in the query builder.

It also works for functions that don't need arguments.

It was discussed on PR #338 
It closes #337

Patch:
diff --git a/README.rst b/README.rst
index 0c6b899..23e89c9 100644
--- a/README.rst
+++ b/README.rst
@@ -628,6 +628,30 @@ There are several string operations and function wrappers included in |Brand|.
     SELECT id,CONCAT(fname, ' ', lname) full_name FROM customers
 
 
+Custom Functions
+"""""""""""""""""
+
+Custom Functions allows us to use any function on queries, as some functions are not covered by PyPika as default, we can appeal
+to Custom functions.
+
+.. code-block:: python
+
+    from pypika import CustomFunction
+
+    customers = Tables('customers')
+    DateDiff = CustomFunction('DATE_DIFF', ['interval', 'start_date', 'end_date'])
+
+    q = Query.from_(customers).select(
+        customers.id,
+        customers.fname,
+        customers.lname,
+        DateDiff('day', customers.created_date, customers.updated_date)
+    )
+
+.. code-block:: sql
+
+    SELECT id,fname,lname,DATE_DIFF('day',created_date,updated_date) FROM customers
+    
 Case Statements
 """""""""""""""
 
diff --git a/pypika/__init__.py b/pypika/__init__.py
index f3460a6..5268d36 100644
--- a/pypika/__init__.py
+++ b/pypika/__init__.py
@@ -73,6 +73,7 @@ from pypika.terms import (
     Parameter,
     Rollup,
     Tuple,
+    CustomFunction,
 )
 # noinspection PyUnresolvedReferences
 from pypika.utils import (
@@ -82,6 +83,7 @@ from pypika.utils import (
     QueryException,
     RollupException,
     UnionException,
+    FunctionException,
 )
 
 __author__ = 'Timothy Heys'
diff --git a/pypika/terms.py b/pypika/terms.py
index 9c821dd..3f8fa88 100644
--- a/pypika/terms.py
+++ b/pypika/terms.py
@@ -19,6 +19,7 @@ from pypika.enums import (
 )
 from pypika.utils import (
     CaseException,
+    FunctionException,
     builder,
     format_alias_sql,
     format_quotes,
@@ -1069,6 +1070,31 @@ class Not(Criterion):
         return self.term.tables_
 
 
+class CustomFunction():
+    def __init__(self, name, params=None):
+        self.name = name
+        self.params = params
+
+    def __call__(self, *args, **kwargs):
+        if not self._has_params():
+            return Function(self.name, alias=kwargs.get('alias'))
+
+        if not self._is_valid_function_call(*args):
+            raise FunctionException("Function {name} require these arguments ({params}), ({args}) passed".format(
+                                        name=self.name,
+                                        params=', '.join(str(p) for p in self.params),
+                                        args=', '.join(str(p) for p in args)
+                                    ))
+
+        return Function(self.name, *args, alias=kwargs.get('alias'))
+
+    def _has_params(self):
+        return self.params is not None
+
+    def _is_valid_function_call(self, *args):
+        return len(args) == len(self.params)
+
+
 class Function(Criterion):
     def __init__(self, name, *args, **kwargs):
         super(Function, self).__init__(kwargs.get('alias'))
diff --git a/pypika/utils.py b/pypika/utils.py
index 9865303..520e8e6 100644
--- a/pypika/utils.py
+++ b/pypika/utils.py
@@ -31,6 +31,9 @@ class RollupException(Exception):
 class DialectNotSupported(Exception):
     pass
 
+class FunctionException(Exception):
+    pass
+
 
 def builder(func):
     """


Test patch:
diff --git a/pypika/tests/test_custom_functions.py b/pypika/tests/test_custom_functions.py
new file mode 100644
index 0000000..8c60020
--- /dev/null
+++ b/pypika/tests/test_custom_functions.py
@@ -0,0 +1,49 @@
+import unittest
+
+from pypika import (
+    FunctionException,
+    Query,
+    Table,
+    functions as fn,
+    CustomFunction
+)
+
+__author__ = "Airton Zanon"
+__email__ = "me@airton.dev"
+
+class TestUnitCustomFunction(unittest.TestCase):
+
+    def test_should_fail_with_wrong_arguments(self):
+        DateDiff = CustomFunction('DATE_DIFF', ['interval', 'start_date', 'end_date']);
+
+        with self.assertRaises(FunctionException):
+            DateDiff('foo')
+    
+    def test_should_return_function_with_arguments(self):
+        DateDiff = CustomFunction('DATE_DIFF', ['interval', 'start_date', 'end_date']);
+
+        self.assertEqual("DATE_DIFF('day','start_date','end_date')", str(DateDiff('day', 'start_date', 'end_date')))
+
+    def test_should_return_function_with_no_arguments(self):
+        CurrentDate = CustomFunction('CURRENT_DATE');
+
+        self.assertEqual("CURRENT_DATE()", str(CurrentDate()))
+
+class TestFunctionalCustomFunction(unittest.TestCase):
+
+    def test_should_use_custom_function_on_select(self):
+        service = Table('service')
+
+        DateDiff = CustomFunction('DATE_DIFF', ['interval', 'start_date', 'end_date'])
+
+        q = Query.from_(service).select(DateDiff("day", service.created_date, service.updated_date))
+
+        self.assertEqual("SELECT DATE_DIFF('day',\"created_date\",\"updated_date\") FROM \"service\"", str(q))
+
+    def test_should_fail_use_custom_function_on_select_with_wrong_arguments(self):
+        service = Table('service')
+
+        DateDiff = CustomFunction('DATE_DIFF', ['interval', 'start_date', 'end_date'])
+
+        with self.assertRaises(FunctionException):
+            Query.from_(service).select(DateDiff("day", service.created_date))
\ No newline at end of file