diff options
Diffstat (limited to 'lib/sqlalchemy/sql.py')
-rw-r--r-- | lib/sqlalchemy/sql.py | 252 |
1 files changed, 176 insertions, 76 deletions
diff --git a/lib/sqlalchemy/sql.py b/lib/sqlalchemy/sql.py index 2c133f6c1..643f9e3d3 100644 --- a/lib/sqlalchemy/sql.py +++ b/lib/sqlalchemy/sql.py @@ -16,9 +16,8 @@ # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -"""base sql module used by all sql implementations. defines abstract units which construct -expression trees that generate into text strings + bind parameters. -""" +"""defines the base components of SQL expression trees.""" + import sqlalchemy.schema as schema import sqlalchemy.util as util import string @@ -26,28 +25,74 @@ import string __ALL__ = ['textclause', 'select', 'join', 'and_', 'or_', 'union', 'desc', 'asc', 'outerjoin', 'alias', 'subquery', 'bindparam', 'sequence'] def desc(column): + """returns a descending ORDER BY clause element""" return CompoundClause(None, column, "DESC") def asc(column): + """returns an ascending ORDER BY clause element""" return CompoundClause(None, column, "ASC") -def outerjoin(left, right, onclause, **params): - return Join(left, right, onclause, isouter = True, **params) +def outerjoin(left, right, onclause, **kwargs): + """returns an OUTER JOIN clause element, given the left and right hand expressions, + as well as the ON condition's expression. When chaining joins together, the previous JOIN + expression should be specified as the left side of this JOIN expression.""" + return Join(left, right, onclause, isouter = True, **kwargs) + +def join(left, right, onclause, **kwargs): + """returns a JOIN clause element (regular inner join), given the left and right hand expressions, + as well as the ON condition's expression. When chaining joins together, the previous JOIN + expression should be specified as the left side of this JOIN expression.""" + return Join(left, right, onclause, **kwargs) + +def select(columns, whereclause = None, from_obj = [], **kwargs): + """returns a SELECT clause element, given a list of columns and/or selectable items to select + columns from, an optional expression for the WHERE clause, an optional list of "FROM" objects + to select from, and additional parameters.""" + return Select(columns, whereclause = whereclause, from_obj = from_obj, **kwargs) + +def insert(table, values = None, **kwargs): + """returns an INSERT clause element. -def join(left, right, onclause, **params): - return Join(left, right, onclause, **params) - -def select(columns, whereclause = None, from_obj = [], **params): - return Select(columns, whereclause = whereclause, from_obj = from_obj, **params) - -def insert(table, values = None, **params): - return Insert(table, values, **params) + 'table' is the table to be inserted into. + 'values' is a dictionary which specifies the column specifications of the INSERT, and is optional. + If left as None, the + column specifications are determined from the bind parameters used during the compile phase of the + INSERT statement. If the bind parameters also are None during the compile phase, then the column + specifications will be generated from the full list of table columns. + + If both 'values' and compile-time bind parameters are present, the compile-time bind parameters + override the information specified within 'values' on a per-key basis. + + The keys within 'values' can be either Column objects or their string identifiers. + Each key may reference one of: a literal data value (i.e. string, number, etc.), a Column object, + or a SELECT statement. If a SELECT statement is specified which references this INSERT statement's + table, the statement will be correlated against the INSERT statement. + """ + return Insert(table, values, **kwargs) -def update(table, whereclause = None, values = None, **params): - return Update(table, whereclause, values, **params) +def update(table, whereclause = None, values = None, **kwargs): + """returns an UPDATE clause element. + + 'table' is the table to be updated. + 'whereclause' is a ClauseElement describing the WHERE condition of the UPDATE statement. + 'values' is a dictionary which specifies the SET conditions of the UPDATE, and is optional. + If left as None, the + SET conditions are determined from the bind parameters used during the compile phase of the + UPDATE statement. If the bind parameters also are None during the compile phase, then the SET + conditions will be generated from the full list of table columns. + + If both 'values' and compile-time bind parameters are present, the compile-time bind parameters + override the information specified within 'values' on a per-key basis. + + The keys within 'values' can be either Column objects or their string identifiers. + Each key may reference one of: a literal data value (i.e. string, number, etc.), a Column object, + or a SELECT statement. If a SELECT statement is specified which references this UPDATE statement's + table, the statement will be correlated against the UPDATE statement. + """ + return Update(table, whereclause, values, **kwargs) -def delete(table, whereclause = None, **params): - return Delete(table, whereclause, **params) +def delete(table, whereclause = None, **kwargs): + return Delete(table, whereclause, **kwargs) def and_(*clauses): return _compound_clause('AND', *clauses) @@ -55,7 +100,7 @@ def and_(*clauses): def or_(*clauses): clause = _compound_clause('OR', *clauses) return clause - + def union(*selects, **params): return _compound_select('UNION', *selects, **params) @@ -73,23 +118,25 @@ def textclause(text): def sequence(): return Sequence() - + def _compound_clause(keyword, *clauses): return CompoundClause(keyword, *clauses) def _compound_select(keyword, *selects, **params): - if len(selects) == 0: return None - + if len(selects) == 0: + return None s = selects[0] for n in selects[1:]: s.append_clause(keyword, n) - + if params.get('order_by', None) is not None: s.order_by(*params['order_by']) return s class ClauseVisitor(schema.SchemaVisitor): + """builds upon SchemaVisitor to define the visiting of SQL statement elements in + addition to Schema elements.""" def visit_columnclause(self, column):pass def visit_fromclause(self, fromclause):pass def visit_bindparam(self, bindparam):pass @@ -101,13 +148,23 @@ class ClauseVisitor(schema.SchemaVisitor): def visit_join(self, join):pass class Compiled(ClauseVisitor): - pass - + """represents a compiled SQL expression. the __str__ method of the Compiled object + should produce the actual text of the statement. Compiled objects are specific to the database + library that created them, and also may or may not be specific to the columns referenced + within a particular set of bind parameters. In no case should the Compiled object be dependent + on the actual values of those bind parameters, even though it may reference those values + as defaults.""" + def __str__(self): + raise NotImplementedError() + def get_params(self, **params): + """returns the bind params for this compiled object, with values overridden by + those given in the **params dictionary""" + raise NotImplementedError() + class ClauseElement(object): - """base class for elements of a generated SQL statement. + """base class for elements of a programmatically constructed SQL expression. - includes a parameter hash to store bind parameter key/value pairs, - as well as a list of 'from objects' which collects items to be placed + includes a list of 'from objects' which collects items to be placed in the FROM clause of a SQL statement. when many ClauseElements are attached together, the from objects and bind @@ -115,26 +172,46 @@ class ClauseElement(object): """ def hash_key(self): + """returns a string that uniquely identifies the concept this ClauseElement represents. + + two ClauseElements can have the same value for hash_key() iff they both correspond to the + exact same generated SQL. This allows the hash_key() values of a collection of ClauseElements + to be constructed into a larger identifying string for the purpose of caching a SQL expression. + + Note that since ClauseElements may be mutable, the hash_key() value is subject to change + if the underlying structure of the ClauseElement changes.""" raise NotImplementedError(repr(self)) def _get_from_objects(self): raise NotImplementedError(repr(self)) def accept_visitor(self, visitor): raise NotImplementedError(repr(self)) - def compile(self, engine, bindparams = None): - return engine.compile(self, bindparams = bindparams) - def copy_structure(self): - """allows the copying of a statement's containers, so that a modified statement - can be produced without affecting the original. containing clauseelements, - like Select, Join, CompoundClause, BinaryClause, etc., should produce a copy of - themselves, whereas "leaf-node" clauseelements should return themselves.""" + def copy_container(self): + """should return a copy of this ClauseElement, iff this ClauseElement contains other + ClauseElements. Otherwise, it should be left alone to return self. This is used to create + copies of expression trees that still reference the same "leaf nodes". The new structure + can then be restructured without affecting the original.""" return self - + def _engine(self): + """should return a SQLEngine instance that is associated with this expression tree. + this engine is usually attached to one of the underlying Table objects within the expression.""" raise NotImplementedError("Object %s has no built-in SQLEngine." % repr(self)) - + + def compile(self, engine, bindparams = None): + """compiles this SQL expression using its underlying SQLEngine to produce + a Compiled object. The actual SQL statement is the Compiled object's string representation. + bindparams is an optional dictionary representing the bind parameters to be used with + the statement. Currently, only the compilations of INSERT and UPDATE statements + use the bind parameters, in order to determine which + table columns should be used in the statement.""" + return engine.compile(self, bindparams = bindparams) + def execute(self, **params): + """compiles and executes this SQL expression using its underlying SQLEngine. + the given **params are used as bind parameters when compiling and executing the expression. + the DBAPI cursor object is returned.""" e = self._engine() c = self.compile(e, bindparams = params) # TODO: do pre-execute right here, for sequences, if the compiled object @@ -142,13 +219,14 @@ class ClauseElement(object): return e.execute(str(c), c.get_params(), echo = getattr(self, 'echo', None)) def result(self, **params): + """the same as execute(), except a RowProxy object is returned instead of a DBAPI cursor.""" e = self._engine() c = self.compile(e, bindparams = params) - return e.result(str(c), c.binds) - + return e.result(str(c), c.get_params(), echo = getattr(self, 'echo', None)) + class ColumnClause(ClauseElement): - """represents a column clause element in a SQL statement.""" - + """represents a textual column clause in a SQL statement.""" + def __init__(self, text, selectable): self.text = text self.table = selectable @@ -165,7 +243,7 @@ class ColumnClause(ClauseElement): def hash_key(self): return "ColumnClause(%s, %s)" % (self.text, self.table.hash_key()) - + def _get_from_objects(self): return [] @@ -236,8 +314,8 @@ class CompoundClause(ClauseElement): if c is None: continue self.append(c) - def copy_structure(self): - clauses = [clause.copy_structure() for clause in self.clauses] + def copy_container(self): + clauses = [clause.copy_container() for clause in self.clauses] return CompoundClause(self.operator, *clauses) def append(self, clause): @@ -279,8 +357,8 @@ class BinaryClause(ClauseElement): self.operator = operator self.parens = False - def copy_structure(self): - return BinaryClause(self.left.copy_structure(), self.right.copy_structure(), self.operator) + def copy_container(self): + return BinaryClause(self.left.copy_container(), self.right.copy_container(), self.operator) def _get_from_objects(self): return self.left._get_from_objects() + self.right._get_from_objects() @@ -394,7 +472,7 @@ class ColumnSelectable(Selectable): self.label = self.column.name self.fullname = self.column.name - def copy_structure(self): + def copy_container(self): return self.column def _get_from_objects(self): @@ -594,29 +672,59 @@ class Select(Selectable): class UpdateBase(ClauseElement): + """forms the base for INSERT, UPDATE, and DELETE statements. + Deals with the special needs of INSERT and UPDATE parameter lists - + these statements have two separate lists of parameters, those + defined when the statement is constructed, and those specified at compile time.""" + + def _process_colparams(self, parameters): + if parameters is None: + return None + + for key in parameters.keys(): + value = parameters[key] + if isinstance(value, Select): + value.append_from(FromClause(from_key=self.table.id)) + elif not isinstance(value, schema.Column) and not isinstance(value, ClauseElement): + try: + col = self.table.c[key] + parameters[key] = bindparam(col.name, value) + except KeyError: + del parameters[key] + + return parameters + def get_colparams(self, parameters): - values = [] + # case one: no parameters in the statement, no parameters in the + # compiled params - just return binds for all the table columns + if parameters is None and self.parameters is None: + return [(c, bindparam(c.name)) for c in self.table.columns] + # if we have statement parameters - set defaults in the + # compiled params if parameters is None: - parameters = self.parameters + parameters = {} - if parameters is None: - for c in self.table.columns: - values.append((c, bindparam(c.name))) - else: - d = {} - for key, value in parameters.iteritems(): - if isinstance(key, schema.Column): - d[key] = value - else: - d[self.table.columns[str(key)]] = value - - for c in self.table.columns: - if d.has_key(c): - value = d[c] - if not isinstance(value, BindParamClause): - value = bindparam(c.name, value) - values.append((c, value)) + if self.parameters is not None: + for k, v in self.parameters.iteritems(): + parameters.setdefault(k, v) + + # now go thru compiled params, get the Column object for each key + d = {} + for key, value in parameters.iteritems(): + if isinstance(key, schema.Column): + d[key] = value + else: + d[self.table.columns[str(key)]] = value + + # create a list of column assignment clauses as tuples + values = [] + for c in self.table.columns: + if d.has_key(c): + value = d[c] + if isinstance(value, str): + value = bindparam(c.name, value) + values.append((c, value)) return values def _engine(self): @@ -635,7 +743,7 @@ class Insert(UpdateBase): def __init__(self, table, parameters = None, **params): self.table = table self.select = None - self.parameters = parameters + self.parameters = self._process_colparams(parameters) self.engine = self.table._engine() def accept_visitor(self, visitor): @@ -650,41 +758,33 @@ class Insert(UpdateBase): def compile(self, engine = None, bindparams = None): if engine is None: engine = self.engine - if engine is None: raise "no engine supplied, and no engine could be located within the clauses!" - return engine.compile(self, bindparams) class Update(UpdateBase): def __init__(self, table, whereclause, parameters = None, **params): self.table = table self.whereclause = whereclause - - self.parameters = parameters + self.parameters = self._process_colparams(parameters) self.engine = self.table._engine() - def accept_visitor(self, visitor): if self.whereclause is not None: self.whereclause.accept_visitor(visitor) - visitor.visit_update(self) class Delete(UpdateBase): def __init__(self, table, whereclause, **params): self.table = table self.whereclause = whereclause - self.engine = self.table._engine() - def accept_visitor(self, visitor): if self.whereclause is not None: self.whereclause.accept_visitor(visitor) - visitor.visit_delete(self) - + class Sequence(BindParamClause): def __init__(self): BindParamClause.__init__(self, 'sequence') |