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

Remove explain and explain_analyze #408

Merged
merged 1 commit into from Dec 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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