]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
:class:`.Index` now supports arbitrary SQL expressions and/or
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 17 Jan 2013 02:04:32 +0000 (21:04 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 17 Jan 2013 02:04:32 +0000 (21:04 -0500)
functions, in addition to straight columns.   Common modifiers
include using ``somecolumn.desc()`` for a descending index and
``func.lower(somecolumn)`` for a case-insensitive index, depending on the
capabilities of the target backend.
[ticket:695]

doc/build/changelog/changelog_08.rst
doc/build/core/schema.rst
lib/sqlalchemy/dialects/mssql/base.py
lib/sqlalchemy/dialects/mysql/base.py
lib/sqlalchemy/dialects/postgresql/base.py
lib/sqlalchemy/dialects/sqlite/base.py
lib/sqlalchemy/dialects/sybase/base.py
lib/sqlalchemy/schema.py
lib/sqlalchemy/sql/compiler.py
test/sql/test_constraints.py

index 3abac5025858324ddd61c97222e19426f6f23fce..7d6035a6d49ce44045099ac561393ca05922a7b9 100644 (file)
@@ -6,6 +6,16 @@
 .. changelog::
     :version: 0.8.0
 
+    .. change::
+        :tags: sql, feature
+        :tickets: 695
+
+      :class:`.Index` now supports arbitrary SQL expressions and/or
+      functions, in addition to straight columns.   Common modifiers
+      include using ``somecolumn.desc()`` for a descending index and
+      ``func.lower(somecolumn)`` for a case-insensitive index, depending on the
+      capabilities of the target backend.
+
     .. change::
         :tags: mssql, bug
         :tickets: 2638
index 2c47f5647199057d9ab5be3a42ac45602883be09..7771e3e4dff498a1634c3d1b0cccc827447c2bf5 100644 (file)
@@ -1170,6 +1170,30 @@ The :class:`~sqlalchemy.schema.Index` object also supports its own ``create()``
     {sql}i.create(engine)
     CREATE INDEX someindex ON mytable (col5){stop}
 
+Functional Indexes
+~~~~~~~~~~~~~~~~~~~
+
+:class:`.Index` supports SQL and function expressions, as supported by the
+target backend.  To create an index against a column using a descending
+value, the :meth:`.ColumnElement.desc` modifier may be used::
+
+    from sqlalchemy import Index
+
+    Index('someindex', mytable.c.somecol.desc())
+
+Or with a backend that supports functional indexes such as Postgresql,
+a "case insensitive" index can be created using the ``lower()`` function::
+
+    from sqlalchemy import func, Index
+
+    Index('someindex', func.lower(mytable.c.somecol))
+
+.. versionadded:: 0.8 :class:`.Index` supports SQL expressions and functions
+   as well as plain columns.
+
+Index API
+---------
+
 .. autoclass:: Index
     :show-inheritance:
     :members:
index d7c29654a3cfc607e6118e0d0a64d0ddde0ba750..58ed65bc9bfbfb686f5ee61d28e59d6b7980d5ac 100644 (file)
@@ -935,9 +935,8 @@ class MSDDLCompiler(compiler.DDLCompiler):
     def visit_drop_index(self, drop):
         return "\nDROP INDEX %s.%s" % (
             self.preparer.quote_identifier(drop.element.table.name),
-            self.preparer.quote(
-                        self._index_identifier(drop.element.name),
-                        drop.element.quote)
+            self._prepared_index_name(drop.element,
+                                        include_schema=True)
             )
 
 
index 6e09a359ee1feaa7fce4f837b0c6f908304aaefa..6bc9bd4a49b25514e253f2bf36d87ffd272128a8 100644 (file)
@@ -1513,11 +1513,14 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
 
     def visit_create_index(self, create):
         index = create.element
+        self._verify_index_table(index)
         preparer = self.preparer
         table = preparer.format_table(index.table)
-        columns = [preparer.quote(c.name, c.quote) for c in index.columns]
+        columns = [self.sql_compiler.process(expr, include_table=False)
+                for expr in index.expressions]
+
         name = preparer.quote(
-                    self._index_identifier(index.name),
+                    self._prepared_index_name(index),
                     index.quote)
 
         text = "CREATE "
@@ -1550,10 +1553,9 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
     def visit_drop_index(self, drop):
         index = drop.element
 
-        return "\nDROP INDEX %s ON %s" % \
-                    (self.preparer.quote(
-                        self._index_identifier(index.name), index.quote
-                    ),
+        return "\nDROP INDEX %s ON %s" % (
+                    self._prepared_index_name(index,
+                                        include_schema=False),
                      self.preparer.format_table(index.table))
 
     def visit_drop_constraint(self, drop):
index 81d2079c0569e188555e15168a42b96a3f1d3feb..afbb23e154f4c566dc8028fb8da6166e9700b497 100644 (file)
@@ -962,13 +962,13 @@ class PGDDLCompiler(compiler.DDLCompiler):
     def visit_create_index(self, create):
         preparer = self.preparer
         index = create.element
+        self._verify_index_table(index)
         text = "CREATE "
         if index.unique:
             text += "UNIQUE "
-        ops = index.kwargs.get('postgresql_ops', {})
         text += "INDEX %s ON %s " % (
-                    preparer.quote(
-                        self._index_identifier(index.name), index.quote),
+                        self._prepared_index_name(index,
+                                include_schema=True),
                     preparer.format_table(index.table)
                 )
 
@@ -976,20 +976,20 @@ class PGDDLCompiler(compiler.DDLCompiler):
             using = index.kwargs['postgresql_using']
             text += "USING %s " % preparer.quote(using, index.quote)
 
+        ops = index.kwargs.get('postgresql_ops', {})
         text += "(%s)" \
                 % (
                     ', '.join([
-                        preparer.format_column(c) +
+                        self.sql_compiler.process(expr, include_table=False) +
+
+
                         (c.key in ops and (' ' + ops[c.key]) or '')
-                        for c in index.columns])
+
+
+                        for expr, c in zip(index.expressions, index.columns)])
                     )
 
