Anonymous avatar Anonymous committed ea8e5a8

- Back-ported the "compiler" extension from SQLA 0.6. This
is a standardized interface which allows the creation of custom
ClauseElement subclasses and compilers. In particular it's
handy as an alternative to text() when you'd like to
build a construct that has database-specific compilations.
See the extension docs for details.

Comments (0)

Files changed (7)

       identifiers, such as 'database.owner'. [ticket: 594, 1341]
 
 - sql
+    - Back-ported the "compiler" extension from SQLA 0.6.  This
+      is a standardized interface which allows the creation of custom
+      ClauseElement subclasses and compilers.  In particular it's
+      handy as an alternative to text() when you'd like to 
+      build a construct that has database-specific compilations.
+      See the extension docs for details.
+      
     - Exception messages are truncated when the list of bound 
       parameters is larger than 10, preventing enormous
       multi-page exceptions from filling up screens and logfiles

doc/build/reference/ext/compiler.rst

+compiler
+========
+
+.. automodule:: sqlalchemy.ext.compiler
+    :members:

doc/build/reference/ext/index.rst

     orderinglist
     serializer
     sqlsoup
+    compiler
 

lib/sqlalchemy/engine/default.py

 class DefaultDialect(base.Dialect):
     """Default implementation of Dialect"""
 
+    name = 'default'
     schemagenerator = compiler.SchemaGenerator
     schemadropper = compiler.SchemaDropper
     statement_compiler = compiler.DefaultCompiler

lib/sqlalchemy/ext/compiler.py

+"""Provides an API for creation of custom ClauseElements and compilers.
+
+Synopsis
+========
+
+Usage involves the creation of one or more :class:`~sqlalchemy.sql.expression.ClauseElement`
+subclasses and one or more callables defining its compilation::
+
+    from sqlalchemy.ext.compiler import compiles
+    from sqlalchemy.sql.expression import ColumnClause
+    
+    class MyColumn(ColumnClause):
+        pass
+            
+    @compiles(MyColumn)
+    def compile_mycolumn(element, compiler, **kw):
+        return "[%s]" % element.name
+        
+Above, ``MyColumn`` extends :class:`~sqlalchemy.sql.expression.ColumnClause`, the
+base expression element for column objects.  The ``compiles`` decorator registers
+itself with the ``MyColumn`` class so that it is invoked when the object 
+is compiled to a string::
+
+    from sqlalchemy import select
+    
+    s = select([MyColumn('x'), MyColumn('y')])
+    print str(s)
+    
+Produces::
+
+    SELECT [x], [y]
+
+Compilers can also be made dialect-specific.  The appropriate compiler will be invoked
+for the dialect in use::
+
+    from sqlalchemy.schema import DDLElement  # this is a SQLA 0.6 construct
+
+    class AlterColumn(DDLElement):
+
+        def __init__(self, column, cmd):
+            self.column = column
+            self.cmd = cmd
+
+    @compiles(AlterColumn)
+    def visit_alter_column(element, compiler, **kw):
+        return "ALTER COLUMN %s ..." % element.column.name
+
+    @compiles(AlterColumn, 'postgres')
+    def visit_alter_column(element, compiler, **kw):
+        return "ALTER TABLE %s ALTER COLUMN %s ..." % (element.table.name, element.column.name)
+
+The second ``visit_alter_table`` will be invoked when any ``postgres`` dialect is used.
+
+The ``compiler`` argument is the :class:`~sqlalchemy.engine.base.Compiled` object
+in use.  This object can be inspected for any information about the in-progress 
+compilation, including ``compiler.dialect``, ``compiler.statement`` etc.
+The :class:`~sqlalchemy.sql.compiler.SQLCompiler` and :class:`~sqlalchemy.sql.compiler.DDLCompiler` (DDLCompiler is 0.6. only)
+both include a ``process()`` method which can be used for compilation of embedded attributes::
+
+    class InsertFromSelect(ClauseElement):
+        def __init__(self, table, select):
+            self.table = table
+            self.select = select
+
+    @compiles(InsertFromSelect)
+    def visit_insert_from_select(element, compiler, **kw):
+        return "INSERT INTO %s (%s)" % (
+            compiler.process(element.table, asfrom=True),
+            compiler.process(element.select)
+        )
+
+    insert = InsertFromSelect(t1, select([t1]).where(t1.c.x>5))
+    print insert
+    
+Produces::
+
+    "INSERT INTO mytable (SELECT mytable.x, mytable.y, mytable.z FROM mytable WHERE mytable.x > :x_1)"
+
+
+"""
+
+def compiles(class_, *specs):
+    def decorate(fn):
+        existing = getattr(class_, '_compiler_dispatcher', None)
+        if not existing:
+            existing = _dispatcher()
+
+            # TODO: why is the lambda needed ?
+            setattr(class_, '_compiler_dispatch', lambda *arg, **kw: existing(*arg, **kw))
+            setattr(class_, '_compiler_dispatcher', existing)
+        
+        if specs:
+            for s in specs:
+                existing.specs[s] = fn
+        else:
+            existing.specs['default'] = fn
+        return fn
+    return decorate
+    
+class _dispatcher(object):
+    def __init__(self):
+        self.specs = {}
+    
+    def __call__(self, element, compiler, **kw):
+        # TODO: yes, this could also switch off of DBAPI in use.
+        fn = self.specs.get(compiler.dialect.name, None)
+        if not fn:
+            fn = self.specs['default']
+        return fn(element, compiler, **kw)
+        

