Skip to content

Commit

Permalink
Remove explain and explain_analyze (#408)
Browse files Browse the repository at this point in the history
  • Loading branch information
kvesteri committed Dec 8, 2019
1 parent 6df59cb commit 627e2a3
Show file tree
Hide file tree
Showing 7 changed files with 8 additions and 259 deletions.
6 changes: 6 additions & 0 deletions CHANGES.rst
Expand Up @@ -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)
^^^^^^^^^^^^^^^^^^^

Expand Down
3 changes: 1 addition & 2 deletions sqlalchemy_utils/__init__.py
Expand Up @@ -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,
Expand Down Expand Up @@ -101,4 +100,4 @@
refresh_materialized_view
)

__version__ = '0.35.0'
__version__ = '0.36.0'
64 changes: 1 addition & 63 deletions sqlalchemy_utils/expressions.py
@@ -1,74 +1,12 @@
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.sql.expression import (
_literal_as_text,
ClauseElement,
ColumnElement,
Executable,
FunctionElement
)
from sqlalchemy.sql.expression import ColumnElement, FunctionElement
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'

Expand Down
1 change: 0 additions & 1 deletion sqlalchemy_utils/functions/__init__.py
@@ -1,5 +1,4 @@
from .database import ( # noqa
analyze,
create_database,
database_exists,
drop_database,
Expand Down
82 changes: 0 additions & 82 deletions sqlalchemy_utils/functions/database.py
Expand Up @@ -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 '<QueryAnalysis runtime=%r>' % 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.
Expand Down
35 changes: 0 additions & 35 deletions tests/functions/test_analyze.py

This file was deleted.

76 changes: 0 additions & 76 deletions tests/test_expressions.py
Expand Up @@ -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
Expand All @@ -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()
Expand Down

0 comments on commit 627e2a3

Please sign in to comment.