-        if "postgres_where" in index.kwargs:
-            whereclause = index.kwargs['postgres_where']
-            util.warn_deprecated(
-                    "The 'postgres_where' argument has been renamed "
-                    "to 'postgresql_where'.")
-        elif 'postgresql_where' in index.kwargs:
+        if 'postgresql_where' in index.kwargs:
             whereclause = index.kwargs['postgresql_where']
         else:
             whereclause = None
index 241d0bcdfc5de46e1d800d8d212bf9348e5497a8..7558accccdd0955fd737a5646e46f39f58d10e73 100644 (file)
@@ -522,18 +522,8 @@ class SQLiteDDLCompiler(compiler.DDLCompiler):
         return preparer.format_table(table, use_schema=False)
 
     def visit_create_index(self, create):
-        index = create.element
-        preparer = self.preparer
-        text = "CREATE "
-        if index.unique:
-            text += "UNIQUE "
-        text += "INDEX %s ON %s (%s)" \
-                    % (preparer.format_index(index,
-                       name=self._index_identifier(index.name)),
-                       preparer.format_table(index.table, use_schema=False),
-                       ', '.join(preparer.quote(c.name, c.quote)
-                                 for c in index.columns))
-        return text
+        return super(SQLiteDDLCompiler, self).\
+                    visit_create_index(create, include_table_schema=False)
 
 
 class SQLiteTypeCompiler(compiler.GenericTypeCompiler):
index 33a0c445bfbcb59aca12bdd931634d5cd3ff9733..a9e5c5fda05505a44ed046a244cd7bab1b41913c 100644 (file)
@@ -412,8 +412,8 @@ class SybaseDDLCompiler(compiler.DDLCompiler):
         index = drop.element
         return "\nDROP INDEX %s.%s" % (
             self.preparer.quote_identifier(index.table.name),
-            self.preparer.quote(
-                    self._index_identifier(index.name), index.quote)
+            self._prepared_index_name(drop.element,
+                                        include_schema=False)
             )
 
 
