diff --git a/pypika/__init__.py b/pypika/__init__.py index 78295a02..3a8254dd 100644 --- a/pypika/__init__.py +++ b/pypika/__init__.py @@ -68,4 +68,4 @@ __author__ = "Timothy Heys" __email__ = "theys@kayak.com" -__version__ = "0.8.0" +__version__ = "0.9.0" diff --git a/pypika/functions.py b/pypika/functions.py index 2414796e..51092b6d 100644 --- a/pypika/functions.py +++ b/pypika/functions.py @@ -2,8 +2,16 @@ """ Package for SQL functions wrappers """ -from pypika.enums import SqlTypes -from pypika.terms import Function, Star, AggregateFunction +from pypika.enums import ( + SqlTypes, + Dialects, +) +from pypika.terms import ( + Function, + Star, + AggregateFunction, + ValueWrapper, +) from pypika.utils import builder __author__ = "Timothy Heys" @@ -179,6 +187,54 @@ def __init__(self, term, alias=None): super(Trim, self).__init__('TRIM', term, alias=alias) +class SplitPart(Function): + def __init__(self, term, delimiter, index, alias=None): + super(SplitPart, self).__init__('SPLIT_PART', term, delimiter, index, alias=alias) + + def get_name_for_dialect(self, dialect=None): + return { + Dialects.MYSQL: 'SUBSTRING_INDEX', + Dialects.POSTGRESQL: 'SPLIT_PART', + Dialects.REDSHIFT: 'SPLIT_PART', + Dialects.VERTICA: 'SPLIT_PART', + Dialects.ORACLE: 'REGEXP_SUBSTR', + }.get(dialect, None) + + def get_args_for_dialect(self, dialect=None): + term, delimiter, index = self.args + + return { + Dialects.MYSQL: (term, delimiter, index), + Dialects.POSTGRESQL: (term, delimiter, index), + Dialects.REDSHIFT: (term, delimiter, index), + Dialects.VERTICA: (term, delimiter, index), + Dialects.ORACLE: (term, ValueWrapper('[^{}]+'.format(delimiter.value)), 1, index) + }.get(dialect, None) + + +class RegexpLike(Function): + def __init__(self, term, pattern, modifiers, alias=None): + super(RegexpLike, self).__init__('REGEXP_LIKE', term, pattern, modifiers, alias=alias) + + def get_name_for_dialect(self, dialect=None): + return { + Dialects.POSTGRESQL: 'REGEXP_MATCHES', + Dialects.REDSHIFT: 'REGEXP_MATCHES', + Dialects.VERTICA: 'REGEXP_LIKE', + Dialects.ORACLE: 'REGEXP_LIKE', + }.get(dialect, self.name) + + def get_args_for_dialect(self, dialect=None): + term, pattern, modifiers = self.args + + return { + Dialects.POSTGRESQL: (term, pattern, modifiers), + Dialects.REDSHIFT: (term, pattern, modifiers), + Dialects.VERTICA: (term, pattern, modifiers), + Dialects.ORACLE: (term, pattern, modifiers) + }.get(dialect, None) + + # Date Functions class Now(Function): def __init__(self, alias=None): diff --git a/pypika/terms.py b/pypika/terms.py index f34c5a78..9f3f4ca0 100644 --- a/pypika/terms.py +++ b/pypika/terms.py @@ -14,6 +14,7 @@ ) from pypika.utils import ( CaseException, + DialectNotSupported, alias_sql, builder, ignoredeepcopy, @@ -654,19 +655,48 @@ def get_special_params_sql(self, **kwargs): def get_function_sql(self, **kwargs): special_params_sql = self.get_special_params_sql(**kwargs) + dialect = kwargs.get('dialect', None) + dialect_name = self.get_name_for_dialect(dialect=dialect) + dialect_args = self.get_args_for_dialect(dialect=dialect) + + if dialect_name is None or dialect_args is None: + raise DialectNotSupported('The function {} has no support for {} dialect'.format(self.name, dialect)) + return '{name}({args}{special})'.format( - name=self.name, + name=dialect_name, args=','.join(p.get_sql(with_alias=False, **kwargs) if hasattr(p, 'get_sql') else str(p) - for p in self.args), + for p in dialect_args), special=(' ' + special_params_sql) if special_params_sql else '', ) + def get_name_for_dialect(self, dialect=None): + """ + This function will transform the original function name into the equivalent for different dialects. + In practice this method should be overriden on subclasses whenever different dialects support is + required. Otherwise the original name will be used. + + :param dialect: one of the options in the Dialects enum. + :return: the function name that should be used by the get_function_sql method when serializing. + """ + return self.name + + def get_args_for_dialect(self, dialect=None): + """ + This function will transform the original function args into the equivalent for different dialects. + In practice this method should be overriden on subclasses whenever different dialects support is + required. Otherwise the original arguments will be used. + + :param dialect: one of the options in the Dialects enum. + :return: the function args that should be used by the get_function_sql method when serializing. + """ + return self.args + def get_sql(self, with_alias=False, with_namespace=False, quote_char=None, **kwargs): # FIXME escape - function_sql = self.get_function_sql(with_namespace=with_namespace, quote_char=quote_char) + function_sql = self.get_function_sql(with_namespace=with_namespace, quote_char=quote_char, **kwargs) if not with_alias or self.alias is None: return function_sql diff --git a/pypika/tests/test_functions.py b/pypika/tests/test_functions.py index a9c50db4..50f5937f 100644 --- a/pypika/tests/test_functions.py +++ b/pypika/tests/test_functions.py @@ -1,16 +1,24 @@ # coding: utf8 import unittest -from pypika import (Query as Q, - Table as T, - Field as F, - functions as fn, - CaseException, - Case, - Interval, - DatePart) +from pypika import ( + Query as Q, + Table as T, + Field as F, + functions as fn, + CaseException, + Case, + Interval, + DatePart, + MySQLQuery, + VerticaQuery, + PostgreSQLQuery, + RedshiftQuery, + OracleQuery, +) from pypika.enums import (SqlTypes, Dialects) +from pypika.utils import DialectNotSupported __author__ = "Timothy Heys" __email__ = "theys@kayak.com" @@ -358,6 +366,65 @@ def test__length__field(self): self.assertEqual("SELECT LENGTH(\"foo\") FROM \"abc\"", str(q)) +class SplitPartFunctionTests(unittest.TestCase): + t = T('abc') + + def test__split_part__field_with_vertica_dialect(self): + q = VerticaQuery.from_(self.t).select(fn.SplitPart(self.t.foo, '|', 3)) + + self.assertEqual("SELECT SPLIT_PART(\"foo\",\'|\',3) FROM \"abc\"", str(q)) + + def test__split_part__field_with_mysql_dialect(self): + q = MySQLQuery.from_(self.t).select(fn.SplitPart(self.t.foo, '|', 3)) + + self.assertEqual("SELECT SUBSTRING_INDEX(`foo`,\'|\',3) FROM `abc`", str(q)) + + def test__split_part__field_with_postgresql_dialect(self): + q = PostgreSQLQuery.from_(self.t).select(fn.SplitPart(self.t.foo, '|', 3)) + + self.assertEqual("SELECT SPLIT_PART(\"foo\",\'|\',3) FROM \"abc\"", str(q)) + + def test__split_part__field_with_redshift_dialect(self): + q = RedshiftQuery.from_(self.t).select(fn.SplitPart(self.t.foo, '|', 3)) + + self.assertEqual("SELECT SPLIT_PART(\"foo\",\'|\',3) FROM \"abc\"", str(q)) + + def test__split_part__field_with_oracle_dialect(self): + q = OracleQuery.from_(self.t).select(fn.SplitPart(self.t.foo, '|', 3)) + + self.assertEqual("SELECT REGEXP_SUBSTR(\"foo\",\'[^|]+\',1,3) FROM \"abc\"", str(q)) + + +class RegexpLikeFunctionTests(unittest.TestCase): + t = T('abc') + + def test__regexp_like__field_with_vertica_dialect(self): + q = VerticaQuery.from_(self.t).select(fn.RegexpLike(self.t.foo, '^a', 'x')) + + self.assertEqual("SELECT REGEXP_LIKE(\"foo\",\'^a\',\'x\') FROM \"abc\"", str(q)) + + def test__regexp_like__field_with_mysql_dialect(self): + q = MySQLQuery.from_(self.t).select(fn.RegexpLike(self.t.foo, '^a', 'x')) + + with self.assertRaises(DialectNotSupported): + str(q) + + def test__regexp_like__field_with_postgresql_dialect(self): + q = PostgreSQLQuery.from_(self.t).select(fn.RegexpLike(self.t.foo, '^a', 'x')) + + self.assertEqual("SELECT REGEXP_MATCHES(\"foo\",\'^a\',\'x\') FROM \"abc\"", str(q)) + + def test__regexp_like__field_with_redshift_dialect(self): + q = RedshiftQuery.from_(self.t).select(fn.RegexpLike(self.t.foo, '^a', 'x')) + + self.assertEqual("SELECT REGEXP_MATCHES(\"foo\",\'^a\',\'x\') FROM \"abc\"", str(q)) + + def test__regexp_like__field_with_oracle_dialect(self): + q = OracleQuery.from_(self.t).select(fn.RegexpLike(self.t.foo, '^a', 'x')) + + self.assertEqual("SELECT REGEXP_LIKE(\"foo\",\'^a\',\'x\') FROM \"abc\"", str(q)) + + class CastTests(unittest.TestCase): t = T('abc') diff --git a/pypika/utils.py b/pypika/utils.py index 722a9ca3..8c110c49 100644 --- a/pypika/utils.py +++ b/pypika/utils.py @@ -28,6 +28,10 @@ class RollupException(Exception): pass +class DialectNotSupported(Exception): + pass + + def builder(func): """ Decorator for wrapper "builder" functions. These are functions on the Query class or other classes used for