diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-04-19 12:31:19 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-04-19 12:31:19 -0400 |
commit | c33d0378802abbc729de55ba205a4309e5d68f6b (patch) | |
tree | 1e8994db447e9908827c6494db8d0241a7a6de52 | |
parent | 1fb4ad75a38ce84d0e7b170927025500b73b5519 (diff) | |
download | sqlalchemy-c33d0378802abbc729de55ba205a4309e5d68f6b.tar.gz |
- Liberalized the contract for :class:`.Index` a bit in that you can
specify a :func:`.text` expression as the target; the index no longer
needs to have a table-bound column present if the index is to be
manually added to the table, either via inline declaration or via
:meth:`.Table.append_constraint`. fixes #3028
-rw-r--r-- | doc/build/changelog/changelog_09.rst | 10 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/base.py | 3 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/naming.py | 11 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/schema.py | 39 | ||||
-rw-r--r-- | lib/sqlalchemy/util/__init__.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/util/compat.py | 4 | ||||
-rw-r--r-- | test/sql/test_constraints.py | 43 | ||||
-rw-r--r-- | test/sql/test_metadata.py | 71 |
8 files changed, 158 insertions, 25 deletions
diff --git a/doc/build/changelog/changelog_09.rst b/doc/build/changelog/changelog_09.rst index a4d5b9b68..0aae51810 100644 --- a/doc/build/changelog/changelog_09.rst +++ b/doc/build/changelog/changelog_09.rst @@ -15,6 +15,16 @@ :version: 0.9.5 .. change:: + :tags: feature, sql + :tickets: 3028 + + Liberalized the contract for :class:`.Index` a bit in that you can + specify a :func:`.text` expression as the target; the index no longer + needs to have a table-bound column present if the index is to be + manually added to the table, either via inline declaration or via + :meth:`.Table.append_constraint`. + + .. change:: :tags: bug, mssql :tickets: 3025 diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py index 59f30ed69..28f324ad9 100644 --- a/lib/sqlalchemy/sql/base.py +++ b/lib/sqlalchemy/sql/base.py @@ -489,6 +489,9 @@ class ColumnCollection(util.OrderedProperties): for this dictionary. """ + if not column.key: + raise exc.ArgumentError( + "Can't add unnamed column to column collection") self[column.key] = column def __delitem__(self, key): diff --git a/lib/sqlalchemy/sql/naming.py b/lib/sqlalchemy/sql/naming.py index 1c5fae193..34a72a011 100644 --- a/lib/sqlalchemy/sql/naming.py +++ b/lib/sqlalchemy/sql/naming.py @@ -158,8 +158,9 @@ def _constraint_name(const, table): metadata = table.metadata convention = _get_convention(metadata.naming_convention, type(const)) if convention is not None: - newname = conv( - convention % ConventionDict(const, table, metadata.naming_convention) - ) - if const.name is None: - const.name = newname + if const.name is None or "constraint_name" in convention: + newname = conv( + convention % ConventionDict(const, table, metadata.naming_convention) + ) + if const.name is None: + const.name = newname diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py index e29fe456f..2aad60c8f 100644 --- a/lib/sqlalchemy/sql/schema.py +++ b/lib/sqlalchemy/sql/schema.py @@ -2724,13 +2724,41 @@ class Index(DialectKWArgs, ColumnCollectionMixin, SchemaItem): Index("some_index", sometable.c.name, sometable.c.address) - Functional indexes are supported as well, keeping in mind that at least - one :class:`.Column` must be present:: + Functional indexes are supported as well, typically by using the + :data:`.func` construct in conjunction with table-bound + :class:`.Column` objects:: Index("some_index", func.lower(sometable.c.name)) .. versionadded:: 0.8 support for functional and expression-based indexes. + An :class:`.Index` can also be manually associated with a :class:`.Table`, + either through inline declaration or using :meth:`.Table.append_constraint`. + When this approach is used, the names of the indexed columns can be specified + as strings:: + + Table("sometable", metadata, + Column("name", String(50)), + Column("address", String(100)), + Index("some_index", "name", "address") + ) + + To support functional or expression-based indexes in this form, the + :func:`.text` construct may be used:: + + from sqlalchemy import text + + Table("sometable", metadata, + Column("name", String(50)), + Column("address", String(100)), + Index("some_index", text("lower(name)")) + ) + + .. versionadded:: 0.9.5 the :func:`.text` construct may be used to + specify :class:`.Index` expressions, provided the :class:`.Index` + is explicitly associated with the :class:`.Table`. + + .. seealso:: :ref:`schema_indexes` - General information on :class:`.Index`. @@ -2785,8 +2813,6 @@ class Index(DialectKWArgs, ColumnCollectionMixin, SchemaItem): visitors.traverse(expr, {}, {'column': cols.append}) if cols: columns.append(cols[0]) - else: - columns.append(expr) self.expressions = expressions self.name = quoted_name(name, kw.pop("quote", None)) @@ -2798,7 +2824,6 @@ class Index(DialectKWArgs, ColumnCollectionMixin, SchemaItem): ColumnCollectionMixin.__init__(self, *columns) - def _set_parent(self, table): ColumnCollectionMixin._set_parent(self, table) @@ -2823,7 +2848,7 @@ class Index(DialectKWArgs, ColumnCollectionMixin, SchemaItem): self.expressions = [ expr if isinstance(expr, ClauseElement) else colexpr - for expr, colexpr in zip(self.expressions, self.columns) + for expr, colexpr in util.zip_longest(self.expressions, self.columns) ] @property @@ -2865,7 +2890,7 @@ class Index(DialectKWArgs, ColumnCollectionMixin, SchemaItem): return 'Index(%s)' % ( ", ".join( [repr(self.name)] + - [repr(c) for c in self.columns] + + [repr(e) for e in self.expressions] + (self.unique and ["unique=True"] or []) )) diff --git a/lib/sqlalchemy/util/__init__.py b/lib/sqlalchemy/util/__init__.py index eba64ed15..79c2d689f 100644 --- a/lib/sqlalchemy/util/__init__.py +++ b/lib/sqlalchemy/util/__init__.py @@ -10,7 +10,7 @@ from .compat import callable, cmp, reduce, \ raise_from_cause, text_type, string_types, int_types, binary_type, \ quote_plus, with_metaclass, print_, itertools_filterfalse, u, ue, b,\ unquote_plus, unquote, b64decode, b64encode, byte_buffer, itertools_filter,\ - iterbytes, StringIO, inspect_getargspec + iterbytes, StringIO, inspect_getargspec, zip_longest from ._collections import KeyedTuple, ImmutableContainer, immutabledict, \ Properties, OrderedProperties, ImmutableProperties, OrderedDict, \ diff --git a/lib/sqlalchemy/util/compat.py b/lib/sqlalchemy/util/compat.py index f1346406e..ac0478b39 100644 --- a/lib/sqlalchemy/util/compat.py +++ b/lib/sqlalchemy/util/compat.py @@ -85,6 +85,8 @@ if py3k: itertools_filterfalse = itertools.filterfalse itertools_filter = filter itertools_imap = map + from itertools import zip_longest + import base64 def b64encode(x): @@ -147,6 +149,8 @@ else: itertools_filterfalse = itertools.ifilterfalse itertools_filter = itertools.ifilter itertools_imap = itertools.imap + from itertools import izip_longest as zip_longest + import time diff --git a/test/sql/test_constraints.py b/test/sql/test_constraints.py index b174ef3c1..8dd15eb02 100644 --- a/test/sql/test_constraints.py +++ b/test/sql/test_constraints.py @@ -1,7 +1,7 @@ from sqlalchemy.testing import assert_raises, assert_raises_message from sqlalchemy import Table, Integer, String, Column, PrimaryKeyConstraint,\ ForeignKeyConstraint, ForeignKey, UniqueConstraint, Index, MetaData, \ - CheckConstraint, func + CheckConstraint, func, text from sqlalchemy import exc, schema from sqlalchemy.testing import fixtures, AssertsExecutionResults, \ AssertsCompiledSQL @@ -323,7 +323,7 @@ class ConstraintGenTest(fixtures.TestBase, AssertsExecutionResults): class ConstraintCompilationTest(fixtures.TestBase, AssertsCompiledSQL): __dialect__ = 'default' - def test_create_plain(self): + def test_create_index_plain(self): t = Table('t', MetaData(), Column('x', Integer)) i = Index("xyz", t.c.x) self.assert_compile( @@ -331,19 +331,19 @@ class ConstraintCompilationTest(fixtures.TestBase, AssertsCompiledSQL): "CREATE INDEX xyz ON t (x)" ) - def test_drop_plain_unattached(self): + def test_drop_index_plain_unattached(self): self.assert_compile( schema.DropIndex(Index(name="xyz")), "DROP INDEX xyz" ) - def test_drop_plain(self): + def test_drop_index_plain(self): self.assert_compile( schema.DropIndex(Index(name="xyz")), "DROP INDEX xyz" ) - def test_create_schema(self): + def test_create_index_schema(self): t = Table('t', MetaData(), Column('x', Integer), schema="foo") i = Index("xyz", t.c.x) self.assert_compile( @@ -351,7 +351,7 @@ class ConstraintCompilationTest(fixtures.TestBase, AssertsCompiledSQL): "CREATE INDEX xyz ON foo.t (x)" ) - def test_drop_schema(self): + def test_drop_index_schema(self): t = Table('t', MetaData(), Column('x', Integer), schema="foo") i = Index("xyz", t.c.x) self.assert_compile( @@ -360,7 +360,7 @@ class ConstraintCompilationTest(fixtures.TestBase, AssertsCompiledSQL): ) - def test_too_long_idx_name(self): + def test_too_long_index_name(self): dialect = testing.db.dialect.__class__() for max_ident, max_index in [(22, None), (256, 22)]: @@ -414,6 +414,32 @@ class ConstraintCompilationTest(fixtures.TestBase, AssertsCompiledSQL): dialect=testing.db.dialect ) + def test_index_against_text_separate(self): + metadata = MetaData() + idx = Index('y', text("some_function(q)")) + t = Table('x', metadata, + Column('q', String(50)) + ) + t.append_constraint(idx) + self.assert_compile( + schema.CreateIndex(idx), + "CREATE INDEX y ON x (some_function(q))" + ) + + def test_index_against_text_inline(self): + metadata = MetaData() + idx = Index('y', text("some_function(q)")) + x = Table('x', metadata, + Column('q', String(50)), + idx + ) + + self.assert_compile( + schema.CreateIndex(idx), + "CREATE INDEX y ON x (some_function(q))" + ) + + def test_index_declaration_inline(self): metadata = MetaData() @@ -933,9 +959,6 @@ class ConstraintAPITest(fixtures.TestBase): def test_no_warning_w_no_columns(self): - # I think the test here is, there is no warning. - # people want to create empty indexes for the purpose of - # a drop. idx = Index(name="foo") assert_raises_message( diff --git a/test/sql/test_metadata.py b/test/sql/test_metadata.py index 118fdf157..e4047872d 100644 --- a/test/sql/test_metadata.py +++ b/test/sql/test_metadata.py @@ -7,7 +7,7 @@ from sqlalchemy import Integer, String, UniqueConstraint, \ CheckConstraint, ForeignKey, MetaData, Sequence, \ ForeignKeyConstraint, PrimaryKeyConstraint, ColumnDefault, Index, event,\ events, Unicode, types as sqltypes, bindparam, \ - Table, Column, Boolean, Enum + Table, Column, Boolean, Enum, func, text from sqlalchemy import schema, exc import sqlalchemy as tsa from sqlalchemy.testing import fixtures @@ -451,7 +451,7 @@ class MetaDataTest(fixtures.TestBase, ComparesTables): "Column('x', String(), table=<bar>), schema=None)"), (schema.DefaultGenerator(for_update=True), "DefaultGenerator(for_update=True)"), - (schema.Index("bar", "c"), "Index('bar')"), + (schema.Index("bar", "c"), "Index('bar', 'c')"), (i1, "Index('bar', Column('x', Integer(), table=<foo>))"), (schema.FetchedValue(), "FetchedValue()"), (ck, @@ -1440,6 +1440,73 @@ class UseExistingTest(fixtures.TablesTest): extend_existing=True) assert "foo" in users.c +class IndexTest(fixtures.TestBase): + def _assert(self, t, i, columns=True): + eq_(t.indexes, set([i])) + if columns: + eq_(list(i.columns), [t.c.x]) + else: + eq_(list(i.columns), []) + assert i.table is t + + def test_separate_decl_columns(self): + m = MetaData() + t = Table('t', m, Column('x', Integer)) + i = Index('i', t.c.x) + self._assert(t, i) + + def test_separate_decl_columns_functional(self): + m = MetaData() + t = Table('t', m, Column('x', Integer)) + i = Index('i', func.foo(t.c.x)) + self._assert(t, i) + + def test_inline_decl_columns(self): + m = MetaData() + c = Column('x', Integer) + i = Index('i', c) + t = Table('t', m, c, i) + self._assert(t, i) + + def test_inline_decl_columns_functional(self): + m = MetaData() + c = Column('x', Integer) + i = Index('i', func.foo(c)) + t = Table('t', m, c, i) + self._assert(t, i) + + def test_inline_decl_string(self): + m = MetaData() + i = Index('i', "x") + t = Table('t', m, Column('x', Integer), i) + self._assert(t, i) + + def test_inline_decl_textonly(self): + m = MetaData() + i = Index('i', text("foobar(x)")) + t = Table('t', m, Column('x', Integer), i) + self._assert(t, i, columns=False) + + def test_separate_decl_textonly(self): + m = MetaData() + i = Index('i', text("foobar(x)")) + t = Table('t', m, Column('x', Integer)) + t.append_constraint(i) + self._assert(t, i, columns=False) + + def test_unnamed_column_exception(self): + # this can occur in some declarative situations + c = Column(Integer) + idx = Index('q', c) + m = MetaData() + t = Table('t', m, Column('q')) + assert_raises_message( + exc.ArgumentError, + "Can't add unnamed column to column collection", + t.append_constraint, idx + ) + + class ConstraintTest(fixtures.TestBase): def _single_fixture(self): m = MetaData() |