test/ext/alltests.py

         'ext.orderinglist',
         'ext.associationproxy',
         'ext.serializer',
+        'ext.compiler',
         )
 
     if sys.version_info < (2, 4):

test/ext/compiler.py

+import testenv; testenv.configure_for_tests()
+from sqlalchemy import *
+from sqlalchemy.sql.expression import ClauseElement, ColumnClause
+from sqlalchemy.ext.compiler import compiles
+from sqlalchemy.sql import table, column
+from testlib import *
+
+class UserDefinedTest(TestBase, AssertsCompiledSQL):
+
+    def test_column(self):
+
+        class MyThingy(ColumnClause):
+            def __init__(self, arg= None):
+                super(MyThingy, self).__init__(arg or 'MYTHINGY!')
+
+        @compiles(MyThingy)
+        def visit_thingy(thingy, compiler, **kw):
+            return ">>%s<<" % thingy.name
+
+        self.assert_compile(
+            select([column('foo'), MyThingy()]),
+            "SELECT foo, >>MYTHINGY!<<"
+        )
+
+        self.assert_compile(
+            select([MyThingy('x'), MyThingy('y')]).where(MyThingy() == 5),
+            "SELECT >>x<<, >>y<< WHERE >>MYTHINGY!<< = :MYTHINGY!_1"
+        )
+
+    def test_stateful(self):
+        class MyThingy(ColumnClause):
+            def __init__(self):
+                super(MyThingy, self).__init__('MYTHINGY!')
+
+        @compiles(MyThingy)
+        def visit_thingy(thingy, compiler, **kw):
+            if not hasattr(compiler, 'counter'):
+                compiler.counter = 0
+            compiler.counter += 1
+            return str(compiler.counter)
+
+        self.assert_compile(
+            select([column('foo'), MyThingy()]).order_by(desc(MyThingy())),
+            "SELECT foo, 1 ORDER BY 2 DESC"
+        )
+
+        self.assert_compile(
+            select([MyThingy(), MyThingy()]).where(MyThingy() == 5),
+            "SELECT 1, 2 WHERE 3 = :MYTHINGY!_1"
+        )
+
+    def test_callout_to_compiler(self):
+        class InsertFromSelect(ClauseElement):
+            def __init__(self, table, select):
+                self.table = table
+                self.select = select
+
+        @compiles(InsertFromSelect)
+        def visit_insert_from_select(element, compiler, **kw):
+            return "INSERT INTO %s (%s)" % (
+                compiler.process(element.table, asfrom=True),
+                compiler.process(element.select)
+            )
+
+        t1 = table("mytable", column('x'), column('y'), column('z'))
+        self.assert_compile(
+            InsertFromSelect(
+                t1,
+                select([t1]).where(t1.c.x>5)
+            ),
+            "INSERT INTO mytable (SELECT mytable.x, mytable.y, mytable.z FROM mytable WHERE mytable.x > :x_1)"
+        )
+
+    def test_dialect_specific(self):
+        class AddThingy(ClauseElement):
+            __visit_name__ = 'add_thingy'
+
+        class DropThingy(ClauseElement):
+            __visit_name__ = 'drop_thingy'
+
+        @compiles(AddThingy, 'sqlite')
+        def visit_add_thingy(thingy, compiler, **kw):
+            return "ADD SPECIAL SL THINGY"
+
+        @compiles(AddThingy)
+        def visit_add_thingy(thingy, compiler, **kw):
+            return "ADD THINGY"
+
+        @compiles(DropThingy)
+        def visit_drop_thingy(thingy, compiler, **kw):
+            return "DROP THINGY"
+
+        self.assert_compile(AddThingy(),
+            "ADD THINGY"
+        )
+
+        self.assert_compile(DropThingy(),
+            "DROP THINGY"
+        )
+
+        from sqlalchemy.databases import sqlite as base
+        self.assert_compile(AddThingy(),
+            "ADD SPECIAL SL THINGY",
+            dialect=base.dialect()
+        )
+
+        self.assert_compile(DropThingy(),
+            "DROP THINGY",
+            dialect=base.dialect()
+        )
+
+        @compiles(DropThingy, 'sqlite')
+        def visit_drop_thingy(thingy, compiler, **kw):
+            return "DROP SPECIAL SL THINGY"
+
+        self.assert_compile(DropThingy(),
+            "DROP SPECIAL SL THINGY",
+            dialect=base.dialect()
+        )
+
+        self.assert_compile(DropThingy(),
+            "DROP THINGY",
+        )
+
+if __name__ == '__main__':
+    testenv.main()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.