index b5e97802d852d65364710f4bfd5e665417000a4f..5cb592857f45e074a7187965acb7c978632e14b4 100644 (file)
@@ -2344,22 +2344,30 @@ class Index(ColumnCollectionMixin, SchemaItem):
 
     :ref:`schema_indexes` - General information on :class:`.Index`.
 
-    :ref:`postgresql_indexes` - PostgreSQL-specific options available for the :class:`.Index` construct.
+    :ref:`postgresql_indexes` - PostgreSQL-specific options available for the
+    :class:`.Index` construct.
+
+    :ref:`mysql_indexes` - MySQL-specific options available for the
+    :class:`.Index` construct.
 
-    :ref:`mysql_indexes` - MySQL-specific options available for the :class:`.Index` construct.
     """
 
     __visit_name__ = 'index'
 
-    def __init__(self, name, *columns, **kw):
+    def __init__(self, name, *expressions, **kw):
         """Construct an index object.
 
         :param name:
           The name of the index
 
-        :param \*columns:
-          Columns to include in the index. All columns must belong to the same
-          table.
+        :param \*expressions:
+          Column expressions to include in the index.   The expressions
+          are normally instances of :class:`.Column`, but may also
+          be arbitrary SQL expressions which ultmately refer to a
+          :class:`.Column`.
+
+          .. versionadded:: 0.8 :class:`.Index` supports SQL expressions as
+             well as plain columns.
 
         :param unique:
             Defaults to False: create a unique index.
