Commits

Mike Bayer  committed 2f62c2e

- [feature] Added a hook to the system of rendering
CREATE TABLE that provides access to the render for each
Column individually, by constructing a @compiles
function against the new schema.CreateColumn
construct. [ticket:2463]

  • Participants
  • Parent commits 641c936

Comments (0)

Files changed (7)

     can be altered, such as in the case for Firebird
     STARTING WITH [ticket:2470]
 
+  - [feature] Added a hook to the system of rendering
+    CREATE TABLE that provides access to the render for each
+    Column individually, by constructing a @compiles
+    function against the new schema.CreateColumn
+    construct.  [ticket:2463]
+
   - [bug] Fixes to the interpretation of the
     Column "default" parameter as a callable
     to not pass ExecutionContext into a keyword

File doc/build/core/schema.rst

 
 .. autoclass:: Column
     :members:
+    :inherited-members:
     :undoc-members:
     :show-inheritance:
 
 
 .. autoclass:: Table
     :members:
+    :inherited-members:
     :undoc-members:
     :show-inheritance:
 
     :undoc-members:
     :show-inheritance:
 
+.. autoclass:: CreateColumn
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
 .. autoclass:: CreateSequence
     :members:
     :undoc-members:

File lib/sqlalchemy/ext/compiler.py

 
 """
 from .. import exc
+from ..sql import visitors
 
 def compiles(class_, *specs):
+    """Register a function as a compiler for a
+    given :class:`.ClauseElement` type."""
+
     def decorate(fn):
         existing = class_.__dict__.get('_compiler_dispatcher', None)
         existing_dispatch = class_.__dict__.get('_compiler_dispatch')
         return fn
     return decorate
 
+def deregister(class_):
+    """Remove all custom compilers associated with a given
+    :class:`.ClauseElement` type."""
+
+    if hasattr(class_, '_compiler_dispatcher'):
+        # regenerate default _compiler_dispatch
+        visitors._generate_dispatch(class_)
+        # remove custom directive
+        del class_._compiler_dispatcher
+
+
 class _dispatcher(object):
     def __init__(self):
         self.specs = {}

File lib/sqlalchemy/schema.py

 
     __visit_name__ = "create_table"
 
+    def __init__(self, element, on=None, bind=None):
+        """Create a :class:`.CreateTable` construct.
+
+        :param element: a :class:`.Table` that's the subject
+         of the CREATE
+        :param on: See the description for 'on' in :class:`.DDL`.
+        :param bind: See the description for 'bind' in :class:`.DDL`.
+
+        """
+        super(CreateTable, self).__init__(element, on=on, bind=bind)
+        self.columns = [CreateColumn(column)
+            for column in element.columns
+        ]
+
+class CreateColumn(visitors.Visitable):
+    """Represent a :class:`.Column` as rendered in a CREATE TABLE statement,
+    via the :class:`.CreateTable` construct.
+
+    This is provided to support custom column DDL within the generation
+    of CREATE TABLE statements, by using the
+    compiler extension documented in :ref:`sqlalchemy.ext.compiler_toplevel`
+    to extend :class:`.CreateColumn`.
+
+    Typical integration is to examine the incoming :class:`.Column`
+    object, and to redirect compilation if a particular flag or condition
+    is found::
+
+        from sqlalchemy import schema
+        from sqlalchemy.ext.compiler import compiles
+
+        @compiles(schema.CreateColumn)
+        def compile(element, compiler, **kw):
+            column = element.element
+
+            if "special" not in column.info:
+                return compiler.visit_create_column(element, **kw)
+
+            text = "%s SPECIAL DIRECTIVE %s" % (
+                    column.name,
+                    compiler.type_compiler.process(column.type)
+                )
+            default = compiler.get_column_default_string(column)
+            if default is not None:
+                text += " DEFAULT " + default
+
+            if not column.nullable:
+                text += " NOT NULL"
+
+            if column.constraints:
+                text += " ".join(
+                            compiler.process(const)
+                            for const in column.constraints)
+            return text
+
+    The above construct can be applied to a :class:`.Table` as follows::
+
+        from sqlalchemy import Table, Metadata, Column, Integer, String
+        from sqlalchemy import schema
+
+        metadata = MetaData()
+
+        table = Table('mytable', MetaData(),
+                Column('x', Integer, info={"special":True}, primary_key=True),
+                Column('y', String(50)),
+                Column('z', String(20), info={"special":True})
+            )
+
+        metadata.create_all(conn)
+
+    Above, the directives we've added to the :attr:`.Column.info` collection
+    will be detected by our custom compilation scheme::
+
+        CREATE TABLE mytable (
+                x SPECIAL DIRECTIVE INTEGER NOT NULL,
+                y VARCHAR(50),
+                z SPECIAL DIRECTIVE VARCHAR(20),
+            PRIMARY KEY (x)
+        )
+
+    .. versionadded:: 0.8 The :class:`.CreateColumn` construct was added
+       to support custom column creation styles.
+
+    """
+    __visit_name__ = 'create_column'
+
+    def __init__(self, element):
+        self.element = element
+
 class DropTable(_CreateDropBase):
     """Represent a DROP TABLE statement."""
 

File lib/sqlalchemy/sql/compiler.py

     def sql_compiler(self):
         return self.dialect.statement_compiler(self.dialect, None)
 
+    @util.memoized_property
+    def type_compiler(self):
+        return self.dialect.type_compiler
+
     @property
     def preparer(self):
         return self.dialect.identifier_preparer
 
         # if only one primary key, specify it along with the column
         first_pk = False
-        for column in table.columns:
+        for create_column in create.columns:
+            column = create_column.element
             try:
                 text += separator
                 separator = ", \n"
-                text += "\t" + self.get_column_specification(
-                                                column,
-                                                first_pk=column.primary_key and \
-                                                not first_pk
-                                            )
+                text += "\t" + self.process(create_column,
+                                    first_pk=column.primary_key
+                                    and not first_pk)
                 if column.primary_key:
                     first_pk = True
-                const = " ".join(self.process(constraint) \
-                                for constraint in column.constraints)
-                if const:
-                    text += " " + const
             except exc.CompileError, ce:
                 # Py3K
                 #raise exc.CompileError("(in table '%s', column '%s'): %s"
         text += "\n)%s\n\n" % self.post_create_table(table)
         return text
 
+    def visit_create_column(self, create, first_pk=False):
+        column = create.element
+
+        text = self.get_column_specification(
+                        column,
+                        first_pk=first_pk
+                    )
+        const = " ".join(self.process(constraint) \
+                        for constraint in column.constraints)
+        if const:
+            text += " " + const
+
+        return text
+
     def create_table_constraints(self, table):
 
         # On some DB order is significant: visit PK first, then the

File test/ext/test_compiler.py

                                     BindParameter
 
 from sqlalchemy.schema import DDLElement
-from sqlalchemy.ext.compiler import compiles
+from sqlalchemy.ext.compiler import compiles, deregister
 from sqlalchemy import exc
 from sqlalchemy.sql import table, column, visitors
 from test.lib.testing import assert_raises_message
 
     def teardown(self):
         for cls in (Select, BindParameter):
-            if hasattr(cls, '_compiler_dispatcher'):
-                visitors._generate_dispatch(cls)
-                del cls._compiler_dispatcher
+            deregister(cls)
 
     def test_select(self):
         t1 = table('t1', column('c1'), column('c2'))

File test/sql/test_metadata.py

             getattr, select([t1.select().alias()]), 'c'
         )
 
+    def test_custom_create(self):
+        from sqlalchemy.ext.compiler import compiles, deregister
+
+        @compiles(schema.CreateColumn)
+        def compile(element, compiler, **kw):
+            column = element.element
+
+            if "special" not in column.info:
+                return compiler.visit_create_column(element, **kw)
+
+            text = "%s SPECIAL DIRECTIVE %s" % (
+                    column.name,
+                    compiler.type_compiler.process(column.type)
+                )
+            default = compiler.get_column_default_string(column)
+            if default is not None:
+                text += " DEFAULT " + default
+
+            if not column.nullable:
+                text += " NOT NULL"
+
+            if column.constraints:
+                text += " ".join(
+                            compiler.process(const)
+                            for const in column.constraints)
+            return text
+
+        t = Table('mytable', MetaData(),
+                Column('x', Integer, info={"special": True}, primary_key=True),
+                Column('y', String(50)),
+                Column('z', String(20), info={"special": True})
+            )
+
+        self.assert_compile(
+            schema.CreateTable(t),
+            "CREATE TABLE mytable (x SPECIAL DIRECTIVE INTEGER "
+                "NOT NULL, y VARCHAR(50), "
+                "z SPECIAL DIRECTIVE VARCHAR(20), PRIMARY KEY (x))"
+        )
+
+        deregister(schema.CreateColumn)
+
 class ColumnDefaultsTest(fixtures.TestBase):
     """test assignment of default fixures to columns"""