Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for JQL #721

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/3_advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,27 @@ 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 ``JiraQuery`` class which just returns an instance of ``JiraQueryBuilder()`` so it could be used directly instead.

.. code-block:: python

from pypika import JiraQuery

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"]))
)

.. 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
------------------

Expand Down
1 change: 1 addition & 0 deletions pypika/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from pypika.dialects import (
ClickHouseQuery,
Dialects,
JiraQuery,
MSSQLQuery,
MySQLQuery,
OracleQuery,
Expand Down
114 changes: 111 additions & 3 deletions pypika/dialects.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -12,8 +12,18 @@
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):
Expand Down Expand Up @@ -868,3 +878,101 @@ 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)

@classmethod
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):
"""
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)
1 change: 1 addition & 0 deletions pypika/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ class SqlTypes:
class Dialects(Enum):
VERTICA = "vertica"
CLICKHOUSE = "clickhouse"
JIRA = "jira"
ORACLE = "oracle"
MSSQL = "mssql"
MYSQL = "mysql"
Expand Down
29 changes: 29 additions & 0 deletions pypika/tests/dialects/test_jira.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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))