From 5d29c265334cbac054da78f962c13e64772416ec Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Wed, 3 Jul 2013 11:17:52 +0300 Subject: [PATCH] Updated docs --- CHANGES.rst | 6 ++++ docs/index.rst | 2 ++ setup.py | 2 +- sqlalchemy_utils/functions.py | 54 +++++++++++++++++++++++++++++++++ tests/__init__.py | 2 +- tests/test_utility_functions.py | 48 +++++++++++++++++++++++++++++ 6 files changed, 112 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2248f53..5e92803 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.14.3 (2013-07-03) +^^^^^^^^^^^^^^^^^^^ + +- Added non_indexed_foreign_keys utility function + + 0.14.2 (2013-07-02) ^^^^^^^^^^^^^^^^^^^ diff --git a/docs/index.rst b/docs/index.rst index 245b440..4564743 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -179,6 +179,8 @@ API Documentation :members: .. autofunction:: sort_query .. autofunction:: escape_like +.. autofunction:: non_indexed_foreign_keys +.. autofunction:: is_indexed_foreign_key .. include:: ../CHANGES.rst diff --git a/setup.py b/setup.py index 4156358..e89176d 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ class PyTest(Command): setup( name='SQLAlchemy-Utils', - version='0.14.2', + version='0.14.3', url='https://github.com/kvesteri/sqlalchemy-utils', license='BSD', author='Konsta Vesterinen', diff --git a/sqlalchemy_utils/functions.py b/sqlalchemy_utils/functions.py index 61e9273..a2e8eed 100644 --- a/sqlalchemy_utils/functions.py +++ b/sqlalchemy_utils/functions.py @@ -1,8 +1,10 @@ +from collections import defaultdict from sqlalchemy.orm import defer from sqlalchemy.orm.mapper import Mapper from sqlalchemy.orm.query import _ColumnEntity from sqlalchemy.orm.properties import ColumnProperty from sqlalchemy.orm.util import AliasedInsp +from sqlalchemy.schema import MetaData, Table, ForeignKeyConstraint from sqlalchemy.sql.expression import desc, asc, Label @@ -241,3 +243,55 @@ def table_name(class_): return class_.__tablename__ except AttributeError: return class_.__table__.name + + +def non_indexed_foreign_keys(metadata, engine=None): + """ + Finds all non indexed foreign keys from all tables of given MetaData. + + Very useful for optimizing postgresql database and finding out which + foreign keys need indexes. + + :param metadata: MetaData object to inspect tables from + """ + reflected_metadata = MetaData() + + if metadata.bind is None and engine is None: + raise Exception( + 'Either pass a metadata object with bind or ' + 'pass engine as a second parameter' + ) + + constraints = defaultdict(list) + + for table_name in metadata.tables.keys(): + table = Table( + table_name, + reflected_metadata, + autoload=True, + autoload_with=metadata.bind or engine + ) + + for constraint in table.constraints: + if not isinstance(constraint, ForeignKeyConstraint): + continue + + if not is_indexed_foreign_key(constraint): + constraints[table.name].append(constraint) + + return dict(constraints) + + +def is_indexed_foreign_key(constraint): + """ + Whether or not given foreign key constraint's columns have been indexed. + + :param constraint: ForeignKeyConstraint object to check the indexes + """ + for index in constraint.table.indexes: + index_column_names = set([ + column.name for column in index.columns + ]) + if index_column_names == set(constraint.columns): + return True + return False diff --git a/tests/__init__.py b/tests/__init__.py index 2f65a82..1d62436 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -23,7 +23,7 @@ class TestCase(object): self.engine = create_engine('sqlite:///:memory:') self.connection = self.engine.connect() self.Base = declarative_base() - + self.Base2 = declarative_base() self.create_models() self.Base.metadata.create_all(self.connection) diff --git a/tests/test_utility_functions.py b/tests/test_utility_functions.py index 025aa97..11e5d9f 100644 --- a/tests/test_utility_functions.py +++ b/tests/test_utility_functions.py @@ -1,4 +1,6 @@ +import sqlalchemy as sa from sqlalchemy_utils import escape_like, defer_except +from sqlalchemy_utils.functions import non_indexed_foreign_keys from tests import TestCase @@ -12,3 +14,49 @@ class TestDeferExcept(TestCase): query = self.session.query(self.Article) query = defer_except(query, ['id']) assert str(query) == 'SELECT article.id AS article_id \nFROM article' + + +class TestFindNonIndexedForeignKeys(TestCase): + def create_models(self): + class User(self.Base): + __tablename__ = 'user' + id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) + name = sa.Column(sa.Unicode(255)) + + class Category(self.Base): + __tablename__ = 'category' + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.Unicode(255)) + + class Article(self.Base): + __tablename__ = 'article' + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.Unicode(255)) + author_id = sa.Column( + sa.Integer, sa.ForeignKey(User.id), index=True + ) + category_id = sa.Column(sa.Integer, sa.ForeignKey(Category.id)) + + category = sa.orm.relationship( + Category, + primaryjoin=category_id == Category.id, + backref=sa.orm.backref( + 'articles', + ) + ) + + self.User = User + self.Category = Category + self.Article = Article + + def test_finds_all_non_indexed_fks(self): + fks = non_indexed_foreign_keys(self.Base.metadata, self.engine) + assert ( + 'article' in + fks + ) + column_names = [ + column_name for column_name in fks['article'][0].columns + ] + assert 'category_id' in column_names + assert 'author_id' not in column_names