# along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-"""builds upon the schema and sql packages to provide a central object for tying schema
-objects and sql constructs to database-specific query compilation and execution"""
+"""Defines the SQLEngine class, which serves as the primary "database" object
+used throughout the sql construction and object-relational mapper packages.
+A SQLEngine is a facade around a single connection pool corresponding to a
+particular set of connection parameters, and provides thread-local transactional
+methods and statement execution methods for Connection objects. It also provides
+a facade around a Cursor object to allow richer column selection for result rows
+as well as type conversion operations, known as a ResultProxy.
+
+A SQLEngine is provided to an application as a subclass that is specific to a particular type
+of DBAPI, and is the central switching point for abstracting different kinds of database
+behavior into a consistent set of behaviors. It provides a variety of factory methods
+to produce everything specific to a certain kind of database, including a Compiler,
+schema creation/dropping objects, and TableImpl and ColumnImpl objects to augment the
+behavior of table metadata objects.
+
+The term "database-specific" will be used to describe any object or function that has behavior
+corresponding to a particular vendor, such as mysql-specific, sqlite-specific, etc.
+"""
import sqlalchemy.schema as schema
import sqlalchemy.pool
__all__ = ['create_engine', 'engine_descriptors']
-def create_engine(name, *args ,**kwargs):
- """creates a new SQLEngine instance.
+def create_engine(name, opts=None,**kwargs):
+ """creates a new SQLEngine instance. There are two forms of calling this method.
- name - the type of engine to load, i.e. 'sqlite', 'postgres', 'oracle'
+ In the first, the "name" argument is the type of engine to load, i.e. 'sqlite', 'postgres',
+ 'oracle', 'mysql'. "opts" is a dictionary of options to be sent to the underlying DBAPI module
+ to create a connection, usually including a hostname, username, password, etc.
- *args, **kwargs - sent directly to the specific engine instance as connect arguments,
- options.
+ In the second, the "name" argument is a URL in the form <enginename>://opt1=val1&opt2=val2.
+ Where <enginename> is the name as above, and the contents of the option dictionary are
+ spelled out as a URL encoded string. The "opts" argument is not used.
+
+ In both cases, **kwargs represents options to be sent to the SQLEngine itself. A possibly
+ partial listing of those options is as follows:
+
+ pool=None : an instance of sqlalchemy.pool.DBProxy to be used as the underlying source
+ for connections (DBProxy is described in the previous section). If None, a default DBProxy
+ will be created using the engine's own database module with the given arguments.
+
+ echo=False : if True, the SQLEngine will log all statements as well as a repr() of their
+ parameter lists to the engines logger, which defaults to sys.stdout. A SQLEngine instances'
+ "echo" data member can be modified at any time to turn logging on and off. If set to the string
+ 'debug', result rows will be printed to the standard output as well.
+
+ logger=None : a file-like object where logging output can be sent, if echo is set to True.
+ This defaults to sys.stdout.
+
+ module=None : used by Oracle and Postgres, this is a reference to a DBAPI2 module to be used
+ instead of the engine's default module. For Postgres, the default is psycopg2, or psycopg1 if
+ 2 cannot be found. For Oracle, its cx_Oracle. For mysql, MySQLdb.
+
+ use_ansi=True : used only by Oracle; when False, the Oracle driver attempts to support a
+ particular "quirk" of some Oracle databases, that the LEFT OUTER JOIN SQL syntax is not
+ supported, and the "Oracle join" syntax of using <column1>(+)=<column2> must be used
+ in order to achieve a LEFT OUTER JOIN. Its advised that the Oracle database be configured to
+ have full ANSI support instead of using this feature.
+
"""
m = re.match(r'(\w+)://(.*)', name)
if m is not None:
return module.engine(*args, **kwargs)
def engine_descriptors():
+ """provides a listing of all the database implementations supported. this data
+ is provided as a list of dictionaries, where each dictionary contains the following
+ key/value pairs:
+
+ name : the name of the engine, suitable for use in the create_engine function
+
+ description: a plain description of the engine.
+
+ arguments : a dictionary describing the name and description of each parameter
+ used to connect to this engine's underlying DBAPI.
+
+ This function is meant for usage in automated configuration tools that wish to
+ query the user for database and connection information.
+ """
result = []
for module in sqlalchemy.databases.__all__:
module = getattr(__import__('sqlalchemy.databases.%s' % module).databases, module)
class SQLEngine(schema.SchemaEngine):
- """base class for a series of database-specific engines. serves as an abstract factory
- for implementation objects as well as database connections, transactions, SQL generators,
- etc."""
+ """
+ The central "database" object used by an application. Subclasses of this object is used
+ by the schema and SQL construction packages to provide database-specific behaviors,
+ as well as an execution and thread-local transaction context.
+
+ SQLEngines are constructed via the create_engine() function inside this package.
+ """
def __init__(self, pool = None, echo = False, logger = None, **params):
+ """constructs a new SQLEngine. SQLEngines should be constructed via the create_engine()
+ function which will construct the appropriate subclass of SQLEngine."""
# get a handle on the connection pool via the connect arguments
# this insures the SQLEngine instance integrates with the pool referenced
# by direct usage of pool.manager(<module>).connect(*args, **params)
self.positional=True
def type_descriptor(self, typeobj):
+ """provides a database-specific TypeEngine object, given the generic object
+ which comes from the types module. Subclasses will usually use the adapt_type()
+ method in the types module to make this job easy."""
if type(typeobj) is type:
typeobj = typeobj()
return typeobj
def schemagenerator(self, proxy, **params):
+ """returns a schema.SchemaVisitor instance that can generate schemas, when it is
+ invoked to traverse a set of schema objects. The
+ "proxy" argument is a callable will execute a given string SQL statement
+ and a dictionary or list of parameters.
+
+ schemagenerator is called via the create() method.
+ """
raise NotImplementedError()
def schemadropper(self, proxy, **params):
+ """returns a schema.SchemaVisitor instance that can drop schemas, when it is
+ invoked to traverse a set of schema objects. The
+ "proxy" argument is a callable will execute a given string SQL statement
+ and a dictionary or list of parameters.
+
+ schemagenerator is called via the drop() method.
+ """
raise NotImplementedError()
def defaultrunner(self, proxy):
+ """Returns a schema.SchemaVisitor instance that can execute the default values on a column.
+ The base class for this visitor is the DefaultRunner class inside this module.
+ This visitor will typically only receive schema.DefaultGenerator schema objects. The given
+ proxy is a callable that takes a string statement and a dictionary of bind parameters
+ to be executed. For engines that require positional arguments, the dictionary should
+ be an instance of OrderedDict which returns its bind parameters in the proper order.
+
+ defaultrunner is called within the context of the execute_compiled() method."""
return DefaultRunner(self, proxy)
def compiler(self, statement, parameters):
+ """returns a sql.ClauseVisitor which will produce a string representation of the given
+ ClauseElement and parameter dictionary. This object is usually a subclass of
+ ansisql.ANSICompiler.
+
+ compiler is called within the context of the compile() method."""
raise NotImplementedError()
def rowid_column_name(self):
return "oid"
def supports_sane_rowcount(self):
- """ill give everyone one guess which database warrants this method."""
+ """Provided to indicate when MySQL is being used, which does not have standard behavior
+ for the "rowcount" function on a statement handle. """
return True
def create(self, table, **params):
- """creates a table given a schema.Table object."""
+ """creates a table within this engine's database connection given a schema.Table object."""
table.accept_visitor(self.schemagenerator(self.proxy(), **params))
def drop(self, table, **params):
- """drops a table given a schema.Table object."""
+ """drops a table within this engine's database connection given a schema.Table object."""
table.accept_visitor(self.schemadropper(self.proxy(), **params))
def compile(self, statement, parameters, **kwargs):
raise NotImplementedError()
def tableimpl(self, table):
- """returns a new sql.TableImpl object to correspond to the given Table object."""
+ """returns a new sql.TableImpl object to correspond to the given Table object.
+ A TableImpl provides SQL statement builder operations on a Table metadata object,
+ and a subclass of this object may be provided by a SQLEngine subclass to provide
+ database-specific behavior."""
return sql.TableImpl(table)
def columnimpl(self, column):
- """returns a new sql.ColumnImpl object to correspond to the given Column object."""
+ """returns a new sql.ColumnImpl object to correspond to the given Column object.
+ A ColumnImpl provides SQL statement builder operations on a Column metadata object,
+ and a subclass of this object may be provided by a SQLEngine subclass to provide
+ database-specific behavior."""
return sql.ColumnImpl(column)
def get_default_schema_name(self):
+ """returns the currently selected schema in the current connection."""
return None
def last_inserted_ids(self):
- """returns a thread-local list of the primary keys for the last insert statement executed.
- This does not apply to straight textual clauses; only to sql.Insert objects compiled against a schema.Table object, which are executed via statement.execute(). The order of items in the list is the same as that of the Table's 'primary_key' attribute."""
+ """returns a thread-local list of the primary key values for the last insert statement executed.
+ This does not apply to straight textual clauses; only to sql.Insert objects compiled against
+ a schema.Table object, which are executed via statement.execute(). The order of items in the
+ list is the same as that of the Table's 'primary_key' attribute.
+
+ In some cases, this method may invoke a query back to the database to retrieve the data, based on
+ the "lastrowid" value in the cursor."""
raise NotImplementedError()
def connect_args(self):
connection.commit()
def proxy(self, **kwargs):
+ """provides a callable that will execute the given string statement and parameters.
+ The statement and parameters should be in the format specific to the particular database;
+ i.e. named or positional."""
return lambda s, p = None: self.execute(s, p, **kwargs)
def connection(self):
def multi_transaction(self, tables, func):
"""provides a transaction boundary across tables which may be in multiple databases.
+ If you have three tables, and a function that operates upon them, providing the tables as a
+ list and the function will result in a begin()/commit() pair invoked for each distinct engine
+ represented within those tables, and the function executed within the context of that transaction.
+ any exceptions will result in a rollback().
clearly, this approach only goes so far, such as if database A commits, then database B commits
and fails, A is already committed. Any failure conditions have to be raised before anyone
engine.commit()
def transaction(self, func):
+ """executes the given function within a transaction boundary. this is a shortcut for
+ explicitly calling begin() and commit() and optionally rollback() when execptions are raised."""
self.begin()
try:
func()
self.commit()
def begin(self):
+ """"begins" a transaction on a pooled connection, and stores the connection in a thread-local
+ context. repeated calls to begin() within the same thread will increment a counter that must be
+ decreased by corresponding commit() statements before an actual commit occurs. this is to provide
+ "nested" behavior of transactions so that different functions can all call begin()/commit() and still
+ call each other."""
if getattr(self.context, 'transaction', None) is None:
conn = self.connection()
self.do_begin(conn)
self.context.tcount += 1
def rollback(self):
+ """rolls back the current thread-local transaction started by begin(). the "begin" counter
+ is cleared and the transaction ended."""
if self.context.transaction is not None:
self.do_rollback(self.context.transaction)
self.context.transaction = None
self.context.tcount = None
def commit(self):
+ """commits the current thread-local transaction started by begin(). If begin() was called multiple
+ times, a counter will be decreased for each call to commit(), with the actual commit operation occuring
+ when the counter reaches zero. this is to provide
+ "nested" behavior of transactions so that different functions can all call begin()/commit() and still
+ call each other."""
if self.context.transaction is not None:
count = self.context.tcount - 1
self.context.tcount = count
def pre_exec(self, proxy, compiled, parameters, **kwargs):
+ """called by execute_compiled before the compiled statement is executed."""
pass
def post_exec(self, proxy, compiled, parameters, **kwargs):
+ """called by execute_compiled after the compiled statement is executed."""
pass
def execute_compiled(self, compiled, parameters, connection=None, cursor=None, echo=None, **kwargs):
except:
self.do_rollback(connection)
raise
- return ResultProxy(cursor, self, typemap = typemap)
+ return ResultProxy(cursor, self, typemap=typemap)
def _execute(self, c, statement, parameters):
c.execute(statement, parameters)
position, case-insensitive column name, or by schema.Column object. e.g.:
row = fetchone()
+
col1 = row[0] # access via integer position
+
col2 = row['col2'] # access via name
- col3 = row[mytable.c.mycol] # access via Column object.
- #the Column's 'label', 'key', and 'name' properties are
- # searched in that order.
+
+ col3 = row[mytable.c.mycol] # access via Column object.
+ ResultProxy also contains a map of TypeEngine objects and will invoke the appropriate
+ convert_result_value() method before returning columns.
"""
class AmbiguousColumn(object):
def __init__(self, key):
raise "Ambiguous column name '%s' in result set! try 'use_labels' option on select statement." % (self.key)
def __init__(self, cursor, engine, typemap = None):
+ """ResultProxy objects are constructed via the execute() method on SQLEngine."""
self.cursor = cursor
self.echo = engine.echo=="debug"
self.rowcount = engine.context.rowcount
class RowProxy:
"""proxies a single cursor row for a parent ResultProxy."""
def __init__(self, parent, row):
+ """RowProxy objects are constructed by ResultProxy objects."""
self.parent = parent
self.row = row
def __iter__(self):
# along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+"""the schema module provides the building blocks for database metadata. This means
+all the entities within a SQL database that we might want to look at, modify, or create
+and delete are described by these objects, in a database-agnostic way.
+
+A structure of SchemaItems also provides a "visitor" interface which is the primary
+method by which other methods operate upon the schema. The SQL package extends this
+structure with its own clause-specific objects as well as the visitor interface, so that
+the schema package "plugs in" to the SQL package.
+
+"""
+
from sqlalchemy.util import *
from sqlalchemy.types import *
import copy, re
-
__all__ = ['SchemaItem', 'Table', 'Column', 'ForeignKey', 'Sequence', 'SchemaEngine', 'SchemaVisitor']
item._set_parent(self)
def accept_visitor(self, visitor):
+ """all schema items implement an accept_visitor method that should call the appropriate
+ visit_XXXX method upon the given visitor object."""
raise NotImplementedError()
def _set_parent(self, parent):
return schema + "." + name
class TableSingleton(type):
+ """a metaclass used by the Table object to provide singleton behavior."""
def __call__(self, name, engine, *args, **kwargs):
try:
name = str(name) # in case of incoming unicode
__metaclass__ = TableSingleton
def __init__(self, name, engine, *args, **kwargs):
+ """Table objects can be constructed directly. The init method is actually called via
+ the TableSingleton metaclass. Arguments are:
+
+ name : the name of this table, exactly as it appears, or will appear, in the database.
+ This property, along with the "schema", indicates the "singleton identity" of this table.
+ Further tables constructed with the same name/schema combination will return the same
+ Table instance.
+
+ engine : a SchemaEngine instance to provide services to this table. Usually a subclass of
+ sql.SQLEngine.
+
+ *args : should contain a listing of the Column objects for this table.
+
+ **kwargs : options include:
+
+ schema=None : the "schema name" for this table, which is required if the table resides in a
+ schema other than the default selected schema for the engine's database connection.
+
+ autoload=False : the Columns for this table should be reflected from the database. Usually
+ there will be no Column objects in the constructor if this property is set.
+
+ redefine=False : if this Table has already been defined in the application, clear out its columns
+ and redefine with new arguments.
+
+ mustexist=False : indicates that this Table must already have been defined elsewhere in the application,
+ else an exception is raised.
+
+ useexisting=False : indicates that if this Table was already defined elsewhere in the application, disregard
+ the rest of the constructor arguments. If this flag and the "redefine" flag are not set, constructing
+ the same table twice will result in an exception.
+
+ """
self.name = name
self.columns = OrderedProperties()
self.c = self.columns
raise "Unknown arguments passed to Table: " + repr(kwargs.keys())
def reload_values(self, *args):
+ """clears out the columns and other properties of this Table, and reloads them from the
+ given argument list. This is used with the "redefine" keyword argument sent to the
+ metaclass constructor."""
self.columns = OrderedProperties()
self.c = self.columns
self.foreign_keys = []
self._init_items(*args)
def append_item(self, item):
+ """appends a Column item or other schema item to this Table."""
self._init_items(item)
def _set_parent(self, schema):
self.schema = schema
def accept_visitor(self, visitor):
+ """traverses the given visitor across the Column objects inside this Table,
+ then calls the visit_table method on the visitor."""
for c in self.columns:
c.accept_visitor(visitor)
return visitor.visit_table(self)
class Column(SchemaItem):
"""represents a column in a database table."""
def __init__(self, name, type, *args, **kwargs):
+ """constructs a new Column object. Arguments are:
+
+ name : the name of this column. this should be the identical name as it appears,
+ or will appear, in the database.
+
+ type : this is the type of column. This can be any subclass of types.TypeEngine,
+ including the database-agnostic types defined in the types module, database-specific types
+ defined within specific database modules, or user-defined types.
+
+ *args : ForeignKey and Sequence objects should be added as list values.
+
+ **kwargs : keyword arguments include:
+
+ key=None : an optional "alias name" for this column. The column will then be identified everywhere
+ in an application, including the column list on its Table, by this key, and not the given name.
+ Generated SQL, however, will still reference the column by its actual name.
+
+ primary_key=False : True if this column is a primary key column. Multiple columns can have this flag
+ set to specify composite primary keys.
+
+ nullable=True : True if this column should allow nulls. Defaults to True unless this column is a primary
+ key column.
+
+ default=None : a scalar, python callable, or ClauseElement representing the "default value" for this column,
+ which will be invoked upon insert if this column is not present in the insert list or is given a value
+ of None.
+
+ hidden=False : indicates this column should not be listed in the table's list of columns. Used for the "oid"
+ column, which generally isnt in column lists.
+ """
self.name = str(name) # in case of incoming unicode
self.type = type
self.args = args
return c
def accept_visitor(self, visitor):
+ """traverses the given visitor to this Column's default and foreign key object,
+ then calls visit_column on the visitor."""
if self.default is not None:
self.default.accept_visitor(visitor)
if self.foreign_key is not None:
def __str__(self): return self._impl.__str__()
class ForeignKey(SchemaItem):
+ """defines a ForeignKey constraint between two columns. ForeignKey is
+ specified as an argument to a Column object."""
def __init__(self, column):
+ """Constructs a new ForeignKey object. "column" can be a schema.Column
+ object representing the relationship, or just its string name given as
+ "tablename.columnname"."""
self._colspec = column
self._column = None
def copy(self):
+ """produces a copy of this ForeignKey object."""
if isinstance(self._colspec, str):
return ForeignKey(self._colspec)
else:
column = property(lambda s: s._init_column())
def accept_visitor(self, visitor):
+ """calls the visit_foreign_key method on the given visitor."""
visitor.visit_foreign_key(self)
def _set_parent(self, column):
self.parent.table.foreign_keys.append(self)
class DefaultGenerator(SchemaItem):
- """represents a "default value generator" for a particular column in a particular
- table. This could correspond to a constant, a callable function, or a SQL clause."""
+ """Base class for column "default" values, which can be a plain default
+ or a Sequence."""
def _set_parent(self, column):
self.column = column
self.column.default = self
- def accept_visitor(self, visitor):
- pass
class ColumnDefault(DefaultGenerator):
+ """A plain default value on a column. this could correspond to a constant,
+ a callable function, or a SQL clause."""
def __init__(self, arg):
self.arg = arg
def accept_visitor(self, visitor):
+ """calls the visit_column_default method on the given visitor."""
return visitor.visit_column_default(self)
class Sequence(DefaultGenerator):
self.increment = increment
self.optional=optional
def accept_visitor(self, visitor):
+ """calls the visit_seauence method on the given visitor."""
return visitor.visit_sequence(self)
class SchemaEngine(object):
- """a factory object used to create implementations for schema objects"""
+ """a factory object used to create implementations for schema objects. This object
+ is the ultimate base class for the engine.SQLEngine class."""
def tableimpl(self, table):
+ """returns a new implementation object for a Table (usually sql.TableImpl)"""
raise NotImplementedError()
def columnimpl(self, column):
+ """returns a new implementation object for a Column (usually sql.ColumnImpl)"""
raise NotImplementedError()
def reflecttable(self, table):
+ """given a table, will query the database and populate its Column and ForeignKey
+ objects."""
raise NotImplementedError()
class SchemaVisitor(object):
- """base class for an object that traverses across Schema objects"""
-
- def visit_schema(self, schema):pass
- def visit_table(self, table):pass
- def visit_column(self, column):pass
- def visit_foreign_key(self, join):pass
- def visit_index(self, index):pass
- def visit_column_default(self, default):pass
- def visit_sequence(self, sequence):pass
+ """base class for an object that traverses across Schema structures."""
+ def visit_schema(self, schema):
+ """visit a generic SchemaItem"""
+ pass
+ def visit_table(self, table):
+ """visit a Table."""
+ pass
+ def visit_column(self, column):
+ """visit a Column."""
+ pass
+ def visit_foreign_key(self, join):
+ """visit a ForeignKey."""
+ pass
+ def visit_index(self, index):
+ """visit an Index (not implemented yet)."""
+ pass
+ def visit_column_default(self, default):
+ """visit a ColumnDefault."""
+ pass
+ def visit_sequence(self, sequence):
+ """visit a Sequence."""
+ pass
\ No newline at end of file