diff --git a/sqlalchemy_utils/expressions.py b/sqlalchemy_utils/expressions.py index b0da0fc..e69226f 100644 --- a/sqlalchemy_utils/expressions.py +++ b/sqlalchemy_utils/expressions.py @@ -1,26 +1,66 @@ import sqlalchemy as sa from sqlalchemy.sql import expression -from sqlalchemy.sql.expression import Executable, ClauseElement, _literal_as_text +from sqlalchemy.sql.expression import ( + Executable, + ClauseElement, + _literal_as_text +) from sqlalchemy.ext.compiler import compiles from sqlalchemy_utils.types import TSVectorType class explain(Executable, ClauseElement): - def __init__(self, stmt, analyze=False): + """ + Define EXPLAIN element. + + http://www.postgresql.org/docs/devel/static/sql-explain.html + """ + def __init__( + self, + stmt, + analyze=False, + verbose=False, + costs=True, + buffers=False, + timing=True, + format='text' + ): self.statement = _literal_as_text(stmt) self.analyze = analyze + self.verbose = verbose + self.costs = costs + self.buffers = buffers + self.timing = timing + self.format = format class explain_analyze(explain): - def __init__(self, stmt): - super(explain_analyze, self).__init__(stmt, analyze=True) + def __init__(self, stmt, **kwargs): + super(explain_analyze, self).__init__( + stmt, + analyze=True, + **kwargs + ) @compiles(explain, 'postgresql') def pg_explain(element, compiler, **kw): text = "EXPLAIN " + options = [] if element.analyze: - text += "ANALYZE " + options.append('ANALYZE true') + if not element.timing: + options.append('TIMING false') + if element.buffers: + options.append('BUFFERS true') + if element.format != 'text': + options.append('FORMAT %s' % element.format) + if element.verbose: + options.append('VERBOSE true') + if not element.costs: + options.append('COSTS false') + if options: + text += '(%s) ' % ', '.join(options) text += compiler.process(element.statement) return text diff --git a/sqlalchemy_utils/types/arrow.py b/sqlalchemy_utils/types/arrow.py index 070718b..03bc7d1 100644 --- a/sqlalchemy_utils/types/arrow.py +++ b/sqlalchemy_utils/types/arrow.py @@ -9,7 +9,7 @@ try: except: pass from sqlalchemy import types -from sqlalchemy_utils import ImproperlyConfigured +from sqlalchemy_utils.exceptions import ImproperlyConfigured from .scalar_coercible import ScalarCoercible diff --git a/sqlalchemy_utils/types/color.py b/sqlalchemy_utils/types/color.py index d5baf17..1d4624b 100644 --- a/sqlalchemy_utils/types/color.py +++ b/sqlalchemy_utils/types/color.py @@ -1,6 +1,6 @@ import six from sqlalchemy import types -from sqlalchemy_utils import ImproperlyConfigured +from sqlalchemy_utils.exceptions import ImproperlyConfigured from .scalar_coercible import ScalarCoercible colour = None diff --git a/sqlalchemy_utils/types/ip_address.py b/sqlalchemy_utils/types/ip_address.py index 7696409..3c78f5a 100644 --- a/sqlalchemy_utils/types/ip_address.py +++ b/sqlalchemy_utils/types/ip_address.py @@ -11,7 +11,7 @@ except ImportError: from sqlalchemy import types -from sqlalchemy_utils import ImproperlyConfigured +from sqlalchemy_utils.exceptions import ImproperlyConfigured from .scalar_coercible import ScalarCoercible diff --git a/sqlalchemy_utils/types/password.py b/sqlalchemy_utils/types/password.py index c558f74..0f9d72e 100644 --- a/sqlalchemy_utils/types/password.py +++ b/sqlalchemy_utils/types/password.py @@ -1,6 +1,6 @@ import six import weakref -from sqlalchemy_utils import ImproperlyConfigured +from sqlalchemy_utils.exceptions import ImproperlyConfigured from sqlalchemy import types from sqlalchemy.dialects import postgresql, oracle from .scalar_coercible import ScalarCoercible diff --git a/sqlalchemy_utils/types/phone_number.py b/sqlalchemy_utils/types/phone_number.py index af67c51..ed18792 100644 --- a/sqlalchemy_utils/types/phone_number.py +++ b/sqlalchemy_utils/types/phone_number.py @@ -1,6 +1,6 @@ import six from sqlalchemy import types -from sqlalchemy_utils import ImproperlyConfigured +from sqlalchemy_utils.exceptions import ImproperlyConfigured from .scalar_coercible import ScalarCoercible diff --git a/sqlalchemy_utils/types/timezone.py b/sqlalchemy_utils/types/timezone.py index 131cea7..c349ea6 100644 --- a/sqlalchemy_utils/types/timezone.py +++ b/sqlalchemy_utils/types/timezone.py @@ -1,6 +1,6 @@ import six from sqlalchemy import types -from sqlalchemy_utils import ImproperlyConfigured +from sqlalchemy_utils.exceptions import ImproperlyConfigured from .scalar_coercible import ScalarCoercible diff --git a/tests/test_expressions.py b/tests/test_expressions.py index 09bb571..27c11fc 100644 --- a/tests/test_expressions.py +++ b/tests/test_expressions.py @@ -27,22 +27,62 @@ class ExpressionTestCase(TestCase): self.Article = Article + def assert_startswith(self, query, query_part): + assert str( + query.compile(dialect=postgresql.dialect()) + ).startswith(query_part) + # Check that query executes properly + self.session.execute(query) + class TestExplain(ExpressionTestCase): def test_render_explain(self): - assert str( - explain(self.session.query(self.Article)).compile( - dialect=postgresql.dialect() - ) - ).startswith('EXPLAIN SELECT') + self.assert_startswith( + explain(self.session.query(self.Article)), + 'EXPLAIN SELECT' + ) def test_render_explain_with_analyze(self): - assert str( - explain(self.session.query(self.Article), analyze=True) - .compile( - dialect=postgresql.dialect() - ) - ).startswith('EXPLAIN ANALYZE SELECT') + self.assert_startswith( + explain(self.session.query(self.Article), analyze=True), + 'EXPLAIN (ANALYZE true) SELECT' + ) + + def test_with_string_as_stmt_param(self): + self.assert_startswith( + explain('SELECT 1 FROM article'), + 'EXPLAIN SELECT' + ) + + def test_format(self): + self.assert_startswith( + explain('SELECT 1 FROM article', format='json'), + 'EXPLAIN (FORMAT json) SELECT' + ) + + def test_timing(self): + self.assert_startswith( + explain('SELECT 1 FROM article', analyze=True, timing=False), + 'EXPLAIN (ANALYZE true, TIMING false) SELECT' + ) + + def test_verbose(self): + self.assert_startswith( + explain('SELECT 1 FROM article', verbose=True), + 'EXPLAIN (VERBOSE true) SELECT' + ) + + def test_buffers(self): + self.assert_startswith( + explain('SELECT 1 FROM article', analyze=True, buffers=True), + 'EXPLAIN (ANALYZE true, BUFFERS true) SELECT' + ) + + def test_costs(self): + self.assert_startswith( + explain('SELECT 1 FROM article', costs=False), + 'EXPLAIN (COSTS false) SELECT' + ) class TestExplainAnalyze(ExpressionTestCase): @@ -52,7 +92,7 @@ class TestExplainAnalyze(ExpressionTestCase): .compile( dialect=postgresql.dialect() ) - ).startswith('EXPLAIN ANALYZE SELECT') + ).startswith('EXPLAIN (ANALYZE true) SELECT') class TestMatchTSVector(ExpressionTestCase):