From 566006b4e26c596c00c9bd0f6360af3341eb2a9e Mon Sep 17 00:00:00 2001 From: Mike England Date: Wed, 13 Jan 2021 15:29:13 +0000 Subject: [PATCH 01/19] Set service=github-actions for coveralls --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index eab43a7c..e46e695e 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -34,4 +34,4 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | tox - coveralls --service=github + coveralls --service=github-actions From ea1588e544b476059d22f106fec1e50670230499 Mon Sep 17 00:00:00 2001 From: Lyz Date: Fri, 22 Jan 2021 13:27:48 +0100 Subject: [PATCH 02/19] feat: add missing join query builder methods Added the following methods: * left_outer_join * right_outer_join * full_outer_join --- README.rst | 3 ++ pypika/queries.py | 9 +++++ pypika/tests/test_joins.py | 82 ++++++++++++++++++++++++-------------- 3 files changed, 64 insertions(+), 30 deletions(-) diff --git a/README.rst b/README.rst index d0cbd90c..6d7a4aae 100644 --- a/README.rst +++ b/README.rst @@ -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) \ ... diff --git a/pypika/queries.py b/pypika/queries.py index 9abd6242..ec8db2ef 100644 --- a/pypika/queries.py +++ b/pypika/queries.py @@ -987,12 +987,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) diff --git a/pypika/tests/test_joins.py b/pypika/tests/test_joins.py index 1d3bbdc5..6e54f883 100644 --- a/pypika/tests/test_joins.py +++ b/pypika/tests/test_joins.py @@ -128,43 +128,65 @@ def test_cross_join(self): self.assertEqual(expected, str(query)) def test_left_outer_join(self): - q = ( - Query.from_(self.table0) - .join(self.table1, how=JoinType.left_outer) - .on(self.table0.foo == self.table1.bar) - .select("*") - ) + expected = 'SELECT * FROM "abc" LEFT OUTER JOIN "efg" ON "abc"."foo"="efg"."bar"' + with self.subTest("join with enum"): + query = ( + Query.from_(self.table0) + .join(self.table1, how=JoinType.left_outer) + .on(self.table0.foo == self.table1.bar) + .select("*") + ) - self.assertEqual( - 'SELECT * FROM "abc" LEFT OUTER JOIN "efg" ON "abc"."foo"="efg"."bar"', - str(q), - ) + self.assertEqual(expected, str(query)) + + with self.subTest("join function"): + query = ( + Query.from_(self.table0).left_outer_join(self.table1).on(self.table0.foo == self.table1.bar).select("*") + ) + + self.assertEqual(expected, str(query)) def test_right_outer_join(self): - q = ( - Query.from_(self.table0) - .join(self.table1, how=JoinType.right_outer) - .on(self.table0.foo == self.table1.bar) - .select("*") - ) + expected = 'SELECT * FROM "abc" RIGHT OUTER JOIN "efg" ON "abc"."foo"="efg"."bar"' + with self.subTest("join with enum"): + query = ( + Query.from_(self.table0) + .join(self.table1, how=JoinType.right_outer) + .on(self.table0.foo == self.table1.bar) + .select("*") + ) - self.assertEqual( - 'SELECT * FROM "abc" RIGHT OUTER JOIN "efg" ON "abc"."foo"="efg"."bar"', - str(q), - ) + self.assertEqual(expected, str(query)) + + with self.subTest("join function"): + query = ( + Query.from_(self.table0) + .right_outer_join(self.table1) + .on(self.table0.foo == self.table1.bar) + .select("*") + ) + + self.assertEqual(expected, str(query)) def test_full_outer_join(self): - q = ( - Query.from_(self.table0) - .join(self.table1, how=JoinType.full_outer) - .on(self.table0.foo == self.table1.bar) - .select("*") - ) + expected = 'SELECT * FROM "abc" FULL OUTER JOIN "efg" ON "abc"."foo"="efg"."bar"' - self.assertEqual( - 'SELECT * FROM "abc" FULL OUTER JOIN "efg" ON "abc"."foo"="efg"."bar"', - str(q), - ) + with self.subTest("join with enum"): + query = ( + Query.from_(self.table0) + .join(self.table1, how=JoinType.full_outer) + .on(self.table0.foo == self.table1.bar) + .select("*") + ) + + self.assertEqual(expected, str(query)) + + with self.subTest("join function"): + query = ( + Query.from_(self.table0).full_outer_join(self.table1).on(self.table0.foo == self.table1.bar).select("*") + ) + + self.assertEqual(expected, str(query)) def test_join_on_field_single(self): query = Query.from_(self.table0).join(self.table1).on_field("foo").select("*") From f6e065375cbaa1aecb84e2e56f74e7df1a2e3fa9 Mon Sep 17 00:00:00 2001 From: Glenn De Jonghe Date: Fri, 22 Jan 2021 14:16:46 +0100 Subject: [PATCH 03/19] pin coveralls to <3.0.0 --- .github/workflows/pythonpackage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index e46e695e..8665b42c 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -26,7 +26,7 @@ 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: @@ -34,4 +34,4 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | tox - coveralls --service=github-actions + coveralls From 011a4f86ce97b24a4a72c8060686435ef12d0266 Mon Sep 17 00:00:00 2001 From: Glenn De Jonghe Date: Fri, 22 Jan 2021 14:28:14 +0100 Subject: [PATCH 04/19] =?UTF-8?q?Bump=20version:=200.47.3=20=E2=86=92=200.?= =?UTF-8?q?47.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pypika/__init__.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pypika/__init__.py b/pypika/__init__.py index 535677cc..f6b36780 100644 --- a/pypika/__init__.py +++ b/pypika/__init__.py @@ -100,7 +100,7 @@ __author__ = "Timothy Heys" __email__ = "theys@kayak.com" -__version__ = "0.47.3" +__version__ = "0.47.4" NULL = NullValue() SYSTEM_TIME = SystemTimeValue() diff --git a/setup.cfg b/setup.cfg index defda5e6..acadb4d3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.47.3 +current_version = 0.47.4 commit = True tag = True From a7eaa7582bbf1b4bd98b0f5b477d6a6a4dd6cb46 Mon Sep 17 00:00:00 2001 From: Nguyen Khac Thanh Date: Thu, 4 Feb 2021 16:59:24 +0700 Subject: [PATCH 05/19] Surround subquery in the args aggregate function --- pypika/terms.py | 2 +- pypika/tests/test_functions.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pypika/terms.py b/pypika/terms.py index 48e85648..d300821e 100644 --- a/pypika/terms.py +++ b/pypika/terms.py @@ -1245,7 +1245,7 @@ def get_function_sql(self, **kwargs: Any) -> str: 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 str(p) for p in self.args ), special=(" " + special_params_sql) if special_params_sql else "", ) diff --git a/pypika/tests/test_functions.py b/pypika/tests/test_functions.py index 0c0d5103..ecd260d6 100644 --- a/pypika/tests/test_functions.py +++ b/pypika/tests/test_functions.py @@ -413,6 +413,11 @@ def test__approx_percentile(self): str(q), ) + def test__subquery_in_params_functions(self): + subquery = Query.from_('table').select('id') + func = fn.Function('func', 'id', subquery) + self.assertEqual("func('id',(SELECT id FROM table))", func.get_sql()) + class ConditionTests(unittest.TestCase): def test__case__raw(self): From a9b7f80f60880b3450b2de8173d906c5e82cb12e Mon Sep 17 00:00:00 2001 From: Nguyen Khac Thanh Date: Thu, 4 Feb 2021 17:04:06 +0700 Subject: [PATCH 06/19] reformat code --- pypika/terms.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pypika/terms.py b/pypika/terms.py index d300821e..41a6253f 100644 --- a/pypika/terms.py +++ b/pypika/terms.py @@ -1245,7 +1245,8 @@ def get_function_sql(self, **kwargs: Any) -> str: return "{name}({args}{special})".format( name=self.name, args=",".join( - p.get_sql(with_alias=False, subquery=True, **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 str(p) + for p in self.args ), special=(" " + special_params_sql) if special_params_sql else "", ) From 807205b69897a9fc20ea6fd2a26c3fdbd796e2bd Mon Sep 17 00:00:00 2001 From: Glenn De Jonghe Date: Sat, 13 Feb 2021 18:26:05 +0100 Subject: [PATCH 07/19] Add is_aggregate to Node class --- pypika/terms.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pypika/terms.py b/pypika/terms.py index 48e85648..74f72f7d 100644 --- a/pypika/terms.py +++ b/pypika/terms.py @@ -27,6 +27,8 @@ class Node: + is_aggregate = None + def nodes_(self) -> Iterator[NodeT]: yield self From 40846dbcd09d56ae475a13a469691a85cd04950d Mon Sep 17 00:00:00 2001 From: Glenn De Jonghe Date: Sat, 13 Feb 2021 18:26:52 +0100 Subject: [PATCH 08/19] =?UTF-8?q?Bump=20version:=200.47.4=20=E2=86=92=200.?= =?UTF-8?q?47.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pypika/__init__.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pypika/__init__.py b/pypika/__init__.py index f6b36780..7b47618f 100644 --- a/pypika/__init__.py +++ b/pypika/__init__.py @@ -100,7 +100,7 @@ __author__ = "Timothy Heys" __email__ = "theys@kayak.com" -__version__ = "0.47.4" +__version__ = "0.47.5" NULL = NullValue() SYSTEM_TIME = SystemTimeValue() diff --git a/setup.cfg b/setup.cfg index acadb4d3..0f4cbdf9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.47.4 +current_version = 0.47.5 commit = True tag = True From 6817438af8038cbd84d8b4efd4b9179b7dc5beda Mon Sep 17 00:00:00 2001 From: Tomas Kislan Date: Mon, 15 Feb 2021 17:10:11 +0100 Subject: [PATCH 09/19] Add support for RLIKE where criterion (#555) --- pypika/enums.py | 1 + pypika/terms.py | 3 +++ pypika/tests/test_criterions.py | 7 +++++++ pypika/tests/test_selects.py | 5 +++++ 4 files changed, 16 insertions(+) diff --git a/pypika/enums.py b/pypika/enums.py index 5f21c052..644806f7 100644 --- a/pypika/enums.py +++ b/pypika/enums.py @@ -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 " diff --git a/pypika/terms.py b/pypika/terms.py index 74f72f7d..a4001685 100644 --- a/pypika/terms.py +++ b/pypika/terms.py @@ -159,6 +159,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)) diff --git a/pypika/tests/test_criterions.py b/pypika/tests/test_criterions.py index 110c1038..223772b8 100644 --- a/pypika/tests/test_criterions.py +++ b/pypika/tests/test_criterions.py @@ -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*") diff --git a/pypika/tests/test_selects.py b/pypika/tests/test_selects.py index 7f155d80..095408c9 100644 --- a/pypika/tests/test_selects.py +++ b/pypika/tests/test_selects.py @@ -435,6 +435,11 @@ def test_where_field_matches_regex(self): self.assertEqual('SELECT * FROM "abc" WHERE "foo" REGEX \'^b\'', str(q)) + def test_where_field_matches_rlike(self): + q = Query.from_(self.t).select(self.t.star).where(self.t.foo.rlike(r"^b")) + + self.assertEqual('SELECT * FROM "abc" WHERE "foo" RLIKE \'^b\'', str(q)) + def test_ignore_empty_criterion(self): q1 = Query.from_(self.t).select("*").where(EmptyCriterion()) From 1e205275eb70725fff8e5ea0d85bb01639d0a9b4 Mon Sep 17 00:00:00 2001 From: Glenn De Jonghe Date: Mon, 15 Feb 2021 17:11:00 +0100 Subject: [PATCH 10/19] =?UTF-8?q?Bump=20version:=200.47.5=20=E2=86=92=200.?= =?UTF-8?q?47.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pypika/__init__.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pypika/__init__.py b/pypika/__init__.py index 7b47618f..e8817b1f 100644 --- a/pypika/__init__.py +++ b/pypika/__init__.py @@ -100,7 +100,7 @@ __author__ = "Timothy Heys" __email__ = "theys@kayak.com" -__version__ = "0.47.5" +__version__ = "0.47.6" NULL = NullValue() SYSTEM_TIME = SystemTimeValue() diff --git a/setup.cfg b/setup.cfg index 0f4cbdf9..cf208ce7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.47.5 +current_version = 0.47.6 commit = True tag = True From 83cb9f5cd267feaa1911e9687f04f9e116e9929f Mon Sep 17 00:00:00 2001 From: Glenn De Jonghe Date: Tue, 16 Feb 2021 16:51:34 +0100 Subject: [PATCH 11/19] Allow negative INTERVALs --- pypika/terms.py | 23 +++++++++++++++-------- pypika/tests/test_date_math.py | 26 ++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/pypika/terms.py b/pypika/terms.py index a4001685..3399fcf2 100644 --- a/pypika/terms.py +++ b/pypika/terms.py @@ -1244,14 +1244,16 @@ 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 - ), + args=",".join(self.get_arg_sql(arg, **kwargs) for arg in self.args), special=(" " + special_params_sql) if special_params_sql else "", ) @@ -1437,14 +1439,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"] @@ -1468,6 +1469,7 @@ def __init__( self.dialect = dialect self.largest = None self.smallest = None + self.is_negative = False if quarters: self.quarters = quarters @@ -1483,8 +1485,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: @@ -1517,6 +1522,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( diff --git a/pypika/tests/test_date_math.py b/pypika/tests/test_date_math.py index 73e09c7a..38db8ef2 100644 --- a/pypika/tests/test_date_math.py +++ b/pypika/tests/test_date_math.py @@ -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) @@ -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) From 61569d7fd6fd6d8c32d1bc26a00c4c0fc4e62c59 Mon Sep 17 00:00:00 2001 From: Mike Reiss Date: Wed, 10 Mar 2021 18:13:30 -0500 Subject: [PATCH 12/19] Support UUID values --- pypika/terms.py | 3 +++ pypika/tests/test_data_types.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/pypika/terms.py b/pypika/terms.py index 3399fcf2..f25d2f8f 100644 --- a/pypika/terms.py +++ b/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 @@ -351,6 +352,8 @@ def get_value_sql(self, **kwargs: Any) -> str: return format_quotes(value, quote_char) if isinstance(self.value, bool): return str.lower(str(self.value)) + if isinstance(self.value, uuid.UUID): + return format_quotes(str(self.value), quote_char) if self.value is None: return "null" return str(self.value) diff --git a/pypika/tests/test_data_types.py b/pypika/tests/test_data_types.py index 67e761aa..52fb4487 100644 --- a/pypika/tests/test_data_types.py +++ b/pypika/tests/test_data_types.py @@ -1,4 +1,5 @@ import unittest +import uuid from pypika.terms import ValueWrapper @@ -6,3 +7,9 @@ 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()) From 92f642e1a12c52646c7cc20f6ddac2e3e06e3e79 Mon Sep 17 00:00:00 2001 From: cbows <32486983+cbows@users.noreply.github.com> Date: Fri, 12 Mar 2021 17:43:31 +0100 Subject: [PATCH 13/19] Make date_part handling consistent in functions Closes #574 --- pypika/functions.py | 10 +++++++--- pypika/tests/test_functions.py | 16 +++++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/pypika/functions.py b/pypika/functions.py index 01432e53..83584297 100644 --- a/pypika/functions.py +++ b/pypika/functions.py @@ -5,6 +5,7 @@ from pypika.terms import ( AggregateFunction, Function, + LiteralValue, Star, ) from pypika.utils import builder @@ -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): @@ -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 @@ -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): diff --git a/pypika/tests/test_functions.py b/pypika/tests/test_functions.py index 0c0d5103..adbeac7a 100644 --- a/pypika/tests/test_functions.py +++ b/pypika/tests/test_functions.py @@ -672,34 +672,44 @@ class DateFunctionsTests(unittest.TestCase): def _test_extract_datepart(self, date_part): q = Q.from_(self.t).select(fn.Extract(date_part, self.t.foo)) - self.assertEqual('SELECT EXTRACT(%s FROM "foo") FROM "abc"' % date_part.value, str(q)) + value = getattr(date_part, 'value', date_part) + self.assertEqual('SELECT EXTRACT(%s FROM "foo") FROM "abc"' % value, str(q)) def test_extract_microsecond(self): self._test_extract_datepart(DatePart.microsecond) + self._test_extract_datepart(DatePart.microsecond.value) def test_extract_second(self): self._test_extract_datepart(DatePart.second) + self._test_extract_datepart(DatePart.second.value) def test_extract_minute(self): self._test_extract_datepart(DatePart.minute) + self._test_extract_datepart(DatePart.minute.value) def test_extract_hour(self): self._test_extract_datepart(DatePart.hour) + self._test_extract_datepart(DatePart.hour.value) def test_extract_day(self): self._test_extract_datepart(DatePart.day) + self._test_extract_datepart(DatePart.day.value) def test_extract_week(self): self._test_extract_datepart(DatePart.week) + self._test_extract_datepart(DatePart.week.value) def test_extract_month(self): self._test_extract_datepart(DatePart.month) + self._test_extract_datepart(DatePart.month.value) def test_extract_quarter(self): self._test_extract_datepart(DatePart.quarter) + self._test_extract_datepart(DatePart.quarter.value) def test_extract_year(self): self._test_extract_datepart(DatePart.year) + self._test_extract_datepart(DatePart.year.value) def test_extract_join(self): q = Q.from_(self.t).join(self.t2).on(self.t.id == self.t2.t_id).select(fn.Extract(DatePart.year, self.t.foo)) @@ -710,7 +720,7 @@ def test_extract_join(self): def test_timestampadd(self): a = fn.TimestampAdd("year", 1, "2017-10-01") - self.assertEqual(str(a), "TIMESTAMPADD('year',1,'2017-10-01')") + self.assertEqual(str(a), "TIMESTAMPADD(year,1,'2017-10-01')") def test_time_diff(self): a = fn.TimeDiff("18:00:00", "10:00:00") @@ -718,7 +728,7 @@ def test_time_diff(self): def test_date_add(self): a = fn.DateAdd("year", 1, "2017-10-01") - self.assertEqual(str(a), "DATE_ADD('year',1,'2017-10-01')") + self.assertEqual(str(a), "DATE_ADD(year,1,'2017-10-01')") def test_now(self): query = Query.select(fn.Now()) From f44681d7a9716c7f0c3ae508df24a7e1d3b256ba Mon Sep 17 00:00:00 2001 From: cbows <32486983+cbows@users.noreply.github.com> Date: Tue, 2 Mar 2021 14:12:16 +0100 Subject: [PATCH 14/19] Properly quote enum values --- pypika/terms.py | 33 ++++++++++++++++++--------------- pypika/tests/test_selects.py | 23 +++++++++++++++++++++++ 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/pypika/terms.py b/pypika/terms.py index f25d2f8f..e3505b5c 100644 --- a/pypika/terms.py +++ b/pypika/terms.py @@ -337,26 +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 isinstance(self.value, uuid.UUID): - return format_quotes(str(self.value), quote_char) - 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) diff --git a/pypika/tests/test_selects.py b/pypika/tests/test_selects.py index 095408c9..5d9916e6 100644 --- a/pypika/tests/test_selects.py +++ b/pypika/tests/test_selects.py @@ -1,4 +1,6 @@ import unittest +from datetime import date +from enum import Enum from pypika import ( AliasedQuery, @@ -339,9 +341,30 @@ def test_temporal_select(self): self.assertEqual('SELECT * FROM "abc" FOR "valid_period" ALL', str(q)) +class MyEnum(Enum): + STR = "foo" + INT = 0 + BOOL = True + DATE = date(2020, 2, 2) + NONE = None + + class WhereTests(unittest.TestCase): t = Table("abc") + def test_where_enum(self): + q1 = Query.from_(self.t).select("*").where(self.t.foo == MyEnum.STR) + q2 = Query.from_(self.t).select("*").where(self.t.foo == MyEnum.INT) + q3 = Query.from_(self.t).select("*").where(self.t.foo == MyEnum.BOOL) + q4 = Query.from_(self.t).select("*").where(self.t.foo == MyEnum.DATE) + q5 = Query.from_(self.t).select("*").where(self.t.foo == MyEnum.NONE) + + self.assertEqual('SELECT * FROM "abc" WHERE "foo"=\'foo\'', str(q1)) + self.assertEqual('SELECT * FROM "abc" WHERE "foo"=0', str(q2)) + self.assertEqual('SELECT * FROM "abc" WHERE "foo"=true', str(q3)) + self.assertEqual('SELECT * FROM "abc" WHERE "foo"=\'2020-02-02\'', str(q4)) + self.assertEqual('SELECT * FROM "abc" WHERE "foo"=null', str(q5)) + def test_where_field_equals(self): q1 = Query.from_(self.t).select("*").where(self.t.foo == self.t.bar) q2 = Query.from_(self.t).select("*").where(self.t.foo.eq(self.t.bar)) From 13d3c8f3f3fc8def737c975ec25f912f4c45ee5f Mon Sep 17 00:00:00 2001 From: Michael England Date: Wed, 17 Mar 2021 08:36:24 +0000 Subject: [PATCH 15/19] Change Case class to inherit from Criterion * This allows CASE statements to be used in where clauses when the where clause contains a Case instance and subsequent instances derived from Criterion --- pypika/terms.py | 2 +- pypika/tests/test_selects.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/pypika/terms.py b/pypika/terms.py index e3505b5c..719421b6 100644 --- a/pypika/terms.py +++ b/pypika/terms.py @@ -1050,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 = [] diff --git a/pypika/tests/test_selects.py b/pypika/tests/test_selects.py index 5d9916e6..3d8ccc51 100644 --- a/pypika/tests/test_selects.py +++ b/pypika/tests/test_selects.py @@ -473,6 +473,26 @@ def test_select_with_force_index_and_where(self): self.assertEqual('SELECT "foo" FROM "abc" FORCE INDEX ("egg") WHERE "foo"="bar"', str(q)) + def test_where_with_multiple_wheres_using_and_case(self): + case_stmt = Case().when(self.t.foo == 'bar', 1).else_(0) + query = Query.from_(self.t).select(case_stmt).where(case_stmt & self.t.blah.isin(['test'])) + + self.assertEqual( + 'SELECT CASE WHEN "foo"=\'bar\' THEN 1 ELSE 0 END FROM "abc" WHERE CASE WHEN "foo"=\'bar\' THEN 1 ELSE 0 ' + 'END AND "blah" IN (\'test\')', + str(query), + ) + + def test_where_with_multiple_wheres_using_or_case(self): + case_stmt = Case().when(self.t.foo == 'bar', 1).else_(0) + query = Query.from_(self.t).select(case_stmt).where(case_stmt | self.t.blah.isin(['test'])) + + self.assertEqual( + 'SELECT CASE WHEN "foo"=\'bar\' THEN 1 ELSE 0 END FROM "abc" WHERE CASE WHEN "foo"=\'bar\' THEN 1 ELSE 0 ' + 'END OR "blah" IN (\'test\')', + str(query), + ) + class PreWhereTests(WhereTests): t = Table("abc") From 9f7b983ecb668f7672a82159305846519d050678 Mon Sep 17 00:00:00 2001 From: Michael England Date: Wed, 17 Mar 2021 10:02:13 +0000 Subject: [PATCH 16/19] =?UTF-8?q?Bump=20version:=200.47.6=20=E2=86=92=200.?= =?UTF-8?q?48.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pypika/__init__.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pypika/__init__.py b/pypika/__init__.py index e8817b1f..c50e9ff4 100644 --- a/pypika/__init__.py +++ b/pypika/__init__.py @@ -100,7 +100,7 @@ __author__ = "Timothy Heys" __email__ = "theys@kayak.com" -__version__ = "0.47.6" +__version__ = "0.48.0" NULL = NullValue() SYSTEM_TIME = SystemTimeValue() diff --git a/setup.cfg b/setup.cfg index cf208ce7..c10b5afd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.47.6 +current_version = 0.48.0 commit = True tag = True From 0feb396a89fe54da592df4f6a364938a62f93195 Mon Sep 17 00:00:00 2001 From: "Michael P. Jung" Date: Tue, 23 Mar 2021 19:34:53 +0100 Subject: [PATCH 17/19] Add support for DELETE ... USING ... for PostgreSQL This fixes #397 --- pypika/dialects.py | 5 +++++ pypika/queries.py | 9 +++++++++ pypika/tests/test_deletes.py | 11 +++++++++++ 3 files changed, 25 insertions(+) diff --git a/pypika/dialects.py b/pypika/dialects.py index 539a2baa..ec5a4e4e 100644 --- a/pypika/dialects.py +++ b/pypika/dialects.py @@ -7,6 +7,7 @@ CreateQueryBuilder, Query, QueryBuilder, + Selectable, Table, DropQueryBuilder, ) @@ -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( diff --git a/pypika/queries.py b/pypika/queries.py index ec8db2ef..36397eb9 100644 --- a/pypika/queries.py +++ b/pypika/queries.py @@ -678,6 +678,7 @@ def __init__( self._orderbys = [] self._joins = [] self._unions = [] + self._using = [] self._limit = None self._offset = None @@ -1264,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) @@ -1388,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), diff --git a/pypika/tests/test_deletes.py b/pypika/tests/test_deletes.py index fadc060f..09df93f5 100644 --- a/pypika/tests/test_deletes.py +++ b/pypika/tests/test_deletes.py @@ -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)) From 42ffe3a5d60f606d5b380ce2929432d31ff4b960 Mon Sep 17 00:00:00 2001 From: Nguyen Khac Thanh Date: Wed, 24 Mar 2021 21:19:07 +0700 Subject: [PATCH 18/19] get sql string as arg sql --- pypika/terms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypika/terms.py b/pypika/terms.py index 28c2ffda..1286637e 100644 --- a/pypika/terms.py +++ b/pypika/terms.py @@ -1260,7 +1260,7 @@ def get_function_sql(self, **kwargs: Any) -> str: return "{name}({args}{special})".format( name=self.name, args=",".join( - p.get_sql(with_alias=False, subquery=True, **kwargs) if hasattr(p, "get_sql") else str(p) + 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 "", From 7fb512773720208c8665ddbffc3c3ad2e1a75e2d Mon Sep 17 00:00:00 2001 From: Nguyen Khac Thanh Date: Wed, 24 Mar 2021 21:28:41 +0700 Subject: [PATCH 19/19] refactor linting --- pypika/terms.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pypika/terms.py b/pypika/terms.py index 1286637e..dfd98175 100644 --- a/pypika/terms.py +++ b/pypika/terms.py @@ -1260,7 +1260,9 @@ def get_function_sql(self, **kwargs: Any) -> str: return "{name}({args}{special})".format( name=self.name, args=",".join( - p.get_sql(with_alias=False, subquery=True, **kwargs) if hasattr(p, "get_sql") else self.get_arg_sql(p, **kwargs) + 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 "",