@@ -2369,9 +2377,25 @@ class Index(ColumnCollectionMixin, SchemaItem):
 
         """
         self.table = None
+
+        columns = []
+        for expr in expressions:
+            if not isinstance(expr, expression.ClauseElement):
+                columns.append(expr)
+            else:
+                cols = []
+                visitors.traverse(expr, {}, {'column': cols.append})
+                if cols:
+                    columns.append(cols[0])
+                else:
+                    columns.append(expr)
+
+        self.expressions = expressions
+
         # will call _set_parent() if table-bound column
         # objects are present
         ColumnCollectionMixin.__init__(self, *columns)
+
         self.name = name
         self.unique = kw.pop('unique', False)
         self.kwargs = kw
@@ -2397,6 +2421,12 @@ class Index(ColumnCollectionMixin, SchemaItem):
                 )
         table.indexes.add(self)
 
+        self.expressions = [
+            expr if isinstance(expr, expression.ClauseElement)
+            else colexpr
+            for expr, colexpr in zip(self.expressions, self.columns)
+        ]
+
     @property
     def bind(self):
         """Return the connectable associated with this Index."""
index 127de1dfaf7b38ecaae175eb3b864e73166faeae..152e68e34f1eacaf282b654c6386a1b905aef281 100644 (file)
@@ -1933,20 +1933,17 @@ class DDLCompiler(engine.Compiled):
     def visit_drop_view(self, drop):
         return "\nDROP VIEW " + self.preparer.format_table(drop.element)
 
-    def _index_identifier(self, ident):
-        if isinstance(ident, sql._truncated_label):
-            max = self.dialect.max_index_name_length or \
-                        self.dialect.max_identifier_length
-            if len(ident) > max:
-                ident = ident[0:max - 8] + \
-                                "_" + util.md5_hex(ident)[-4:]
-        else:
-            self.dialect.validate_identifier(ident)
 
-        return ident
+    def _verify_index_table(self, index):
+        if index.table is None:
+            raise exc.CompileError("Index '%s' is not associated "
+                            "with any table." % index.name)
+
 
-    def visit_create_index(self, create, include_schema=False):
+    def visit_create_index(self, create, include_schema=False,
+                                include_table_schema=True):
         index = create.element
+        self._verify_index_table(index)
         preparer = self.preparer
         text = "CREATE "
         if index.unique:
@@ -1955,9 +1952,13 @@ class DDLCompiler(engine.Compiled):
                     % (
                         self._prepared_index_name(index,
                                 include_schema=include_schema),
-                       preparer.format_table(index.table),
-                       ', '.join(preparer.quote(c.name, c.quote)
-                                 for c in index.columns))
+                       preparer.format_table(index.table,
+                                    use_schema=include_table_schema),
+                       ', '.join(
+                            self.sql_compiler.process(expr,
+                                include_table=False) for
+                                expr in index.expressions)
+                        )
         return text
 
     def visit_drop_index(self, drop):
@@ -1973,8 +1974,18 @@ class DDLCompiler(engine.Compiled):
         else:
             schema_name = None
 
+        ident = index.name
+        if isinstance(ident, sql._truncated_label):
+            max_ = self.dialect.max_index_name_length or \
+                        self.dialect.max_identifier_length
+            if len(ident) > max_:
+                ident = ident[0:max_ - 8] + \
+                                "_" + util.md5_hex(ident)[-4:]
+        else:
+            self.dialect.validate_identifier(ident)
+
         index_name = self.preparer.quote(
-                            self._index_identifier(index.name),
+                                    ident,
                                     index.quote)
 
         if schema_name:
index 036a388bbdb4124cba281cd86c96cf1f0dd073c8..ab294e1ebb2390b2a40e5bd1f64fe64d966fdfec 100644 (file)
@@ -297,6 +297,23 @@ class ConstraintGenTest(fixtures.TestBase, AssertsExecutionResults):
             )
         )
 
+    @testing.provide_metadata
+    def test_index_functional_create(self):
+        metadata = self.metadata
+
+        t = Table('sometable', metadata,
+                Column('id', Integer, primary_key=True),
+                Column('data', String(50))
+            )
+        Index('myindex', t.c.data.desc())
+        self.assert_sql_execution(
+            testing.db,
+            lambda: t.create(testing.db),
+            CompiledSQL('CREATE TABLE sometable (id INTEGER NOT NULL, '
+                            'data VARCHAR(50), PRIMARY KEY (id))'),
+            ExactSQL('CREATE INDEX myindex ON sometable (data DESC)')
+        )
+
 class ConstraintCompilationTest(fixtures.TestBase, AssertsCompiledSQL):
     __dialect__ = 'default'
 
@@ -373,6 +390,24 @@ class ConstraintCompilationTest(fixtures.TestBase, AssertsCompiledSQL):
             dialect=dialect
         )
 
+    def test_functional_index(self):
+        metadata = MetaData()
+        x = Table('x', metadata,
+                Column('q', String(50))
+            )
+        idx = Index('y', func.lower(x.c.q))
+
+        self.assert_compile(
+            schema.CreateIndex(idx),
+            "CREATE INDEX y ON x (lower(q))"
+        )
+
+        self.assert_compile(
+            schema.CreateIndex(idx),
+            "CREATE INDEX y ON x (lower(q))",
+            dialect=testing.db.dialect
+        )
+
     def test_index_declaration_inline(self):
         metadata = MetaData()
 
@@ -819,8 +854,37 @@ class ConstraintAPITest(fixtures.TestBase):
             Index, "foo", 5
         )
 
+    def test_raise_expr_no_column(self):
+        idx = Index('foo', func.lower(5))
+
+        assert_raises_message(
+            exc.CompileError,
+            "Index 'foo' is not associated with any table.",
+            schema.CreateIndex(idx).compile, dialect=testing.db.dialect
+        )
+        assert_raises_message(
+            exc.CompileError,
+            "Index 'foo' is not associated with any table.",
+            schema.CreateIndex(idx).compile
+        )
+
+
     def test_no_warning_w_no_columns(self):
-        Index(name="foo")
+        # 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(
+            exc.CompileError,
+            "Index 'foo' is not associated with any table.",
+            schema.CreateIndex(idx).compile, dialect=testing.db.dialect
+        )
+        assert_raises_message(
+            exc.CompileError,
+            "Index 'foo' is not associated with any table.",
+            schema.CreateIndex(idx).compile
+        )
 
     def test_raise_clauseelement_not_a_column(self):
         m = MetaData()