diff --git a/CHANGES.rst b/CHANGES.rst index 78f62125..0812c0b3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Utils release. +0.36.0 (2019-12-08) +^^^^^^^^^^^^^^^^^^^ + +- Removed explain and explain_analyze due to the internal changes in SQLAlchemy version 1.3. + + 0.35.0 (2019-11-01) ^^^^^^^^^^^^^^^^^^^ diff --git a/sqlalchemy_utils/__init__.py b/sqlalchemy_utils/__init__.py index 93915958..ad0999a1 100644 --- a/sqlalchemy_utils/__init__.py +++ b/sqlalchemy_utils/__init__.py @@ -9,7 +9,6 @@ from .exceptions import ImproperlyConfigured # noqa from .expressions import Asterisk, row_to_json # noqa from .functions import ( # noqa - analyze, cast_if, create_database, create_mock_engine, @@ -101,4 +100,4 @@ refresh_materialized_view ) -__version__ = '0.35.0' +__version__ = '0.36.0' diff --git a/sqlalchemy_utils/expressions.py b/sqlalchemy_utils/expressions.py index 15ba4de7..b11d5782 100644 --- a/sqlalchemy_utils/expressions.py +++ b/sqlalchemy_utils/expressions.py @@ -2,73 +2,17 @@ from sqlalchemy.dialects import postgresql from sqlalchemy.ext.compiler import compiles from sqlalchemy.sql.expression import ( - _literal_as_text, ClauseElement, ColumnElement, Executable, - FunctionElement + FunctionElement, + Visitable ) from sqlalchemy.sql.functions import GenericFunction from .functions.orm import quote -class explain(Executable, ClauseElement): - """ - 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, **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: - 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 - - class array_get(FunctionElement): name = 'array_get' diff --git a/sqlalchemy_utils/functions/__init__.py b/sqlalchemy_utils/functions/__init__.py index dcb5a57d..63a01d4c 100644 --- a/sqlalchemy_utils/functions/__init__.py +++ b/sqlalchemy_utils/functions/__init__.py @@ -1,5 +1,4 @@ from .database import ( # noqa - analyze, create_database, database_exists, drop_database, diff --git a/sqlalchemy_utils/functions/database.py b/sqlalchemy_utils/functions/database.py index 48faf28d..fa384d42 100644 --- a/sqlalchemy_utils/functions/database.py +++ b/sqlalchemy_utils/functions/database.py @@ -10,92 +10,10 @@ from sqlalchemy.engine.url import make_url from sqlalchemy.exc import OperationalError, ProgrammingError -from ..expressions import explain_analyze from ..utils import starts_with from .orm import quote -class PlanAnalysis(object): - def __init__(self, plan): - self.plan = plan - - @property - def node_types(self): - types = [self.plan['Node Type']] - if 'Plans' in self.plan: - for plan in self.plan['Plans']: - analysis = PlanAnalysis(plan) - types.extend(analysis.node_types) - return types - - -class QueryAnalysis(object): - def __init__(self, result_set): - self.plan = result_set[0]['Plan'] - if 'Total Runtime' in result_set[0]: - # PostgreSQL versions < 9.4 - self.runtime = result_set[0]['Total Runtime'] - else: - # PostgreSQL versions >= 9.4 - self.runtime = ( - result_set[0]['Execution Time'] + - result_set[0]['Planning Time'] - ) - - @property - def node_types(self): - return list(PlanAnalysis(self.plan).node_types) - - def __repr__(self): - return '' % self.runtime - - -def analyze(conn, query): - """ - Analyze query using given connection and return :class:`QueryAnalysis` - object. Analysis is performed using database specific EXPLAIN ANALYZE - construct and then examining the results into structured format. Currently - only PostgreSQL is supported. - - - Getting query runtime (in database level) :: - - - from sqlalchemy_utils import analyze - - - analysis = analyze(conn, 'SELECT * FROM article') - analysis.runtime # runtime as milliseconds - - - Analyze can be very useful when testing that query doesn't issue a - sequential scan (scanning all rows in table). You can for example write - simple performance tests this way.:: - - - query = ( - session.query(Article.name) - .order_by(Article.name) - .limit(10) - ) - analysis = analyze(self.connection, query) - analysis.node_types # [u'Limit', u'Index Only Scan'] - - assert 'Seq Scan' not in analysis.node_types - - - .. versionadded: 0.26.17 - - :param conn: SQLAlchemy Connection object - :param query: SQLAlchemy Query object or query as a string - """ - return QueryAnalysis( - conn.execute( - explain_analyze(query, buffers=True, format='json') - ).scalar() - ) - - def escape_like(string, escape_char='*'): """ Escape the string paremeter used in SQL LIKE expressions. diff --git a/tests/test_expressions.py b/tests/test_expressions.py index f67368bb..57c727f3 100644 --- a/tests/test_expressions.py +++ b/tests/test_expressions.py @@ -3,7 +3,6 @@ from sqlalchemy.dialects import postgresql from sqlalchemy_utils import Asterisk, row_to_json -from sqlalchemy_utils.expressions import explain, explain_analyze @pytest.fixture @@ -27,81 +26,6 @@ class Article(Base): return Article -@pytest.mark.usefixtures('postgresql_dsn') -class TestExplain(object): - - def test_render_explain(self, session, assert_startswith, Article): - assert_startswith( - explain(session.query(Article)), - 'EXPLAIN SELECT' - ) - - def test_render_explain_with_analyze( - self, - session, - assert_startswith, - Article - ): - assert_startswith( - explain(session.query(Article), analyze=True), - 'EXPLAIN (ANALYZE true) SELECT' - ) - - def test_with_string_as_stmt_param(self, assert_startswith): - assert_startswith( - explain(sa.text('SELECT 1 FROM article')), - 'EXPLAIN SELECT' - ) - - def test_format(self, assert_startswith): - assert_startswith( - explain(sa.text('SELECT 1 FROM article'), format='json'), - 'EXPLAIN (FORMAT json) SELECT' - ) - - def test_timing(self, assert_startswith): - assert_startswith( - explain( - sa.text('SELECT 1 FROM article'), - analyze=True, - timing=False - ), - 'EXPLAIN (ANALYZE true, TIMING false) SELECT' - ) - - def test_verbose(self, assert_startswith): - assert_startswith( - explain(sa.text('SELECT 1 FROM article'), verbose=True), - 'EXPLAIN (VERBOSE true) SELECT' - ) - - def test_buffers(self, assert_startswith): - assert_startswith( - explain( - sa.text('SELECT 1 FROM article'), - analyze=True, - buffers=True - ), - 'EXPLAIN (ANALYZE true, BUFFERS true) SELECT' - ) - - def test_costs(self, assert_startswith): - assert_startswith( - explain(sa.text('SELECT 1 FROM article'), costs=False), - 'EXPLAIN (COSTS false) SELECT' - ) - - -class TestExplainAnalyze(object): - def test_render_explain_analyze(self, session, Article): - assert str( - explain_analyze(session.query(Article)) - .compile( - dialect=postgresql.dialect() - ) - ).startswith('EXPLAIN (ANALYZE true) SELECT') - - class TestAsterisk(object): def test_with_table_object(self): Base = sa.ext.declarative.declarative_base()