From 09291e5a0cef3598e1c97c59229652ec338acf41 Mon Sep 17 00:00:00 2001 From: ds Date: Sat, 25 Mar 2023 18:16:13 +0300 Subject: [PATCH 1/9] Initial support for JQL --- pypika/dialects.py | 82 ++++++++++++++++++++++++++++++++++++++++++++-- pypika/enums.py | 1 + 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/pypika/dialects.py b/pypika/dialects.py index 6e151d68..cb54f285 100644 --- a/pypika/dialects.py +++ b/pypika/dialects.py @@ -12,8 +12,8 @@ Query, QueryBuilder, ) -from pypika.terms import ArithmeticExpression, Criterion, EmptyCriterion, Field, Function, Star, Term, ValueWrapper -from pypika.utils import QueryException, builder, format_quotes +from pypika.terms import ArithmeticExpression, Criterion, EmptyCriterion, Field, Function, NullCriterion, Star, Term, ValueWrapper +from pypika.utils import QueryException, builder, format_alias_sql, format_quotes class SnowflakeQuery(Query): @@ -868,3 +868,81 @@ def insert_or_replace(self, *terms: Any) -> "SQLLiteQueryBuilder": def _replace_sql(self, **kwargs: Any) -> str: prefix = "INSERT OR " if self._insert_or_replace else "" return prefix + super()._replace_sql(**kwargs) + + +class JiraQuery(Query): + """ + Defines a query class for use with Jira. + """ + + @classmethod + def _builder(cls, **kwargs) -> "JiraQueryBuilder": + return JiraQueryBuilder(**kwargs) + + +class JiraQueryBuilder(QueryBuilder): + """ + Defines a main query builder class to produce JQL expression + """ + + QUOTE_CHAR = "" + SECONDARY_QUOTE_CHAR = '"' + QUERY_CLS = JiraQuery + + def __init__(self, **kwargs) -> None: + super().__init__(dialect=Dialects.JIRA, **kwargs) + self._from = [JiraTable()] + self._selects = [Star()] + self._select_star = True + + def get_sql(self, with_alias: bool = False, subquery: bool = False, **kwargs) -> str: + return super().get_sql(with_alias, subquery, **kwargs).strip() + + def _from_sql(self, with_namespace: bool = False, **_: Any) -> str: + """ + JQL doen't have from statements + """ + return "" + + def _select_sql(self, **_: Any) -> str: + """ + JQL doen't have select statements + """ + return "" + + def _where_sql(self, quote_char=None, **kwargs: Any) -> str: + return self._wheres.get_sql(quote_char=quote_char, subquery=True, **kwargs) + + +class JiraEmptyCriterion(NullCriterion): + def get_sql(self, with_alias: bool = False, **kwargs: Any) -> str: + del with_alias + sql = "{term} is EMPTY".format( + term=self.term.get_sql(**kwargs), + ) + return format_alias_sql(sql, self.alias, **kwargs) + + +class JiraNotEmptyCriterion(JiraEmptyCriterion): + def get_sql(self, with_alias: bool = False, **kwargs) -> str: + del with_alias + sql = "{term} is not EMPTY".format( + term=self.term.get_sql(**kwargs), + ) + return format_alias_sql(sql, self.alias, **kwargs) + + +class JiraField(Field): + def isempty(self) -> JiraEmptyCriterion: + return JiraEmptyCriterion(self) + + def notempty(self) -> JiraNotEmptyCriterion: + return JiraNotEmptyCriterion(self) + + +class JiraTable(Table): + def __init__(self): + super().__init__("issues") + + def field(self, name: str) -> JiraField: + return JiraField(name, table=self) diff --git a/pypika/enums.py b/pypika/enums.py index 751889c4..855b5782 100644 --- a/pypika/enums.py +++ b/pypika/enums.py @@ -136,6 +136,7 @@ class SqlTypes: class Dialects(Enum): VERTICA = "vertica" CLICKHOUSE = "clickhouse" + JIRA = "jira" ORACLE = "oracle" MSSQL = "mssql" MYSQL = "mysql" From 0a8a291a632ff2823db60592e8e13471ac5120e8 Mon Sep 17 00:00:00 2001 From: ds Date: Sun, 24 Sep 2023 14:46:41 +0300 Subject: [PATCH 2/9] add test for jql --- pypika/tests/dialects/test_jql.py | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 pypika/tests/dialects/test_jql.py diff --git a/pypika/tests/dialects/test_jql.py b/pypika/tests/dialects/test_jql.py new file mode 100644 index 00000000..e18fab8e --- /dev/null +++ b/pypika/tests/dialects/test_jql.py @@ -0,0 +1,39 @@ +import unittest + +from pypika.dialects import JiraQueryBuilder, JiraTable + + +class SelectTests(unittest.TestCase): + table_abc = JiraTable() + + def test_in_query(self): + q = ( + JiraQueryBuilder() + .where(self.table_abc.project.isin(["PROJ1", "PROJ2"])) + ) + + self.assertEqual('project IN ("PROJ1","PROJ2")', str(q)) + + def test_eq_query(self): + q = ( + JiraQueryBuilder() + .where(self.table_abc.issuetype == "My issue") + ) + + self.assertEqual('issuetype="My issue"', str(q)) + + def test_or_query(self): + q = ( + JiraQueryBuilder() + .where(self.table_abc.labels.isempty() | self.table_abc.labels.notin(["stale", "bug fix"])) + ) + + self.assertEqual('labels is EMPTY OR labels NOT IN ("stale","bug fix")', str(q)) + + def test_and_query(self): + q = ( + JiraQueryBuilder() + .where(self.table_abc.repos.notempty() & self.table_abc.repos.notin(["main", "dev"])) + ) + + self.assertEqual('repos is not EMPTY AND repos NOT IN ("main","dev")', str(q)) From efcc459a40202840386ef76a40d3b5a7ffa5caba Mon Sep 17 00:00:00 2001 From: ds Date: Sun, 24 Sep 2023 17:21:13 +0300 Subject: [PATCH 3/9] add JiraQueryBuilder mention to docs --- docs/3_advanced.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/3_advanced.rst b/docs/3_advanced.rst index d6ae1e5e..e2137430 100644 --- a/docs/3_advanced.rst +++ b/docs/3_advanced.rst @@ -24,6 +24,25 @@ the platform-specific Query classes can be used. You can use these query classes as a drop in replacement for the default ``Query`` class shown in the other examples. Again, if you encounter any issues specific to a platform, please create a GitHub issue on this repository. +Or even different query languages +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Some services created their own query language similar to SQL. To generate expressions for Jira there is a ``JiraQueryBuilder`` class. + +.. code-block:: python + + from pypika import MySQLQuery, MSSQLQuery, PostgreSQLQuery, OracleQuery, VerticaQuery + + J = JiraTable() + j = ( + JiraQueryBuilder() + .where(J.project.isin(["PROJ1", "PROJ2"])) + .where(J.issuetype == "My issue") + .where(J.labels.isempty() | J.labels.notin(["stale", "bug"])) + .where(J.repos.notempty() & J.repos.notin(["main", "dev"])) + ) + print(j.get_sql()) + GROUP BY Modifiers ------------------ From 6a123a31152040f438c58e6fa77d577d81fbdd7b Mon Sep 17 00:00:00 2001 From: ds Date: Mon, 25 Sep 2023 16:36:07 +0300 Subject: [PATCH 4/9] fix copypaste in docs --- docs/3_advanced.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/3_advanced.rst b/docs/3_advanced.rst index e2137430..72ce3b91 100644 --- a/docs/3_advanced.rst +++ b/docs/3_advanced.rst @@ -31,7 +31,7 @@ Some services created their own query language similar to SQL. To generate expre .. code-block:: python - from pypika import MySQLQuery, MSSQLQuery, PostgreSQLQuery, OracleQuery, VerticaQuery + from pypika import JiraTable, JiraQueryBuilder J = JiraTable() j = ( From 5bc8d996f1eae10b25c5473262f7ed21c86a170e Mon Sep 17 00:00:00 2001 From: ds Date: Sat, 30 Sep 2023 01:02:08 +0300 Subject: [PATCH 5/9] fix code formatting --- pypika/dialects.py | 12 +++++++++++- pypika/tests/dialects/test_jql.py | 20 +++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pypika/dialects.py b/pypika/dialects.py index cb54f285..9f8c8257 100644 --- a/pypika/dialects.py +++ b/pypika/dialects.py @@ -12,7 +12,17 @@ Query, QueryBuilder, ) -from pypika.terms import ArithmeticExpression, Criterion, EmptyCriterion, Field, Function, NullCriterion, Star, Term, ValueWrapper +from pypika.terms import ( + ArithmeticExpression, + Criterion, + EmptyCriterion, + Field, + Function, + NullCriterion, + Star, + Term, + ValueWrapper, +) from pypika.utils import QueryException, builder, format_alias_sql, format_quotes diff --git a/pypika/tests/dialects/test_jql.py b/pypika/tests/dialects/test_jql.py index e18fab8e..1631f632 100644 --- a/pypika/tests/dialects/test_jql.py +++ b/pypika/tests/dialects/test_jql.py @@ -7,33 +7,23 @@ class SelectTests(unittest.TestCase): table_abc = JiraTable() def test_in_query(self): - q = ( - JiraQueryBuilder() - .where(self.table_abc.project.isin(["PROJ1", "PROJ2"])) - ) + q = JiraQueryBuilder().where(self.table_abc.project.isin(["PROJ1", "PROJ2"])) self.assertEqual('project IN ("PROJ1","PROJ2")', str(q)) def test_eq_query(self): - q = ( - JiraQueryBuilder() - .where(self.table_abc.issuetype == "My issue") - ) + q = JiraQueryBuilder().where(self.table_abc.issuetype == "My issue") self.assertEqual('issuetype="My issue"', str(q)) def test_or_query(self): - q = ( - JiraQueryBuilder() - .where(self.table_abc.labels.isempty() | self.table_abc.labels.notin(["stale", "bug fix"])) + q = JiraQueryBuilder().where( + self.table_abc.labels.isempty() | self.table_abc.labels.notin(["stale", "bug fix"]) ) self.assertEqual('labels is EMPTY OR labels NOT IN ("stale","bug fix")', str(q)) def test_and_query(self): - q = ( - JiraQueryBuilder() - .where(self.table_abc.repos.notempty() & self.table_abc.repos.notin(["main", "dev"])) - ) + q = JiraQueryBuilder().where(self.table_abc.repos.notempty() & self.table_abc.repos.notin(["main", "dev"])) self.assertEqual('repos is not EMPTY AND repos NOT IN ("main","dev")', str(q)) From 9c69e029969da8aa76945d8ed49ec880e9edeab1 Mon Sep 17 00:00:00 2001 From: ds Date: Tue, 17 Oct 2023 14:17:24 +0300 Subject: [PATCH 6/9] make jira test and example consistent --- docs/3_advanced.rst | 6 +++--- pypika/tests/dialects/{test_jql.py => test_jira.py} | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename pypika/tests/dialects/{test_jql.py => test_jira.py} (100%) diff --git a/docs/3_advanced.rst b/docs/3_advanced.rst index 72ce3b91..98179699 100644 --- a/docs/3_advanced.rst +++ b/docs/3_advanced.rst @@ -27,15 +27,15 @@ Again, if you encounter any issues specific to a platform, please create a GitHu Or even different query languages ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Some services created their own query language similar to SQL. To generate expressions for Jira there is a ``JiraQueryBuilder`` class. +Some services created their own query language similar to SQL. To generate expressions for Jira there is a ``JiraQuery`` class which just returns an instance of ``JiraQueryBuilder()`` so it could be used directly instead. .. code-block:: python - from pypika import JiraTable, JiraQueryBuilder + from pypika import JiraTable, JiraQuery J = JiraTable() j = ( - JiraQueryBuilder() + JiraQuery._builder().from_(J) .where(J.project.isin(["PROJ1", "PROJ2"])) .where(J.issuetype == "My issue") .where(J.labels.isempty() | J.labels.notin(["stale", "bug"])) diff --git a/pypika/tests/dialects/test_jql.py b/pypika/tests/dialects/test_jira.py similarity index 100% rename from pypika/tests/dialects/test_jql.py rename to pypika/tests/dialects/test_jira.py From f828d1d34f8dab6ee303f249e2d9ee87c88d9d98 Mon Sep 17 00:00:00 2001 From: ds Date: Tue, 17 Oct 2023 14:57:44 +0300 Subject: [PATCH 7/9] add JiraQuery.where() method --- docs/3_advanced.rst | 2 +- pypika/dialects.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/3_advanced.rst b/docs/3_advanced.rst index 98179699..8dbfae88 100644 --- a/docs/3_advanced.rst +++ b/docs/3_advanced.rst @@ -35,7 +35,7 @@ Some services created their own query language similar to SQL. To generate expre J = JiraTable() j = ( - JiraQuery._builder().from_(J) + JiraQuery .where(J.project.isin(["PROJ1", "PROJ2"])) .where(J.issuetype == "My issue") .where(J.labels.isempty() | J.labels.notin(["stale", "bug"])) diff --git a/pypika/dialects.py b/pypika/dialects.py index 9f8c8257..c4c06867 100644 --- a/pypika/dialects.py +++ b/pypika/dialects.py @@ -889,6 +889,10 @@ class JiraQuery(Query): def _builder(cls, **kwargs) -> "JiraQueryBuilder": return JiraQueryBuilder(**kwargs) + @classmethod + def where(cls, *args, **kwargs) -> "QueryBuilder": + return JiraQueryBuilder().where(*args, **kwargs) + class JiraQueryBuilder(QueryBuilder): """ From afefb4658cb9fc9df0a1bf98daec894e2750b01f Mon Sep 17 00:00:00 2001 From: ds Date: Tue, 17 Oct 2023 23:05:09 +0300 Subject: [PATCH 8/9] add Table, Tables methods --- pypika/__init__.py | 1 + pypika/dialects.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pypika/__init__.py b/pypika/__init__.py index 538bdfbd..109de9b8 100644 --- a/pypika/__init__.py +++ b/pypika/__init__.py @@ -34,6 +34,7 @@ from pypika.dialects import ( ClickHouseQuery, Dialects, + JiraQuery, MSSQLQuery, MySQLQuery, OracleQuery, diff --git a/pypika/dialects.py b/pypika/dialects.py index c4c06867..466a8de8 100644 --- a/pypika/dialects.py +++ b/pypika/dialects.py @@ -1,6 +1,6 @@ import itertools from copy import copy -from typing import Any, Optional, Union, Tuple as TypedTuple +from typing import Any, List, Optional, Union, Tuple as TypedTuple from pypika.enums import Dialects from pypika.queries import ( @@ -893,6 +893,22 @@ def _builder(cls, **kwargs) -> "JiraQueryBuilder": def where(cls, *args, **kwargs) -> "QueryBuilder": return JiraQueryBuilder().where(*args, **kwargs) + @classmethod + def Table(cls, table_name: str = '', **_) -> "JiraTable": + """ + Convenience method for creating a JiraTable + """ + del table_name + return JiraTable() + + @classmethod + def Tables(cls, *names: Union[TypedTuple[str, str], str], **kwargs: Any) -> List["JiraTable"]: + """ + Convenience method for creating many JiraTable instances + """ + del kwargs + return [JiraTable() for _ in range(len(names))] + class JiraQueryBuilder(QueryBuilder): """ From a2496c7ece8c81589123b511471b70b372e0de12 Mon Sep 17 00:00:00 2001 From: ds Date: Tue, 17 Oct 2023 23:05:27 +0300 Subject: [PATCH 9/9] show generated jql in example --- docs/3_advanced.rst | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/3_advanced.rst b/docs/3_advanced.rst index 8dbfae88..99b1fce0 100644 --- a/docs/3_advanced.rst +++ b/docs/3_advanced.rst @@ -31,17 +31,19 @@ Some services created their own query language similar to SQL. To generate expre .. code-block:: python - from pypika import JiraTable, JiraQuery + from pypika import JiraQuery - J = JiraTable() - j = ( - JiraQuery - .where(J.project.isin(["PROJ1", "PROJ2"])) + J = JiraQuery.Table() + query = ( + JiraQuery.where(J.project.isin(["PROJ1", "PROJ2"])) .where(J.issuetype == "My issue") .where(J.labels.isempty() | J.labels.notin(["stale", "bug"])) .where(J.repos.notempty() & J.repos.notin(["main", "dev"])) ) - print(j.get_sql()) + +.. code-block:: sql + + project IN ("PROJ1","PROJ2") AND issuetype="My issue" AND (labels is EMPTY OR labels NOT IN ("stale","bug")) AND repos is not EMPTY AND repos NOT IN ("main","dev") GROUP BY Modifiers ------------------