]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
more consistent treatment of columns, differentiation of selectable/non-selectable,
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 4 Dec 2005 18:27:52 +0000 (18:27 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 4 Dec 2005 18:27:52 +0000 (18:27 +0000)
docstrings

lib/sqlalchemy/ansisql.py
lib/sqlalchemy/sql.py

index c5de1adfd4455e2e615676792900ccf4ad4a3323..2a709e3b757ab621c655d2472658364ffdf54cdb 100644 (file)
@@ -53,6 +53,7 @@ class ANSICompiler(sql.Compiled):
         self.froms = {}
         self.wheres = {}
         self.strings = {}
+        self.select_stack = []
         self.typemap = typemap or {}
         self.isinsert = False
         
@@ -127,6 +128,13 @@ class ANSICompiler(sql.Compiled):
             return d
 
     def visit_column(self, column):
+        if len(self.select_stack):
+            # if we are within a visit to a Select, set up the "typemap"
+            # for this column which is used to translate result set values
+            if self.select_stack[-1].use_labels:
+                self.typemap.setdefault(column.label, column.type)
+            else:
+                self.typemap.setdefault(column.key, column.type)
         if column.table.name is None:
             self.strings[column] = column.name
         else:
@@ -210,20 +218,17 @@ class ANSICompiler(sql.Compiled):
     def visit_select(self, select):
         inner_columns = []
 
+        self.select_stack.append(select)
         for c in select._raw_columns:
-            # TODO:  hackish.  try to get a more polymorphic approach.
-            if hasattr(c, 'columns'):
+            if c.is_selectable():
                 for co in c.columns:
                     co.accept_visitor(self)
                     inner_columns.append(co)
-                    if select.use_labels:
-                        self.typemap.setdefault(co.label, co.type)
-                    else:
-                        self.typemap.setdefault(co.key, co.type)
             else:
                 c.accept_visitor(self)
                 inner_columns.append(c)
-                
+        self.select_stack.pop(-1)
+        
         if select.use_labels:
             collist = string.join(["%s AS %s" % (self.get_str(c), c.label) for c in inner_columns], ', ')
         else:
index 4f7090cf7bc74c6f010ffc191f0499bf4d65ffa1..a5ff50900eddceeed5b79e8df6c6c7fad4849ab6 100644 (file)
@@ -23,7 +23,7 @@ import sqlalchemy.util as util
 import sqlalchemy.types as types
 import string, re
 
-__all__ = ['text', 'column', 'func', 'select', 'update', 'insert', 'delete', 'join', 'and_', 'or_', 'not_', 'union', 'union_all', 'desc', 'asc', 'outerjoin', 'alias', 'subquery', 'literal', 'bindparam', 'sequence', 'exists']
+__all__ = ['text', 'column', 'func', 'select', 'update', 'insert', 'delete', 'join', 'and_', 'or_', 'not_', 'union', 'union_all', 'desc', 'asc', 'outerjoin', 'alias', 'subquery', 'literal', 'bindparam', 'exists']
 
 def desc(column):
     """returns a descending ORDER BY clause element, e.g.:
@@ -51,7 +51,7 @@ def join(left, right, onclause, **kwargs):
     together, use the resulting Join object's "join()" or "outerjoin()" methods."""
     return Join(left, right, onclause, **kwargs)
 
-def select(columns, whereclause = None, from_obj = [], **kwargs):
+def select(columns=None, whereclause = None, from_obj = [], **kwargs):
     """returns a SELECT clause element.
     
     'columns' is a list of columns and/or selectable items to select columns from
@@ -113,19 +113,18 @@ def delete(table, whereclause = None, **kwargs):
     return Delete(table, whereclause, **kwargs)
 
 def and_(*clauses):
+    """joins a list of clauses together by the AND operator.  the & operator can be used as well."""
     return _compound_clause('AND', *clauses)
 
 def or_(*clauses):
-    clause = _compound_clause('OR', *clauses)
-    return clause
+    """joins a list of clauses together by the OR operator.  the | operator can be used as well."""
+    return _compound_clause('OR', *clauses)
 
 def not_(clause):
+    """returns a negation of the given clause, i.e. NOT(clause).  the ~ operator can be used as well."""
     clause.parens=True
     return BinaryClause(TextClause("NOT"), clause, None)
-
             
-def column(table, text):
-    return ColumnClause(text, table)
         
 def exists(*args, **params):
     s = select(*args, **params)
@@ -144,23 +143,47 @@ def subquery(alias, *args, **params):
     return Alias(Select(*args, **params), alias)
 
 def literal(value, type=None):
+    """returns a literal clause, bound to a bind parameter.  
+    
+    literal clauses are created automatically when used as the right-hand 
+    side of a boolean or math operation against a column object.  use this 
+    function when a literal is needed on the left-hand side (and optionally on the right as well).
+    
+    the optional type parameter is a sqlalchemy.types.TypeEngine object which indicates bind-parameter
+    and result-set translation for this literal.
+    """
     return BindParamClause('literal', value, type=type)
+
+def column(table, text):
+    """returns a textual column clause, relative to a table.  this differs from using straight text
+    or text() in that the column is treated like a regular column, i.e. gets added to a Selectable's list
+    of columns."""
+    return ColumnClause(text, table)
     
 def bindparam(key, value = None, type=None):
+    """creates a bind parameter clause with the given key.  
+    
+    An optional default value can be specified by the value parameter, and the optional type parameter
+    is a sqlalchemy.types.TypeEngine object which indicates bind-parameter and result-set translation for
+    this bind parameter."""
     if isinstance(key, schema.Column):
         return BindParamClause(key.name, value, type=key.type)
     else:
         return BindParamClause(key, value, type=type)
 
 def text(text, engine=None):
+    """creates literal text to be inserted into a query.  
+    
+    When constructing a query from a select(), update(), insert() or delete(), using 
+    plain strings for argument values will usually result in text objects being created
+    automatically.  Use this function when creating textual clauses outside of other
+    ClauseElement objects, or optionally wherever plain text is to be used."""
     return TextClause(text, engine=engine)
 
 def null():
+    """returns a Null object, which compiles to NULL in a sql statement."""
     return Null()
     
-def sequence():
-    return Sequence()
-
 class FunctionGateway(object):
     """returns a callable based on an attribute name, which then returns a Function 
     object with that name."""
@@ -273,6 +296,11 @@ class ClauseElement(object):
         new structure can then be restructured without affecting the original."""
         return self
 
+    def is_selectable(self):
+        """returns True if this ClauseElement is Selectable, i.e. it contains a list of Column
+        objects and can be used as the target of a select statement."""
+        return False
+
     def _find_engine(self):
         try:
             if self._engine is not None:
@@ -288,6 +316,13 @@ class ClauseElement(object):
             
     engine = property(lambda s: s._find_engine())
 
+    def _get_columns(self):
+        try:
+            return self._columns
+        except AttributeError:
+            return [self]
+    columns = property(lambda s: s._get_columns())
+    
     def compile(self, engine = None, bindparams = None, typemap=None):
         """compiles this SQL expression using its underlying SQLEngine to produce
         a Compiled object.  If no engine can be found, an ansisql engine is used.
@@ -397,50 +432,6 @@ class CompareMixin(object):
         return BinaryClause(self, obj, operator)
 
         
-class ColumnClause(ClauseElement, CompareMixin):
-    """represents a textual column clause in a SQL statement."""
-
-    def __init__(self, text, selectable=None):
-        self.text = text
-        self.table = selectable
-        self._impl = ColumnImpl(self)
-        self.type = types.NullTypeEngine()
-        
-    columns = property(lambda self: [self])
-    name = property(lambda self:self.text)
-    key = property(lambda self:self.text)
-    label = property(lambda self:self.text)
-
-    def accept_visitor(self, visitor): 
-        visitor.visit_columnclause(self)
-
-    def hash_key(self):
-        if self.table is not None:
-            return "ColumnClause(%s, %s)" % (self.text, self.table.hash_key())
-        else:
-            return "ColumnClause(%s)" % self.text
-
-    def _get_from_objects(self):
-        return []
-
-    def _compare(self, operator, obj):
-        if _is_literal(obj):
-            if obj is None:
-                if operator != '=':
-                    raise "Only '=' operator can be used with NULL"
-                return BinaryClause(self, null(), 'IS')
-            elif self.table.name is None:
-                obj = BindParamClause(self.text, obj, shortname=self.text, type=self.type)
-            else:
-                obj = BindParamClause(self.table.name + "_" + self.text, obj, shortname = self.text, type=self.type)
-
-        return BinaryClause(self, obj, operator)
-
-    def _make_proxy(self, selectable, name = None):
-        c = ColumnClause(self.text or name, selectable)
-        selectable.columns[c.key] = c
-        c._impl = ColumnImpl(c)
-        return c
 
 class FromClause(ClauseElement):
     """represents a FROM clause element in a SQL statement."""
@@ -568,7 +559,6 @@ class Function(ClauseList, CompareMixin):
         self.type = kwargs.get('type', None)
         self.label = kwargs.get('label', None)
         ClauseList.__init__(self, parens=True, *clauses)
-    columns = property(lambda self: [self])
     key = property(lambda self:self.label or self.name)
     def append(self, clause):
         if _is_literal(clause):
@@ -597,7 +587,7 @@ class Function(ClauseList, CompareMixin):
         return self
 
         
-class BinaryClause(ClauseElement):
+class BinaryClause(ClauseElement, CompareMixin):
     """represents two clauses with an operator in between"""
     
     def __init__(self, left, right, operator):
@@ -633,6 +623,9 @@ class Selectable(FromClause):
     def accept_visitor(self, visitor):
         raise NotImplementedError()
     
+    def is_selectable(self):
+        return True
+        
     def select(self, whereclauses = None, **params):
         return select([self], whereclauses, **params)
 
@@ -664,9 +657,9 @@ class Join(Selectable):
         self.id = self.left.id + "_" + self.right.id
         self.allcols = allcols
         if allcols:
-            self.columns = [c for c in self.left.columns] + [c for c in self.right.columns]
+            self._columns = [c for c in self.left.columns] + [c for c in self.right.columns]
         else:
-            self.columns = self.right.columns
+            self._columns = self.right.columns
 
         # TODO: if no onclause, do NATURAL JOIN
         self.onclause = onclause
@@ -721,7 +714,7 @@ class Join(Selectable):
 class Alias(Selectable):
     def __init__(self, selectable, alias = None):
         self.selectable = selectable
-        self.columns = util.OrderedProperties()
+        self._columns = util.OrderedProperties()
         self.foreign_keys = []
         if alias is None:
             alias = id(self)
@@ -752,6 +745,50 @@ class Alias(Selectable):
         
     engine = property(lambda s: s.selectable.engine)
 
+class ColumnClause(Selectable, CompareMixin):
+    """represents a textual column clause in a SQL statement. allows the creation
+    of an additional ad-hoc column that is compiled against a particular table."""
+
+    def __init__(self, text, selectable=None):
+        self.text = text
+        self.table = selectable
+        self._impl = ColumnImpl(self)
+        self.type = types.NullTypeEngine()
+
+    name = property(lambda self:self.text)
+    key = property(lambda self:self.text)
+    label = property(lambda self:self.text)
+
+    def accept_visitor(self, visitor): 
+        visitor.visit_columnclause(self)
+
+    def hash_key(self):
+        if self.table is not None:
+            return "ColumnClause(%s, %s)" % (self.text, self.table.hash_key())
+        else:
+            return "ColumnClause(%s)" % self.text
+
+    def _get_from_objects(self):
+        return []
+
+    def _compare(self, operator, obj):
+        if _is_literal(obj):
+            if obj is None:
+                if operator != '=':
+                    raise "Only '=' operator can be used with NULL"
+                return BinaryClause(self, null(), 'IS')
+            elif self.table.name is None:
+                obj = BindParamClause(self.text, obj, shortname=self.text, type=self.type)
+            else:
+                obj = BindParamClause(self.table.name + "_" + self.text, obj, shortname = self.text, type=self.type)
+
+        return BinaryClause(self, obj, operator)
+
+    def _make_proxy(self, selectable, name = None):
+        c = ColumnClause(self.text or name, selectable)
+        selectable.columns[c.key] = c
+        c._impl = ColumnImpl(c)
+        return c
 
 class ColumnImpl(Selectable, CompareMixin):
     """Selectable implementation that gets attached to a schema.Column object."""
@@ -759,7 +796,7 @@ class ColumnImpl(Selectable, CompareMixin):
     def __init__(self, column):
         self.column = column
         self.name = column.name
-        self.columns = [self.column]
+        self._columns = [self.column]
         
         if column.table.name:
             self.label = column.table.name + "_" + self.column.name
@@ -796,8 +833,6 @@ class ColumnImpl(Selectable, CompareMixin):
 
         return BinaryClause(self.column, obj, operator)
 
-
-
 class TableImpl(Selectable):
     """attached to a schema.Table to provide it with a Selectable interface
     as well as other functions
@@ -914,8 +949,8 @@ class CompoundSelect(Selectable, TailClauseMixin):
 class Select(Selectable, TailClauseMixin):
     """finally, represents a SELECT statement, with appendable clauses, as well as 
     the ability to execute itself and return a result set."""
-    def __init__(self, columns, whereclause = None, from_obj = [], order_by = None, group_by=None, having=None, use_labels = False, distinct=False, engine = None):
-        self.columns = util.OrderedProperties()
+    def __init__(self, columns=None, whereclause = None, from_obj = [], order_by = None, group_by=None, having=None, use_labels = False, distinct=False, engine = None):
+        self._columns = util.OrderedProperties()
         self._froms = util.OrderedDict()
         self.use_labels = use_labels
         self.id = "Select(%d)" % id(self)
@@ -939,8 +974,9 @@ class Select(Selectable, TailClauseMixin):
         self._correlator = Select.CorrelatedVisitor(self, False)
         self._wherecorrelator = Select.CorrelatedVisitor(self, True)
         
-        for c in columns:
-            self.append_column(c)
+        if columns is not None:
+            for c in columns:
+                self.append_column(c)
             
         if whereclause is not None:
             self.append_whereclause(whereclause)
@@ -981,9 +1017,7 @@ class Select(Selectable, TailClauseMixin):
                 self.rowid_column = f.rowid_column._make_proxy(self)
         column._process_from_dict(self._froms, False)
 
-        # TODO: dont use hasattr here, get a general way to locate
-        # selectable columns off stuff working completely (i.e. Selectable)
-        if hasattr(column, 'columns'):
+        if column.is_selectable():
             for co in column.columns:
                 if self.use_labels:
                     co._make_proxy(self, name = co.label)
@@ -1175,11 +1209,4 @@ class Delete(UpdateBase):
             self.whereclause.accept_visitor(visitor)
         visitor.visit_delete(self)
 
-class Sequence(BindParamClause):
-    def __init__(self):
-        BindParamClause.__init__(self, 'sequence')
-
-    def accept_visitor(self, visitor):
-        visitor.visit_sequence(self)
-