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 5 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
19 changes: 19 additions & 0 deletions docs/3_advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 JiraTable, JiraQueryBuilder
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the example use JiraQuery instead of JiraQueryBuilder?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think that that would provide consistency with the rest of the API

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest I just don't know how to make in consistent.
There are no such thing as tables (so JiraTable is just a dummy placeholder).

Best thing I can do is probably create a bunch of overrides for from_, into and other methods which would return cls._builder(**kwargs)

Is it ok?

>>> table = JiraTable()
>>> JiraQuery._builder().where(table.project.isin(["PROJ1", "PROJ2"]))
project IN ("PROJ1","PROJ2")

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. I guess JiraQuery.from_(table).where(table.project.isin(["PROJ1", "PROJ2"])) looks kind consistent.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

@wd60622 wd60622 Oct 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the JiraQuery could have a where class method as well that initializes table.

Personally, I tend to used Fields

Maybe it would read like this

query = (
    JiraQuery
    .where(
        (Field("project").isin(["PROJ1", "PROJ2"]))
        & (Field("issuetype") == "my_issue_type")
    )
)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.
Working in both ways:

>>> JiraQuery.where(Field("project").isin(["PROJ1", "PROJ2"]) & (Field("issuetype") == "my_issue_type"))
project IN ("PROJ1","PROJ2") AND issuetype="my_issue_type"
>>> table = JiraTable()
>>> JiraQuery.where(table.project.isin(["PROJ1", "PROJ2"]) | table.project == 2)
project IN ("PROJ1","PROJ2") OR project=2


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())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the other examples show the result explicitly

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.
Also I forgot to import JiraQuery into main class. So I also had to add Table method for consistency


GROUP BY Modifiers
------------------

Expand Down
92 changes: 90 additions & 2 deletions pypika/dialects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,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)
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_jql.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))
Loading