diff --git a/docs/3_advanced.rst b/docs/3_advanced.rst index d6ae1e5e..99b1fce0 100644 --- a/docs/3_advanced.rst +++ b/docs/3_advanced.rst @@ -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 ------------------ 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 6e151d68..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 ( @@ -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): @@ -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) 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" diff --git a/pypika/tests/dialects/test_jira.py b/pypika/tests/dialects/test_jira.py new file mode 100644 index 00000000..1631f632 --- /dev/null +++ b/pypika/tests/dialects/test_jira.py @@ -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))