Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
lyz-code committed Apr 6, 2021
2 parents bdd05da + 7d0bc58 commit 28eb1d4
Show file tree
Hide file tree
Showing 16 changed files with 248 additions and 62 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/pythonpackage.yml
Expand Up @@ -26,12 +26,12 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
pip install coveralls==3.0.0
pip install "coveralls<3.0.0"
- name: Run test suite
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tox
coveralls --service=github
coveralls
3 changes: 3 additions & 0 deletions README.rst
Expand Up @@ -408,9 +408,12 @@ All join types are supported by |Brand|.
.from_(base_table)
...
.left_join(join_table) \
.left_outer_join(join_table) \
.right_join(join_table) \
.right_outer_join(join_table) \
.inner_join(join_table) \
.outer_join(join_table) \
.full_outer_join(join_table) \
.cross_join(join_table) \
.hash_join(join_table) \
...
Expand Down
2 changes: 1 addition & 1 deletion pypika/__init__.py
Expand Up @@ -100,7 +100,7 @@

__author__ = "Timothy Heys"
__email__ = "theys@kayak.com"
__version__ = "0.47.3"
__version__ = "0.48.0"

NULL = NullValue()
SYSTEM_TIME = SystemTimeValue()
5 changes: 5 additions & 0 deletions pypika/dialects.py
Expand Up @@ -7,6 +7,7 @@
CreateQueryBuilder,
Query,
QueryBuilder,
Selectable,
Table,
DropQueryBuilder,
)
Expand Down Expand Up @@ -433,6 +434,10 @@ def where(self, criterion: Criterion) -> "PostgreSQLQueryBuilder":
else:
raise QueryException('Can not have fieldless ON CONFLICT WHERE')

@builder
def using(self, table: Union[Selectable, str]) -> "QueryBuilder":
self._using.append(table)

