Skip to content

Commit d9f5b52

Browse files
doc-sheetdsAzisK
authored
Initial support for JQL (#721)
* Initial support for JQL * add test for jql * add JiraQueryBuilder mention to docs * fix copypaste in docs * fix code formatting * make jira test and example consistent * add JiraQuery.where() method * add Table, Tables methods * show generated jql in example --------- Co-authored-by: ds <ds@local> Co-authored-by: Azis <azuolas.krusna@yahoo.com>
1 parent 89c1ef0 commit d9f5b52

File tree

5 files changed

+163
-3
lines changed

5 files changed

+163
-3
lines changed

docs/3_advanced.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,27 @@ the platform-specific Query classes can be used.
2424
You can use these query classes as a drop in replacement for the default ``Query`` class shown in the other examples.
2525
Again, if you encounter any issues specific to a platform, please create a GitHub issue on this repository.
2626

27+
Or even different query languages
28+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
29+
30+
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.
31+
32+
.. code-block:: python
33+
34+
from pypika import JiraQuery
35+
36+
J = JiraQuery.Table()
37+
query = (
38+
JiraQuery.where(J.project.isin(["PROJ1", "PROJ2"]))
39+
.where(J.issuetype == "My issue")
40+
.where(J.labels.isempty() | J.labels.notin(["stale", "bug"]))
41+
.where(J.repos.notempty() & J.repos.notin(["main", "dev"]))
42+
)
43+
44+
.. code-block:: sql
45+
46+
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")
47+
2748
GROUP BY Modifiers
2849
------------------
2950

pypika/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from pypika.dialects import (
4949
ClickHouseQuery,
5050
Dialects,
51+
JiraQuery,
5152
MSSQLQuery,
5253
MySQLQuery,
5354
OracleQuery,

pypika/dialects.py

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import itertools
22
import warnings
33
from copy import copy
4-
from typing import Any, Optional, Union, Tuple as TypedTuple, List
4+
from typing import Any, List, Optional, Union, Tuple as TypedTuple
55

66
from pypika.enums import Dialects
77
from pypika.queries import (
@@ -13,8 +13,18 @@
1313
Query,
1414
QueryBuilder,
1515
)
16-
from pypika.terms import ArithmeticExpression, Criterion, EmptyCriterion, Field, Function, Star, Term, ValueWrapper
17-
from pypika.utils import QueryException, builder, format_quotes
16+
from pypika.terms import (
17+
ArithmeticExpression,
18+
Criterion,
19+
EmptyCriterion,
20+
Field,
21+
Function,
22+
NullCriterion,
23+
Star,
24+
Term,
25+
ValueWrapper,
26+
)
27+
from pypika.utils import QueryException, builder, format_alias_sql, format_quotes
1828

1929

2030
class SnowflakeQuery(Query):
@@ -962,3 +972,101 @@ def insert_or_replace(self, *terms: Any) -> "SQLLiteQueryBuilder":
962972
def _replace_sql(self, **kwargs: Any) -> str:
963973
prefix = "INSERT OR " if self._insert_or_replace else ""
964974
return prefix + super()._replace_sql(**kwargs)
975+
976+
977+
class JiraQuery(Query):
978+
"""
979+
Defines a query class for use with Jira.
980+
"""
981+
982+
@classmethod
983+
def _builder(cls, **kwargs) -> "JiraQueryBuilder":
984+
return JiraQueryBuilder(**kwargs)
985+
986+
@classmethod
987+
def where(cls, *args, **kwargs) -> "QueryBuilder":
988+
return JiraQueryBuilder().where(*args, **kwargs)
989+
990+
@classmethod
991+
def Table(cls, table_name: str = '', **_) -> "JiraTable":
992+
"""
993+
Convenience method for creating a JiraTable
994+
"""
995+
del table_name
996+
return JiraTable()
997+
998+
@classmethod
999+
def Tables(cls, *names: Union[TypedTuple[str, str], str], **kwargs: Any) -> List["JiraTable"]:
1000+
"""
1001+
Convenience method for creating many JiraTable instances
1002+
"""
1003+
del kwargs
1004+
return [JiraTable() for _ in range(len(names))]
1005+
1006+
1007+
class JiraQueryBuilder(QueryBuilder):
1008+
"""
1009+
Defines a main query builder class to produce JQL expression
1010+
"""
1011+
1012+
QUOTE_CHAR = ""
1013+
SECONDARY_QUOTE_CHAR = '"'
1014+
QUERY_CLS = JiraQuery
1015+
1016+
def __init__(self, **kwargs) -> None:
1017+
super().__init__(dialect=Dialects.JIRA, **kwargs)
1018+
self._from = [JiraTable()]
1019+
self._selects = [Star()]
1020+
self._select_star = True
1021+
1022+
def get_sql(self, with_alias: bool = False, subquery: bool = False, **kwargs) -> str:
1023+
return super().get_sql(with_alias, subquery, **kwargs).strip()
1024+
1025+
def _from_sql(self, with_namespace: bool = False, **_: Any) -> str:
1026+
"""
1027+
JQL doen't have from statements
1028+
"""
1029+
return ""
1030+
1031+
def _select_sql(self, **_: Any) -> str:
1032+
"""
1033+
JQL doen't have select statements
1034+
"""
1035+
return ""
1036+
1037+
def _where_sql(self, quote_char=None, **kwargs: Any) -> str:
1038+
return self._wheres.get_sql(quote_char=quote_char, subquery=True, **kwargs)
1039+
1040+
1041+
class JiraEmptyCriterion(NullCriterion):
1042+
def get_sql(self, with_alias: bool = False, **kwargs: Any) -> str:
1043+
del with_alias
1044+
sql = "{term} is EMPTY".format(
1045+
term=self.term.get_sql(**kwargs),
1046+
)
1047+
return format_alias_sql(sql, self.alias, **kwargs)
1048+
1049+
1050+
class JiraNotEmptyCriterion(JiraEmptyCriterion):
1051+
def get_sql(self, with_alias: bool = False, **kwargs) -> str:
1052+
del with_alias
1053+
sql = "{term} is not EMPTY".format(
1054+
term=self.term.get_sql(**kwargs),
1055+
)
1056+
return format_alias_sql(sql, self.alias, **kwargs)
1057+
1058+
1059+
class JiraField(Field):
1060+
def isempty(self) -> JiraEmptyCriterion:
1061+
return JiraEmptyCriterion(self)
1062+
1063+
def notempty(self) -> JiraNotEmptyCriterion:
1064+
return JiraNotEmptyCriterion(self)
1065+
1066+
1067+
class JiraTable(Table):
1068+
def __init__(self):
1069+
super().__init__("issues")
1070+
1071+
def field(self, name: str) -> JiraField:
1072+
return JiraField(name, table=self)

pypika/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ class SqlTypes:
136136
class Dialects(Enum):
137137
VERTICA = "vertica"
138138
CLICKHOUSE = "clickhouse"
139+
JIRA = "jira"
139140
ORACLE = "oracle"
140141
MSSQL = "mssql"
141142
MYSQL = "mysql"

pypika/tests/dialects/test_jira.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import unittest
2+
3+
from pypika.dialects import JiraQueryBuilder, JiraTable
4+
5+
6+
class SelectTests(unittest.TestCase):
7+
table_abc = JiraTable()
8+
9+
def test_in_query(self):
10+
q = JiraQueryBuilder().where(self.table_abc.project.isin(["PROJ1", "PROJ2"]))
11+
12+
self.assertEqual('project IN ("PROJ1","PROJ2")', str(q))
13+
14+
def test_eq_query(self):
15+
q = JiraQueryBuilder().where(self.table_abc.issuetype == "My issue")
16+
17+
self.assertEqual('issuetype="My issue"', str(q))
18+
19+
def test_or_query(self):
20+
q = JiraQueryBuilder().where(
21+
self.table_abc.labels.isempty() | self.table_abc.labels.notin(["stale", "bug fix"])
22+
)
23+
24+
self.assertEqual('labels is EMPTY OR labels NOT IN ("stale","bug fix")', str(q))
25+
26+
def test_and_query(self):
27+
q = JiraQueryBuilder().where(self.table_abc.repos.notempty() & self.table_abc.repos.notin(["main", "dev"]))
28+
29+
self.assertEqual('repos is not EMPTY AND repos NOT IN ("main","dev")', str(q))

0 commit comments

Comments
 (0)