def _distinct_sql(self, **kwargs: Any) -> str:
if self._distinct_on:
return "DISTINCT ON({distinct_on}) ".format(
Expand Down
1 change: 1 addition & 0 deletions pypika/enums.py
Expand Up @@ -30,6 +30,7 @@ class Matching(Comparator):
like = " LIKE "
not_ilike = " NOT ILIKE "
ilike = " ILIKE "
rlike = " RLIKE "
regex = " REGEX "
bin_regex = " REGEX BINARY "
as_of = " AS OF "
Expand Down
10 changes: 7 additions & 3 deletions pypika/functions.py
Expand Up @@ -5,6 +5,7 @@
from pypika.terms import (
AggregateFunction,
Function,
LiteralValue,
Star,
)
from pypika.utils import builder
Expand Down Expand Up @@ -156,7 +157,8 @@ def __init__(self, start_time, end_time, alias=None):

class DateAdd(Function):
def __init__(self, date_part, interval, term, alias=None):
super(DateAdd, self).__init__("DATE_ADD", date_part, interval, term, alias=alias)
date_part = getattr(date_part, "value", date_part)
super(DateAdd, self).__init__("DATE_ADD", LiteralValue(date_part), interval, term, alias=alias)


class ToDate(Function):
Expand All @@ -171,7 +173,8 @@ def __init__(self, term, alias=None):

class TimestampAdd(Function):
def __init__(self, date_part, interval, term, alias=None):
super(TimestampAdd, self).__init__("TIMESTAMPADD", date_part, interval, term, alias=alias)
date_part = getattr(date_part, 'value', date_part)
super(TimestampAdd, self).__init__("TIMESTAMPADD", LiteralValue(date_part), interval, term, alias=alias)


# String Functions
Expand Down Expand Up @@ -279,7 +282,8 @@ def __init__(self, alias=None):

class Extract(Function):
def __init__(self, date_part, field, alias=None):
super(Extract, self).__init__("EXTRACT", date_part, alias=alias)
date_part = getattr(date_part, "value", date_part)
super(Extract, self).__init__("EXTRACT", LiteralValue(date_part), alias=alias)
self.field = field

def get_special_params_sql(self, **kwargs):
Expand Down
18 changes: 18 additions & 0 deletions pypika/queries.py
Expand Up @@ -678,6 +678,7 @@ def __init__(
self._orderbys = []
self._joins = []
self._unions = []
self._using = []

self._limit = None
self._offset = None
Expand Down Expand Up @@ -987,12 +988,21 @@ def inner_join(self, item: Union[Table, "QueryBuilder", AliasedQuery]) -> "Joine
def left_join(self, item: Union[Table, "QueryBuilder", AliasedQuery]) -> "Joiner":
return self.join(item, JoinType.left)

def left_outer_join(self, item: Union[Table, "QueryBuilder", AliasedQuery]) -> "Joiner":
return self.join(item, JoinType.left_outer)

def right_join(self, item: Union[Table, "QueryBuilder", AliasedQuery]) -> "Joiner":
return self.join(item, JoinType.right)

def right_outer_join(self, item: Union[Table, "QueryBuilder", AliasedQuery]) -> "Joiner":
return self.join(item, JoinType.right_outer)

def outer_join(self, item: Union[Table, "QueryBuilder", AliasedQuery]) -> "Joiner":
return self.join(item, JoinType.outer)

def full_outer_join(self, item: Union[Table, "QueryBuilder", AliasedQuery]) -> "Joiner":
return self.join(item, JoinType.full_outer)

def cross_join(self, item: Union[Table, "QueryBuilder", AliasedQuery]) -> "Joiner":
return self.join(item, JoinType.cross)

Expand Down Expand Up @@ -1255,6 +1265,9 @@ def get_sql(self, with_alias: bool = False, subquery: bool = False, **kwargs: An
if self._from:
querystring += self._from_sql(**kwargs)

if self._using:
querystring += self._using_sql(**kwargs)

if self._force_indexes:
querystring += self._force_index_sql(**kwargs)

Expand Down Expand Up @@ -1379,6 +1392,11 @@ def _from_sql(self, with_namespace: bool = False, **kwargs: Any) -> str:
selectable=",".join(clause.get_sql(subquery=True, with_alias=True, **kwargs) for clause in self._from)
)

def _using_sql(self, with_namespace: bool = False, **kwargs: Any) -> str:
return " USING {selectable}".format(
selectable=",".join(clause.get_sql(subquery=True, with_alias=True, **kwargs) for clause in self._using)
)

def _force_index_sql(self, **kwargs: Any) -> str:
return " FORCE INDEX ({indexes})".format(
indexes=",".join(index.get_sql(**kwargs) for index in self._force_indexes),
Expand Down
63 changes: 43 additions & 20 deletions pypika/terms.py
@@ -1,5 +1,6 @@
import inspect
import re
import uuid
from datetime import date
from enum import Enum
from typing import Any, Iterable, Iterator, List, Optional, Sequence, Set, TYPE_CHECKING, Type, TypeVar, Union
Expand Down Expand Up @@ -27,6 +28,8 @@


class Node:
is_aggregate = None

def nodes_(self) -> Iterator[NodeT]:
yield self

Expand Down Expand Up @@ -157,6 +160,9 @@ def ilike(self, expr: str) -> "BasicCriterion":
def not_ilike(self, expr: str) -> "BasicCriterion":
return BasicCriterion(Matching.not_ilike, self, self.wrap_constant(expr))

def rlike(self, expr: str) -> "BasicCriterion":
return BasicCriterion(Matching.rlike, self, self.wrap_constant(expr))

def regex(self, pattern: str) -> "BasicCriterion":
return BasicCriterion(Matching.regex, self, self.wrap_constant(pattern))

Expand Down Expand Up @@ -331,24 +337,29 @@ def __init__(self, value: Any, alias: Optional[str] = None) -> None:
self.value = value

def get_value_sql(self, **kwargs: Any) -> str:
return self.get_formatted_value(self.value, **kwargs)

@classmethod
def get_formatted_value(cls, value: Any, **kwargs):
quote_char = kwargs.get("secondary_quote_char") or ""

# FIXME escape values
if isinstance(self.value, Term):
return self.value.get_sql(**kwargs)
if isinstance(self.value, Enum):
return self.value.value
if isinstance(self.value, date):
value = self.value.isoformat()
return format_quotes(value, quote_char)
if isinstance(self.value, str):
value = self.value.replace(quote_char, quote_char * 2)
if isinstance(value, Term):
return value.get_sql(**kwargs)
if isinstance(value, Enum):
return cls.get_formatted_value(value.value, **kwargs)
if isinstance(value, date):
return cls.get_formatted_value(value.isoformat(), **kwargs)
if isinstance(value, str):
value = value.replace(quote_char, quote_char * 2)
return format_quotes(value, quote_char)
if isinstance(self.value, bool):
return str.lower(str(self.value))
if self.value is None:
if isinstance(value, bool):
return str.lower(str(value))
if isinstance(value, uuid.UUID):
return cls.get_formatted_value(str(value), **kwargs)
if value is None:
return "null"
return str(self.value)
return str(value)

def get_sql(self, quote_char: Optional[str] = None, secondary_quote_char: str = "'", **kwargs: Any) -> str:
sql = self.get_value_sql(quote_char=quote_char, secondary_quote_char=secondary_quote_char, **kwargs)
Expand Down Expand Up @@ -1039,7 +1050,7 @@ def get_sql(self, with_alias: bool = False, **kwargs: Any) -> str:
return arithmetic_sql


class Case(Term):
class Case(Criterion):
def __init__(self, alias: Optional[str] = None) -> None:
super().__init__(alias=alias)
self._cases = []
Expand Down Expand Up @@ -1239,13 +1250,20 @@ def replace_table(self, current_table: Optional["Table"], new_table: Optional["T
def get_special_params_sql(self, **kwargs: Any) -> Any:
pass

@staticmethod
def get_arg_sql(arg, **kwargs):
return arg.get_sql(with_alias=False, **kwargs) if hasattr(arg, "get_sql") else str(arg)

def get_function_sql(self, **kwargs: Any) -> str:
special_params_sql = self.get_special_params_sql(**kwargs)

return "{name}({args}{special})".format(
name=self.name,
args=",".join(
p.get_sql(with_alias=False, **kwargs) if hasattr(p, "get_sql") else str(p) for p in self.args
p.get_sql(with_alias=False, subquery=True, **kwargs)
if hasattr(p, "get_sql")
else self.get_arg_sql(p, **kwargs)
for p in self.args
),
special=(" " + special_params_sql) if special_params_sql else "",
)
Expand Down Expand Up @@ -1432,14 +1450,13 @@ def get_special_params_sql(self, **kwargs: Any) -> Optional[str]:

class Interval(Node):
templates = {
# MySQL requires no single quotes around the expr and unit
Dialects.MYSQL: "INTERVAL {expr} {unit}",
# PostgreSQL, Redshift and Vertica require quotes around the expr and unit e.g. INTERVAL '1 week'
Dialects.POSTGRESQL: "INTERVAL '{expr} {unit}'",
Dialects.REDSHIFT: "INTERVAL '{expr} {unit}'",
Dialects.VERTICA: "INTERVAL '{expr} {unit}'",
# Oracle requires just single quotes around the expr
# Oracle and MySQL requires just single quotes around the expr
Dialects.ORACLE: "INTERVAL '{expr}' {unit}",
Dialects.MYSQL: "INTERVAL '{expr}' {unit}",
}

units = ["years", "months", "days", "hours", "minutes", "seconds", "microseconds"]
Expand All @@ -1463,6 +1480,7 @@ def __init__(
self.dialect = dialect
self.largest = None
self.smallest = None
self.is_negative = False

if quarters:
self.quarters = quarters
Expand All @@ -1478,8 +1496,11 @@ def __init__(
[years, months, days, hours, minutes, seconds, microseconds],
):
if value:
setattr(self, unit, int(value))
self.largest = self.largest or label
int_value = int(value)
setattr(self, unit, abs(int_value))
if self.largest is None:
self.largest = label
self.is_negative = int_value < 0
self.smallest = label

def __str__(self) -> str:
Expand Down Expand Up @@ -1512,6 +1533,8 @@ def get_sql(self, **kwargs: Any) -> str:
microseconds=getattr(self, "microseconds", 0),
)
expr = self.trim_pattern.sub("", expr)
if self.is_negative:
expr = "-" + expr

unit = (
"{largest}_{smallest}".format(
Expand Down
7 changes: 7 additions & 0 deletions pypika/tests/test_criterions.py
Expand Up @@ -575,6 +575,13 @@ def test_not_ilike_single_chars_and_various_chars(self):
self.assertEqual("\"foo\" NOT ILIKE 'a_b%c'", str(c1))
self.assertEqual('"like"."foo" NOT ILIKE \'a_b%c\'', str(c2))

def test_rlike_escape_chars(self):
c1 = Field("foo").rlike("\\\\d+$")
c2 = Field("foo", table=self.t).rlike("\\\\d+$")

self.assertEqual("\"foo\" RLIKE '\\\\d+$'", str(c1))
self.assertEqual('"like"."foo" RLIKE \'\\\\d+$\'', str(c2))

def test_glob_single_chars_and_various_chars(self):
c1 = Field("foo").glob("a_b*")
c2 = Field("foo", table=self.t).glob("a_b*")
Expand Down
7 changes: 7 additions & 0 deletions pypika/tests/test_data_types.py
@@ -1,8 +1,15 @@
import unittest
import uuid

from pypika.terms import ValueWrapper


class StringTests(unittest.TestCase):
def test_inline_string_concatentation(self):
self.assertEqual("'it''s'", ValueWrapper("it's").get_sql())


class UuidTests(unittest.TestCase):
def test_uuid_string_generation(self):
id = uuid.uuid4()
self.assertEqual("'{}'".format(id), ValueWrapper(id).get_sql())
26 changes: 24 additions & 2 deletions pypika/tests/test_date_math.py
Expand Up @@ -124,9 +124,9 @@ def test_add_value_complex_expressions(self):


class DialectIntervalTests(unittest.TestCase):
def test_mysql_dialect_does_not_use_quotes_around_interval(self):
def test_mysql_dialect_uses_single_quotes_around_expression_in_an_interval(self):
c = Interval(days=1).get_sql(dialect=Dialects.MYSQL)
self.assertEqual("INTERVAL 1 DAY", c)
self.assertEqual("INTERVAL '1' DAY", c)

def test_oracle_dialect_uses_single_quotes_around_expression_in_an_interval(self):
c = Interval(days=1).get_sql(dialect=Dialects.ORACLE)
Expand All @@ -145,6 +145,28 @@ def test_postgresql_dialect_uses_single_quotes_around_interval(self):
self.assertEqual("INTERVAL '1 DAY'", c)


class TestNegativeIntervals(unittest.TestCase):
def test_day(self):
c = Interval(days=-1).get_sql()
self.assertEqual("INTERVAL '-1 DAY'", c)

def test_week(self):
c = Interval(weeks=-1).get_sql()
self.assertEqual("INTERVAL '-1 WEEK'", c)

def test_month(self):
c = Interval(months=-1).get_sql()
self.assertEqual("INTERVAL '-1 MONTH'", c)

def test_year(self):
c = Interval(years=-1).get_sql()
self.assertEqual("INTERVAL '-1 YEAR'", c)

def test_year_month(self):
c = Interval(years=-1, months=-4).get_sql()
self.assertEqual("INTERVAL '-1-4 YEAR_MONTH'", c)


class TruncateTrailingZerosTests(unittest.TestCase):
def test_do_not_truncate_integer_values(self):
i = Interval(seconds=10)
Expand Down
11 changes: 11 additions & 0 deletions pypika/tests/test_deletes.py
Expand Up @@ -84,3 +84,14 @@ def test_delete_returning_star(self):
)

self.assertEqual('DELETE FROM "abc" WHERE "foo"="bar" RETURNING *', str(q1))

def test_delete_using(self):
table_trash = Table('trash')
q1 = (
PostgreSQLQuery.from_(self.table_abc)
.using(table_trash)
.where(self.table_abc.id == table_trash.abc_id)
.delete()
)

self.assertEqual('DELETE FROM "abc" USING "trash" WHERE "abc"."id"="trash"."abc_id"', str(q1))

0 comments on commit 28eb1d4

Please sign in to comment.