Packages update
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# sqlalchemy/__init__.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -8,7 +8,6 @@ import inspect
|
||||
import sys
|
||||
|
||||
import sqlalchemy.exc as exceptions
|
||||
sys.modules['sqlalchemy.exceptions'] = exceptions
|
||||
|
||||
from sqlalchemy.sql import (
|
||||
alias,
|
||||
@@ -39,6 +38,7 @@ from sqlalchemy.sql import (
|
||||
or_,
|
||||
outerjoin,
|
||||
outparam,
|
||||
over,
|
||||
select,
|
||||
subquery,
|
||||
text,
|
||||
@@ -75,6 +75,7 @@ from sqlalchemy.types import (
|
||||
NUMERIC,
|
||||
Numeric,
|
||||
PickleType,
|
||||
REAL,
|
||||
SMALLINT,
|
||||
SmallInteger,
|
||||
String,
|
||||
@@ -83,6 +84,7 @@ from sqlalchemy.types import (
|
||||
TIMESTAMP,
|
||||
Text,
|
||||
Time,
|
||||
TypeDecorator,
|
||||
Unicode,
|
||||
UnicodeText,
|
||||
VARCHAR,
|
||||
@@ -115,6 +117,9 @@ from sqlalchemy.engine import create_engine, engine_from_config
|
||||
__all__ = sorted(name for name, obj in locals().items()
|
||||
if not (name.startswith('_') or inspect.ismodule(obj)))
|
||||
|
||||
__version__ = '0.6.6'
|
||||
__version__ = '0.7.5'
|
||||
|
||||
del inspect, sys
|
||||
|
||||
from sqlalchemy import util as _sa_util
|
||||
_sa_util.importlater.resolve_all()
|
||||
|
||||
@@ -66,13 +66,24 @@ str_to_datetime(PyObject *self, PyObject *arg)
|
||||
{
|
||||
const char *str;
|
||||
unsigned int year, month, day, hour, minute, second, microsecond = 0;
|
||||
PyObject *err_repr;
|
||||
|
||||
if (arg == Py_None)
|
||||
Py_RETURN_NONE;
|
||||
|
||||
str = PyString_AsString(arg);
|
||||
if (str == NULL)
|
||||
if (str == NULL) {
|
||||
err_repr = PyObject_Repr(arg);
|
||||
if (err_repr == NULL)
|
||||
return NULL;
|
||||
PyErr_Format(
|
||||
PyExc_ValueError,
|
||||
"Couldn't parse datetime string '%.200s' "
|
||||
"- value is not a string.",
|
||||
PyString_AsString(err_repr));
|
||||
Py_DECREF(err_repr);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* microseconds are optional */
|
||||
/*
|
||||
@@ -82,7 +93,14 @@ str_to_datetime(PyObject *self, PyObject *arg)
|
||||
*/
|
||||
if (sscanf(str, "%4u-%2u-%2u %2u:%2u:%2u.%6u", &year, &month, &day,
|
||||
&hour, &minute, &second, µsecond) < 6) {
|
||||
PyErr_SetString(PyExc_ValueError, "Couldn't parse datetime string.");
|
||||
err_repr = PyObject_Repr(arg);
|
||||
if (err_repr == NULL)
|
||||
return NULL;
|
||||
PyErr_Format(
|
||||
PyExc_ValueError,
|
||||
"Couldn't parse datetime string: %.200s",
|
||||
PyString_AsString(err_repr));
|
||||
Py_DECREF(err_repr);
|
||||
return NULL;
|
||||
}
|
||||
return PyDateTime_FromDateAndTime(year, month, day,
|
||||
@@ -94,13 +112,23 @@ str_to_time(PyObject *self, PyObject *arg)
|
||||
{
|
||||
const char *str;
|
||||
unsigned int hour, minute, second, microsecond = 0;
|
||||
PyObject *err_repr;
|
||||
|
||||
if (arg == Py_None)
|
||||
Py_RETURN_NONE;
|
||||
|
||||
str = PyString_AsString(arg);
|
||||
if (str == NULL)
|
||||
if (str == NULL) {
|
||||
err_repr = PyObject_Repr(arg);
|
||||
if (err_repr == NULL)
|
||||
return NULL;
|
||||
PyErr_Format(
|
||||
PyExc_ValueError,
|
||||
"Couldn't parse time string '%.200s' - value is not a string.",
|
||||
PyString_AsString(err_repr));
|
||||
Py_DECREF(err_repr);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* microseconds are optional */
|
||||
/*
|
||||
@@ -110,7 +138,14 @@ str_to_time(PyObject *self, PyObject *arg)
|
||||
*/
|
||||
if (sscanf(str, "%2u:%2u:%2u.%6u", &hour, &minute, &second,
|
||||
µsecond) < 3) {
|
||||
PyErr_SetString(PyExc_ValueError, "Couldn't parse time string.");
|
||||
err_repr = PyObject_Repr(arg);
|
||||
if (err_repr == NULL)
|
||||
return NULL;
|
||||
PyErr_Format(
|
||||
PyExc_ValueError,
|
||||
"Couldn't parse time string: %.200s",
|
||||
PyString_AsString(err_repr));
|
||||
Py_DECREF(err_repr);
|
||||
return NULL;
|
||||
}
|
||||
return PyTime_FromTime(hour, minute, second, microsecond);
|
||||
@@ -121,16 +156,33 @@ str_to_date(PyObject *self, PyObject *arg)
|
||||
{
|
||||
const char *str;
|
||||
unsigned int year, month, day;
|
||||
PyObject *err_repr;
|
||||
|
||||
if (arg == Py_None)
|
||||
Py_RETURN_NONE;
|
||||
|
||||
str = PyString_AsString(arg);
|
||||
if (str == NULL)
|
||||
if (str == NULL) {
|
||||
err_repr = PyObject_Repr(arg);
|
||||
if (err_repr == NULL)
|
||||
return NULL;
|
||||
PyErr_Format(
|
||||
PyExc_ValueError,
|
||||
"Couldn't parse date string '%.200s' - value is not a string.",
|
||||
PyString_AsString(err_repr));
|
||||
Py_DECREF(err_repr);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (sscanf(str, "%4u-%2u-%2u", &year, &month, &day) != 3) {
|
||||
PyErr_SetString(PyExc_ValueError, "Couldn't parse date string.");
|
||||
err_repr = PyObject_Repr(arg);
|
||||
if (err_repr == NULL)
|
||||
return NULL;
|
||||
PyErr_Format(
|
||||
PyExc_ValueError,
|
||||
"Couldn't parse date string: %.200s",
|
||||
PyString_AsString(err_repr));
|
||||
Py_DECREF(err_repr);
|
||||
return NULL;
|
||||
}
|
||||
return PyDate_FromDate(year, month, day);
|
||||
|
||||
@@ -13,6 +13,8 @@ typedef int Py_ssize_t;
|
||||
#define PY_SSIZE_T_MAX INT_MAX
|
||||
#define PY_SSIZE_T_MIN INT_MIN
|
||||
typedef Py_ssize_t (*lenfunc)(PyObject *);
|
||||
#define PyInt_FromSsize_t(x) PyInt_FromLong(x)
|
||||
typedef intargfunc ssizeargfunc;
|
||||
#endif
|
||||
|
||||
|
||||
@@ -276,7 +278,7 @@ BaseRowProxy_subscript(BaseRowProxy *self, PyObject *key)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
indexobject = PyTuple_GetItem(record, 1);
|
||||
indexobject = PyTuple_GetItem(record, 2);
|
||||
if (indexobject == NULL)
|
||||
return NULL;
|
||||
|
||||
@@ -296,7 +298,7 @@ BaseRowProxy_subscript(BaseRowProxy *self, PyObject *key)
|
||||
return NULL;
|
||||
|
||||
PyErr_Format(exception,
|
||||
"Ambiguous column name '%s' in result set! "
|
||||
"Ambiguous column name '%.200s' in result set! "
|
||||
"try 'use_labels' option on select statement.", cstr_key);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# connectors/__init__.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# connectors/mxodbc.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -21,11 +21,8 @@ For more info on mxODBC, see http://www.egenix.com/
|
||||
import sys
|
||||
import re
|
||||
import warnings
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy.connectors import Connector
|
||||
from sqlalchemy import types as sqltypes
|
||||
import sqlalchemy.processors as processors
|
||||
|
||||
class MxODBCConnector(Connector):
|
||||
driver='mxodbc'
|
||||
@@ -109,9 +106,9 @@ class MxODBCConnector(Connector):
|
||||
opts.pop('database', None)
|
||||
return (args,), opts
|
||||
|
||||
def is_disconnect(self, e):
|
||||
# eGenix recommends checking connection.closed here,
|
||||
# but how can we get a handle on the current connection?
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
# TODO: eGenix recommends checking connection.closed here
|
||||
# Does that detect dropped connections ?
|
||||
if isinstance(e, self.dbapi.ProgrammingError):
|
||||
return "connection already closed" in str(e)
|
||||
elif isinstance(e, self.dbapi.Error):
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
"""Define behaviors common to MySQLdb dialects.
|
||||
|
||||
Currently includes MySQL and Drizzle.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy.connectors import Connector
|
||||
from sqlalchemy.engine import base as engine_base, default
|
||||
from sqlalchemy.sql import operators as sql_operators
|
||||
from sqlalchemy import exc, log, schema, sql, types as sqltypes, util
|
||||
from sqlalchemy import processors
|
||||
import re
|
||||
|
||||
# the subclassing of Connector by all classes
|
||||
# here is not strictly necessary
|
||||
|
||||
class MySQLDBExecutionContext(Connector):
|
||||
|
||||
@property
|
||||
def rowcount(self):
|
||||
if hasattr(self, '_rowcount'):
|
||||
return self._rowcount
|
||||
else:
|
||||
return self.cursor.rowcount
|
||||
|
||||
class MySQLDBCompiler(Connector):
|
||||
def visit_mod(self, binary, **kw):
|
||||
return self.process(binary.left) + " %% " + self.process(binary.right)
|
||||
|
||||
def post_process_text(self, text):
|
||||
return text.replace('%', '%%')
|
||||
|
||||
class MySQLDBIdentifierPreparer(Connector):
|
||||
|
||||
def _escape_identifier(self, value):
|
||||
value = value.replace(self.escape_quote, self.escape_to_quote)
|
||||
return value.replace("%", "%%")
|
||||
|
||||
class MySQLDBConnector(Connector):
|
||||
driver = 'mysqldb'
|
||||
supports_unicode_statements = False
|
||||
supports_sane_rowcount = True
|
||||
supports_sane_multi_rowcount = True
|
||||
|
||||
supports_native_decimal = True
|
||||
|
||||
default_paramstyle = 'format'
|
||||
|
||||
@classmethod
|
||||
def dbapi(cls):
|
||||
# is overridden when pymysql is used
|
||||
return __import__('MySQLdb')
|
||||
|
||||
def do_executemany(self, cursor, statement, parameters, context=None):
|
||||
rowcount = cursor.executemany(statement, parameters)
|
||||
if context is not None:
|
||||
context._rowcount = rowcount
|
||||
|
||||
def create_connect_args(self, url):
|
||||
opts = url.translate_connect_args(database='db', username='user',
|
||||
password='passwd')
|
||||
opts.update(url.query)
|
||||
|
||||
util.coerce_kw_type(opts, 'compress', bool)
|
||||
util.coerce_kw_type(opts, 'connect_timeout', int)
|
||||
util.coerce_kw_type(opts, 'client_flag', int)
|
||||
util.coerce_kw_type(opts, 'local_infile', int)
|
||||
# Note: using either of the below will cause all strings to be returned
|
||||
# as Unicode, both in raw SQL operations and with column types like
|
||||
# String and MSString.
|
||||
util.coerce_kw_type(opts, 'use_unicode', bool)
|
||||
util.coerce_kw_type(opts, 'charset', str)
|
||||
|
||||
# Rich values 'cursorclass' and 'conv' are not supported via
|
||||
# query string.
|
||||
|
||||
ssl = {}
|
||||
for key in ['ssl_ca', 'ssl_key', 'ssl_cert', 'ssl_capath', 'ssl_cipher']:
|
||||
if key in opts:
|
||||
ssl[key[4:]] = opts[key]
|
||||
util.coerce_kw_type(ssl, key[4:], str)
|
||||
del opts[key]
|
||||
if ssl:
|
||||
opts['ssl'] = ssl
|
||||
|
||||
# FOUND_ROWS must be set in CLIENT_FLAGS to enable
|
||||
# supports_sane_rowcount.
|
||||
client_flag = opts.get('client_flag', 0)
|
||||
if self.dbapi is not None:
|
||||
try:
|
||||
CLIENT_FLAGS = __import__(
|
||||
self.dbapi.__name__ + '.constants.CLIENT'
|
||||
).constants.CLIENT
|
||||
client_flag |= CLIENT_FLAGS.FOUND_ROWS
|
||||
except (AttributeError, ImportError):
|
||||
self.supports_sane_rowcount = False
|
||||
opts['client_flag'] = client_flag
|
||||
return [[], opts]
|
||||
|
||||
def _get_server_version_info(self, connection):
|
||||
dbapi_con = connection.connection
|
||||
version = []
|
||||
r = re.compile('[.\-]')
|
||||
for n in r.split(dbapi_con.get_server_info()):
|
||||
try:
|
||||
version.append(int(n))
|
||||
except ValueError:
|
||||
version.append(n)
|
||||
return tuple(version)
|
||||
|
||||
def _extract_error_code(self, exception):
|
||||
return exception.args[0]
|
||||
|
||||
def _detect_charset(self, connection):
|
||||
"""Sniff out the character set in use for connection results."""
|
||||
|
||||
# Note: MySQL-python 1.2.1c7 seems to ignore changes made
|
||||
# on a connection via set_character_set()
|
||||
if self.server_version_info < (4, 1, 0):
|
||||
try:
|
||||
return connection.connection.character_set_name()
|
||||
except AttributeError:
|
||||
# < 1.2.1 final MySQL-python drivers have no charset support.
|
||||
# a query is needed.
|
||||
pass
|
||||
|
||||
# Prefer 'character_set_results' for the current connection over the
|
||||
# value in the driver. SET NAMES or individual variable SETs will
|
||||
# change the charset without updating the driver's view of the world.
|
||||
#
|
||||
# If it's decided that issuing that sort of SQL leaves you SOL, then
|
||||
# this can prefer the driver value.
|
||||
rs = connection.execute("SHOW VARIABLES LIKE 'character_set%%'")
|
||||
opts = dict([(row[0], row[1]) for row in self._compat_fetchall(rs)])
|
||||
|
||||
if 'character_set_results' in opts:
|
||||
return opts['character_set_results']
|
||||
try:
|
||||
return connection.connection.character_set_name()
|
||||
except AttributeError:
|
||||
# Still no charset on < 1.2.1 final...
|
||||
if 'character_set' in opts:
|
||||
return opts['character_set']
|
||||
else:
|
||||
util.warn(
|
||||
"Could not detect the connection character set with this "
|
||||
"combination of MySQL server and MySQL-python. "
|
||||
"MySQL-python >= 1.2.2 is recommended. Assuming latin1.")
|
||||
return 'latin1'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# connectors/pyodbc.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -10,7 +10,6 @@ from sqlalchemy.util import asbool
|
||||
import sys
|
||||
import re
|
||||
import urllib
|
||||
import decimal
|
||||
|
||||
class PyODBCConnector(Connector):
|
||||
driver='pyodbc'
|
||||
@@ -30,6 +29,14 @@ class PyODBCConnector(Connector):
|
||||
# if the freetds.so is detected
|
||||
freetds = False
|
||||
|
||||
# will be set to the string version of
|
||||
# the FreeTDS driver if freetds is detected
|
||||
freetds_driver_version = None
|
||||
|
||||
# will be set to True after initialize()
|
||||
# if the libessqlsrv.so is detected
|
||||
easysoft = False
|
||||
|
||||
@classmethod
|
||||
def dbapi(cls):
|
||||
return __import__('pyodbc')
|
||||
@@ -82,7 +89,7 @@ class PyODBCConnector(Connector):
|
||||
connectors.extend(['%s=%s' % (k,v) for k,v in keys.iteritems()])
|
||||
return [[";".join (connectors)], connect_args]
|
||||
|
||||
def is_disconnect(self, e):
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
if isinstance(e, self.dbapi.ProgrammingError):
|
||||
return "The cursor's connection has been closed." in str(e) or \
|
||||
'Attempt to use a closed connection.' in str(e)
|
||||
@@ -99,20 +106,43 @@ class PyODBCConnector(Connector):
|
||||
|
||||
dbapi_con = connection.connection
|
||||
|
||||
self.freetds = bool(re.match(r".*libtdsodbc.*\.so",
|
||||
dbapi_con.getinfo(pyodbc.SQL_DRIVER_NAME)
|
||||
_sql_driver_name = dbapi_con.getinfo(pyodbc.SQL_DRIVER_NAME)
|
||||
self.freetds = bool(re.match(r".*libtdsodbc.*\.so", _sql_driver_name
|
||||
))
|
||||
self.easysoft = bool(re.match(r".*libessqlsrv.*\.so", _sql_driver_name
|
||||
))
|
||||
|
||||
if self.freetds:
|
||||
self.freetds_driver_version = dbapi_con.getinfo(pyodbc.SQL_DRIVER_VER)
|
||||
|
||||
# the "Py2K only" part here is theoretical.
|
||||
# have not tried pyodbc + python3.1 yet.
|
||||
# Py2K
|
||||
self.supports_unicode_statements = not self.freetds
|
||||
self.supports_unicode_binds = not self.freetds
|
||||
self.supports_unicode_statements = not self.freetds and not self.easysoft
|
||||
self.supports_unicode_binds = (not self.freetds or
|
||||
self.freetds_driver_version >= '0.91') and not self.easysoft
|
||||
# end Py2K
|
||||
|
||||
# run other initialization which asks for user name, etc.
|
||||
super(PyODBCConnector, self).initialize(connection)
|
||||
|
||||
def _dbapi_version(self):
|
||||
if not self.dbapi:
|
||||
return ()
|
||||
return self._parse_dbapi_version(self.dbapi.version)
|
||||
|
||||
def _parse_dbapi_version(self, vers):
|
||||
m = re.match(
|
||||
r'(?:py.*-)?([\d\.]+)(?:-(\w+))?',
|
||||
vers
|
||||
)
|
||||
if not m:
|
||||
return ()
|
||||
vers = tuple([int(x) for x in m.group(1).split(".")])
|
||||
if m.group(2):
|
||||
vers += (m.group(2),)
|
||||
return vers
|
||||
|
||||
def _get_server_version_info(self, connection):
|
||||
dbapi_con = connection.connection
|
||||
version = []
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# connectors/zxJDBC.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -46,7 +46,7 @@ class ZxJDBCConnector(Connector):
|
||||
self.jdbc_driver_name],
|
||||
opts]
|
||||
|
||||
def is_disconnect(self, e):
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
if not isinstance(e, self.dbapi.ProgrammingError):
|
||||
return False
|
||||
e = str(e)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# databases/__init__.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -12,6 +12,7 @@ from sqlalchemy.dialects.sqlite import base as sqlite
|
||||
from sqlalchemy.dialects.postgresql import base as postgresql
|
||||
postgres = postgresql
|
||||
from sqlalchemy.dialects.mysql import base as mysql
|
||||
from sqlalchemy.dialects.drizzle import base as drizzle
|
||||
from sqlalchemy.dialects.oracle import base as oracle
|
||||
from sqlalchemy.dialects.firebird import base as firebird
|
||||
from sqlalchemy.dialects.maxdb import base as maxdb
|
||||
@@ -23,6 +24,7 @@ from sqlalchemy.dialects.sybase import base as sybase
|
||||
|
||||
__all__ = (
|
||||
'access',
|
||||
'drizzle',
|
||||
'firebird',
|
||||
'informix',
|
||||
'maxdb',
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# dialects/__init__.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
__all__ = (
|
||||
# 'access',
|
||||
'drizzle',
|
||||
'firebird',
|
||||
# 'informix',
|
||||
# 'maxdb',
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
"""
|
||||
Support for the Microsoft Access database.
|
||||
|
||||
This dialect is *not* ported to SQLAlchemy 0.6.
|
||||
This dialect is *not* ported to SQLAlchemy 0.6 or 0.7.
|
||||
|
||||
This dialect is *not* tested on SQLAlchemy 0.6.
|
||||
This dialect is *not* tested on SQLAlchemy 0.6 or 0.7.
|
||||
|
||||
|
||||
"""
|
||||
@@ -51,15 +51,10 @@ class AcSmallInteger(types.SmallInteger):
|
||||
return "SMALLINT"
|
||||
|
||||
class AcDateTime(types.DateTime):
|
||||
def __init__(self, *a, **kw):
|
||||
super(AcDateTime, self).__init__(False)
|
||||
|
||||
def get_col_spec(self):
|
||||
return "DATETIME"
|
||||
|
||||
class AcDate(types.Date):
|
||||
def __init__(self, *a, **kw):
|
||||
super(AcDate, self).__init__(False)
|
||||
|
||||
def get_col_spec(self):
|
||||
return "DATETIME"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
from sqlalchemy.dialects.drizzle import base, mysqldb
|
||||
|
||||
# default dialect
|
||||
base.dialect = mysqldb.dialect
|
||||
|
||||
from sqlalchemy.dialects.drizzle.base import \
|
||||
BIGINT, BINARY, BLOB, BOOLEAN, CHAR, DATE, DATETIME, \
|
||||
DECIMAL, DOUBLE, ENUM, \
|
||||
FLOAT, INTEGER, \
|
||||
NUMERIC, REAL, TEXT, TIME, TIMESTAMP, \
|
||||
VARBINARY, VARCHAR, dialect
|
||||
|
||||
__all__ = (
|
||||
'BIGINT', 'BINARY', 'BLOB', 'BOOLEAN', 'CHAR', 'DATE', 'DATETIME', 'DECIMAL', 'DOUBLE',
|
||||
'ENUM', 'FLOAT', 'INTEGER',
|
||||
'NUMERIC', 'SET', 'REAL', 'TEXT', 'TIME', 'TIMESTAMP',
|
||||
'VARBINARY', 'VARCHAR', 'dialect'
|
||||
)
|
||||
@@ -0,0 +1,582 @@
|
||||
# drizzle/base.py
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2010-2011 Monty Taylor <mordred@inaugust.com>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
"""Support for the Drizzle database.
|
||||
|
||||
Supported Versions and Features
|
||||
-------------------------------
|
||||
|
||||
SQLAlchemy supports the Drizzle database starting with 2010.08.
|
||||
with capabilities increasing with more modern servers.
|
||||
|
||||
Most available DBAPI drivers are supported; see below.
|
||||
|
||||
===================================== ===============
|
||||
Feature Minimum Version
|
||||
===================================== ===============
|
||||
sqlalchemy.orm 2010.08
|
||||
Table Reflection 2010.08
|
||||
DDL Generation 2010.08
|
||||
utf8/Full Unicode Connections 2010.08
|
||||
Transactions 2010.08
|
||||
Two-Phase Transactions 2010.08
|
||||
Nested Transactions 2010.08
|
||||
===================================== ===============
|
||||
|
||||
See the official Drizzle documentation for detailed information about features
|
||||
supported in any given server release.
|
||||
|
||||
Connecting
|
||||
----------
|
||||
|
||||
See the API documentation on individual drivers for details on connecting.
|
||||
|
||||
Connection Timeouts
|
||||
-------------------
|
||||
|
||||
Drizzle features an automatic connection close behavior, for connections that
|
||||
have been idle for eight hours or more. To circumvent having this issue, use
|
||||
the ``pool_recycle`` option which controls the maximum age of any connection::
|
||||
|
||||
engine = create_engine('drizzle+mysqldb://...', pool_recycle=3600)
|
||||
|
||||
Storage Engines
|
||||
---------------
|
||||
|
||||
Drizzle defaults to the ``InnoDB`` storage engine, which is transactional.
|
||||
|
||||
Storage engines can be elected when creating tables in SQLAlchemy by supplying
|
||||
a ``drizzle_engine='whatever'`` to the ``Table`` constructor. Any Drizzle table
|
||||
creation option can be specified in this syntax::
|
||||
|
||||
Table('mytable', metadata,
|
||||
Column('data', String(32)),
|
||||
drizzle_engine='InnoDB',
|
||||
)
|
||||
|
||||
Keys
|
||||
----
|
||||
|
||||
Not all Drizzle storage engines support foreign keys. For ``BlitzDB`` and
|
||||
similar engines, the information loaded by table reflection will not include
|
||||
foreign keys. For these tables, you may supply a
|
||||
:class:`~sqlalchemy.ForeignKeyConstraint` at reflection time::
|
||||
|
||||
Table('mytable', metadata,
|
||||
ForeignKeyConstraint(['other_id'], ['othertable.other_id']),
|
||||
autoload=True
|
||||
)
|
||||
|
||||
When creating tables, SQLAlchemy will automatically set ``AUTO_INCREMENT`` on
|
||||
an integer primary key column::
|
||||
|
||||
>>> t = Table('mytable', metadata,
|
||||
... Column('mytable_id', Integer, primary_key=True)
|
||||
... )
|
||||
>>> t.create()
|
||||
CREATE TABLE mytable (
|
||||
id INTEGER NOT NULL AUTO_INCREMENT,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
|
||||
You can disable this behavior by supplying ``autoincrement=False`` to the
|
||||
:class:`~sqlalchemy.Column`. This flag can also be used to enable
|
||||
auto-increment on a secondary column in a multi-column key for some storage
|
||||
engines::
|
||||
|
||||
Table('mytable', metadata,
|
||||
Column('gid', Integer, primary_key=True, autoincrement=False),
|
||||
Column('id', Integer, primary_key=True)
|
||||
)
|
||||
|
||||
Drizzle SQL Extensions
|
||||
----------------------
|
||||
|
||||
Many of the Drizzle SQL extensions are handled through SQLAlchemy's generic
|
||||
function and operator support::
|
||||
|
||||
table.select(table.c.password==func.md5('plaintext'))
|
||||
table.select(table.c.username.op('regexp')('^[a-d]'))
|
||||
|
||||
And of course any valid Drizzle statement can be executed as a string as well.
|
||||
|
||||
Some limited direct support for Drizzle extensions to SQL is currently
|
||||
available.
|
||||
|
||||
* SELECT pragma::
|
||||
|
||||
select(..., prefixes=['HIGH_PRIORITY', 'SQL_SMALL_RESULT'])
|
||||
|
||||
* UPDATE with LIMIT::
|
||||
|
||||
update(..., drizzle_limit=10)
|
||||
|
||||
"""
|
||||
|
||||
import datetime, inspect, re, sys
|
||||
|
||||
from sqlalchemy import schema as sa_schema
|
||||
from sqlalchemy import exc, log, sql, util
|
||||
from sqlalchemy.sql import operators as sql_operators
|
||||
from sqlalchemy.sql import functions as sql_functions
|
||||
from sqlalchemy.sql import compiler
|
||||
from array import array as _array
|
||||
|
||||
from sqlalchemy.engine import reflection
|
||||
from sqlalchemy.engine import base as engine_base, default
|
||||
from sqlalchemy import types as sqltypes
|
||||
from sqlalchemy.dialects.mysql import base as mysql_dialect
|
||||
|
||||
from sqlalchemy.types import DATE, DATETIME, BOOLEAN, TIME, \
|
||||
BLOB, BINARY, VARBINARY
|
||||
|
||||
class _NumericType(object):
|
||||
"""Base for Drizzle numeric types."""
|
||||
|
||||
def __init__(self, **kw):
|
||||
super(_NumericType, self).__init__(**kw)
|
||||
|
||||
class _FloatType(_NumericType, sqltypes.Float):
|
||||
def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
|
||||
if isinstance(self, (REAL, DOUBLE)) and \
|
||||
(
|
||||
(precision is None and scale is not None) or
|
||||
(precision is not None and scale is None)
|
||||
):
|
||||
raise exc.ArgumentError(
|
||||
"You must specify both precision and scale or omit "
|
||||
"both altogether.")
|
||||
|
||||
super(_FloatType, self).__init__(precision=precision, asdecimal=asdecimal, **kw)
|
||||
self.scale = scale
|
||||
|
||||
class _StringType(mysql_dialect._StringType):
|
||||
"""Base for Drizzle string types."""
|
||||
|
||||
def __init__(self, collation=None,
|
||||
binary=False,
|
||||
**kw):
|
||||
kw['national'] = False
|
||||
super(_StringType, self).__init__(collation=collation,
|
||||
binary=binary,
|
||||
**kw)
|
||||
|
||||
|
||||
class NUMERIC(_NumericType, sqltypes.NUMERIC):
|
||||
"""Drizzle NUMERIC type."""
|
||||
|
||||
__visit_name__ = 'NUMERIC'
|
||||
|
||||
def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
|
||||
"""Construct a NUMERIC.
|
||||
|
||||
:param precision: Total digits in this number. If scale and precision
|
||||
are both None, values are stored to limits allowed by the server.
|
||||
|
||||
:param scale: The number of digits after the decimal point.
|
||||
|
||||
"""
|
||||
super(NUMERIC, self).__init__(precision=precision, scale=scale, asdecimal=asdecimal, **kw)
|
||||
|
||||
|
||||
class DECIMAL(_NumericType, sqltypes.DECIMAL):
|
||||
"""Drizzle DECIMAL type."""
|
||||
|
||||
__visit_name__ = 'DECIMAL'
|
||||
|
||||
def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
|
||||
"""Construct a DECIMAL.
|
||||
|
||||
:param precision: Total digits in this number. If scale and precision
|
||||
are both None, values are stored to limits allowed by the server.
|
||||
|
||||
:param scale: The number of digits after the decimal point.
|
||||
|
||||
"""
|
||||
super(DECIMAL, self).__init__(precision=precision, scale=scale,
|
||||
asdecimal=asdecimal, **kw)
|
||||
|
||||
|
||||
class DOUBLE(_FloatType):
|
||||
"""Drizzle DOUBLE type."""
|
||||
|
||||
__visit_name__ = 'DOUBLE'
|
||||
|
||||
def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
|
||||
"""Construct a DOUBLE.
|
||||
|
||||
:param precision: Total digits in this number. If scale and precision
|
||||
are both None, values are stored to limits allowed by the server.
|
||||
|
||||
:param scale: The number of digits after the decimal point.
|
||||
|
||||
"""
|
||||
super(DOUBLE, self).__init__(precision=precision, scale=scale,
|
||||
asdecimal=asdecimal, **kw)
|
||||
|
||||
class REAL(_FloatType, sqltypes.REAL):
|
||||
"""Drizzle REAL type."""
|
||||
|
||||
__visit_name__ = 'REAL'
|
||||
|
||||
def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
|
||||
"""Construct a REAL.
|
||||
|
||||
:param precision: Total digits in this number. If scale and precision
|
||||
are both None, values are stored to limits allowed by the server.
|
||||
|
||||
:param scale: The number of digits after the decimal point.
|
||||
|
||||
"""
|
||||
super(REAL, self).__init__(precision=precision, scale=scale,
|
||||
asdecimal=asdecimal, **kw)
|
||||
|
||||
class FLOAT(_FloatType, sqltypes.FLOAT):
|
||||
"""Drizzle FLOAT type."""
|
||||
|
||||
__visit_name__ = 'FLOAT'
|
||||
|
||||
def __init__(self, precision=None, scale=None, asdecimal=False, **kw):
|
||||
"""Construct a FLOAT.
|
||||
|
||||
:param precision: Total digits in this number. If scale and precision
|
||||
are both None, values are stored to limits allowed by the server.
|
||||
|
||||
:param scale: The number of digits after the decimal point.
|
||||
|
||||
"""
|
||||
super(FLOAT, self).__init__(precision=precision, scale=scale,
|
||||
asdecimal=asdecimal, **kw)
|
||||
|
||||
def bind_processor(self, dialect):
|
||||
return None
|
||||
|
||||
class INTEGER(sqltypes.INTEGER):
|
||||
"""Drizzle INTEGER type."""
|
||||
|
||||
__visit_name__ = 'INTEGER'
|
||||
|
||||
def __init__(self, **kw):
|
||||
"""Construct an INTEGER.
|
||||
|
||||
"""
|
||||
super(INTEGER, self).__init__(**kw)
|
||||
|
||||
class BIGINT(sqltypes.BIGINT):
|
||||
"""Drizzle BIGINTEGER type."""
|
||||
|
||||
__visit_name__ = 'BIGINT'
|
||||
|
||||
def __init__(self, **kw):
|
||||
"""Construct a BIGINTEGER.
|
||||
|
||||
"""
|
||||
super(BIGINT, self).__init__(**kw)
|
||||
|
||||
|
||||
class _DrizzleTime(mysql_dialect._MSTime):
|
||||
"""Drizzle TIME type."""
|
||||
|
||||
class TIMESTAMP(sqltypes.TIMESTAMP):
|
||||
"""Drizzle TIMESTAMP type."""
|
||||
__visit_name__ = 'TIMESTAMP'
|
||||
|
||||
class TEXT(_StringType, sqltypes.TEXT):
|
||||
"""Drizzle TEXT type, for text up to 2^16 characters."""
|
||||
|
||||
__visit_name__ = 'TEXT'
|
||||
|
||||
def __init__(self, length=None, **kw):
|
||||
"""Construct a TEXT.
|
||||
|
||||
:param length: Optional, if provided the server may optimize storage
|
||||
by substituting the smallest TEXT type sufficient to store
|
||||
``length`` characters.
|
||||
|
||||
:param collation: Optional, a column-level collation for this string
|
||||
value. Takes precedence to 'binary' short-hand.
|
||||
|
||||
:param binary: Defaults to False: short-hand, pick the binary
|
||||
collation type that matches the column's character set. Generates
|
||||
BINARY in schema. This does not affect the type of data stored,
|
||||
only the collation of character data.
|
||||
|
||||
"""
|
||||
super(TEXT, self).__init__(length=length, **kw)
|
||||
|
||||
class VARCHAR(_StringType, sqltypes.VARCHAR):
|
||||
"""Drizzle VARCHAR type, for variable-length character data."""
|
||||
|
||||
__visit_name__ = 'VARCHAR'
|
||||
|
||||
def __init__(self, length=None, **kwargs):
|
||||
"""Construct a VARCHAR.
|
||||
|
||||
:param collation: Optional, a column-level collation for this string
|
||||
value. Takes precedence to 'binary' short-hand.
|
||||
|
||||
:param binary: Defaults to False: short-hand, pick the binary
|
||||
collation type that matches the column's character set. Generates
|
||||
BINARY in schema. This does not affect the type of data stored,
|
||||
only the collation of character data.
|
||||
|
||||
"""
|
||||
super(VARCHAR, self).__init__(length=length, **kwargs)
|
||||
|
||||
class CHAR(_StringType, sqltypes.CHAR):
|
||||
"""Drizzle CHAR type, for fixed-length character data."""
|
||||
|
||||
__visit_name__ = 'CHAR'
|
||||
|
||||
def __init__(self, length=None, **kwargs):
|
||||
"""Construct a CHAR.
|
||||
|
||||
:param length: Maximum data length, in characters.
|
||||
|
||||
:param binary: Optional, use the default binary collation for the
|
||||
national character set. This does not affect the type of data
|
||||
stored, use a BINARY type for binary data.
|
||||
|
||||
:param collation: Optional, request a particular collation. Must be
|
||||
compatible with the national character set.
|
||||
|
||||
"""
|
||||
super(CHAR, self).__init__(length=length, **kwargs)
|
||||
|
||||
class ENUM(mysql_dialect.ENUM):
|
||||
"""Drizzle ENUM type."""
|
||||
|
||||
def __init__(self, *enums, **kw):
|
||||
"""Construct an ENUM.
|
||||
|
||||
Example:
|
||||
|
||||
Column('myenum', ENUM("foo", "bar", "baz"))
|
||||
|
||||
:param enums: The range of valid values for this ENUM. Values will be
|
||||
quoted when generating the schema according to the quoting flag (see
|
||||
below).
|
||||
|
||||
:param strict: Defaults to False: ensure that a given value is in this
|
||||
ENUM's range of permissible values when inserting or updating rows.
|
||||
Note that Drizzle will not raise a fatal error if you attempt to store
|
||||
an out of range value- an alternate value will be stored instead.
|
||||
(See Drizzle ENUM documentation.)
|
||||
|
||||
:param collation: Optional, a column-level collation for this string
|
||||
value. Takes precedence to 'binary' short-hand.
|
||||
|
||||
:param binary: Defaults to False: short-hand, pick the binary
|
||||
collation type that matches the column's character set. Generates
|
||||
BINARY in schema. This does not affect the type of data stored,
|
||||
only the collation of character data.
|
||||
|
||||
:param quoting: Defaults to 'auto': automatically determine enum value
|
||||
quoting. If all enum values are surrounded by the same quoting
|
||||
character, then use 'quoted' mode. Otherwise, use 'unquoted' mode.
|
||||
|
||||
'quoted': values in enums are already quoted, they will be used
|
||||
directly when generating the schema - this usage is deprecated.
|
||||
|
||||
'unquoted': values in enums are not quoted, they will be escaped and
|
||||
surrounded by single quotes when generating the schema.
|
||||
|
||||
Previous versions of this type always required manually quoted
|
||||
values to be supplied; future versions will always quote the string
|
||||
literals for you. This is a transitional option.
|
||||
|
||||
"""
|
||||
super(ENUM, self).__init__(*enums, **kw)
|
||||
|
||||
class _DrizzleBoolean(sqltypes.Boolean):
|
||||
def get_dbapi_type(self, dbapi):
|
||||
return dbapi.NUMERIC
|
||||
|
||||
colspecs = {
|
||||
sqltypes.Numeric: NUMERIC,
|
||||
sqltypes.Float: FLOAT,
|
||||
sqltypes.Time: _DrizzleTime,
|
||||
sqltypes.Enum: ENUM,
|
||||
sqltypes.Boolean: _DrizzleBoolean,
|
||||
}
|
||||
|
||||
# All the types we have in Drizzle
|
||||
ischema_names = {
|
||||
'BIGINT': BIGINT,
|
||||
'BINARY': BINARY,
|
||||
'BLOB': BLOB,
|
||||
'BOOLEAN': BOOLEAN,
|
||||
'CHAR': CHAR,
|
||||
'DATE': DATE,
|
||||
'DATETIME': DATETIME,
|
||||
'DECIMAL': DECIMAL,
|
||||
'DOUBLE': DOUBLE,
|
||||
'ENUM': ENUM,
|
||||
'FLOAT': FLOAT,
|
||||
'INT': INTEGER,
|
||||
'INTEGER': INTEGER,
|
||||
'NUMERIC': NUMERIC,
|
||||
'TEXT': TEXT,
|
||||
'TIME': TIME,
|
||||
'TIMESTAMP': TIMESTAMP,
|
||||
'VARBINARY': VARBINARY,
|
||||
'VARCHAR': VARCHAR,
|
||||
}
|
||||
|
||||
class DrizzleCompiler(mysql_dialect.MySQLCompiler):
|
||||
|
||||
def visit_typeclause(self, typeclause):
|
||||
type_ = typeclause.type.dialect_impl(self.dialect)
|
||||
if isinstance(type_, sqltypes.Integer):
|
||||
return 'INTEGER'
|
||||
else:
|
||||
return super(DrizzleCompiler, self).visit_typeclause(typeclause)
|
||||
|
||||
def visit_cast(self, cast, **kwargs):
|
||||
type_ = self.process(cast.typeclause)
|
||||
if type_ is None:
|
||||
return self.process(cast.clause)
|
||||
|
||||
return 'CAST(%s AS %s)' % (self.process(cast.clause), type_)
|
||||
|
||||
|
||||
class DrizzleDDLCompiler(mysql_dialect.MySQLDDLCompiler):
|
||||
pass
|
||||
|
||||
class DrizzleTypeCompiler(mysql_dialect.MySQLTypeCompiler):
|
||||
def _extend_numeric(self, type_, spec):
|
||||
return spec
|
||||
|
||||
def _extend_string(self, type_, defaults, spec):
|
||||
"""Extend a string-type declaration with standard SQL
|
||||
COLLATE annotations and Drizzle specific extensions.
|
||||
|
||||
"""
|
||||
|
||||
def attr(name):
|
||||
return getattr(type_, name, defaults.get(name))
|
||||
|
||||
if attr('collation'):
|
||||
collation = 'COLLATE %s' % type_.collation
|
||||
elif attr('binary'):
|
||||
collation = 'BINARY'
|
||||
else:
|
||||
collation = None
|
||||
|
||||
return ' '.join([c for c in (spec, collation)
|
||||
if c is not None])
|
||||
|
||||
def visit_NCHAR(self, type):
|
||||
raise NotImplementedError("Drizzle does not support NCHAR")
|
||||
|
||||
def visit_NVARCHAR(self, type):
|
||||
raise NotImplementedError("Drizzle does not support NVARCHAR")
|
||||
|
||||
def visit_FLOAT(self, type_):
|
||||
if type_.scale is not None and type_.precision is not None:
|
||||
return "FLOAT(%s, %s)" % (type_.precision, type_.scale)
|
||||
else:
|
||||
return "FLOAT"
|
||||
|
||||
def visit_BOOLEAN(self, type_):
|
||||
return "BOOLEAN"
|
||||
|
||||
def visit_BLOB(self, type_):
|
||||
return "BLOB"
|
||||
|
||||
|
||||
class DrizzleExecutionContext(mysql_dialect.MySQLExecutionContext):
|
||||
pass
|
||||
|
||||
class DrizzleIdentifierPreparer(mysql_dialect.MySQLIdentifierPreparer):
|
||||
pass
|
||||
|
||||
class DrizzleDialect(mysql_dialect.MySQLDialect):
|
||||
"""Details of the Drizzle dialect. Not used directly in application code."""
|
||||
|
||||
name = 'drizzle'
|
||||
|
||||
_supports_cast = True
|
||||
supports_sequences = False
|
||||
supports_native_boolean = True
|
||||
supports_views = False
|
||||
|
||||
|
||||
default_paramstyle = 'format'
|
||||
colspecs = colspecs
|
||||
|
||||
statement_compiler = DrizzleCompiler
|
||||
ddl_compiler = DrizzleDDLCompiler
|
||||
type_compiler = DrizzleTypeCompiler
|
||||
ischema_names = ischema_names
|
||||
preparer = DrizzleIdentifierPreparer
|
||||
|
||||
def on_connect(self):
|
||||
"""Force autocommit - Drizzle Bug#707842 doesn't set this
|
||||
properly"""
|
||||
def connect(conn):
|
||||
conn.autocommit(False)
|
||||
return connect
|
||||
|
||||
def do_commit(self, connection):
|
||||
"""Execute a COMMIT."""
|
||||
|
||||
connection.commit()
|
||||
|
||||
def do_rollback(self, connection):
|
||||
"""Execute a ROLLBACK."""
|
||||
|
||||
connection.rollback()
|
||||
|
||||
@reflection.cache
|
||||
def get_table_names(self, connection, schema=None, **kw):
|
||||
"""Return a Unicode SHOW TABLES from a given schema."""
|
||||
if schema is not None:
|
||||
current_schema = schema
|
||||
else:
|
||||
current_schema = self.default_schema_name
|
||||
|
||||
charset = 'utf8'
|
||||
rp = connection.execute("SHOW TABLES FROM %s" %
|
||||
self.identifier_preparer.quote_identifier(current_schema))
|
||||
return [row[0] for row in self._compat_fetchall(rp, charset=charset)]
|
||||
|
||||
@reflection.cache
|
||||
def get_view_names(self, connection, schema=None, **kw):
|
||||
raise NotImplementedError
|
||||
|
||||
def _detect_casing(self, connection):
|
||||
"""Sniff out identifier case sensitivity.
|
||||
|
||||
Cached per-connection. This value can not change without a server
|
||||
restart.
|
||||
|
||||
"""
|
||||
return 0
|
||||
|
||||
def _detect_collations(self, connection):
|
||||
"""Pull the active COLLATIONS list from the server.
|
||||
|
||||
Cached per-connection.
|
||||
"""
|
||||
|
||||
collations = {}
|
||||
charset = self._connection_charset
|
||||
rs = connection.execute('SELECT CHARACTER_SET_NAME, COLLATION_NAME from data_dictionary.COLLATIONS')
|
||||
for row in self._compat_fetchall(rs, charset):
|
||||
collations[row[0]] = row[1]
|
||||
return collations
|
||||
|
||||
def _detect_ansiquotes(self, connection):
|
||||
"""Detect and adjust for the ANSI_QUOTES sql mode."""
|
||||
|
||||
self._server_ansiquotes = False
|
||||
|
||||
self._backslash_escapes = False
|
||||
|
||||
log.class_logger(DrizzleDialect)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Support for the Drizzle database via the Drizzle-python adapter.
|
||||
|
||||
Drizzle-Python is available at:
|
||||
|
||||
http://sourceforge.net/projects/mysql-python
|
||||
|
||||
At least version 1.2.1 or 1.2.2 should be used.
|
||||
|
||||
Connecting
|
||||
-----------
|
||||
|
||||
Connect string format::
|
||||
|
||||
drizzle+mysqldb://<user>:<password>@<host>[:<port>]/<dbname>
|
||||
|
||||
Unicode
|
||||
-------
|
||||
|
||||
Drizzle accommodates Python ``unicode`` objects directly and
|
||||
uses the ``utf8`` encoding in all cases.
|
||||
|
||||
Known Issues
|
||||
-------------
|
||||
|
||||
Drizzle-python at least as of version 1.2.2 has a serious memory leak related
|
||||
to unicode conversion, a feature which is disabled via ``use_unicode=0``.
|
||||
The recommended connection form with SQLAlchemy is::
|
||||
|
||||
engine = create_engine('mysql://scott:tiger@localhost/test?charset=utf8&use_unicode=0', pool_recycle=3600)
|
||||
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy.dialects.drizzle.base import (DrizzleDialect,
|
||||
DrizzleExecutionContext,
|
||||
DrizzleCompiler, DrizzleIdentifierPreparer)
|
||||
from sqlalchemy.connectors.mysqldb import (
|
||||
MySQLDBExecutionContext,
|
||||
MySQLDBCompiler,
|
||||
MySQLDBIdentifierPreparer,
|
||||
MySQLDBConnector
|
||||
)
|
||||
|
||||
class DrizzleExecutionContext_mysqldb(
|
||||
MySQLDBExecutionContext,
|
||||
DrizzleExecutionContext):
|
||||
pass
|
||||
|
||||
|
||||
class DrizzleCompiler_mysqldb(MySQLDBCompiler, DrizzleCompiler):
|
||||
pass
|
||||
|
||||
|
||||
class DrizzleIdentifierPreparer_mysqldb(
|
||||
MySQLDBIdentifierPreparer,
|
||||
DrizzleIdentifierPreparer):
|
||||
pass
|
||||
|
||||
class DrizzleDialect_mysqldb(MySQLDBConnector, DrizzleDialect):
|
||||
execution_ctx_cls = DrizzleExecutionContext_mysqldb
|
||||
statement_compiler = DrizzleCompiler_mysqldb
|
||||
preparer = DrizzleIdentifierPreparer_mysqldb
|
||||
|
||||
def _detect_charset(self, connection):
|
||||
"""Sniff out the character set in use for connection results."""
|
||||
return 'utf8'
|
||||
|
||||
|
||||
dialect = DrizzleDialect_mysqldb
|
||||
@@ -1,5 +1,5 @@
|
||||
# firebird/__init__.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# firebird/base.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -244,6 +244,10 @@ class FBCompiler(sql.compiler.SQLCompiler):
|
||||
visit_char_length_func = visit_length_func
|
||||
|
||||
def function_argspec(self, func, **kw):
|
||||
# TODO: this probably will need to be
|
||||
# narrowed to a fixed list, some no-arg functions
|
||||
# may require parens - see similar example in the oracle
|
||||
# dialect
|
||||
if func.clauses is not None and len(func.clauses):
|
||||
return self.process(func.clause_expr)
|
||||
else:
|
||||
@@ -263,9 +267,9 @@ class FBCompiler(sql.compiler.SQLCompiler):
|
||||
|
||||
result = ""
|
||||
if select._limit:
|
||||
result += "FIRST %d " % select._limit
|
||||
result += "FIRST %s " % self.process(sql.literal(select._limit))
|
||||
if select._offset:
|
||||
result +="SKIP %d " % select._offset
|
||||
result +="SKIP %s " % self.process(sql.literal(select._offset))
|
||||
if select._distinct:
|
||||
result += "DISTINCT "
|
||||
return result
|
||||
@@ -331,12 +335,13 @@ class FBIdentifierPreparer(sql.compiler.IdentifierPreparer):
|
||||
|
||||
|
||||
class FBExecutionContext(default.DefaultExecutionContext):
|
||||
def fire_sequence(self, seq):
|
||||
def fire_sequence(self, seq, type_):
|
||||
"""Get the next value from the sequence using ``gen_id()``."""
|
||||
|
||||
return self._execute_scalar(
|
||||
"SELECT gen_id(%s, 1) FROM rdb$database" %
|
||||
self.dialect.identifier_preparer.format_sequence(seq)
|
||||
self.dialect.identifier_preparer.format_sequence(seq),
|
||||
type_
|
||||
)
|
||||
|
||||
|
||||
@@ -357,7 +362,6 @@ class FBDialect(default.DefaultDialect):
|
||||
requires_name_normalize = True
|
||||
supports_empty_insert = False
|
||||
|
||||
|
||||
statement_compiler = FBCompiler
|
||||
ddl_compiler = FBDDLCompiler
|
||||
preparer = FBIdentifierPreparer
|
||||
@@ -374,7 +378,13 @@ class FBDialect(default.DefaultDialect):
|
||||
|
||||
def initialize(self, connection):
|
||||
super(FBDialect, self).initialize(connection)
|
||||
self._version_two = self.server_version_info > (2, )
|
||||
self._version_two = ('firebird' in self.server_version_info and \
|
||||
self.server_version_info >= (2, )
|
||||
) or \
|
||||
('interbase' in self.server_version_info and \
|
||||
self.server_version_info >= (6, )
|
||||
)
|
||||
|
||||
if not self._version_two:
|
||||
# TODO: whatever other pre < 2.0 stuff goes here
|
||||
self.ischema_names = ischema_names.copy()
|
||||
@@ -382,8 +392,9 @@ class FBDialect(default.DefaultDialect):
|
||||
self.colspecs = {
|
||||
sqltypes.DateTime: sqltypes.DATE
|
||||
}
|
||||
else:
|
||||
self.implicit_returning = True
|
||||
|
||||
self.implicit_returning = self._version_two and \
|
||||
self.__dict__.get('implicit_returning', True)
|
||||
|
||||
def normalize_name(self, name):
|
||||
# Remove trailing spaces: FB uses a CHAR() type,
|
||||
@@ -509,7 +520,7 @@ class FBDialect(default.DefaultDialect):
|
||||
def get_columns(self, connection, table_name, schema=None, **kw):
|
||||
# Query to extract the details of all the fields of the given table
|
||||
tblqry = """
|
||||
SELECT DISTINCT r.rdb$field_name AS fname,
|
||||
SELECT r.rdb$field_name AS fname,
|
||||
r.rdb$null_flag AS null_flag,
|
||||
t.rdb$type_name AS ftype,
|
||||
f.rdb$field_sub_type AS stype,
|
||||
@@ -585,7 +596,8 @@ class FBDialect(default.DefaultDialect):
|
||||
'name' : name,
|
||||
'type' : coltype,
|
||||
'nullable' : not bool(row['null_flag']),
|
||||
'default' : defvalue
|
||||
'default' : defvalue,
|
||||
'autoincrement':defvalue is None
|
||||
}
|
||||
|
||||
if orig_colname.lower() == orig_colname:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# firebird/kinterbasdb.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -48,7 +48,9 @@ __ http://kinterbasdb.sourceforge.net/dist_docs/usage.html#special_issue_concurr
|
||||
from sqlalchemy.dialects.firebird.base import FBDialect, \
|
||||
FBCompiler, FBExecutionContext
|
||||
from sqlalchemy import util, types as sqltypes
|
||||
import decimal
|
||||
from sqlalchemy.util.compat import decimal
|
||||
from re import match
|
||||
|
||||
|
||||
class _FBNumeric_kinterbasdb(sqltypes.Numeric):
|
||||
def bind_processor(self, dialect):
|
||||
@@ -133,20 +135,25 @@ class FBDialect_kinterbasdb(FBDialect):
|
||||
# that for backward compatibility reasons returns a string like
|
||||
# LI-V6.3.3.12981 Firebird 2.0
|
||||
# where the first version is a fake one resembling the old
|
||||
# Interbase signature. This is more than enough for our purposes,
|
||||
# as this is mainly (only?) used by the testsuite.
|
||||
|
||||
from re import match
|
||||
# Interbase signature.
|
||||
|
||||
fbconn = connection.connection
|
||||
version = fbconn.server_version
|
||||
m = match('\w+-V(\d+)\.(\d+)\.(\d+)\.(\d+) \w+ (\d+)\.(\d+)', version)
|
||||
|
||||
return self._parse_version_info(version)
|
||||
|
||||
def _parse_version_info(self, version):
|
||||
m = match('\w+-V(\d+)\.(\d+)\.(\d+)\.(\d+)( \w+ (\d+)\.(\d+))?', version)
|
||||
if not m:
|
||||
raise AssertionError(
|
||||
"Could not determine version from string '%s'" % version)
|
||||
return tuple([int(x) for x in m.group(5, 6, 4)])
|
||||
|
||||
def is_disconnect(self, e):
|
||||
if m.group(5) != None:
|
||||
return tuple([int(x) for x in m.group(6, 7, 4)] + ['firebird'])
|
||||
else:
|
||||
return tuple([int(x) for x in m.group(1, 2, 3)] + ['interbase'])
|
||||
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
if isinstance(e, (self.dbapi.OperationalError,
|
||||
self.dbapi.ProgrammingError)):
|
||||
msg = str(e)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# informix/__init__.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# informix/base.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# coding: gbk
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
@@ -20,6 +20,124 @@ from sqlalchemy.sql import compiler, text
|
||||
from sqlalchemy.engine import default, reflection
|
||||
from sqlalchemy import types as sqltypes
|
||||
|
||||
RESERVED_WORDS = set(
|
||||
["abs", "absolute", "access", "access_method", "acos", "active", "add",
|
||||
"address", "add_months", "admin", "after", "aggregate", "alignment",
|
||||
"all", "allocate", "all_rows", "altere", "and", "ansi", "any", "append",
|
||||
"array", "as", "asc", "ascii", "asin", "at", "atan", "atan2", "attach",
|
||||
"attributes", "audit", "authentication", "authid", "authorization",
|
||||
"authorized", "auto", "autofree", "auto_reprepare", "auto_stat_mode",
|
||||
"avg", "avoid_execute", "avoid_fact", "avoid_full", "avoid_hash",
|
||||
"avoid_index", "avoid_index_sj", "avoid_multi_index", "avoid_nl",
|
||||
"avoid_star_join", "avoid_subqf", "based", "before", "begin",
|
||||
"between", "bigint", "bigserial", "binary", "bitand", "bitandnot",
|
||||
"bitnot", "bitor", "bitxor", "blob", "blobdir", "boolean", "both",
|
||||
"bound_impl_pdq", "buffered", "builtin", "by", "byte", "cache", "call",
|
||||
"cannothash", "cardinality", "cascade", "case", "cast", "ceil", "char",
|
||||
"character", "character_length", "char_length", "check", "class",
|
||||
"class_origin", "client", "clob", "clobdir", "close", "cluster",
|
||||
"clustersize", "cobol", "codeset", "collation", "collection",
|
||||
"column", "columns", "commit", "committed", "commutator", "component",
|
||||
"components", "concat", "concurrent", "connect", "connection",
|
||||
"connection_name", "connect_by_iscycle", "connect_by_isleaf",
|
||||
"connect_by_rootconst", "constraint", "constraints", "constructor",
|
||||
"context", "continue", "copy", "cos", "costfunc", "count", "crcols",
|
||||
"create", "cross", "current", "current_role", "currval", "cursor",
|
||||
"cycle", "database", "datafiles", "dataskip", "date", "datetime",
|
||||
"day", "dba", "dbdate", "dbinfo", "dbpassword", "dbsecadm",
|
||||
"dbservername", "deallocate", "debug", "debugmode", "debug_env", "dec",
|
||||
"decimal", "declare", "decode", "decrypt_binary", "decrypt_char",
|
||||
"dec_t", "default", "default_role", "deferred", "deferred_prepare",
|
||||
"define", "delay", "delete", "deleting", "delimited", "delimiter",
|
||||
"deluxe", "desc", "describe", "descriptor", "detach", "diagnostics",
|
||||
"directives", "dirty", "disable", "disabled", "disconnect", "disk",
|
||||
"distinct", "distributebinary", "distributesreferences",
|
||||
"distributions", "document", "domain", "donotdistribute", "dormant",
|
||||
"double", "drop", "dtime_t", "each", "elif", "else", "enabled",
|
||||
"encryption", "encrypt_aes", "encrypt_tdes", "end", "enum",
|
||||
"environment", "error", "escape", "exception", "exclusive", "exec",
|
||||
"execute", "executeanywhere", "exemption", "exists", "exit", "exp",
|
||||
"explain", "explicit", "express", "expression", "extdirectives",
|
||||
"extend", "extent", "external", "fact", "false", "far", "fetch",
|
||||
"file", "filetoblob", "filetoclob", "fillfactor", "filtering", "first",
|
||||
"first_rows", "fixchar", "fixed", "float", "floor", "flush", "for",
|
||||
"force", "forced", "force_ddl_exec", "foreach", "foreign", "format",
|
||||
"format_units", "fortran", "found", "fraction", "fragment",
|
||||
"fragments", "free", "from", "full", "function", "general", "get",
|
||||
"gethint", "global", "go", "goto", "grant", "greaterthan",
|
||||
"greaterthanorequal", "group", "handlesnulls", "hash", "having", "hdr",
|
||||
"hex", "high", "hint", "hold", "home", "hour", "idslbacreadarray",
|
||||
"idslbacreadset", "idslbacreadtree", "idslbacrules",
|
||||
"idslbacwritearray", "idslbacwriteset", "idslbacwritetree",
|
||||
"idssecuritylabel", "if", "ifx_auto_reprepare", "ifx_batchedread_table",
|
||||
"ifx_int8_t", "ifx_lo_create_spec_t", "ifx_lo_stat_t", "immediate",
|
||||
"implicit", "implicit_pdq", "in", "inactive", "increment", "index",
|
||||
"indexes", "index_all", "index_sj", "indicator", "informix", "init",
|
||||
"initcap", "inline", "inner", "inout", "insert", "inserting", "instead",
|
||||
"int", "int8", "integ", "integer", "internal", "internallength",
|
||||
"interval", "into", "intrvl_t", "is", "iscanonical", "isolation",
|
||||
"item", "iterator", "java", "join", "keep", "key", "label", "labeleq",
|
||||
"labelge", "labelglb", "labelgt", "labelle", "labellt", "labellub",
|
||||
"labeltostring", "language", "last", "last_day", "leading", "left",
|
||||
"length", "lessthan", "lessthanorequal", "let", "level", "like",
|
||||
"limit", "list", "listing", "load", "local", "locator", "lock", "locks",
|
||||
"locopy", "loc_t", "log", "log10", "logn", "long", "loop", "lotofile",
|
||||
"low", "lower", "lpad", "ltrim", "lvarchar", "matched", "matches",
|
||||
"max", "maxerrors", "maxlen", "maxvalue", "mdy", "median", "medium",
|
||||
"memory", "memory_resident", "merge", "message_length", "message_text",
|
||||
"middle", "min", "minute", "minvalue", "mod", "mode", "moderate",
|
||||
"modify", "module", "money", "month", "months_between", "mounting",
|
||||
"multiset", "multi_index", "name", "nchar", "negator", "new", "next",
|
||||
"nextval", "next_day", "no", "nocache", "nocycle", "nomaxvalue",
|
||||
"nomigrate", "nominvalue", "none", "non_dim", "non_resident", "noorder",
|
||||
"normal", "not", "notemplatearg", "notequal", "null", "nullif",
|
||||
"numeric", "numrows", "numtodsinterval", "numtoyminterval", "nvarchar",
|
||||
"nvl", "octet_length", "of", "off", "old", "on", "online", "only",
|
||||
"opaque", "opclass", "open", "optcompind", "optical", "optimization",
|
||||
"option", "or", "order", "ordered", "out", "outer", "output",
|
||||
"override", "page", "parallelizable", "parameter", "partition",
|
||||
"pascal", "passedbyvalue", "password", "pdqpriority", "percaltl_cos",
|
||||
"pipe", "pli", "pload", "policy", "pow", "power", "precision",
|
||||
"prepare", "previous", "primary", "prior", "private", "privileges",
|
||||
"procedure", "properties", "public", "put", "raise", "range", "raw",
|
||||
"read", "real", "recordend", "references", "referencing", "register",
|
||||
"rejectfile", "relative", "release", "remainder", "rename",
|
||||
"reoptimization", "repeatable", "replace", "replication", "reserve",
|
||||
"resolution", "resource", "restart", "restrict", "resume", "retain",
|
||||
"retainupdatelocks", "return", "returned_sqlstate", "returning",
|
||||
"returns", "reuse", "revoke", "right", "robin", "role", "rollback",
|
||||
"rollforward", "root", "round", "routine", "row", "rowid", "rowids",
|
||||
"rows", "row_count", "rpad", "rtrim", "rule", "sameas", "samples",
|
||||
"sampling", "save", "savepoint", "schema", "scroll", "seclabel_by_comp",
|
||||
"seclabel_by_name", "seclabel_to_char", "second", "secondary",
|
||||
"section", "secured", "security", "selconst", "select", "selecting",
|
||||
"selfunc", "selfuncargs", "sequence", "serial", "serial8",
|
||||
"serializable", "serveruuid", "server_name", "session", "set",
|
||||
"setsessionauth", "share", "short", "siblings", "signed", "sin",
|
||||
"sitename", "size", "skall", "skinhibit", "skip", "skshow",
|
||||
"smallfloat", "smallint", "some", "specific", "sql", "sqlcode",
|
||||
"sqlcontext", "sqlerror", "sqlstate", "sqlwarning", "sqrt",
|
||||
"stability", "stack", "standard", "start", "star_join", "statchange",
|
||||
"statement", "static", "statistics", "statlevel", "status", "stdev",
|
||||
"step", "stop", "storage", "store", "strategies", "string",
|
||||
"stringtolabel", "struct", "style", "subclass_origin", "substr",
|
||||
"substring", "sum", "support", "sync", "synonym", "sysdate",
|
||||
"sysdbclose", "sysdbopen", "system", "sys_connect_by_path", "table",
|
||||
"tables", "tan", "task", "temp", "template", "test", "text", "then",
|
||||
"time", "timeout", "to", "today", "to_char", "to_date",
|
||||
"to_dsinterval", "to_number", "to_yminterval", "trace", "trailing",
|
||||
"transaction", "transition", "tree", "trigger", "triggers", "trim",
|
||||
"true", "trunc", "truncate", "trusted", "type", "typedef", "typeid",
|
||||
"typename", "typeof", "uid", "uncommitted", "under", "union",
|
||||
"unique", "units", "unknown", "unload", "unlock", "unsigned",
|
||||
"update", "updating", "upon", "upper", "usage", "use",
|
||||
"uselastcommitted", "user", "use_hash", "use_nl", "use_subqf",
|
||||
"using", "value", "values", "var", "varchar", "variable", "variance",
|
||||
"variant", "varying", "vercols", "view", "violations", "void",
|
||||
"volatile", "wait", "warning", "weekday", "when", "whenever", "where",
|
||||
"while", "with", "without", "work", "write", "writedown", "writeup",
|
||||
"xadatasource", "xid", "xload", "xunload", "year"
|
||||
])
|
||||
|
||||
class InfoDateTime(sqltypes.DateTime):
|
||||
def bind_processor(self, dialect):
|
||||
@@ -213,6 +331,10 @@ class InfoDDLCompiler(compiler.DDLCompiler):
|
||||
text += "CONSTRAINT %s " % self.preparer.format_constraint(constraint)
|
||||
return text
|
||||
|
||||
class InformixIdentifierPreparer(compiler.IdentifierPreparer):
|
||||
|
||||
reserved_words = RESERVED_WORDS
|
||||
|
||||
|
||||
class InformixDialect(default.DefaultDialect):
|
||||
name = 'informix'
|
||||
@@ -224,6 +346,7 @@ class InformixDialect(default.DefaultDialect):
|
||||
ddl_compiler = InfoDDLCompiler
|
||||
colspecs = colspecs
|
||||
ischema_names = ischema_names
|
||||
preparer = InformixIdentifierPreparer
|
||||
default_paramstyle = 'qmark'
|
||||
|
||||
def __init__(self, has_transactions=True, *args, **kwargs):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# informix/informixdb.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -62,7 +62,7 @@ class InformixDialect_informixdb(InformixDialect):
|
||||
v = VERSION_RE.split(connection.connection.dbms_version)
|
||||
return (int(v[1]), int(v[2]), v[3])
|
||||
|
||||
def is_disconnect(self, e):
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
if isinstance(e, self.dbapi.OperationalError):
|
||||
return 'closed the connection' in str(e) \
|
||||
or 'connection not open' in str(e)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# maxdb/__init__.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# maxdb/base.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
"""Support for the MaxDB database.
|
||||
|
||||
This dialect is *not* ported to SQLAlchemy 0.6.
|
||||
This dialect is *not* ported to SQLAlchemy 0.6 or 0.7.
|
||||
|
||||
This dialect is *not* tested on SQLAlchemy 0.6.
|
||||
This dialect is *not* tested on SQLAlchemy 0.6 or 0.7.
|
||||
|
||||
Overview
|
||||
--------
|
||||
@@ -31,8 +31,6 @@ use upper case for DB-API.
|
||||
Implementation Notes
|
||||
--------------------
|
||||
|
||||
Also check the DatabaseNotes page on the wiki for detailed information.
|
||||
|
||||
With the 7.6.00.37 driver and Python 2.5, it seems that all DB-API
|
||||
generated exceptions are broken and can cause Python to crash.
|
||||
|
||||
@@ -58,6 +56,62 @@ required components such as an Max-aware 'old oracle style' join compiler
|
||||
integration- email the devel list if you're interested in working on
|
||||
this.
|
||||
|
||||
Versions tested: 7.6.03.07 and 7.6.00.37, native Python DB-API
|
||||
|
||||
* MaxDB has severe limitations on OUTER JOINs, which are essential to ORM
|
||||
eager loading. And rather than raise an error if a SELECT can't be serviced,
|
||||
the database simply returns incorrect results.
|
||||
* Version 7.6.03.07 seems to JOIN properly, however the docs do not show the
|
||||
OUTER restrictions being lifted (as of this writing), and no changelog is
|
||||
available to confirm either. If you are using a different server version and
|
||||
your tasks require the ORM or any semi-advanced SQL through the SQL layer,
|
||||
running the SQLAlchemy test suite against your database is HIGHLY
|
||||
recommended before you begin.
|
||||
* Version 7.6.00.37 is LHS/RHS sensitive in `FROM lhs LEFT OUTER JOIN rhs ON
|
||||
lhs.col=rhs.col` vs `rhs.col=lhs.col`!
|
||||
* Version 7.6.00.37 is confused by `SELECT DISTINCT col as alias FROM t ORDER
|
||||
BY col` - these aliased, DISTINCT, ordered queries need to be re-written to
|
||||
order by the alias name.
|
||||
* Version 7.6.x supports creating a SAVEPOINT but not its RELEASE.
|
||||
* MaxDB supports autoincrement-style columns (DEFAULT SERIAL) and independent
|
||||
sequences. When including a DEFAULT SERIAL column in an insert, 0 needs to
|
||||
be inserted rather than NULL to generate a value.
|
||||
* MaxDB supports ANSI and "old Oracle style" theta joins with (+) outer join
|
||||
indicators.
|
||||
* The SQLAlchemy dialect is schema-aware and probably won't function correctly
|
||||
on server versions (pre-7.6?). Support for schema-less server versions could
|
||||
be added if there's call.
|
||||
* ORDER BY is not supported in subqueries. LIMIT is not supported in
|
||||
subqueries. In 7.6.00.37, TOP does work in subqueries, but without limit not
|
||||
so useful. OFFSET does not work in 7.6 despite being in the docs. Row number
|
||||
tricks in WHERE via ROWNO may be possible but it only seems to allow
|
||||
less-than comparison!
|
||||
* Version 7.6.03.07 can't LIMIT if a derived table is in FROM: `SELECT * FROM
|
||||
(SELECT * FROM a) LIMIT 2`
|
||||
* MaxDB does not support sql's CAST and can only usefullly cast two types.
|
||||
There isn't much implicit type conversion, so be precise when creating
|
||||
`PassiveDefaults` in DDL generation: `'3'` and `3` aren't the same.
|
||||
|
||||
sapdb.dbapi
|
||||
^^^^^^^^^^^
|
||||
|
||||
* As of 2007-10-22 the Python 2.4 and 2.5 compatible versions of the DB-API
|
||||
are no longer available. A forum posting at SAP states that the Python
|
||||
driver will be available again "in the future". The last release from MySQL
|
||||
AB works if you can find it.
|
||||
* sequence.NEXTVAL skips every other value!
|
||||
* No rowcount for executemany()
|
||||
* If an INSERT into a table with a DEFAULT SERIAL column inserts the results
|
||||
of a function `INSERT INTO t VALUES (LENGTH('foo'))`, the cursor won't have
|
||||
the serial id. It needs to be manually yanked from tablename.CURRVAL.
|
||||
* Super-duper picky about where bind params can be placed. Not smart about
|
||||
converting Python types for some functions, such as `MOD(5, ?)`.
|
||||
* LONG (text, binary) values in result sets are read-once. The dialect uses a
|
||||
caching RowProxy when these types are present.
|
||||
* Connection objects seem like they want to be either `close()`d or garbage
|
||||
collected, but not both. There's a warning issued but it seems harmless.
|
||||
|
||||
|
||||
"""
|
||||
import datetime, itertools, re
|
||||
|
||||
@@ -117,15 +171,13 @@ class _StringType(sqltypes.String):
|
||||
class MaxString(_StringType):
|
||||
_type = 'VARCHAR'
|
||||
|
||||
def __init__(self, *a, **kw):
|
||||
super(MaxString, self).__init__(*a, **kw)
|
||||
|
||||
|
||||
class MaxUnicode(_StringType):
|
||||
_type = 'VARCHAR'
|
||||
|
||||
def __init__(self, length=None, **kw):
|
||||
super(MaxUnicode, self).__init__(length=length, encoding='unicode')
|
||||
kw['encoding'] = 'unicode'
|
||||
super(MaxUnicode, self).__init__(length=length, **kw)
|
||||
|
||||
|
||||
class MaxChar(_StringType):
|
||||
@@ -135,8 +187,8 @@ class MaxChar(_StringType):
|
||||
class MaxText(_StringType):
|
||||
_type = 'LONG'
|
||||
|
||||
def __init__(self, *a, **kw):
|
||||
super(MaxText, self).__init__(*a, **kw)
|
||||
def __init__(self, length=None, **kw):
|
||||
super(MaxText, self).__init__(length, **kw)
|
||||
|
||||
def get_col_spec(self):
|
||||
spec = 'LONG'
|
||||
@@ -583,7 +635,7 @@ class MaxDBCompiler(compiler.SQLCompiler):
|
||||
# LIMIT. Right? Other dialects seem to get away with
|
||||
# dropping order.
|
||||
if select._limit:
|
||||
raise exc.InvalidRequestError(
|
||||
raise exc.CompileError(
|
||||
"MaxDB does not support ORDER BY in subqueries")
|
||||
else:
|
||||
return ""
|
||||
@@ -604,6 +656,7 @@ class MaxDBCompiler(compiler.SQLCompiler):
|
||||
def limit_clause(self, select):
|
||||
# The docs say offsets are supported with LIMIT. But they're not.
|
||||
# TODO: maybe emulate by adding a ROWNO/ROWNUM predicate?
|
||||
# TODO: does MaxDB support bind params for LIMIT / TOP ?
|
||||
if self.is_subquery():
|
||||
# sub queries need TOP
|
||||
return ''
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# maxdb/sapdb.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mssql/__init__.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mssql/adodbapi.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -8,6 +8,7 @@
|
||||
The adodbapi dialect is not implemented for 0.6 at this time.
|
||||
|
||||
"""
|
||||
import datetime
|
||||
from sqlalchemy import types as sqltypes, util
|
||||
from sqlalchemy.dialects.mssql.base import MSDateTime, MSDialect
|
||||
import sys
|
||||
@@ -61,7 +62,7 @@ class MSDialect_adodbapi(MSDialect):
|
||||
connectors.append("Integrated Security=SSPI")
|
||||
return [[";".join (connectors)], {}]
|
||||
|
||||
def is_disconnect(self, e):
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
return isinstance(e, self.dbapi.adodbapi.DatabaseError) and \
|
||||
"'connection failure'" in str(e)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mssql/base.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -56,7 +56,7 @@ MSNVarchar, MSText, and MSNText. For example::
|
||||
from sqlalchemy.dialects.mssql import VARCHAR
|
||||
Column('login', VARCHAR(32, collation='Latin1_General_CI_AS'))
|
||||
|
||||
When such a column is associated with a :class:`Table`, the
|
||||
When such a column is associated with a :class:`.Table`, the
|
||||
CREATE TABLE statement for this column will yield::
|
||||
|
||||
login VARCHAR(32) COLLATE Latin1_General_CI_AS NULL
|
||||
@@ -130,17 +130,57 @@ which has triggers::
|
||||
# ...,
|
||||
implicit_returning=False
|
||||
)
|
||||
|
||||
|
||||
Declarative form::
|
||||
|
||||
class MyClass(Base):
|
||||
# ...
|
||||
__table_args__ = {'implicit_returning':False}
|
||||
|
||||
|
||||
|
||||
|
||||
This option can also be specified engine-wide using the
|
||||
``implicit_returning=False`` argument on :func:`.create_engine`.
|
||||
|
||||
Enabling Snapshot Isolation
|
||||
---------------------------
|
||||
|
||||
Not necessarily specific to SQLAlchemy, SQL Server has a default transaction
|
||||
isolation mode that locks entire tables, and causes even mildly concurrent
|
||||
applications to have long held locks and frequent deadlocks.
|
||||
Enabling snapshot isolation for the database as a whole is recommended
|
||||
for modern levels of concurrency support. This is accomplished via the
|
||||
following ALTER DATABASE commands executed at the SQL prompt::
|
||||
|
||||
ALTER DATABASE MyDatabase SET ALLOW_SNAPSHOT_ISOLATION ON
|
||||
|
||||
ALTER DATABASE MyDatabase SET READ_COMMITTED_SNAPSHOT ON
|
||||
|
||||
Background on SQL Server snapshot isolation is available at
|
||||
http://msdn.microsoft.com/en-us/library/ms175095.aspx.
|
||||
|
||||
Scalar Select Comparisons
|
||||
-------------------------
|
||||
|
||||
The MSSQL dialect contains a legacy behavior whereby comparing
|
||||
a scalar select to a value using the ``=`` or ``!=`` operator
|
||||
will resolve to IN or NOT IN, respectively. This behavior is
|
||||
deprecated and will be removed in 0.8 - the ``s.in_()``/``~s.in_()`` operators
|
||||
should be used when IN/NOT IN are desired.
|
||||
|
||||
For the time being, the existing behavior prevents a comparison
|
||||
between scalar select and another value that actually wants to use ``=``.
|
||||
To remove this behavior in a forwards-compatible way, apply this
|
||||
compilation rule by placing the following code at the module import
|
||||
level::
|
||||
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
from sqlalchemy.sql.expression import _BinaryExpression
|
||||
from sqlalchemy.sql.compiler import SQLCompiler
|
||||
|
||||
@compiles(_BinaryExpression, 'mssql')
|
||||
def override_legacy_binary(element, compiler, **kw):
|
||||
return SQLCompiler.visit_binary(compiler, element, **kw)
|
||||
|
||||
Known Issues
|
||||
------------
|
||||
|
||||
@@ -149,20 +189,19 @@ Known Issues
|
||||
SQL Server 2005
|
||||
|
||||
"""
|
||||
import datetime, decimal, inspect, operator, sys, re
|
||||
import itertools
|
||||
import datetime, operator, re
|
||||
|
||||
from sqlalchemy import sql, schema as sa_schema, exc, util
|
||||
from sqlalchemy.sql import select, compiler, expression, \
|
||||
operators as sql_operators, \
|
||||
functions as sql_functions, util as sql_util
|
||||
util as sql_util, cast
|
||||
from sqlalchemy.engine import default, base, reflection
|
||||
from sqlalchemy import types as sqltypes
|
||||
from sqlalchemy import processors
|
||||
from sqlalchemy.types import INTEGER, BIGINT, SMALLINT, DECIMAL, NUMERIC, \
|
||||
FLOAT, TIMESTAMP, DATETIME, DATE, BINARY,\
|
||||
VARBINARY, BLOB
|
||||
|
||||
|
||||
from sqlalchemy.dialects.mssql import information_schema as ischema
|
||||
|
||||
MS_2008_VERSION = (10,)
|
||||
@@ -200,14 +239,13 @@ RESERVED_WORDS = set(
|
||||
'writetext',
|
||||
])
|
||||
|
||||
|
||||
class REAL(sqltypes.Float):
|
||||
"""A type for ``real`` numbers."""
|
||||
|
||||
class REAL(sqltypes.REAL):
|
||||
__visit_name__ = 'REAL'
|
||||
|
||||
def __init__(self):
|
||||
super(REAL, self).__init__(precision=24)
|
||||
def __init__(self, **kw):
|
||||
# REAL is a synonym for FLOAT(24) on SQL server
|
||||
kw['precision'] = 24
|
||||
super(REAL, self).__init__(**kw)
|
||||
|
||||
class TINYINT(sqltypes.Integer):
|
||||
__visit_name__ = 'TINYINT'
|
||||
@@ -258,7 +296,7 @@ class TIME(sqltypes.TIME):
|
||||
return value
|
||||
return process
|
||||
|
||||
_reg = re.compile(r"(\d+):(\d+):(\d+)(?:\.(\d+))?")
|
||||
_reg = re.compile(r"(\d+):(\d+):(\d+)(?:\.(\d{0,6}))?")
|
||||
def result_processor(self, dialect, coltype):
|
||||
def process(value):
|
||||
if isinstance(value, datetime.datetime):
|
||||
@@ -289,7 +327,8 @@ class SMALLDATETIME(_DateTimeBase, sqltypes.DateTime):
|
||||
class DATETIME2(_DateTimeBase, sqltypes.DateTime):
|
||||
__visit_name__ = 'DATETIME2'
|
||||
|
||||
def __init__(self, precision=None, **kwargs):
|
||||
def __init__(self, precision=None, **kw):
|
||||
super(DATETIME2, self).__init__(**kw)
|
||||
self.precision = precision
|
||||
|
||||
|
||||
@@ -309,16 +348,15 @@ class _StringType(object):
|
||||
class TEXT(_StringType, sqltypes.TEXT):
|
||||
"""MSSQL TEXT type, for variable-length text up to 2^31 characters."""
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
def __init__(self, length=None, collation=None, **kw):
|
||||
"""Construct a TEXT.
|
||||
|
||||
:param collation: Optional, a column-level collation for this string
|
||||
value. Accepts a Windows Collation Name or a SQL Collation Name.
|
||||
|
||||
"""
|
||||
collation = kw.pop('collation', None)
|
||||
_StringType.__init__(self, collation)
|
||||
sqltypes.Text.__init__(self, *args, **kw)
|
||||
sqltypes.Text.__init__(self, length, **kw)
|
||||
|
||||
class NTEXT(_StringType, sqltypes.UnicodeText):
|
||||
"""MSSQL NTEXT type, for variable-length unicode text up to 2^30
|
||||
@@ -326,24 +364,22 @@ class NTEXT(_StringType, sqltypes.UnicodeText):
|
||||
|
||||
__visit_name__ = 'NTEXT'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, length=None, collation=None, **kw):
|
||||
"""Construct a NTEXT.
|
||||
|
||||
:param collation: Optional, a column-level collation for this string
|
||||
value. Accepts a Windows Collation Name or a SQL Collation Name.
|
||||
|
||||
"""
|
||||
collation = kwargs.pop('collation', None)
|
||||
_StringType.__init__(self, collation)
|
||||
length = kwargs.pop('length', None)
|
||||
sqltypes.UnicodeText.__init__(self, length, **kwargs)
|
||||
sqltypes.UnicodeText.__init__(self, length, **kw)
|
||||
|
||||
|
||||
class VARCHAR(_StringType, sqltypes.VARCHAR):
|
||||
"""MSSQL VARCHAR type, for variable-length non-Unicode data with a maximum
|
||||
of 8,000 characters."""
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
def __init__(self, length=None, collation=None, **kw):
|
||||
"""Construct a VARCHAR.
|
||||
|
||||
:param length: Optinal, maximum data length, in characters.
|
||||
@@ -364,16 +400,15 @@ class VARCHAR(_StringType, sqltypes.VARCHAR):
|
||||
value. Accepts a Windows Collation Name or a SQL Collation Name.
|
||||
|
||||
"""
|
||||
collation = kw.pop('collation', None)
|
||||
_StringType.__init__(self, collation)
|
||||
sqltypes.VARCHAR.__init__(self, *args, **kw)
|
||||
sqltypes.VARCHAR.__init__(self, length, **kw)
|
||||
|
||||
class NVARCHAR(_StringType, sqltypes.NVARCHAR):
|
||||
"""MSSQL NVARCHAR type.
|
||||
|
||||
For variable-length unicode character data up to 4,000 characters."""
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
def __init__(self, length=None, collation=None, **kw):
|
||||
"""Construct a NVARCHAR.
|
||||
|
||||
:param length: Optional, Maximum data length, in characters.
|
||||
@@ -382,15 +417,14 @@ class NVARCHAR(_StringType, sqltypes.NVARCHAR):
|
||||
value. Accepts a Windows Collation Name or a SQL Collation Name.
|
||||
|
||||
"""
|
||||
collation = kw.pop('collation', None)
|
||||
_StringType.__init__(self, collation)
|
||||
sqltypes.NVARCHAR.__init__(self, *args, **kw)
|
||||
sqltypes.NVARCHAR.__init__(self, length, **kw)
|
||||
|
||||
class CHAR(_StringType, sqltypes.CHAR):
|
||||
"""MSSQL CHAR type, for fixed-length non-Unicode data with a maximum
|
||||
of 8,000 characters."""
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
def __init__(self, length=None, collation=None, **kw):
|
||||
"""Construct a CHAR.
|
||||
|
||||
:param length: Optinal, maximum data length, in characters.
|
||||
@@ -411,16 +445,15 @@ class CHAR(_StringType, sqltypes.CHAR):
|
||||
value. Accepts a Windows Collation Name or a SQL Collation Name.
|
||||
|
||||
"""
|
||||
collation = kw.pop('collation', None)
|
||||
_StringType.__init__(self, collation)
|
||||
sqltypes.CHAR.__init__(self, *args, **kw)
|
||||
sqltypes.CHAR.__init__(self, length, **kw)
|
||||
|
||||
class NCHAR(_StringType, sqltypes.NCHAR):
|
||||
"""MSSQL NCHAR type.
|
||||
|
||||
For fixed-length unicode character data up to 4,000 characters."""
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
def __init__(self, length=None, collation=None, **kw):
|
||||
"""Construct an NCHAR.
|
||||
|
||||
:param length: Optional, Maximum data length, in characters.
|
||||
@@ -429,9 +462,8 @@ class NCHAR(_StringType, sqltypes.NCHAR):
|
||||
value. Accepts a Windows Collation Name or a SQL Collation Name.
|
||||
|
||||
"""
|
||||
collation = kw.pop('collation', None)
|
||||
_StringType.__init__(self, collation)
|
||||
sqltypes.NCHAR.__init__(self, *args, **kw)
|
||||
sqltypes.NCHAR.__init__(self, length, **kw)
|
||||
|
||||
class IMAGE(sqltypes.LargeBinary):
|
||||
__visit_name__ = 'IMAGE'
|
||||
@@ -510,7 +542,7 @@ ischema_names = {
|
||||
|
||||
|
||||
class MSTypeCompiler(compiler.GenericTypeCompiler):
|
||||
def _extend(self, spec, type_):
|
||||
def _extend(self, spec, type_, length=None):
|
||||
"""Extend a string-type declaration with standard SQL
|
||||
COLLATE annotations.
|
||||
|
||||
@@ -521,8 +553,11 @@ class MSTypeCompiler(compiler.GenericTypeCompiler):
|
||||
else:
|
||||
collation = None
|
||||
|
||||
if type_.length:
|
||||
spec = spec + "(%d)" % type_.length
|
||||
if not length:
|
||||
length = type_.length
|
||||
|
||||
if length:
|
||||
spec = spec + "(%s)" % length
|
||||
|
||||
return ' '.join([c for c in (spec, collation)
|
||||
if c is not None])
|
||||
@@ -534,9 +569,6 @@ class MSTypeCompiler(compiler.GenericTypeCompiler):
|
||||
else:
|
||||
return "FLOAT(%(precision)s)" % {'precision': precision}
|
||||
|
||||
def visit_REAL(self, type_):
|
||||
return "REAL"
|
||||
|
||||
def visit_TINYINT(self, type_):
|
||||
return "TINYINT"
|
||||
|
||||
@@ -576,7 +608,8 @@ class MSTypeCompiler(compiler.GenericTypeCompiler):
|
||||
return self._extend("TEXT", type_)
|
||||
|
||||
def visit_VARCHAR(self, type_):
|
||||
return self._extend("VARCHAR", type_)
|
||||
return self._extend("VARCHAR", type_,
|
||||
length = type_.length or 'max')
|
||||
|
||||
def visit_CHAR(self, type_):
|
||||
return self._extend("CHAR", type_)
|
||||
@@ -585,7 +618,8 @@ class MSTypeCompiler(compiler.GenericTypeCompiler):
|
||||
return self._extend("NCHAR", type_)
|
||||
|
||||
def visit_NVARCHAR(self, type_):
|
||||
return self._extend("NVARCHAR", type_)
|
||||
return self._extend("NVARCHAR", type_,
|
||||
length = type_.length or 'max')
|
||||
|
||||
def visit_date(self, type_):
|
||||
if self.dialect.server_version_info < MS_2008_VERSION:
|
||||
@@ -605,6 +639,12 @@ class MSTypeCompiler(compiler.GenericTypeCompiler):
|
||||
def visit_IMAGE(self, type_):
|
||||
return "IMAGE"
|
||||
|
||||
def visit_VARBINARY(self, type_):
|
||||
return self._extend(
|
||||
"VARBINARY",
|
||||
type_,
|
||||
length=type_.length or 'max')
|
||||
|
||||
def visit_boolean(self, type_):
|
||||
return self.visit_BIT(type_)
|
||||
|
||||
@@ -709,8 +749,8 @@ class MSSQLCompiler(compiler.SQLCompiler):
|
||||
})
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MSSQLCompiler, self).__init__(*args, **kwargs)
|
||||
self.tablealiases = {}
|
||||
super(MSSQLCompiler, self).__init__(*args, **kwargs)
|
||||
|
||||
def visit_now_func(self, fn, **kw):
|
||||
return "CURRENT_TIMESTAMP"
|
||||
@@ -736,15 +776,21 @@ class MSSQLCompiler(compiler.SQLCompiler):
|
||||
|
||||
def get_select_precolumns(self, select):
|
||||
""" MS-SQL puts TOP, it's version of LIMIT here """
|
||||
if select._distinct or select._limit:
|
||||
if select._distinct or select._limit is not None:
|
||||
s = select._distinct and "DISTINCT " or ""
|
||||
|
||||
if select._limit:
|
||||
# ODBC drivers and possibly others
|
||||
# don't support bind params in the SELECT clause on SQL Server.
|
||||
# so have to use literal here.
|
||||
if select._limit is not None:
|
||||
if not select._offset:
|
||||
s += "TOP %s " % (select._limit,)
|
||||
s += "TOP %d " % select._limit
|
||||
return s
|
||||
return compiler.SQLCompiler.get_select_precolumns(self, select)
|
||||
|
||||
def get_from_hint_text(self, table, text):
|
||||
return text
|
||||
|
||||
def limit_clause(self, select):
|
||||
# Limit in mssql is after the select keyword
|
||||
return ""
|
||||
@@ -758,7 +804,7 @@ class MSSQLCompiler(compiler.SQLCompiler):
|
||||
# to use ROW_NUMBER(), an ORDER BY is required.
|
||||
orderby = self.process(select._order_by_clause)
|
||||
if not orderby:
|
||||
raise exc.InvalidRequestError('MSSQL requires an order_by when '
|
||||
raise exc.CompileError('MSSQL requires an order_by when '
|
||||
'using an offset.')
|
||||
|
||||
_offset = select._offset
|
||||
@@ -769,12 +815,12 @@ class MSSQLCompiler(compiler.SQLCompiler):
|
||||
% orderby).label("mssql_rn")
|
||||
).order_by(None).alias()
|
||||
|
||||
mssql_rn = sql.column('mssql_rn')
|
||||
limitselect = sql.select([c for c in select.c if
|
||||
c.key!='mssql_rn'])
|
||||
limitselect.append_whereclause("mssql_rn>%d" % _offset)
|
||||
limitselect.append_whereclause(mssql_rn> _offset)
|
||||
if _limit is not None:
|
||||
limitselect.append_whereclause("mssql_rn<=%d" %
|
||||
(_limit + _offset))
|
||||
limitselect.append_whereclause(mssql_rn<=(_limit + _offset))
|
||||
return self.process(limitselect, iswrapper=True, **kwargs)
|
||||
else:
|
||||
return compiler.SQLCompiler.visit_select(self, select, **kwargs)
|
||||
@@ -800,7 +846,6 @@ class MSSQLCompiler(compiler.SQLCompiler):
|
||||
|
||||
def visit_alias(self, alias, **kwargs):
|
||||
# translate for schema-qualified table aliases
|
||||
self.tablealiases[alias.original] = alias
|
||||
kwargs['mssql_aliased'] = alias.original
|
||||
return super(MSSQLCompiler, self).visit_alias(alias, **kwargs)
|
||||
|
||||
@@ -809,6 +854,9 @@ class MSSQLCompiler(compiler.SQLCompiler):
|
||||
return 'DATEPART("%s", %s)' % \
|
||||
(field, self.process(extract.expr, **kw))
|
||||
|
||||
def visit_savepoint(self, savepoint_stmt):
|
||||
return "SAVE TRANSACTION %s" % self.preparer.format_savepoint(savepoint_stmt)
|
||||
|
||||
def visit_rollback_to_savepoint(self, savepoint_stmt):
|
||||
return ("ROLLBACK TRANSACTION %s"
|
||||
% self.preparer.format_savepoint(savepoint_stmt))
|
||||
@@ -866,6 +914,10 @@ class MSSQLCompiler(compiler.SQLCompiler):
|
||||
)
|
||||
):
|
||||
op = binary.operator == operator.eq and "IN" or "NOT IN"
|
||||
util.warn_deprecated("Comparing a scalar select using ``=``/``!=`` will "
|
||||
"no longer produce IN/NOT IN in 0.8. To remove this "
|
||||
"behavior immediately, use the recipe at "
|
||||
"http://www.sqlalchemy.org/docs/07/dialects/mssql.html#scalar-select-comparisons")
|
||||
return self.process(
|
||||
expression._BinaryExpression(binary.left,
|
||||
binary.right, op),
|
||||
@@ -977,7 +1029,7 @@ class MSDDLCompiler(compiler.DDLCompiler):
|
||||
colspec += " NULL"
|
||||
|
||||
if column.table is None:
|
||||
raise exc.InvalidRequestError(
|
||||
raise exc.CompileError(
|
||||
"mssql requires Table-bound columns "
|
||||
"in order to generate DDL")
|
||||
|
||||
@@ -1066,12 +1118,12 @@ class MSDialect(default.DefaultDialect):
|
||||
super(MSDialect, self).__init__(**opts)
|
||||
|
||||
def do_savepoint(self, connection, name):
|
||||
util.warn("Savepoint support in mssql is experimental and "
|
||||
"may lead to data loss.")
|
||||
# give the DBAPI a push
|
||||
connection.execute("IF @@TRANCOUNT = 0 BEGIN TRANSACTION")
|
||||
connection.execute("SAVE TRANSACTION %s" % name)
|
||||
super(MSDialect, self).do_savepoint(connection, name)
|
||||
|
||||
def do_release_savepoint(self, connection, name):
|
||||
# SQL Server does not support RELEASE SAVEPOINT
|
||||
pass
|
||||
|
||||
def initialize(self, connection):
|
||||
@@ -1108,15 +1160,20 @@ class MSDialect(default.DefaultDialect):
|
||||
pass
|
||||
return self.schema_name
|
||||
|
||||
def _unicode_cast(self, column):
|
||||
if self.server_version_info >= MS_2005_VERSION:
|
||||
return cast(column, NVARCHAR(_warn_on_bytestring=False))
|
||||
else:
|
||||
return column
|
||||
|
||||
def has_table(self, connection, tablename, schema=None):
|
||||
current_schema = schema or self.default_schema_name
|
||||
columns = ischema.columns
|
||||
|
||||
whereclause = self._unicode_cast(columns.c.table_name)==tablename
|
||||
if current_schema:
|
||||
whereclause = sql.and_(columns.c.table_name==tablename,
|
||||
whereclause = sql.and_(whereclause,
|
||||
columns.c.table_schema==current_schema)
|
||||
else:
|
||||
whereclause = columns.c.table_name==tablename
|
||||
s = sql.select([columns], whereclause)
|
||||
c = connection.execute(s)
|
||||
return c.first() is not None
|
||||
@@ -1180,7 +1237,10 @@ class MSDialect(default.DefaultDialect):
|
||||
sqltypes.String(convert_unicode=True)),
|
||||
sql.bindparam('schname', current_schema,
|
||||
sqltypes.String(convert_unicode=True))
|
||||
]
|
||||
],
|
||||
typemap = {
|
||||
'name':sqltypes.Unicode()
|
||||
}
|
||||
)
|
||||
)
|
||||
indexes = {}
|
||||
@@ -1206,7 +1266,11 @@ class MSDialect(default.DefaultDialect):
|
||||
sqltypes.String(convert_unicode=True)),
|
||||
sql.bindparam('schname', current_schema,
|
||||
sqltypes.String(convert_unicode=True))
|
||||
]),
|
||||
],
|
||||
typemap = {
|
||||
'name':sqltypes.Unicode()
|
||||
}
|
||||
),
|
||||
)
|
||||
for row in rp:
|
||||
if row['index_id'] in indexes:
|
||||
@@ -1217,14 +1281,25 @@ class MSDialect(default.DefaultDialect):
|
||||
@reflection.cache
|
||||
def get_view_definition(self, connection, viewname, schema=None, **kw):
|
||||
current_schema = schema or self.default_schema_name
|
||||
views = ischema.views
|
||||
s = sql.select([views.c.view_definition],
|
||||
sql.and_(
|
||||
views.c.table_schema == current_schema,
|
||||
views.c.table_name == viewname
|
||||
),
|
||||
|
||||
rp = connection.execute(
|
||||
sql.text(
|
||||
"select definition from sys.sql_modules as mod, "
|
||||
"sys.views as views, "
|
||||
"sys.schemas as sch"
|
||||
" where "
|
||||
"mod.object_id=views.object_id and "
|
||||
"views.schema_id=sch.schema_id and "
|
||||
"views.name=:viewname and sch.name=:schname",
|
||||
bindparams=[
|
||||
sql.bindparam('viewname', viewname,
|
||||
sqltypes.String(convert_unicode=True)),
|
||||
sql.bindparam('schname', current_schema,
|
||||
sqltypes.String(convert_unicode=True))
|
||||
]
|
||||
)
|
||||
)
|
||||
rp = connection.execute(s)
|
||||
|
||||
if rp:
|
||||
view_def = rp.scalar()
|
||||
return view_def
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# mssql/information_schema.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
# TODO: should be using the sys. catalog with SQL Server, not information schema
|
||||
|
||||
from sqlalchemy import Table, MetaData, Column, ForeignKey
|
||||
from sqlalchemy import Table, MetaData, Column
|
||||
from sqlalchemy.types import String, Unicode, Integer, TypeDecorator
|
||||
|
||||
ischema = MetaData()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mssql/mxodbc.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -51,15 +51,11 @@ of ``False`` will uncondtionally use string-escaped parameters.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
from sqlalchemy import types as sqltypes
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy.connectors.mxodbc import MxODBCConnector
|
||||
from sqlalchemy.dialects.mssql.pyodbc import MSExecutionContext_pyodbc
|
||||
from sqlalchemy.dialects.mssql.base import (MSExecutionContext, MSDialect,
|
||||
MSSQLCompiler,
|
||||
from sqlalchemy.dialects.mssql.base import (MSDialect,
|
||||
MSSQLStrictCompiler,
|
||||
_MSDateTime, _MSDate, TIME)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mssql/pymssql.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -41,7 +41,6 @@ Please consult the pymssql documentation for further information.
|
||||
from sqlalchemy.dialects.mssql.base import MSDialect
|
||||
from sqlalchemy import types as sqltypes, util, processors
|
||||
import re
|
||||
import decimal
|
||||
|
||||
class _MSNumeric_pymssql(sqltypes.Numeric):
|
||||
def result_processor(self, dialect, type_):
|
||||
@@ -52,7 +51,6 @@ class _MSNumeric_pymssql(sqltypes.Numeric):
|
||||
|
||||
class MSDialect_pymssql(MSDialect):
|
||||
supports_sane_rowcount = False
|
||||
max_identifier_length = 30
|
||||
driver = 'pymssql'
|
||||
|
||||
colspecs = util.update_copy(
|
||||
@@ -96,7 +94,7 @@ class MSDialect_pymssql(MSDialect):
|
||||
opts['host'] = "%s:%s" % (opts['host'], port)
|
||||
return [[], opts]
|
||||
|
||||
def is_disconnect(self, e):
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
for msg in (
|
||||
"Error 10054",
|
||||
"Not connected to any MS SQL server",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mssql/pyodbc.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -35,27 +35,31 @@ Examples of pyodbc connection string URLs:
|
||||
|
||||
dsn=mydsn;UID=user;PWD=pass;LANGUAGE=us_english
|
||||
|
||||
* ``mssql+pyodbc://user:pass@host/db`` - connects using a connection string
|
||||
dynamically created that would appear like::
|
||||
* ``mssql+pyodbc://user:pass@host/db`` - connects using a connection
|
||||
that would appear like::
|
||||
|
||||
DRIVER={SQL Server};Server=host;Database=db;UID=user;PWD=pass
|
||||
|
||||
* ``mssql+pyodbc://user:pass@host:123/db`` - connects using a connection
|
||||
string that is dynamically created, which also includes the port
|
||||
information using the comma syntax. If your connection string
|
||||
requires the port information to be passed as a ``port`` keyword
|
||||
see the next example. This will create the following connection
|
||||
string::
|
||||
string which includes the port
|
||||
information using the comma syntax. This will create the following
|
||||
connection string::
|
||||
|
||||
DRIVER={SQL Server};Server=host,123;Database=db;UID=user;PWD=pass
|
||||
|
||||
* ``mssql+pyodbc://user:pass@host/db?port=123`` - connects using a connection
|
||||
string that is dynamically created that includes the port
|
||||
string that includes the port
|
||||
information as a separate ``port`` keyword. This will create the
|
||||
following connection string::
|
||||
|
||||
DRIVER={SQL Server};Server=host;Database=db;UID=user;PWD=pass;port=123
|
||||
|
||||
* ``mssql+pyodbc://user:pass@host/db?driver=MyDriver`` - connects using a connection
|
||||
string that includes a custom
|
||||
ODBC driver name. This will create the following connection string::
|
||||
|
||||
DRIVER={MyDriver};Server=host;Database=db;UID=user;PWD=pass
|
||||
|
||||
If you require a connection string that is outside the options
|
||||
presented above, use the ``odbc_connect`` keyword to pass in a
|
||||
urlencoded connection string. What gets passed in will be urldecoded
|
||||
@@ -94,7 +98,12 @@ class _MSNumeric_pyodbc(sqltypes.Numeric):
|
||||
"""
|
||||
|
||||
def bind_processor(self, dialect):
|
||||
super_process = super(_MSNumeric_pyodbc, self).bind_processor(dialect)
|
||||
|
||||
super_process = super(_MSNumeric_pyodbc, self).\
|
||||
bind_processor(dialect)
|
||||
|
||||
if not dialect._need_decimal_fix:
|
||||
return super_process
|
||||
|
||||
def process(value):
|
||||
if self.asdecimal and \
|
||||
@@ -112,31 +121,35 @@ class _MSNumeric_pyodbc(sqltypes.Numeric):
|
||||
return value
|
||||
return process
|
||||
|
||||
# these routines needed for older versions of pyodbc.
|
||||
# as of 2.1.8 this logic is integrated.
|
||||
|
||||
def _small_dec_to_string(self, value):
|
||||
return "%s0.%s%s" % (
|
||||
(value < 0 and '-' or ''),
|
||||
'0' * (abs(value.adjusted()) - 1),
|
||||
"".join([str(nint) for nint in value._int]))
|
||||
"".join([str(nint) for nint in value.as_tuple()[1]]))
|
||||
|
||||
def _large_dec_to_string(self, value):
|
||||
_int = value.as_tuple()[1]
|
||||
if 'E' in str(value):
|
||||
result = "%s%s%s" % (
|
||||
(value < 0 and '-' or ''),
|
||||
"".join([str(s) for s in value._int]),
|
||||
"0" * (value.adjusted() - (len(value._int)-1)))
|
||||
"".join([str(s) for s in _int]),
|
||||
"0" * (value.adjusted() - (len(_int)-1)))
|
||||
else:
|
||||
if (len(value._int) - 1) > value.adjusted():
|
||||
if (len(_int) - 1) > value.adjusted():
|
||||
result = "%s%s.%s" % (
|
||||
(value < 0 and '-' or ''),
|
||||
"".join(
|
||||
[str(s) for s in value._int][0:value.adjusted() + 1]),
|
||||
[str(s) for s in _int][0:value.adjusted() + 1]),
|
||||
"".join(
|
||||
[str(s) for s in value._int][value.adjusted() + 1:]))
|
||||
[str(s) for s in _int][value.adjusted() + 1:]))
|
||||
else:
|
||||
result = "%s%s" % (
|
||||
(value < 0 and '-' or ''),
|
||||
"".join(
|
||||
[str(s) for s in value._int][0:value.adjusted() + 1]))
|
||||
[str(s) for s in _int][0:value.adjusted() + 1]))
|
||||
return result
|
||||
|
||||
|
||||
@@ -206,5 +219,7 @@ class MSDialect_pyodbc(PyODBCConnector, MSDialect):
|
||||
self.description_encoding = description_encoding
|
||||
self.use_scope_identity = self.dbapi and \
|
||||
hasattr(self.dbapi.Cursor, 'nextset')
|
||||
self._need_decimal_fix = self.dbapi and \
|
||||
self._dbapi_version() < (2, 1, 8)
|
||||
|
||||
dialect = MSDialect_pyodbc
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mssql/zxjdbc.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# mysql/__init__.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
from sqlalchemy.dialects.mysql import base, mysqldb, oursql, \
|
||||
pyodbc, zxjdbc, mysqlconnector
|
||||
pyodbc, zxjdbc, mysqlconnector, pymysql
|
||||
|
||||
# default dialect
|
||||
base.dialect = mysqldb.dialect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mysql/base.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -68,6 +68,22 @@ creation option can be specified in this syntax::
|
||||
mysql_charset='utf8'
|
||||
)
|
||||
|
||||
Case Sensitivity and Table Reflection
|
||||
-------------------------------------
|
||||
|
||||
MySQL has inconsistent support for case-sensitive identifier
|
||||
names, basing support on specific details of the underlying
|
||||
operating system. However, it has been observed that no matter
|
||||
what case sensitivity behavior is present, the names of tables in
|
||||
foreign key declarations are *always* received from the database
|
||||
as all-lower case, making it impossible to accurately reflect a
|
||||
schema where inter-related tables use mixed-case identifier names.
|
||||
|
||||
Therefore it is strongly advised that table names be declared as
|
||||
all lower case both within SQLAlchemy as well as on the MySQL
|
||||
database itself, especially if database reflection features are
|
||||
to be used.
|
||||
|
||||
Keys
|
||||
----
|
||||
|
||||
@@ -81,7 +97,7 @@ foreign keys. For these tables, you may supply a
|
||||
autoload=True
|
||||
)
|
||||
|
||||
When creating tables, SQLAlchemy will automatically set ``AUTO_INCREMENT``` on
|
||||
When creating tables, SQLAlchemy will automatically set ``AUTO_INCREMENT`` on
|
||||
an integer primary key column::
|
||||
|
||||
>>> t = Table('mytable', metadata,
|
||||
@@ -152,14 +168,61 @@ available.
|
||||
|
||||
update(..., mysql_limit=10)
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
CAST Support
|
||||
------------
|
||||
|
||||
If you have problems that seem server related, first check that you are
|
||||
using the most recent stable MySQL-Python package available. The Database
|
||||
Notes page on the wiki at http://www.sqlalchemy.org is a good resource for
|
||||
timely information affecting MySQL in SQLAlchemy.
|
||||
MySQL documents the CAST operator as available in version 4.0.2. When using the
|
||||
SQLAlchemy :func:`.cast` function, SQLAlchemy
|
||||
will not render the CAST token on MySQL before this version, based on server version
|
||||
detection, instead rendering the internal expression directly.
|
||||
|
||||
CAST may still not be desirable on an early MySQL version post-4.0.2, as it didn't
|
||||
add all datatype support until 4.1.1. If your application falls into this
|
||||
narrow area, the behavior of CAST can be controlled using the :ref:`sqlalchemy.ext.compiler_toplevel`
|
||||
system, as per the recipe below::
|
||||
|
||||
from sqlalchemy.sql.expression import _Cast
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
|
||||
@compiles(_Cast, 'mysql')
|
||||
def _check_mysql_version(element, compiler, **kw):
|
||||
if compiler.dialect.server_version_info < (4, 1, 0):
|
||||
return compiler.process(element.clause, **kw)
|
||||
else:
|
||||
return compiler.visit_cast(element, **kw)
|
||||
|
||||
The above function, which only needs to be declared once
|
||||
within an application, overrides the compilation of the
|
||||
:func:`.cast` construct to check for version 4.1.0 before
|
||||
fully rendering CAST; else the internal element of the
|
||||
construct is rendered directly.
|
||||
|
||||
|
||||
.. _mysql_indexes:
|
||||
|
||||
MySQL Specific Index Options
|
||||
----------------------------
|
||||
|
||||
MySQL-specific extensions to the :class:`.Index` construct are available.
|
||||
|
||||
Index Length
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
MySQL provides an option to create index entries with a certain length, where
|
||||
"length" refers to the number of characters or bytes in each value which will
|
||||
become part of the index. SQLAlchemy provides this feature via the
|
||||
``mysql_length`` parameter::
|
||||
|
||||
Index('my_index', my_table.c.data, mysql_length=10)
|
||||
|
||||
Prefix lengths are given in characters for nonbinary string types and in bytes
|
||||
for binary string types. The value passed to the keyword argument will be
|
||||
simply passed through to the underlying CREATE INDEX command, so it *must* be
|
||||
an integer. MySQL only allows a length for an index if it is for a CHAR,
|
||||
VARCHAR, TEXT, BINARY, VARBINARY and BLOB.
|
||||
|
||||
More information can be found at:
|
||||
http://dev.mysql.com/doc/refman/5.0/en/create-index.html
|
||||
"""
|
||||
|
||||
import datetime, inspect, re, sys
|
||||
@@ -174,7 +237,7 @@ from array import array as _array
|
||||
from sqlalchemy.engine import reflection
|
||||
from sqlalchemy.engine import base as engine_base, default
|
||||
from sqlalchemy import types as sqltypes
|
||||
|
||||
from sqlalchemy.util import topological
|
||||
from sqlalchemy.types import DATE, DATETIME, BOOLEAN, TIME, \
|
||||
BLOB, BINARY, VARBINARY
|
||||
|
||||
@@ -231,9 +294,9 @@ SET_RE = re.compile(
|
||||
class _NumericType(object):
|
||||
"""Base for MySQL numeric types."""
|
||||
|
||||
def __init__(self, **kw):
|
||||
self.unsigned = kw.pop('unsigned', False)
|
||||
self.zerofill = kw.pop('zerofill', False)
|
||||
def __init__(self, unsigned=False, zerofill=False, **kw):
|
||||
self.unsigned = unsigned
|
||||
self.zerofill = zerofill
|
||||
super(_NumericType, self).__init__(**kw)
|
||||
|
||||
class _FloatType(_NumericType, sqltypes.Float):
|
||||
@@ -362,7 +425,7 @@ class DOUBLE(_FloatType):
|
||||
super(DOUBLE, self).__init__(precision=precision, scale=scale,
|
||||
asdecimal=asdecimal, **kw)
|
||||
|
||||
class REAL(_FloatType):
|
||||
class REAL(_FloatType, sqltypes.REAL):
|
||||
"""MySQL REAL type."""
|
||||
|
||||
__visit_name__ = 'REAL'
|
||||
@@ -747,7 +810,7 @@ class CHAR(_StringType, sqltypes.CHAR):
|
||||
|
||||
__visit_name__ = 'CHAR'
|
||||
|
||||
def __init__(self, length, **kwargs):
|
||||
def __init__(self, length=None, **kwargs):
|
||||
"""Construct a CHAR.
|
||||
|
||||
:param length: Maximum data length, in characters.
|
||||
@@ -942,6 +1005,10 @@ class ENUM(sqltypes.Enum, _StringType):
|
||||
return value
|
||||
return process
|
||||
|
||||
def adapt(self, impltype, **kw):
|
||||
kw['strict'] = self.strict
|
||||
return sqltypes.Enum.adapt(self, impltype, **kw)
|
||||
|
||||
class SET(_StringType):
|
||||
"""MySQL SET type."""
|
||||
|
||||
@@ -988,8 +1055,8 @@ class SET(_StringType):
|
||||
strip_values.append(a)
|
||||
|
||||
self.values = strip_values
|
||||
length = max([len(v) for v in strip_values] + [0])
|
||||
super(SET, self).__init__(length=length, **kw)
|
||||
kw.setdefault('length', max([len(v) for v in strip_values] + [0]))
|
||||
super(SET, self).__init__(**kw)
|
||||
|
||||
def result_processor(self, dialect, coltype):
|
||||
def process(value):
|
||||
@@ -1113,6 +1180,9 @@ class MySQLExecutionContext(default.DefaultExecutionContext):
|
||||
|
||||
class MySQLCompiler(compiler.SQLCompiler):
|
||||
|
||||
render_table_with_column_in_update_from = True
|
||||
"""Overridden from base SQLCompiler value"""
|
||||
|
||||
extract_map = compiler.SQLCompiler.extract_map.copy()
|
||||
extract_map.update ({
|
||||
'milliseconds': 'millisecond',
|
||||
@@ -1157,7 +1227,7 @@ class MySQLCompiler(compiler.SQLCompiler):
|
||||
return 'CHAR'
|
||||
elif isinstance(type_, sqltypes._Binary):
|
||||
return 'BINARY'
|
||||
elif isinstance(type_, NUMERIC):
|
||||
elif isinstance(type_, sqltypes.NUMERIC):
|
||||
return self.dialect.type_compiler.process(type_).replace('NUMERIC', 'DECIMAL')
|
||||
else:
|
||||
return None
|
||||
@@ -1180,6 +1250,15 @@ class MySQLCompiler(compiler.SQLCompiler):
|
||||
return value
|
||||
|
||||
def get_select_precolumns(self, select):
|
||||
"""Add special MySQL keywords in place of DISTINCT.
|
||||
|
||||
.. note::
|
||||
|
||||
this usage is deprecated. :meth:`.Select.prefix_with`
|
||||
should be used for special keywords at the start
|
||||
of a SELECT.
|
||||
|
||||
"""
|
||||
if isinstance(select._distinct, basestring):
|
||||
return select._distinct.upper() + " "
|
||||
elif select._distinct:
|
||||
@@ -1222,32 +1301,39 @@ class MySQLCompiler(compiler.SQLCompiler):
|
||||
elif offset is not None:
|
||||
# As suggested by the MySQL docs, need to apply an
|
||||
# artificial limit if one wasn't provided
|
||||
# http://dev.mysql.com/doc/refman/5.0/en/select.html
|
||||
if limit is None:
|
||||
limit = 18446744073709551615
|
||||
return ' \n LIMIT %s, %s' % (offset, limit)
|
||||
# hardwire the upper limit. Currently
|
||||
# needed by OurSQL with Python 3
|
||||
# (https://bugs.launchpad.net/oursql/+bug/686232),
|
||||
# but also is consistent with the usage of the upper
|
||||
# bound as part of MySQL's "syntax" for OFFSET with
|
||||
# no LIMIT
|
||||
return ' \n LIMIT %s, %s' % (
|
||||
self.process(sql.literal(offset)),
|
||||
"18446744073709551615")
|
||||
else:
|
||||
return ' \n LIMIT %s, %s' % (
|
||||
self.process(sql.literal(offset)),
|
||||
self.process(sql.literal(limit)))
|
||||
else:
|
||||
# No offset provided, so just use the limit
|
||||
return ' \n LIMIT %s' % (limit,)
|
||||
return ' \n LIMIT %s' % (self.process(sql.literal(limit)),)
|
||||
|
||||
def visit_update(self, update_stmt):
|
||||
self.stack.append({'from': set([update_stmt.table])})
|
||||
|
||||
self.isupdate = True
|
||||
colparams = self._get_colparams(update_stmt)
|
||||
|
||||
text = "UPDATE " + self.preparer.format_table(update_stmt.table) + \
|
||||
" SET " + ', '.join(["%s=%s" % (self.preparer.format_column(c[0]), c[1]) for c in colparams])
|
||||
|
||||
if update_stmt._whereclause is not None:
|
||||
text += " WHERE " + self.process(update_stmt._whereclause)
|
||||
|
||||
limit = update_stmt.kwargs.get('mysql_limit', None)
|
||||
def update_limit_clause(self, update_stmt):
|
||||
limit = update_stmt.kwargs.get('%s_limit' % self.dialect.name, None)
|
||||
if limit:
|
||||
text += " LIMIT %s" % limit
|
||||
return "LIMIT %s" % limit
|
||||
else:
|
||||
return None
|
||||
|
||||
self.stack.pop(-1)
|
||||
def update_tables_clause(self, update_stmt, from_table, extra_froms, **kw):
|
||||
return ', '.join(t._compiler_dispatch(self, asfrom=True, **kw)
|
||||
for t in [from_table] + list(extra_froms))
|
||||
|
||||
def update_from_clause(self, update_stmt, from_table, extra_froms, **kw):
|
||||
return None
|
||||
|
||||
return text
|
||||
|
||||
# ug. "InnoDB needs indexes on foreign keys and referenced keys [...].
|
||||
# Starting with MySQL 4.1.2, these indexes are created automatically.
|
||||
@@ -1259,8 +1345,9 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
|
||||
"""Get table constraints."""
|
||||
constraint_string = super(MySQLDDLCompiler, self).create_table_constraints(table)
|
||||
|
||||
is_innodb = table.kwargs.has_key('mysql_engine') and \
|
||||
table.kwargs['mysql_engine'].lower() == 'innodb'
|
||||
engine_key = '%s_engine' % self.dialect.name
|
||||
is_innodb = table.kwargs.has_key(engine_key) and \
|
||||
table.kwargs[engine_key].lower() == 'innodb'
|
||||
|
||||
auto_inc_column = table._autoincrement_column
|
||||
|
||||
@@ -1293,16 +1380,8 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
|
||||
elif column.nullable and is_timestamp and default is None:
|
||||
colspec.append('NULL')
|
||||
|
||||
if column.primary_key and column.autoincrement:
|
||||
try:
|
||||
first = [c for c in column.table.primary_key.columns
|
||||
if (c.autoincrement and
|
||||
isinstance(c.type, sqltypes.Integer) and
|
||||
not c.foreign_keys)].pop(0)
|
||||
if column is first:
|
||||
colspec.append('AUTO_INCREMENT')
|
||||
except IndexError:
|
||||
pass
|
||||
if column is column.table._autoincrement_column and column.server_default is None:
|
||||
colspec.append('AUTO_INCREMENT')
|
||||
|
||||
return ' '.join(colspec)
|
||||
|
||||
@@ -1310,27 +1389,62 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
|
||||
"""Build table-level CREATE options like ENGINE and COLLATE."""
|
||||
|
||||
table_opts = []
|
||||
for k in table.kwargs:
|
||||
if k.startswith('mysql_'):
|
||||
opt = k[6:].upper()
|
||||
|
||||
arg = table.kwargs[k]
|
||||
if opt in _options_of_type_string:
|
||||
arg = "'%s'" % arg.replace("\\", "\\\\").replace("'", "''")
|
||||
opts = dict(
|
||||
(
|
||||
k[len(self.dialect.name)+1:].upper(),
|
||||
v
|
||||
)
|
||||
for k, v in table.kwargs.items()
|
||||
if k.startswith('%s_' % self.dialect.name)
|
||||
)
|
||||
|
||||
if opt in ('DATA_DIRECTORY', 'INDEX_DIRECTORY',
|
||||
'DEFAULT_CHARACTER_SET', 'CHARACTER_SET', 'DEFAULT_CHARSET',
|
||||
'DEFAULT_COLLATE'):
|
||||
opt = opt.replace('_', ' ')
|
||||
for opt in topological.sort([
|
||||
('DEFAULT_CHARSET', 'COLLATE'),
|
||||
('DEFAULT_CHARACTER_SET', 'COLLATE')
|
||||
], opts):
|
||||
arg = opts[opt]
|
||||
if opt in _options_of_type_string:
|
||||
arg = "'%s'" % arg.replace("\\", "\\\\").replace("'", "''")
|
||||
|
||||
joiner = '='
|
||||
if opt in ('TABLESPACE', 'DEFAULT CHARACTER SET',
|
||||
'CHARACTER SET', 'COLLATE'):
|
||||
joiner = ' '
|
||||
if opt in ('DATA_DIRECTORY', 'INDEX_DIRECTORY',
|
||||
'DEFAULT_CHARACTER_SET', 'CHARACTER_SET',
|
||||
'DEFAULT_CHARSET',
|
||||
'DEFAULT_COLLATE'):
|
||||
opt = opt.replace('_', ' ')
|
||||
|
||||
table_opts.append(joiner.join((opt, arg)))
|
||||
joiner = '='
|
||||
if opt in ('TABLESPACE', 'DEFAULT CHARACTER SET',
|
||||
'CHARACTER SET', 'COLLATE'):
|
||||
joiner = ' '
|
||||
|
||||
table_opts.append(joiner.join((opt, arg)))
|
||||
return ' '.join(table_opts)
|
||||
|
||||
def visit_create_index(self, create):
|
||||
index = create.element
|
||||
preparer = self.preparer
|
||||
text = "CREATE "
|
||||
if index.unique:
|
||||
text += "UNIQUE "
|
||||
text += "INDEX %s ON %s " \
|
||||
% (preparer.quote(self._index_identifier(index.name),
|
||||
index.quote),preparer.format_table(index.table))
|
||||
if 'mysql_length' in index.kwargs:
|
||||
length = index.kwargs['mysql_length']
|
||||
else:
|
||||
length = None
|
||||
if length is not None:
|
||||
text+= "(%s(%d))" \
|
||||
% (', '.join(preparer.quote(c.name, c.quote)
|
||||
for c in index.columns), length)
|
||||
else:
|
||||
text+= "(%s)" \
|
||||
% (', '.join(preparer.quote(c.name, c.quote)
|
||||
for c in index.columns))
|
||||
return text
|
||||
|
||||
|
||||
def visit_drop_index(self, drop):
|
||||
index = drop.element
|
||||
|
||||
@@ -1408,17 +1522,25 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
|
||||
if type_.precision is None:
|
||||
return self._extend_numeric(type_, "NUMERIC")
|
||||
elif type_.scale is None:
|
||||
return self._extend_numeric(type_, "NUMERIC(%(precision)s)" % {'precision': type_.precision})
|
||||
return self._extend_numeric(type_,
|
||||
"NUMERIC(%(precision)s)" %
|
||||
{'precision': type_.precision})
|
||||
else:
|
||||
return self._extend_numeric(type_, "NUMERIC(%(precision)s, %(scale)s)" % {'precision': type_.precision, 'scale' : type_.scale})
|
||||
return self._extend_numeric(type_,
|
||||
"NUMERIC(%(precision)s, %(scale)s)" %
|
||||
{'precision': type_.precision, 'scale' : type_.scale})
|
||||
|
||||
def visit_DECIMAL(self, type_):
|
||||
if type_.precision is None:
|
||||
return self._extend_numeric(type_, "DECIMAL")
|
||||
elif type_.scale is None:
|
||||
return self._extend_numeric(type_, "DECIMAL(%(precision)s)" % {'precision': type_.precision})
|
||||
return self._extend_numeric(type_,
|
||||
"DECIMAL(%(precision)s)" %
|
||||
{'precision': type_.precision})
|
||||
else:
|
||||
return self._extend_numeric(type_, "DECIMAL(%(precision)s, %(scale)s)" % {'precision': type_.precision, 'scale' : type_.scale})
|
||||
return self._extend_numeric(type_,
|
||||
"DECIMAL(%(precision)s, %(scale)s)" %
|
||||
{'precision': type_.precision, 'scale' : type_.scale})
|
||||
|
||||
def visit_DOUBLE(self, type_):
|
||||
if type_.precision is not None and type_.scale is not None:
|
||||
@@ -1437,8 +1559,11 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
|
||||
return self._extend_numeric(type_, 'REAL')
|
||||
|
||||
def visit_FLOAT(self, type_):
|
||||
if self._mysql_type(type_) and type_.scale is not None and type_.precision is not None:
|
||||
return self._extend_numeric(type_, "FLOAT(%s, %s)" % (type_.precision, type_.scale))
|
||||
if self._mysql_type(type_) and \
|
||||
type_.scale is not None and \
|
||||
type_.precision is not None:
|
||||
return self._extend_numeric(type_,
|
||||
"FLOAT(%s, %s)" % (type_.precision, type_.scale))
|
||||
elif type_.precision is not None:
|
||||
return self._extend_numeric(type_, "FLOAT(%s)" % (type_.precision,))
|
||||
else:
|
||||
@@ -1446,19 +1571,25 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
|
||||
|
||||
def visit_INTEGER(self, type_):
|
||||
if self._mysql_type(type_) and type_.display_width is not None:
|
||||
return self._extend_numeric(type_, "INTEGER(%(display_width)s)" % {'display_width': type_.display_width})
|
||||
return self._extend_numeric(type_,
|
||||
"INTEGER(%(display_width)s)" %
|
||||
{'display_width': type_.display_width})
|
||||
else:
|
||||
return self._extend_numeric(type_, "INTEGER")
|
||||
|
||||
def visit_BIGINT(self, type_):
|
||||
if self._mysql_type(type_) and type_.display_width is not None:
|
||||
return self._extend_numeric(type_, "BIGINT(%(display_width)s)" % {'display_width': type_.display_width})
|
||||
return self._extend_numeric(type_,
|
||||
"BIGINT(%(display_width)s)" %
|
||||
{'display_width': type_.display_width})
|
||||
else:
|
||||
return self._extend_numeric(type_, "BIGINT")
|
||||
|
||||
def visit_MEDIUMINT(self, type_):
|
||||
if self._mysql_type(type_) and type_.display_width is not None:
|
||||
return self._extend_numeric(type_, "MEDIUMINT(%(display_width)s)" % {'display_width': type_.display_width})
|
||||
return self._extend_numeric(type_,
|
||||
"MEDIUMINT(%(display_width)s)" %
|
||||
{'display_width': type_.display_width})
|
||||
else:
|
||||
return self._extend_numeric(type_, "MEDIUMINT")
|
||||
|
||||
@@ -1470,7 +1601,10 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
|
||||
|
||||
def visit_SMALLINT(self, type_):
|
||||
if self._mysql_type(type_) and type_.display_width is not None:
|
||||
return self._extend_numeric(type_, "SMALLINT(%(display_width)s)" % {'display_width': type_.display_width})
|
||||
return self._extend_numeric(type_,
|
||||
"SMALLINT(%(display_width)s)" %
|
||||
{'display_width': type_.display_width}
|
||||
)
|
||||
else:
|
||||
return self._extend_numeric(type_, "SMALLINT")
|
||||
|
||||
@@ -1517,7 +1651,9 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
|
||||
if type_.length:
|
||||
return self._extend_string(type_, {}, "VARCHAR(%d)" % type_.length)
|
||||
else:
|
||||
raise exc.InvalidRequestError("VARCHAR requires a length when rendered on MySQL")
|
||||
raise exc.CompileError(
|
||||
"VARCHAR requires a length on dialect %s" %
|
||||
self.dialect.name)
|
||||
|
||||
def visit_CHAR(self, type_):
|
||||
if type_.length:
|
||||
@@ -1531,7 +1667,9 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
|
||||
if type_.length:
|
||||
return self._extend_string(type_, {'national':True}, "VARCHAR(%(length)s)" % {'length': type_.length})
|
||||
else:
|
||||
raise exc.InvalidRequestError("NVARCHAR requires a length when rendered on MySQL")
|
||||
raise exc.CompileError(
|
||||
"NVARCHAR requires a length on dialect %s" %
|
||||
self.dialect.name)
|
||||
|
||||
def visit_NCHAR(self, type_):
|
||||
# We'll actually generate the equiv. "NATIONAL CHAR" instead of "NCHAR".
|
||||
@@ -1685,7 +1823,7 @@ class MySQLDialect(default.DefaultDialect):
|
||||
resultset = connection.execute("XA RECOVER")
|
||||
return [row['data'][0:row['gtrid_length']] for row in resultset]
|
||||
|
||||
def is_disconnect(self, e):
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
if isinstance(e, self.dbapi.OperationalError):
|
||||
return self._extract_error_code(e) in \
|
||||
(2006, 2013, 2014, 2045, 2055)
|
||||
@@ -1741,7 +1879,7 @@ class MySQLDialect(default.DefaultDialect):
|
||||
have = rs.rowcount > 0
|
||||
rs.close()
|
||||
return have
|
||||
except exc.SQLError, e:
|
||||
except exc.DBAPIError, e:
|
||||
if self._extract_error_code(e.orig) == 1146:
|
||||
return False
|
||||
raise
|
||||
@@ -1941,17 +2079,6 @@ class MySQLDialect(default.DefaultDialect):
|
||||
sql = parser._describe_to_create(table_name, columns)
|
||||
return parser.parse(sql, charset)
|
||||
|
||||
def _adjust_casing(self, table, charset=None):
|
||||
"""Adjust Table name to the server case sensitivity, if needed."""
|
||||
|
||||
casing = self._server_casing
|
||||
|
||||
# For winxx database hosts. TODO: is this really needed?
|
||||
if casing == 1 and table.name != table.name.lower():
|
||||
table.name = table.name.lower()
|
||||
lc_alias = sa_schema._get_table_key(table.name, table.schema)
|
||||
table.metadata.tables[lc_alias] = table
|
||||
|
||||
def _detect_charset(self, connection):
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -2029,7 +2156,7 @@ class MySQLDialect(default.DefaultDialect):
|
||||
rp = None
|
||||
try:
|
||||
rp = connection.execute(st)
|
||||
except exc.SQLError, e:
|
||||
except exc.DBAPIError, e:
|
||||
if self._extract_error_code(e.orig) == 1146:
|
||||
raise exc.NoSuchTableError(full_name)
|
||||
else:
|
||||
@@ -2053,7 +2180,7 @@ class MySQLDialect(default.DefaultDialect):
|
||||
try:
|
||||
try:
|
||||
rp = connection.execute(st)
|
||||
except exc.SQLError, e:
|
||||
except exc.DBAPIError, e:
|
||||
if self._extract_error_code(e.orig) == 1146:
|
||||
raise exc.NoSuchTableError(full_name)
|
||||
else:
|
||||
@@ -2185,7 +2312,7 @@ class MySQLTableDefinitionParser(object):
|
||||
options.pop(nope, None)
|
||||
|
||||
for opt, val in options.items():
|
||||
state.table_options['mysql_%s' % opt] = val
|
||||
state.table_options['%s_%s' % (self.dialect.name, opt)] = val
|
||||
|
||||
def _parse_column(self, line, state):
|
||||
"""Extract column details.
|
||||
@@ -2432,9 +2559,7 @@ class MySQLTableDefinitionParser(object):
|
||||
# PARTITION
|
||||
#
|
||||
# punt!
|
||||
self._re_partition = _re_compile(
|
||||
r' '
|
||||
r'(?:SUB)?PARTITION')
|
||||
self._re_partition = _re_compile(r'(?:.*)(?:SUB)?PARTITION(?:.*)')
|
||||
|
||||
# Table-level options (COLLATE, ENGINE, etc.)
|
||||
# Do the string options first, since they have quoted strings we need to get rid of.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mysql/mysqlconnector.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -118,7 +118,7 @@ class MySQLDialect_mysqlconnector(MySQLDialect):
|
||||
def _extract_error_code(self, exception):
|
||||
return exception.errno
|
||||
|
||||
def is_disconnect(self, e):
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
errnos = (2006, 2013, 2014, 2045, 2055, 2048)
|
||||
exceptions = (self.dbapi.OperationalError,self.dbapi.InterfaceError)
|
||||
if isinstance(e, exceptions):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mysql/mysqldb.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -19,187 +19,64 @@ Connect string format::
|
||||
|
||||
mysql+mysqldb://<user>:<password>@<host>[:<port>]/<dbname>
|
||||
|
||||
Character Sets
|
||||
--------------
|
||||
Unicode
|
||||
-------
|
||||
|
||||
Many MySQL server installations default to a ``latin1`` encoding for client
|
||||
connections. All data sent through the connection will be converted into
|
||||
``latin1``, even if you have ``utf8`` or another character set on your tables
|
||||
MySQLdb will accommodate Python ``unicode`` objects if the
|
||||
``use_unicode=1`` parameter, or the ``charset`` parameter,
|
||||
is passed as a connection argument.
|
||||
|
||||
Without this setting, many MySQL server installations default to
|
||||
a ``latin1`` encoding for client connections, which has the effect
|
||||
of all data being converted into ``latin1``, even if you have ``utf8``
|
||||
or another character set configured on your tables
|
||||
and columns. With versions 4.1 and higher, you can change the connection
|
||||
character set either through server configuration or by including the
|
||||
``charset`` parameter in the URL used for ``create_engine``. The ``charset``
|
||||
option is passed through to MySQL-Python and has the side-effect of also
|
||||
enabling ``use_unicode`` in the driver by default. For regular encoded
|
||||
strings, also pass ``use_unicode=0`` in the connection arguments::
|
||||
``charset`` parameter. The ``charset``
|
||||
parameter as received by MySQL-Python also has the side-effect of
|
||||
enabling ``use_unicode=1``::
|
||||
|
||||
# set client encoding to utf8; all strings come back as unicode
|
||||
create_engine('mysql+mysqldb:///mydb?charset=utf8')
|
||||
# set client encoding to utf8; all strings come back as unicode
|
||||
create_engine('mysql+mysqldb:///mydb?charset=utf8')
|
||||
|
||||
# set client encoding to utf8; all strings come back as utf8 str
|
||||
create_engine('mysql+mysqldb:///mydb?charset=utf8&use_unicode=0')
|
||||
Manually configuring ``use_unicode=0`` will cause MySQL-python to
|
||||
return encoded strings::
|
||||
|
||||
# set client encoding to utf8; all strings come back as utf8 str
|
||||
create_engine('mysql+mysqldb:///mydb?charset=utf8&use_unicode=0')
|
||||
|
||||
Known Issues
|
||||
-------------
|
||||
|
||||
MySQL-python at least as of version 1.2.2 has a serious memory leak related
|
||||
MySQL-python version 1.2.2 has a serious memory leak related
|
||||
to unicode conversion, a feature which is disabled via ``use_unicode=0``.
|
||||
The recommended connection form with SQLAlchemy is::
|
||||
|
||||
engine = create_engine('mysql://scott:tiger@localhost/test?charset=utf8&use_unicode=0', pool_recycle=3600)
|
||||
|
||||
It is strongly advised to use the latest version of MySQL-Python.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from sqlalchemy.dialects.mysql.base import (MySQLDialect, MySQLExecutionContext,
|
||||
MySQLCompiler, MySQLIdentifierPreparer)
|
||||
from sqlalchemy.engine import base as engine_base, default
|
||||
from sqlalchemy.sql import operators as sql_operators
|
||||
from sqlalchemy import exc, log, schema, sql, types as sqltypes, util
|
||||
from sqlalchemy import processors
|
||||
from sqlalchemy.connectors.mysqldb import (
|
||||
MySQLDBExecutionContext,
|
||||
MySQLDBCompiler,
|
||||
MySQLDBIdentifierPreparer,
|
||||
MySQLDBConnector
|
||||
)
|
||||
|
||||
class MySQLExecutionContext_mysqldb(MySQLExecutionContext):
|
||||
|
||||
@property
|
||||
def rowcount(self):
|
||||
if hasattr(self, '_rowcount'):
|
||||
return self._rowcount
|
||||
else:
|
||||
return self.cursor.rowcount
|
||||
class MySQLExecutionContext_mysqldb(MySQLDBExecutionContext, MySQLExecutionContext):
|
||||
pass
|
||||
|
||||
|
||||
class MySQLCompiler_mysqldb(MySQLCompiler):
|
||||
def visit_mod(self, binary, **kw):
|
||||
return self.process(binary.left) + " %% " + self.process(binary.right)
|
||||
|
||||
def post_process_text(self, text):
|
||||
return text.replace('%', '%%')
|
||||
class MySQLCompiler_mysqldb(MySQLDBCompiler, MySQLCompiler):
|
||||
pass
|
||||
|
||||
|
||||
class MySQLIdentifierPreparer_mysqldb(MySQLIdentifierPreparer):
|
||||
class MySQLIdentifierPreparer_mysqldb(MySQLDBIdentifierPreparer, MySQLIdentifierPreparer):
|
||||
pass
|
||||
|
||||
def _escape_identifier(self, value):
|
||||
value = value.replace(self.escape_quote, self.escape_to_quote)
|
||||
return value.replace("%", "%%")
|
||||
|
||||
class MySQLDialect_mysqldb(MySQLDialect):
|
||||
driver = 'mysqldb'
|
||||
supports_unicode_statements = False
|
||||
supports_sane_rowcount = True
|
||||
supports_sane_multi_rowcount = True
|
||||
|
||||
supports_native_decimal = True
|
||||
|
||||
default_paramstyle = 'format'
|
||||
class MySQLDialect_mysqldb(MySQLDBConnector, MySQLDialect):
|
||||
execution_ctx_cls = MySQLExecutionContext_mysqldb
|
||||
statement_compiler = MySQLCompiler_mysqldb
|
||||
preparer = MySQLIdentifierPreparer_mysqldb
|
||||
|
||||
colspecs = util.update_copy(
|
||||
MySQLDialect.colspecs,
|
||||
{
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def dbapi(cls):
|
||||
return __import__('MySQLdb')
|
||||
|
||||
def do_executemany(self, cursor, statement, parameters, context=None):
|
||||
rowcount = cursor.executemany(statement, parameters)
|
||||
if context is not None:
|
||||
context._rowcount = rowcount
|
||||
|
||||
def create_connect_args(self, url):
|
||||
opts = url.translate_connect_args(database='db', username='user',
|
||||
password='passwd')
|
||||
opts.update(url.query)
|
||||
|
||||
util.coerce_kw_type(opts, 'compress', bool)
|
||||
util.coerce_kw_type(opts, 'connect_timeout', int)
|
||||
util.coerce_kw_type(opts, 'client_flag', int)
|
||||
util.coerce_kw_type(opts, 'local_infile', int)
|
||||
# Note: using either of the below will cause all strings to be returned
|
||||
# as Unicode, both in raw SQL operations and with column types like
|
||||
# String and MSString.
|
||||
util.coerce_kw_type(opts, 'use_unicode', bool)
|
||||
util.coerce_kw_type(opts, 'charset', str)
|
||||
|
||||
# Rich values 'cursorclass' and 'conv' are not supported via
|
||||
# query string.
|
||||
|
||||
ssl = {}
|
||||
for key in ['ssl_ca', 'ssl_key', 'ssl_cert', 'ssl_capath', 'ssl_cipher']:
|
||||
if key in opts:
|
||||
ssl[key[4:]] = opts[key]
|
||||
util.coerce_kw_type(ssl, key[4:], str)
|
||||
del opts[key]
|
||||
if ssl:
|
||||
opts['ssl'] = ssl
|
||||
|
||||
# FOUND_ROWS must be set in CLIENT_FLAGS to enable
|
||||
# supports_sane_rowcount.
|
||||
client_flag = opts.get('client_flag', 0)
|
||||
if self.dbapi is not None:
|
||||
try:
|
||||
from MySQLdb.constants import CLIENT as CLIENT_FLAGS
|
||||
client_flag |= CLIENT_FLAGS.FOUND_ROWS
|
||||
except:
|
||||
pass
|
||||
opts['client_flag'] = client_flag
|
||||
return [[], opts]
|
||||
|
||||
def _get_server_version_info(self, connection):
|
||||
dbapi_con = connection.connection
|
||||
version = []
|
||||
r = re.compile('[.\-]')
|
||||
for n in r.split(dbapi_con.get_server_info()):
|
||||
try:
|
||||
version.append(int(n))
|
||||
except ValueError:
|
||||
version.append(n)
|
||||
return tuple(version)
|
||||
|
||||
def _extract_error_code(self, exception):
|
||||
return exception.args[0]
|
||||
|
||||
def _detect_charset(self, connection):
|
||||
"""Sniff out the character set in use for connection results."""
|
||||
|
||||
# Note: MySQL-python 1.2.1c7 seems to ignore changes made
|
||||
# on a connection via set_character_set()
|
||||
if self.server_version_info < (4, 1, 0):
|
||||
try:
|
||||
return connection.connection.character_set_name()
|
||||
except AttributeError:
|
||||
# < 1.2.1 final MySQL-python drivers have no charset support.
|
||||
# a query is needed.
|
||||
pass
|
||||
|
||||
# Prefer 'character_set_results' for the current connection over the
|
||||
# value in the driver. SET NAMES or individual variable SETs will
|
||||
# change the charset without updating the driver's view of the world.
|
||||
#
|
||||
# If it's decided that issuing that sort of SQL leaves you SOL, then
|
||||
# this can prefer the driver value.
|
||||
rs = connection.execute("SHOW VARIABLES LIKE 'character_set%%'")
|
||||
opts = dict([(row[0], row[1]) for row in self._compat_fetchall(rs)])
|
||||
|
||||
if 'character_set_results' in opts:
|
||||
return opts['character_set_results']
|
||||
try:
|
||||
return connection.connection.character_set_name()
|
||||
except AttributeError:
|
||||
# Still no charset on < 1.2.1 final...
|
||||
if 'character_set' in opts:
|
||||
return opts['character_set']
|
||||
else:
|
||||
util.warn(
|
||||
"Could not detect the connection character set with this "
|
||||
"combination of MySQL server and MySQL-python. "
|
||||
"MySQL-python >= 1.2.2 is recommended. Assuming latin1.")
|
||||
return 'latin1'
|
||||
|
||||
|
||||
dialect = MySQLDialect_mysqldb
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mysql/oursql.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -17,8 +17,8 @@ Connect string format::
|
||||
|
||||
mysql+oursql://<user>:<password>@<host>[:<port>]/<dbname>
|
||||
|
||||
Character Sets
|
||||
--------------
|
||||
Unicode
|
||||
-------
|
||||
|
||||
oursql defaults to using ``utf8`` as the connection charset, but other
|
||||
encodings may be used instead. Like the MySQL-Python driver, unicode support
|
||||
@@ -64,8 +64,6 @@ class MySQLExecutionContext_oursql(MySQLExecutionContext):
|
||||
|
||||
class MySQLDialect_oursql(MySQLDialect):
|
||||
driver = 'oursql'
|
||||
# Py3K
|
||||
# description_encoding = None
|
||||
# Py2K
|
||||
supports_unicode_binds = True
|
||||
supports_unicode_statements = True
|
||||
@@ -107,6 +105,7 @@ class MySQLDialect_oursql(MySQLDialect):
|
||||
# Py3K
|
||||
# charset = self._connection_charset
|
||||
# arg = connection.connection._escape_string(xid.encode(charset)).decode(charset)
|
||||
arg = "'%s'" % arg
|
||||
connection.execution_options(_oursql_plain_query=True).execute(query % arg)
|
||||
|
||||
# Because mysql is bad, these methods have to be
|
||||
@@ -115,23 +114,23 @@ class MySQLDialect_oursql(MySQLDialect):
|
||||
# the parameterized query API, or refuse to be parameterized
|
||||
# in the first place.
|
||||
def do_begin_twophase(self, connection, xid):
|
||||
self._xa_query(connection, 'XA BEGIN "%s"', xid)
|
||||
self._xa_query(connection, 'XA BEGIN %s', xid)
|
||||
|
||||
def do_prepare_twophase(self, connection, xid):
|
||||
self._xa_query(connection, 'XA END "%s"', xid)
|
||||
self._xa_query(connection, 'XA PREPARE "%s"', xid)
|
||||
self._xa_query(connection, 'XA END %s', xid)
|
||||
self._xa_query(connection, 'XA PREPARE %s', xid)
|
||||
|
||||
def do_rollback_twophase(self, connection, xid, is_prepared=True,
|
||||
recover=False):
|
||||
if not is_prepared:
|
||||
self._xa_query(connection, 'XA END "%s"', xid)
|
||||
self._xa_query(connection, 'XA ROLLBACK "%s"', xid)
|
||||
self._xa_query(connection, 'XA END %s', xid)
|
||||
self._xa_query(connection, 'XA ROLLBACK %s', xid)
|
||||
|
||||
def do_commit_twophase(self, connection, xid, is_prepared=True,
|
||||
recover=False):
|
||||
if not is_prepared:
|
||||
self.do_prepare_twophase(connection, xid)
|
||||
self._xa_query(connection, 'XA COMMIT "%s"', xid)
|
||||
self._xa_query(connection, 'XA COMMIT %s', xid)
|
||||
|
||||
# Q: why didn't we need all these "plain_query" overrides earlier ?
|
||||
# am i on a newer/older version of OurSQL ?
|
||||
@@ -195,7 +194,7 @@ class MySQLDialect_oursql(MySQLDialect):
|
||||
execution_options(_oursql_plain_query=True),
|
||||
table, charset, full_name)
|
||||
|
||||
def is_disconnect(self, e):
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
if isinstance(e, self.dbapi.ProgrammingError):
|
||||
return e.errno is None and 'cursor' not in e.args[1] and e.args[1].endswith('closed')
|
||||
else:
|
||||
@@ -222,6 +221,16 @@ class MySQLDialect_oursql(MySQLDialect):
|
||||
# supports_sane_rowcount.
|
||||
opts.setdefault('found_rows', True)
|
||||
|
||||
ssl = {}
|
||||
for key in ['ssl_ca', 'ssl_key', 'ssl_cert',
|
||||
'ssl_capath', 'ssl_cipher']:
|
||||
if key in opts:
|
||||
ssl[key[4:]] = opts[key]
|
||||
util.coerce_kw_type(ssl, key[4:], str)
|
||||
del opts[key]
|
||||
if ssl:
|
||||
opts['ssl'] = ssl
|
||||
|
||||
return [[], opts]
|
||||
|
||||
def _get_server_version_info(self, connection):
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# mysql/pymysql.py
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
"""Support for the MySQL database via the pymysql adapter.
|
||||
|
||||
pymysql is available at:
|
||||
|
||||
http://code.google.com/p/pymysql/
|
||||
|
||||
Connecting
|
||||
----------
|
||||
|
||||
Connect string::
|
||||
|
||||
mysql+pymysql://<username>:<password>@<host>/<dbname>[?<options>]
|
||||
|
||||
MySQL-Python Compatibility
|
||||
--------------------------
|
||||
|
||||
The pymysql DBAPI is a pure Python port of the MySQL-python (MySQLdb) driver,
|
||||
and targets 100% compatibility. Most behavioral notes for MySQL-python apply to
|
||||
the pymysql driver as well.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy.dialects.mysql.mysqldb import MySQLDialect_mysqldb
|
||||
|
||||
class MySQLDialect_pymysql(MySQLDialect_mysqldb):
|
||||
driver = 'pymysql'
|
||||
|
||||
description_encoding = None
|
||||
@classmethod
|
||||
def dbapi(cls):
|
||||
return __import__('pymysql')
|
||||
|
||||
dialect = MySQLDialect_pymysql
|
||||
@@ -1,5 +1,5 @@
|
||||
# mysql/pyodbc.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mysql/zxjdbc.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# oracle/__init__.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# oracle/base.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -22,6 +22,8 @@ affect the behavior of the dialect regardless of driver in use.
|
||||
|
||||
* *optimize_limits* - defaults to ``False``. see the section on LIMIT/OFFSET.
|
||||
|
||||
* *use_binds_for_limits* - defaults to ``True``. see the section on LIMIT/OFFSET.
|
||||
|
||||
Auto Increment Behavior
|
||||
-----------------------
|
||||
|
||||
@@ -74,13 +76,27 @@ requires NLS_LANG to be set.
|
||||
LIMIT/OFFSET Support
|
||||
--------------------
|
||||
|
||||
Oracle has no support for the LIMIT or OFFSET keywords. Whereas previous versions of SQLAlchemy
|
||||
used the "ROW NUMBER OVER..." construct to simulate LIMIT/OFFSET, SQLAlchemy 0.5 now uses
|
||||
a wrapped subquery approach in conjunction with ROWNUM. The exact methodology is taken from
|
||||
http://www.oracle.com/technology/oramag/oracle/06-sep/o56asktom.html . Note that the
|
||||
"FIRST ROWS()" optimization keyword mentioned is not used by default, as the user community felt
|
||||
this was stepping into the bounds of optimization that is better left on the DBA side, but this
|
||||
prefix can be added by enabling the optimize_limits=True flag on create_engine().
|
||||
Oracle has no support for the LIMIT or OFFSET keywords. SQLAlchemy uses
|
||||
a wrapped subquery approach in conjunction with ROWNUM. The exact methodology
|
||||
is taken from
|
||||
http://www.oracle.com/technology/oramag/oracle/06-sep/o56asktom.html .
|
||||
|
||||
There are two options which affect its behavior:
|
||||
|
||||
* the "FIRST ROWS()" optimization keyword is not used by default. To enable the usage of this
|
||||
optimization directive, specify ``optimize_limits=True`` to :func:`.create_engine`.
|
||||
* the values passed for the limit/offset are sent as bound parameters. Some users have observed
|
||||
that Oracle produces a poor query plan when the values are sent as binds and not
|
||||
rendered literally. To render the limit/offset values literally within the SQL
|
||||
statement, specify ``use_binds_for_limits=False`` to :func:`.create_engine`.
|
||||
|
||||
Some users have reported better performance when the entirely different approach of a
|
||||
window query is used, i.e. ROW_NUMBER() OVER (ORDER BY), to provide LIMIT/OFFSET (note
|
||||
that the majority of users don't observe this). To suit this case the
|
||||
method used for LIMIT/OFFSET can be replaced entirely. See the recipe at
|
||||
http://www.sqlalchemy.org/trac/wiki/UsageRecipes/WindowFunctionsByDefault
|
||||
which installs a select compiler that overrides the generation of limit/offset with
|
||||
a window function.
|
||||
|
||||
ON UPDATE CASCADE
|
||||
-----------------
|
||||
@@ -133,24 +149,30 @@ from sqlalchemy import types as sqltypes
|
||||
from sqlalchemy.types import VARCHAR, NVARCHAR, CHAR, DATE, DATETIME, \
|
||||
BLOB, CLOB, TIMESTAMP, FLOAT
|
||||
|
||||
RESERVED_WORDS = set('SHARE RAW DROP BETWEEN FROM DESC OPTION PRIOR LONG THEN '
|
||||
'DEFAULT ALTER IS INTO MINUS INTEGER NUMBER GRANT IDENTIFIED '
|
||||
'ALL TO ORDER ON FLOAT DATE HAVING CLUSTER NOWAIT RESOURCE ANY '
|
||||
'TABLE INDEX FOR UPDATE WHERE CHECK SMALLINT WITH DELETE BY ASC '
|
||||
'REVOKE LIKE SIZE RENAME NOCOMPRESS NULL GROUP VALUES AS IN VIEW '
|
||||
'EXCLUSIVE COMPRESS SYNONYM SELECT INSERT EXISTS NOT TRIGGER '
|
||||
'ELSE CREATE INTERSECT PCTFREE DISTINCT USER CONNECT SET MODE '
|
||||
'OF UNIQUE VARCHAR2 VARCHAR LOCK OR CHAR DECIMAL UNION PUBLIC '
|
||||
'AND START UID COMMENT'.split())
|
||||
RESERVED_WORDS = \
|
||||
set('SHARE RAW DROP BETWEEN FROM DESC OPTION PRIOR LONG THEN '\
|
||||
'DEFAULT ALTER IS INTO MINUS INTEGER NUMBER GRANT IDENTIFIED '\
|
||||
'ALL TO ORDER ON FLOAT DATE HAVING CLUSTER NOWAIT RESOURCE '\
|
||||
'ANY TABLE INDEX FOR UPDATE WHERE CHECK SMALLINT WITH DELETE '\
|
||||
'BY ASC REVOKE LIKE SIZE RENAME NOCOMPRESS NULL GROUP VALUES '\
|
||||
'AS IN VIEW EXCLUSIVE COMPRESS SYNONYM SELECT INSERT EXISTS '\
|
||||
'NOT TRIGGER ELSE CREATE INTERSECT PCTFREE DISTINCT USER '\
|
||||
'CONNECT SET MODE OF UNIQUE VARCHAR2 VARCHAR LOCK OR CHAR '\
|
||||
'DECIMAL UNION PUBLIC AND START UID COMMENT CURRENT'.split())
|
||||
|
||||
class RAW(sqltypes.LargeBinary):
|
||||
pass
|
||||
NO_ARG_FNS = set('UID CURRENT_DATE SYSDATE USER '
|
||||
'CURRENT_TIME CURRENT_TIMESTAMP'.split())
|
||||
|
||||
class RAW(sqltypes._Binary):
|
||||
__visit_name__ = 'RAW'
|
||||
OracleRaw = RAW
|
||||
|
||||
class NCLOB(sqltypes.Text):
|
||||
__visit_name__ = 'NCLOB'
|
||||
|
||||
VARCHAR2 = VARCHAR
|
||||
class VARCHAR2(VARCHAR):
|
||||
__visit_name__ = 'VARCHAR2'
|
||||
|
||||
NVARCHAR2 = NVARCHAR
|
||||
|
||||
class NUMBER(sqltypes.Numeric, sqltypes.Integer):
|
||||
@@ -216,10 +238,6 @@ class INTERVAL(sqltypes.TypeEngine):
|
||||
return INTERVAL(day_precision=interval.day_precision,
|
||||
second_precision=interval.second_precision)
|
||||
|
||||
def adapt(self, impltype):
|
||||
return impltype(day_precision=self.day_precision,
|
||||
second_precision=self.second_precision)
|
||||
|
||||
@property
|
||||
def _type_affinity(self):
|
||||
return sqltypes.Interval
|
||||
@@ -277,9 +295,9 @@ class OracleTypeCompiler(compiler.GenericTypeCompiler):
|
||||
|
||||
def visit_unicode(self, type_):
|
||||
if self.dialect._supports_nchar:
|
||||
return self.visit_NVARCHAR(type_)
|
||||
return self.visit_NVARCHAR2(type_)
|
||||
else:
|
||||
return self.visit_VARCHAR(type_)
|
||||
return self.visit_VARCHAR2(type_)
|
||||
|
||||
def visit_INTERVAL(self, type_):
|
||||
return "INTERVAL DAY%s TO SECOND%s" % (
|
||||
@@ -317,14 +335,27 @@ class OracleTypeCompiler(compiler.GenericTypeCompiler):
|
||||
else:
|
||||
return "%(name)s(%(precision)s, %(scale)s)" % {'name':name,'precision': precision, 'scale' : scale}
|
||||
|
||||
def visit_VARCHAR(self, type_):
|
||||
if self.dialect._supports_char_length:
|
||||
return "VARCHAR(%(length)s CHAR)" % {'length' : type_.length}
|
||||
else:
|
||||
return "VARCHAR(%(length)s)" % {'length' : type_.length}
|
||||
def visit_string(self, type_):
|
||||
return self.visit_VARCHAR2(type_)
|
||||
|
||||
def visit_NVARCHAR(self, type_):
|
||||
return "NVARCHAR2(%(length)s)" % {'length' : type_.length}
|
||||
def visit_VARCHAR2(self, type_):
|
||||
return self._visit_varchar(type_, '', '2')
|
||||
|
||||
def visit_NVARCHAR2(self, type_):
|
||||
return self._visit_varchar(type_, 'N', '2')
|
||||
visit_NVARCHAR = visit_NVARCHAR2
|
||||
|
||||
def visit_VARCHAR(self, type_):
|
||||
return self._visit_varchar(type_, '', '')
|
||||
|
||||
def _visit_varchar(self, type_, n, num):
|
||||
if not n and self.dialect._supports_char_length:
|
||||
return "VARCHAR%(two)s(%(length)s CHAR)" % {
|
||||
'length' : type_.length,
|
||||
'two':num}
|
||||
else:
|
||||
return "%(n)sVARCHAR%(two)s(%(length)s)" % {'length' : type_.length,
|
||||
'two':num, 'n':n}
|
||||
|
||||
def visit_text(self, type_):
|
||||
return self.visit_CLOB(type_)
|
||||
@@ -345,7 +376,10 @@ class OracleTypeCompiler(compiler.GenericTypeCompiler):
|
||||
return self.visit_SMALLINT(type_)
|
||||
|
||||
def visit_RAW(self, type_):
|
||||
return "RAW(%(length)s)" % {'length' : type_.length}
|
||||
if type_.length:
|
||||
return "RAW(%(length)s)" % {'length' : type_.length}
|
||||
else:
|
||||
return "RAW"
|
||||
|
||||
def visit_ROWID(self, type_):
|
||||
return "ROWID"
|
||||
@@ -364,9 +398,9 @@ class OracleCompiler(compiler.SQLCompiler):
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(OracleCompiler, self).__init__(*args, **kwargs)
|
||||
self.__wheres = {}
|
||||
self._quoted_bind_names = {}
|
||||
super(OracleCompiler, self).__init__(*args, **kwargs)
|
||||
|
||||
def visit_mod(self, binary, **kw):
|
||||
return "mod(%s, %s)" % (self.process(binary.left), self.process(binary.right))
|
||||
@@ -386,13 +420,14 @@ class OracleCompiler(compiler.SQLCompiler):
|
||||
)
|
||||
|
||||
def function_argspec(self, fn, **kw):
|
||||
if len(fn.clauses) > 0:
|
||||
if len(fn.clauses) > 0 or fn.name.upper() not in NO_ARG_FNS:
|
||||
return compiler.SQLCompiler.function_argspec(self, fn, **kw)
|
||||
else:
|
||||
return ""
|
||||
|
||||
def default_from(self):
|
||||
"""Called when a ``SELECT`` statement has no froms, and no ``FROM`` clause is to be appended.
|
||||
"""Called when a ``SELECT`` statement has no froms,
|
||||
and no ``FROM`` clause is to be appended.
|
||||
|
||||
The Oracle compiler tacks a "FROM DUAL" to the statement.
|
||||
"""
|
||||
@@ -523,6 +558,8 @@ class OracleCompiler(compiler.SQLCompiler):
|
||||
max_row = select._limit
|
||||
if select._offset is not None:
|
||||
max_row += select._offset
|
||||
if not self.dialect.use_binds_for_limits:
|
||||
max_row = sql.literal_column("%d" % max_row)
|
||||
limitselect.append_whereclause(
|
||||
sql.literal_column("ROWNUM")<=max_row)
|
||||
|
||||
@@ -541,8 +578,11 @@ class OracleCompiler(compiler.SQLCompiler):
|
||||
offsetselect._oracle_visit = True
|
||||
offsetselect._is_wrapper = True
|
||||
|
||||
offset_value = select._offset
|
||||
if not self.dialect.use_binds_for_limits:
|
||||
offset_value = sql.literal_column("%d" % offset_value)
|
||||
offsetselect.append_whereclause(
|
||||
sql.literal_column("ora_rn")>select._offset)
|
||||
sql.literal_column("ora_rn")>offset_value)
|
||||
|
||||
offsetselect.for_update = select.for_update
|
||||
select = offsetselect
|
||||
@@ -597,10 +637,10 @@ class OracleIdentifierPreparer(compiler.IdentifierPreparer):
|
||||
|
||||
|
||||
class OracleExecutionContext(default.DefaultExecutionContext):
|
||||
def fire_sequence(self, seq):
|
||||
return int(self._execute_scalar("SELECT " +
|
||||
def fire_sequence(self, seq, type_):
|
||||
return self._execute_scalar("SELECT " +
|
||||
self.dialect.identifier_preparer.format_sequence(seq) +
|
||||
".nextval FROM DUAL"))
|
||||
".nextval FROM DUAL", type_)
|
||||
|
||||
class OracleDialect(default.DefaultDialect):
|
||||
name = 'oracle'
|
||||
@@ -634,10 +674,12 @@ class OracleDialect(default.DefaultDialect):
|
||||
def __init__(self,
|
||||
use_ansi=True,
|
||||
optimize_limits=False,
|
||||
use_binds_for_limits=True,
|
||||
**kwargs):
|
||||
default.DefaultDialect.__init__(self, **kwargs)
|
||||
self.use_ansi = use_ansi
|
||||
self.optimize_limits = optimize_limits
|
||||
self.use_binds_for_limits = use_binds_for_limits
|
||||
|
||||
def initialize(self, connection):
|
||||
super(OracleDialect, self).initialize(connection)
|
||||
@@ -861,6 +903,7 @@ class OracleDialect(default.DefaultDialect):
|
||||
'type': coltype,
|
||||
'nullable': nullable,
|
||||
'default': default,
|
||||
'autoincrement':default is None
|
||||
}
|
||||
if orig_colname.lower() == orig_colname:
|
||||
cdict['quote'] = True
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# oracle/cx_oracle.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -50,6 +50,8 @@ Unicode
|
||||
cx_oracle 5 fully supports Python unicode objects. SQLAlchemy will pass
|
||||
all unicode strings directly to cx_oracle, and additionally uses an output
|
||||
handler so that all string based result values are returned as unicode as well.
|
||||
Generally, the ``NLS_LANG`` environment variable determines the nature
|
||||
of the encoding to be used.
|
||||
|
||||
Note that this behavior is disabled when Oracle 8 is detected, as it has been
|
||||
observed that issues remain when passing Python unicodes to cx_oracle with Oracle 8.
|
||||
@@ -127,7 +129,8 @@ from sqlalchemy.engine import base
|
||||
from sqlalchemy import types as sqltypes, util, exc, processors
|
||||
from datetime import datetime
|
||||
import random
|
||||
from decimal import Decimal
|
||||
import collections
|
||||
from sqlalchemy.util.compat import decimal
|
||||
import re
|
||||
|
||||
class _OracleNumeric(sqltypes.Numeric):
|
||||
@@ -154,10 +157,10 @@ class _OracleNumeric(sqltypes.Numeric):
|
||||
def to_decimal(value):
|
||||
if value is None:
|
||||
return None
|
||||
elif isinstance(value, Decimal):
|
||||
elif isinstance(value, decimal.Decimal):
|
||||
return value
|
||||
else:
|
||||
return Decimal(fstring % value)
|
||||
return decimal.Decimal(fstring % value)
|
||||
return to_decimal
|
||||
else:
|
||||
if self.precision is None and self.scale is None:
|
||||
@@ -293,10 +296,15 @@ class OracleExecutionContext_cx_oracle(OracleExecutionContext):
|
||||
quoted_bind_names = \
|
||||
getattr(self.compiled, '_quoted_bind_names', None)
|
||||
if quoted_bind_names:
|
||||
if not self.dialect.supports_unicode_binds:
|
||||
if not self.dialect.supports_unicode_statements:
|
||||
# if DBAPI doesn't accept unicode statements,
|
||||
# keys in self.parameters would have been encoded
|
||||
# here. so convert names in quoted_bind_names
|
||||
# to encoded as well.
|
||||
quoted_bind_names = \
|
||||
dict(
|
||||
(fromname, toname.encode(self.dialect.encoding))
|
||||
(fromname.encode(self.dialect.encoding),
|
||||
toname.encode(self.dialect.encoding))
|
||||
for fromname, toname in
|
||||
quoted_bind_names.items()
|
||||
)
|
||||
@@ -333,7 +341,7 @@ class OracleExecutionContext_cx_oracle(OracleExecutionContext):
|
||||
self.out_parameters[name]
|
||||
|
||||
def create_cursor(self):
|
||||
c = self._connection.connection.cursor()
|
||||
c = self._dbapi_connection.cursor()
|
||||
if self.dialect.arraysize:
|
||||
c.arraysize = self.dialect.arraysize
|
||||
|
||||
@@ -423,8 +431,8 @@ class ReturningResultProxy(base.FullyBufferedResultProxy):
|
||||
return ret
|
||||
|
||||
def _buffer_rows(self):
|
||||
return [tuple(self._returning_params["ret_%d" % i]
|
||||
for i, c in enumerate(self._returning_params))]
|
||||
return collections.deque([tuple(self._returning_params["ret_%d" % i]
|
||||
for i, c in enumerate(self._returning_params))])
|
||||
|
||||
class OracleDialect_cx_oracle(OracleDialect):
|
||||
execution_ctx_cls = OracleExecutionContext_cx_oracle
|
||||
@@ -575,15 +583,15 @@ class OracleDialect_cx_oracle(OracleDialect):
|
||||
self._detect_decimal = \
|
||||
lambda value: _detect_decimal(value.replace(char, '.'))
|
||||
self._to_decimal = \
|
||||
lambda value: Decimal(value.replace(char, '.'))
|
||||
lambda value: decimal.Decimal(value.replace(char, '.'))
|
||||
|
||||
def _detect_decimal(self, value):
|
||||
if "." in value:
|
||||
return Decimal(value)
|
||||
return decimal.Decimal(value)
|
||||
else:
|
||||
return int(value)
|
||||
|
||||
_to_decimal = Decimal
|
||||
_to_decimal = decimal.Decimal
|
||||
|
||||
def on_connect(self):
|
||||
if self.cx_oracle_ver < (5,):
|
||||
@@ -679,11 +687,20 @@ class OracleDialect_cx_oracle(OracleDialect):
|
||||
for x in connection.connection.version.split('.')
|
||||
)
|
||||
|
||||
def is_disconnect(self, e):
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
error, = e.args
|
||||
if isinstance(e, self.dbapi.InterfaceError):
|
||||
return "not connected" in str(e)
|
||||
elif hasattr(error, 'code'):
|
||||
# ORA-00028: your session has been killed
|
||||
# ORA-03114: not connected to ORACLE
|
||||
# ORA-03113: end-of-file on communication channel
|
||||
# ORA-03135: connection lost contact
|
||||
# ORA-01033: ORACLE initialization or shutdown in progress
|
||||
# TODO: Others ?
|
||||
return error.code in (28, 3114, 3113, 3135, 1033)
|
||||
else:
|
||||
return "ORA-03114" in str(e) or "ORA-03113" in str(e)
|
||||
return False
|
||||
|
||||
def create_xid(self):
|
||||
"""create a two-phase transaction ID.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# oracle/zxjdbc.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -21,6 +21,7 @@ from sqlalchemy.connectors.zxJDBC import ZxJDBCConnector
|
||||
from sqlalchemy.dialects.oracle.base import OracleCompiler, OracleDialect, OracleExecutionContext
|
||||
from sqlalchemy.engine import base, default
|
||||
from sqlalchemy.sql import expression
|
||||
import collections
|
||||
|
||||
SQLException = zxJDBC = None
|
||||
|
||||
@@ -115,7 +116,7 @@ class OracleExecutionContext_zxjdbc(OracleExecutionContext):
|
||||
return base.ResultProxy(self)
|
||||
|
||||
def create_cursor(self):
|
||||
cursor = self._connection.connection.cursor()
|
||||
cursor = self._dbapi_connection.cursor()
|
||||
cursor.datahandler = self.dialect.DataHandler(cursor.datahandler)
|
||||
return cursor
|
||||
|
||||
@@ -138,7 +139,7 @@ class ReturningResultProxy(base.FullyBufferedResultProxy):
|
||||
return ret
|
||||
|
||||
def _buffer_rows(self):
|
||||
return [self._returning_row]
|
||||
return collections.deque([self._returning_row])
|
||||
|
||||
|
||||
class ReturningParam(object):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# dialects/postgres.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# postgresql/__init__.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# postgresql/base.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -51,6 +51,35 @@ parameter are ``READ_COMMITTED``, ``READ_UNCOMMITTED``, ``REPEATABLE_READ``,
|
||||
and ``SERIALIZABLE``. Note that the psycopg2 dialect does *not* use this
|
||||
technique and uses psycopg2-specific APIs (see that dialect for details).
|
||||
|
||||
Remote / Cross-Schema Table Introspection
|
||||
-----------------------------------------
|
||||
|
||||
Tables can be introspected from any accessible schema, including
|
||||
inter-schema foreign key relationships. However, care must be taken
|
||||
when specifying the "schema" argument for a given :class:`.Table`, when
|
||||
the given schema is also present in PostgreSQL's ``search_path`` variable
|
||||
for the current connection.
|
||||
|
||||
If a FOREIGN KEY constraint reports that the remote table's schema is within
|
||||
the current ``search_path``, the "schema" attribute of the resulting
|
||||
:class:`.Table` will be set to ``None``, unless the actual schema of the
|
||||
remote table matches that of the referencing table, and the "schema" argument
|
||||
was explicitly stated on the referencing table.
|
||||
|
||||
The best practice here is to not use the ``schema`` argument
|
||||
on :class:`.Table` for any schemas that are present in ``search_path``.
|
||||
``search_path`` defaults to "public", but care should be taken
|
||||
to inspect the actual value using::
|
||||
|
||||
SHOW search_path;
|
||||
|
||||
Prior to version 0.7.3, cross-schema foreign keys when the schemas
|
||||
were also in the ``search_path`` could make an incorrect assumption
|
||||
if the schemas were explicitly stated on each :class:`.Table`.
|
||||
|
||||
Background on PG's ``search_path`` is at:
|
||||
http://www.postgresql.org/docs/9.0/static/ddl-schemas.html#DDL-SCHEMAS-PATH
|
||||
|
||||
INSERT/UPDATE...RETURNING
|
||||
-------------------------
|
||||
|
||||
@@ -75,23 +104,64 @@ use the :meth:`._UpdateBase.returning` method on a per-statement basis::
|
||||
where(table.c.name=='foo')
|
||||
print result.fetchall()
|
||||
|
||||
Indexes
|
||||
-------
|
||||
|
||||
PostgreSQL supports partial indexes. To create them pass a postgresql_where
|
||||
option to the Index constructor::
|
||||
.. _postgresql_indexes:
|
||||
|
||||
Postgresql-Specific Index Options
|
||||
---------------------------------
|
||||
|
||||
Several extensions to the :class:`.Index` construct are available, specific
|
||||
to the PostgreSQL dialect.
|
||||
|
||||
Partial Indexes
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
Partial indexes add criterion to the index definition so that the index is
|
||||
applied to a subset of rows. These can be specified on :class:`.Index`
|
||||
using the ``postgresql_where`` keyword argument::
|
||||
|
||||
Index('my_index', my_table.c.id, postgresql_where=tbl.c.value > 10)
|
||||
|
||||
Operator Classes
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
PostgreSQL allows the specification of an *operator class* for each column of
|
||||
an index (see http://www.postgresql.org/docs/8.3/interactive/indexes-opclass.html).
|
||||
The :class:`.Index` construct allows these to be specified via the ``postgresql_ops``
|
||||
keyword argument (new as of SQLAlchemy 0.7.2)::
|
||||
|
||||
Index('my_index', my_table.c.id, my_table.c.data,
|
||||
postgresql_ops={
|
||||
'data': 'text_pattern_ops',
|
||||
'id': 'int4_ops'
|
||||
})
|
||||
|
||||
Note that the keys in the ``postgresql_ops`` dictionary are the "key" name of
|
||||
the :class:`.Column`, i.e. the name used to access it from the ``.c`` collection
|
||||
of :class:`.Table`, which can be configured to be different than the actual
|
||||
name of the column as expressed in the database.
|
||||
|
||||
Index Types
|
||||
^^^^^^^^^^^^
|
||||
|
||||
PostgreSQL provides several index types: B-Tree, Hash, GiST, and GIN, as well as
|
||||
the ability for users to create their own (see
|
||||
http://www.postgresql.org/docs/8.3/static/indexes-types.html). These can be
|
||||
specified on :class:`.Index` using the ``postgresql_using`` keyword argument::
|
||||
|
||||
Index('my_index', my_table.c.data, postgresql_using='gin')
|
||||
|
||||
The value passed to the keyword argument will be simply passed through to the
|
||||
underlying CREATE INDEX command, so it *must* be a valid index type for your
|
||||
version of PostgreSQL.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from sqlalchemy import schema as sa_schema
|
||||
from sqlalchemy import sql, schema, exc, util
|
||||
from sqlalchemy.engine import base, default, reflection
|
||||
from sqlalchemy.engine import default, reflection
|
||||
from sqlalchemy.sql import compiler, expression, util as sql_util
|
||||
from sqlalchemy.sql import operators as sql_operators
|
||||
from sqlalchemy import types as sqltypes
|
||||
|
||||
try:
|
||||
@@ -101,15 +171,30 @@ except ImportError:
|
||||
|
||||
from sqlalchemy.types import INTEGER, BIGINT, SMALLINT, VARCHAR, \
|
||||
CHAR, TEXT, FLOAT, NUMERIC, \
|
||||
DATE, BOOLEAN
|
||||
DATE, BOOLEAN, REAL
|
||||
|
||||
RESERVED_WORDS = set(
|
||||
["all", "analyse", "analyze", "and", "any", "array", "as", "asc",
|
||||
"asymmetric", "both", "case", "cast", "check", "collate", "column",
|
||||
"constraint", "create", "current_catalog", "current_date",
|
||||
"current_role", "current_time", "current_timestamp", "current_user",
|
||||
"default", "deferrable", "desc", "distinct", "do", "else", "end",
|
||||
"except", "false", "fetch", "for", "foreign", "from", "grant", "group",
|
||||
"having", "in", "initially", "intersect", "into", "leading", "limit",
|
||||
"localtime", "localtimestamp", "new", "not", "null", "off", "offset",
|
||||
"old", "on", "only", "or", "order", "placing", "primary", "references",
|
||||
"returning", "select", "session_user", "some", "symmetric", "table",
|
||||
"then", "to", "trailing", "true", "union", "unique", "user", "using",
|
||||
"variadic", "when", "where", "window", "with", "authorization",
|
||||
"between", "binary", "cross", "current_schema", "freeze", "full",
|
||||
"ilike", "inner", "is", "isnull", "join", "left", "like", "natural",
|
||||
"notnull", "outer", "over", "overlaps", "right", "similar", "verbose"
|
||||
])
|
||||
|
||||
_DECIMAL_TYPES = (1231, 1700)
|
||||
_FLOAT_TYPES = (700, 701, 1021, 1022)
|
||||
_INT_TYPES = (20, 21, 23, 26, 1005, 1007, 1016)
|
||||
|
||||
class REAL(sqltypes.Float):
|
||||
__visit_name__ = "REAL"
|
||||
|
||||
class BYTEA(sqltypes.LargeBinary):
|
||||
__visit_name__ = 'BYTEA'
|
||||
|
||||
@@ -133,6 +218,7 @@ class TIMESTAMP(sqltypes.TIMESTAMP):
|
||||
super(TIMESTAMP, self).__init__(timezone=timezone)
|
||||
self.precision = precision
|
||||
|
||||
|
||||
class TIME(sqltypes.TIME):
|
||||
def __init__(self, timezone=False, precision=None):
|
||||
super(TIME, self).__init__(timezone=timezone)
|
||||
@@ -149,9 +235,6 @@ class INTERVAL(sqltypes.TypeEngine):
|
||||
def __init__(self, precision=None):
|
||||
self.precision = precision
|
||||
|
||||
def adapt(self, impltype):
|
||||
return impltype(self.precision)
|
||||
|
||||
@classmethod
|
||||
def _adapt_from_generic_interval(cls, interval):
|
||||
return INTERVAL(precision=interval.second_precision)
|
||||
@@ -164,6 +247,15 @@ PGInterval = INTERVAL
|
||||
|
||||
class BIT(sqltypes.TypeEngine):
|
||||
__visit_name__ = 'BIT'
|
||||
def __init__(self, length=None, varying=False):
|
||||
if not varying:
|
||||
# BIT without VARYING defaults to length 1
|
||||
self.length = length or 1
|
||||
else:
|
||||
# but BIT VARYING can be unlimited-length, so no default
|
||||
self.length = length
|
||||
self.varying = varying
|
||||
|
||||
PGBit = BIT
|
||||
|
||||
class UUID(sqltypes.TypeEngine):
|
||||
@@ -224,15 +316,11 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine):
|
||||
The ARRAY type may not be supported on all DBAPIs.
|
||||
It is known to work on psycopg2 and not pg8000.
|
||||
|
||||
**Note:** be sure to read the notes for
|
||||
:class:`.MutableType` regarding ORM
|
||||
performance implications. The :class:`.ARRAY` type's
|
||||
mutability can be disabled using the "mutable" flag.
|
||||
|
||||
"""
|
||||
__visit_name__ = 'ARRAY'
|
||||
|
||||
def __init__(self, item_type, mutable=True, as_tuple=False):
|
||||
def __init__(self, item_type, mutable=False, as_tuple=False):
|
||||
"""Construct an ARRAY.
|
||||
|
||||
E.g.::
|
||||
@@ -247,14 +335,25 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine):
|
||||
``ARRAY(ARRAY(Integer))`` or such. The type mapping figures out on
|
||||
the fly
|
||||
|
||||
:param mutable=True: Specify whether lists passed to this
|
||||
class should be considered mutable. If so, generic copy operations
|
||||
(typically used by the ORM) will shallow-copy values.
|
||||
:param mutable=False: Specify whether lists passed to this
|
||||
class should be considered mutable - this enables
|
||||
"mutable types" mode in the ORM. Be sure to read the
|
||||
notes for :class:`.MutableType` regarding ORM
|
||||
performance implications (default changed from ``True`` in
|
||||
0.7.0).
|
||||
|
||||
:param as_tuple=False: Specify whether return results should be converted
|
||||
to tuples from lists. DBAPIs such as psycopg2 return lists by default.
|
||||
When tuples are returned, the results are hashable. This flag can only
|
||||
be set to ``True`` when ``mutable`` is set to ``False``. (new in 0.6.5)
|
||||
.. note::
|
||||
|
||||
This functionality is now superseded by the
|
||||
``sqlalchemy.ext.mutable`` extension described in
|
||||
:ref:`mutable_toplevel`.
|
||||
|
||||
:param as_tuple=False: Specify whether return results
|
||||
should be converted to tuples from lists. DBAPIs such
|
||||
as psycopg2 return lists by default. When tuples are
|
||||
returned, the results are hashable. This flag can only
|
||||
be set to ``True`` when ``mutable`` is set to
|
||||
``False``. (new in 0.6.5)
|
||||
|
||||
"""
|
||||
if isinstance(item_type, ARRAY):
|
||||
@@ -284,23 +383,8 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine):
|
||||
def is_mutable(self):
|
||||
return self.mutable
|
||||
|
||||
def dialect_impl(self, dialect, **kwargs):
|
||||
impl = super(ARRAY, self).dialect_impl(dialect, **kwargs)
|
||||
if impl is self:
|
||||
impl = self.__class__.__new__(self.__class__)
|
||||
impl.__dict__.update(self.__dict__)
|
||||
impl.item_type = self.item_type.dialect_impl(dialect)
|
||||
return impl
|
||||
|
||||
def adapt(self, impltype):
|
||||
return impltype(
|
||||
self.item_type,
|
||||
mutable=self.mutable,
|
||||
as_tuple=self.as_tuple
|
||||
)
|
||||
|
||||
def bind_processor(self, dialect):
|
||||
item_proc = self.item_type.bind_processor(dialect)
|
||||
item_proc = self.item_type.dialect_impl(dialect).bind_processor(dialect)
|
||||
if item_proc:
|
||||
def convert_item(item):
|
||||
if isinstance(item, (list, tuple)):
|
||||
@@ -320,7 +404,7 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine):
|
||||
return process
|
||||
|
||||
def result_processor(self, dialect, coltype):
|
||||
item_proc = self.item_type.result_processor(dialect, coltype)
|
||||
item_proc = self.item_type.dialect_impl(dialect).result_processor(dialect, coltype)
|
||||
if item_proc:
|
||||
def convert_item(item):
|
||||
if isinstance(item, list):
|
||||
@@ -350,8 +434,74 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine):
|
||||
PGArray = ARRAY
|
||||
|
||||
class ENUM(sqltypes.Enum):
|
||||
"""Postgresql ENUM type.
|
||||
|
||||
This is a subclass of :class:`.types.Enum` which includes
|
||||
support for PG's ``CREATE TYPE``.
|
||||
|
||||
:class:`~.postgresql.ENUM` is used automatically when
|
||||
using the :class:`.types.Enum` type on PG assuming
|
||||
the ``native_enum`` is left as ``True``. However, the
|
||||
:class:`~.postgresql.ENUM` class can also be instantiated
|
||||
directly in order to access some additional Postgresql-specific
|
||||
options, namely finer control over whether or not
|
||||
``CREATE TYPE`` should be emitted.
|
||||
|
||||
Note that both :class:`.types.Enum` as well as
|
||||
:class:`~.postgresql.ENUM` feature create/drop
|
||||
methods; the base :class:`.types.Enum` type ultimately
|
||||
delegates to the :meth:`~.postgresql.ENUM.create` and
|
||||
:meth:`~.postgresql.ENUM.drop` methods present here.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *enums, **kw):
|
||||
"""Construct an :class:`~.postgresql.ENUM`.
|
||||
|
||||
Arguments are the same as that of
|
||||
:class:`.types.Enum`, but also including
|
||||
the following parameters.
|
||||
|
||||
:param create_type: Defaults to True.
|
||||
Indicates that ``CREATE TYPE`` should be
|
||||
emitted, after optionally checking for the
|
||||
presence of the type, when the parent
|
||||
table is being created; and additionally
|
||||
that ``DROP TYPE`` is called when the table
|
||||
is dropped. When ``False``, no check
|
||||
will be performed and no ``CREATE TYPE``
|
||||
or ``DROP TYPE`` is emitted, unless
|
||||
:meth:`~.postgresql.ENUM.create`
|
||||
or :meth:`~.postgresql.ENUM.drop`
|
||||
are called directly.
|
||||
Setting to ``False`` is helpful
|
||||
when invoking a creation scheme to a SQL file
|
||||
without access to the actual database -
|
||||
the :meth:`~.postgresql.ENUM.create` and
|
||||
:meth:`~.postgresql.ENUM.drop` methods can
|
||||
be used to emit SQL to a target bind.
|
||||
(new in 0.7.4)
|
||||
|
||||
"""
|
||||
self.create_type = kw.pop("create_type", True)
|
||||
super(ENUM, self).__init__(*enums, **kw)
|
||||
|
||||
def create(self, bind=None, checkfirst=True):
|
||||
"""Emit ``CREATE TYPE`` for this
|
||||
:class:`~.postgresql.ENUM`.
|
||||
|
||||
If the underlying dialect does not support
|
||||
Postgresql CREATE TYPE, no action is taken.
|
||||
|
||||
:param bind: a connectable :class:`.Engine`,
|
||||
:class:`.Connection`, or similar object to emit
|
||||
SQL.
|
||||
:param checkfirst: if ``True``, a query against
|
||||
the PG catalog will be first performed to see
|
||||
if the type does not exist already before
|
||||
creating.
|
||||
|
||||
"""
|
||||
if not bind.dialect.supports_native_enum:
|
||||
return
|
||||
|
||||
@@ -360,6 +510,20 @@ class ENUM(sqltypes.Enum):
|
||||
bind.execute(CreateEnumType(self))
|
||||
|
||||
def drop(self, bind=None, checkfirst=True):
|
||||
"""Emit ``DROP TYPE`` for this
|
||||
:class:`~.postgresql.ENUM`.
|
||||
|
||||
If the underlying dialect does not support
|
||||
Postgresql DROP TYPE, no action is taken.
|
||||
|
||||
:param bind: a connectable :class:`.Engine`,
|
||||
:class:`.Connection`, or similar object to emit
|
||||
SQL.
|
||||
:param checkfirst: if ``True``, a query against
|
||||
the PG catalog will be first performed to see
|
||||
if the type actually exists before dropping.
|
||||
|
||||
"""
|
||||
if not bind.dialect.supports_native_enum:
|
||||
return
|
||||
|
||||
@@ -367,15 +531,41 @@ class ENUM(sqltypes.Enum):
|
||||
bind.dialect.has_type(bind, self.name, schema=self.schema):
|
||||
bind.execute(DropEnumType(self))
|
||||
|
||||
def _on_table_create(self, event, target, bind, **kw):
|
||||
self.create(bind=bind, checkfirst=True)
|
||||
def _check_for_name_in_memos(self, checkfirst, kw):
|
||||
"""Look in the 'ddl runner' for 'memos', then
|
||||
note our name in that collection.
|
||||
|
||||
This to ensure a particular named enum is operated
|
||||
upon only once within any kind of create/drop
|
||||
sequence without relying upon "checkfirst".
|
||||
|
||||
def _on_metadata_create(self, event, target, bind, **kw):
|
||||
if self.metadata is not None:
|
||||
self.create(bind=bind, checkfirst=True)
|
||||
"""
|
||||
if not self.create_type:
|
||||
return True
|
||||
if '_ddl_runner' in kw:
|
||||
ddl_runner = kw['_ddl_runner']
|
||||
if '_pg_enums' in ddl_runner.memo:
|
||||
pg_enums = ddl_runner.memo['_pg_enums']
|
||||
else:
|
||||
pg_enums = ddl_runner.memo['_pg_enums'] = set()
|
||||
present = self.name in pg_enums
|
||||
pg_enums.add(self.name)
|
||||
return present
|
||||
else:
|
||||
return False
|
||||
|
||||
def _on_metadata_drop(self, event, target, bind, **kw):
|
||||
self.drop(bind=bind, checkfirst=True)
|
||||
def _on_table_create(self, target, bind, checkfirst, **kw):
|
||||
if not self._check_for_name_in_memos(checkfirst, kw):
|
||||
self.create(bind=bind, checkfirst=checkfirst)
|
||||
|
||||
def _on_metadata_create(self, target, bind, checkfirst, **kw):
|
||||
if self.metadata is not None and \
|
||||
not self._check_for_name_in_memos(checkfirst, kw):
|
||||
self.create(bind=bind, checkfirst=checkfirst)
|
||||
|
||||
def _on_metadata_drop(self, target, bind, checkfirst, **kw):
|
||||
if not self._check_for_name_in_memos(checkfirst, kw):
|
||||
self.drop(bind=bind, checkfirst=checkfirst)
|
||||
|
||||
colspecs = {
|
||||
sqltypes.Interval:INTERVAL,
|
||||
@@ -397,7 +587,8 @@ ischema_names = {
|
||||
'inet': INET,
|
||||
'cidr': CIDR,
|
||||
'uuid': UUID,
|
||||
'bit':BIT,
|
||||
'bit': BIT,
|
||||
'bit varying': BIT,
|
||||
'macaddr': MACADDR,
|
||||
'double precision' : DOUBLE_PRECISION,
|
||||
'timestamp' : TIMESTAMP,
|
||||
@@ -447,19 +638,16 @@ class PGCompiler(compiler.SQLCompiler):
|
||||
return value
|
||||
|
||||
def visit_sequence(self, seq):
|
||||
if seq.optional:
|
||||
return None
|
||||
else:
|
||||
return "nextval('%s')" % self.preparer.format_sequence(seq)
|
||||
return "nextval('%s')" % self.preparer.format_sequence(seq)
|
||||
|
||||
def limit_clause(self, select):
|
||||
text = ""
|
||||
if select._limit is not None:
|
||||
text += " \n LIMIT " + str(select._limit)
|
||||
text += " \n LIMIT " + self.process(sql.literal(select._limit))
|
||||
if select._offset is not None:
|
||||
if select._limit is None:
|
||||
text += " \n LIMIT ALL"
|
||||
text += " OFFSET " + str(select._offset)
|
||||
text += " OFFSET " + self.process(sql.literal(select._offset))
|
||||
return text
|
||||
|
||||
def get_select_precolumns(self, select):
|
||||
@@ -468,11 +656,10 @@ class PGCompiler(compiler.SQLCompiler):
|
||||
return "DISTINCT "
|
||||
elif isinstance(select._distinct, (list, tuple)):
|
||||
return "DISTINCT ON (" + ', '.join(
|
||||
[(isinstance(col, basestring) and col
|
||||
or self.process(col)) for col in select._distinct]
|
||||
[self.process(col) for col in select._distinct]
|
||||
)+ ") "
|
||||
else:
|
||||
return "DISTINCT ON (" + unicode(select._distinct) + ") "
|
||||
return "DISTINCT ON (" + self.process(select._distinct) + ") "
|
||||
else:
|
||||
return ""
|
||||
|
||||
@@ -517,15 +704,18 @@ class PGCompiler(compiler.SQLCompiler):
|
||||
class PGDDLCompiler(compiler.DDLCompiler):
|
||||
def get_column_specification(self, column, **kwargs):
|
||||
colspec = self.preparer.format_column(column)
|
||||
impl_type = column.type.dialect_impl(self.dialect)
|
||||
if column.primary_key and \
|
||||
len(column.foreign_keys)==0 and \
|
||||
column.autoincrement and \
|
||||
isinstance(column.type, sqltypes.Integer) and \
|
||||
not isinstance(column.type, sqltypes.SmallInteger) and \
|
||||
(column.default is None or
|
||||
(isinstance(column.default, schema.Sequence) and
|
||||
column.default.optional)):
|
||||
if isinstance(column.type, sqltypes.BigInteger):
|
||||
column is column.table._autoincrement_column and \
|
||||
not isinstance(impl_type, sqltypes.SmallInteger) and \
|
||||
(
|
||||
column.default is None or
|
||||
(
|
||||
isinstance(column.default, schema.Sequence) and
|
||||
column.default.optional
|
||||
)
|
||||
):
|
||||
if isinstance(impl_type, sqltypes.BigInteger):
|
||||
colspec += " BIGSERIAL"
|
||||
else:
|
||||
colspec += " SERIAL"
|
||||
@@ -560,12 +750,24 @@ class PGDDLCompiler(compiler.DDLCompiler):
|
||||
text = "CREATE "
|
||||
if index.unique:
|
||||
text += "UNIQUE "
|
||||
text += "INDEX %s ON %s (%s)" \
|
||||
% (preparer.quote(
|
||||
self._index_identifier(index.name), index.quote),
|
||||
preparer.format_table(index.table),
|
||||
', '.join([preparer.format_column(c)
|
||||
for c in index.columns]))
|
||||
ops = index.kwargs.get('postgresql_ops', {})
|
||||
text += "INDEX %s ON %s " % (
|
||||
preparer.quote(
|
||||
self._index_identifier(index.name), index.quote),
|
||||
preparer.format_table(index.table)
|
||||
)
|
||||
|
||||
if 'postgresql_using' in index.kwargs:
|
||||
using = index.kwargs['postgresql_using']
|
||||
text += "USING %s " % preparer.quote(using, index.quote)
|
||||
|
||||
text += "(%s)" \
|
||||
% (
|
||||
', '.join([
|
||||
preparer.format_column(c) +
|
||||
(c.key in ops and (' ' + ops[c.key]) or '')
|
||||
for c in index.columns])
|
||||
)
|
||||
|
||||
if "postgres_where" in index.kwargs:
|
||||
whereclause = index.kwargs['postgres_where']
|
||||
@@ -639,7 +841,13 @@ class PGTypeCompiler(compiler.GenericTypeCompiler):
|
||||
return "INTERVAL"
|
||||
|
||||
def visit_BIT(self, type_):
|
||||
return "BIT"
|
||||
if type_.varying:
|
||||
compiled = "BIT VARYING"
|
||||
if type_.length is not None:
|
||||
compiled += "(%d)" % type_.length
|
||||
else:
|
||||
compiled = "BIT(%d)" % type_.length
|
||||
return compiled
|
||||
|
||||
def visit_UUID(self, type_):
|
||||
return "UUID"
|
||||
@@ -650,14 +858,14 @@ class PGTypeCompiler(compiler.GenericTypeCompiler):
|
||||
def visit_BYTEA(self, type_):
|
||||
return "BYTEA"
|
||||
|
||||
def visit_REAL(self, type_):
|
||||
return "REAL"
|
||||
|
||||
def visit_ARRAY(self, type_):
|
||||
return self.process(type_.item_type) + '[]'
|
||||
|
||||
|
||||
class PGIdentifierPreparer(compiler.IdentifierPreparer):
|
||||
|
||||
reserved_words = RESERVED_WORDS
|
||||
|
||||
def _unquote_identifier(self, value):
|
||||
if value[0] == self.initial_quote:
|
||||
value = value[1:-1].\
|
||||
@@ -666,7 +874,7 @@ class PGIdentifierPreparer(compiler.IdentifierPreparer):
|
||||
|
||||
def format_type(self, type_, use_schema=True):
|
||||
if not type_.name:
|
||||
raise exc.ArgumentError("Postgresql ENUM type requires a name.")
|
||||
raise exc.CompileError("Postgresql ENUM type requires a name.")
|
||||
|
||||
name = self.quote(type_.name, type_.quote)
|
||||
if not self.omit_schema and use_schema and type_.schema is not None:
|
||||
@@ -691,40 +899,44 @@ class DropEnumType(schema._CreateDropBase):
|
||||
__visit_name__ = "drop_enum_type"
|
||||
|
||||
class PGExecutionContext(default.DefaultExecutionContext):
|
||||
def fire_sequence(self, seq):
|
||||
if not seq.optional:
|
||||
return self._execute_scalar(("select nextval('%s')" % \
|
||||
self.dialect.identifier_preparer.format_sequence(seq)))
|
||||
else:
|
||||
return None
|
||||
def fire_sequence(self, seq, type_):
|
||||
return self._execute_scalar(("select nextval('%s')" % \
|
||||
self.dialect.identifier_preparer.format_sequence(seq)), type_)
|
||||
|
||||
def get_insert_default(self, column):
|
||||
if column.primary_key:
|
||||
if (isinstance(column.server_default, schema.DefaultClause) and
|
||||
column.server_default.arg is not None):
|
||||
if column.primary_key and column is column.table._autoincrement_column:
|
||||
if column.server_default and column.server_default.has_argument:
|
||||
|
||||
# pre-execute passive defaults on primary key columns
|
||||
return self._execute_scalar("select %s" %
|
||||
column.server_default.arg)
|
||||
column.server_default.arg, column.type)
|
||||
|
||||
elif column is column.table._autoincrement_column \
|
||||
and (column.default is None or
|
||||
(isinstance(column.default, schema.Sequence) and
|
||||
elif (column.default is None or
|
||||
(column.default.is_sequence and
|
||||
column.default.optional)):
|
||||
|
||||
# execute the sequence associated with a SERIAL primary
|
||||
# key column. for non-primary-key SERIAL, the ID just
|
||||
# generates server side.
|
||||
|
||||
try:
|
||||
seq_name = column._postgresql_seq_name
|
||||
except AttributeError:
|
||||
tab = column.table.name
|
||||
col = column.name
|
||||
tab = tab[0:29 + max(0, (29 - len(col)))]
|
||||
col = col[0:29 + max(0, (29 - len(tab)))]
|
||||
column._postgresql_seq_name = seq_name = "%s_%s_seq" % (tab, col)
|
||||
|
||||
sch = column.table.schema
|
||||
|
||||
if sch is not None:
|
||||
exc = "select nextval('\"%s\".\"%s_%s_seq\"')" % \
|
||||
(sch, column.table.name, column.name)
|
||||
exc = "select nextval('\"%s\".\"%s\"')" % \
|
||||
(sch, seq_name)
|
||||
else:
|
||||
exc = "select nextval('\"%s_%s_seq\"')" % \
|
||||
(column.table.name, column.name)
|
||||
exc = "select nextval('\"%s\"')" % \
|
||||
(seq_name, )
|
||||
|
||||
return self._execute_scalar(exc)
|
||||
return self._execute_scalar(exc, column.type)
|
||||
|
||||
return super(PGExecutionContext, self).get_insert_default(column)
|
||||
|
||||
@@ -778,16 +990,36 @@ class PGDialect(default.DefaultDialect):
|
||||
def on_connect(self):
|
||||
if self.isolation_level is not None:
|
||||
def connect(conn):
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SET SESSION CHARACTERISTICS AS TRANSACTION "
|
||||
"ISOLATION LEVEL %s" % self.isolation_level)
|
||||
cursor.execute("COMMIT")
|
||||
cursor.close()
|
||||
self.set_isolation_level(conn, self.isolation_level)
|
||||
return connect
|
||||
else:
|
||||
return None
|
||||
|
||||
_isolation_lookup = set(['SERIALIZABLE',
|
||||
'READ UNCOMMITTED', 'READ COMMITTED', 'REPEATABLE READ'])
|
||||
|
||||
def set_isolation_level(self, connection, level):
|
||||
level = level.replace('_', ' ')
|
||||
if level not in self._isolation_lookup:
|
||||
raise exc.ArgumentError(
|
||||
"Invalid value '%s' for isolation_level. "
|
||||
"Valid isolation levels for %s are %s" %
|
||||
(level, self.name, ", ".join(self._isolation_lookup))
|
||||
)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(
|
||||
"SET SESSION CHARACTERISTICS AS TRANSACTION "
|
||||
"ISOLATION LEVEL %s" % level)
|
||||
cursor.execute("COMMIT")
|
||||
cursor.close()
|
||||
|
||||
def get_isolation_level(self, connection):
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('show transaction isolation level')
|
||||
val = cursor.fetchone()[0]
|
||||
cursor.close()
|
||||
return val.upper()
|
||||
|
||||
def do_begin_twophase(self, connection, xid):
|
||||
self.do_begin(connection.connection)
|
||||
|
||||
@@ -828,6 +1060,19 @@ class PGDialect(default.DefaultDialect):
|
||||
def _get_default_schema_name(self, connection):
|
||||
return connection.scalar("select current_schema()")
|
||||
|
||||
def has_schema(self, connection, schema):
|
||||
cursor = connection.execute(
|
||||
sql.text(
|
||||
"select nspname from pg_namespace where lower(nspname)=:schema",
|
||||
bindparams=[
|
||||
sql.bindparam(
|
||||
'schema', unicode(schema.lower()),
|
||||
type_=sqltypes.Unicode)]
|
||||
)
|
||||
)
|
||||
|
||||
return bool(cursor.first())
|
||||
|
||||
def has_table(self, connection, table_name, schema=None):
|
||||
# seems like case gets folded in pg_class...
|
||||
if schema is None:
|
||||
@@ -835,9 +1080,9 @@ class PGDialect(default.DefaultDialect):
|
||||
sql.text(
|
||||
"select relname from pg_class c join pg_namespace n on "
|
||||
"n.oid=c.relnamespace where n.nspname=current_schema() and "
|
||||
"lower(relname)=:name",
|
||||
"relname=:name",
|
||||
bindparams=[
|
||||
sql.bindparam('name', unicode(table_name.lower()),
|
||||
sql.bindparam('name', unicode(table_name),
|
||||
type_=sqltypes.Unicode)]
|
||||
)
|
||||
)
|
||||
@@ -846,10 +1091,10 @@ class PGDialect(default.DefaultDialect):
|
||||
sql.text(
|
||||
"select relname from pg_class c join pg_namespace n on "
|
||||
"n.oid=c.relnamespace where n.nspname=:schema and "
|
||||
"lower(relname)=:name",
|
||||
"relname=:name",
|
||||
bindparams=[
|
||||
sql.bindparam('name',
|
||||
unicode(table_name.lower()), type_=sqltypes.Unicode),
|
||||
unicode(table_name), type_=sqltypes.Unicode),
|
||||
sql.bindparam('schema',
|
||||
unicode(schema), type_=sqltypes.Unicode)]
|
||||
)
|
||||
@@ -863,9 +1108,9 @@ class PGDialect(default.DefaultDialect):
|
||||
"SELECT relname FROM pg_class c join pg_namespace n on "
|
||||
"n.oid=c.relnamespace where relkind='S' and "
|
||||
"n.nspname=current_schema() "
|
||||
"and lower(relname)=:name",
|
||||
"and relname=:name",
|
||||
bindparams=[
|
||||
sql.bindparam('name', unicode(sequence_name.lower()),
|
||||
sql.bindparam('name', unicode(sequence_name),
|
||||
type_=sqltypes.Unicode)
|
||||
]
|
||||
)
|
||||
@@ -875,9 +1120,9 @@ class PGDialect(default.DefaultDialect):
|
||||
sql.text(
|
||||
"SELECT relname FROM pg_class c join pg_namespace n on "
|
||||
"n.oid=c.relnamespace where relkind='S' and "
|
||||
"n.nspname=:schema and lower(relname)=:name",
|
||||
"n.nspname=:schema and relname=:name",
|
||||
bindparams=[
|
||||
sql.bindparam('name', unicode(sequence_name.lower()),
|
||||
sql.bindparam('name', unicode(sequence_name),
|
||||
type_=sqltypes.Unicode),
|
||||
sql.bindparam('schema',
|
||||
unicode(schema), type_=sqltypes.Unicode)
|
||||
@@ -1084,6 +1329,7 @@ class PGDialect(default.DefaultDialect):
|
||||
if charlen:
|
||||
charlen = charlen.group(1)
|
||||
kwargs = {}
|
||||
args = None
|
||||
|
||||
if attype == 'numeric':
|
||||
if charlen:
|
||||
@@ -1094,7 +1340,7 @@ class PGDialect(default.DefaultDialect):
|
||||
elif attype == 'double precision':
|
||||
args = (53, )
|
||||
elif attype == 'integer':
|
||||
args = (32, 0)
|
||||
args = ()
|
||||
elif attype in ('timestamp with time zone',
|
||||
'time with time zone'):
|
||||
kwargs['timezone'] = True
|
||||
@@ -1107,6 +1353,12 @@ class PGDialect(default.DefaultDialect):
|
||||
if charlen:
|
||||
kwargs['precision'] = int(charlen)
|
||||
args = ()
|
||||
elif attype == 'bit varying':
|
||||
kwargs['varying'] = True
|
||||
if charlen:
|
||||
args = (int(charlen),)
|
||||
else:
|
||||
args = ()
|
||||
elif attype in ('interval','interval year to month',
|
||||
'interval day to second'):
|
||||
if charlen:
|
||||
@@ -1177,13 +1429,19 @@ class PGDialect(default.DefaultDialect):
|
||||
def get_primary_keys(self, connection, table_name, schema=None, **kw):
|
||||
table_oid = self.get_table_oid(connection, table_name, schema,
|
||||
info_cache=kw.get('info_cache'))
|
||||
|
||||
PK_SQL = """
|
||||
SELECT attname FROM pg_attribute
|
||||
WHERE attrelid = (
|
||||
SELECT indexrelid FROM pg_index i
|
||||
WHERE i.indrelid = :table_oid
|
||||
AND i.indisprimary = 't')
|
||||
ORDER BY attnum
|
||||
SELECT a.attname
|
||||
FROM
|
||||
pg_class t
|
||||
join pg_index ix on t.oid = ix.indrelid
|
||||
join pg_attribute a
|
||||
on t.oid=a.attrelid and a.attnum=ANY(ix.indkey)
|
||||
WHERE
|
||||
t.oid = :table_oid and
|
||||
ix.indisprimary = 't'
|
||||
ORDER BY
|
||||
a.attnum
|
||||
"""
|
||||
t = sql.text(PK_SQL, typemap={'attname':sqltypes.Unicode})
|
||||
c = connection.execute(t, table_oid=table_oid)
|
||||
@@ -1217,10 +1475,19 @@ class PGDialect(default.DefaultDialect):
|
||||
preparer = self.identifier_preparer
|
||||
table_oid = self.get_table_oid(connection, table_name, schema,
|
||||
info_cache=kw.get('info_cache'))
|
||||
|
||||
FK_SQL = """
|
||||
SELECT conname, pg_catalog.pg_get_constraintdef(oid, true) as condef
|
||||
FROM pg_catalog.pg_constraint r
|
||||
WHERE r.conrelid = :table AND r.contype = 'f'
|
||||
SELECT r.conname,
|
||||
pg_catalog.pg_get_constraintdef(r.oid, true) as condef,
|
||||
n.nspname as conschema
|
||||
FROM pg_catalog.pg_constraint r,
|
||||
pg_namespace n,
|
||||
pg_class c
|
||||
|
||||
WHERE r.conrelid = :table AND
|
||||
r.contype = 'f' AND
|
||||
c.oid = confrelid AND
|
||||
n.oid = c.relnamespace
|
||||
ORDER BY 1
|
||||
"""
|
||||
|
||||
@@ -1229,20 +1496,24 @@ class PGDialect(default.DefaultDialect):
|
||||
'condef':sqltypes.Unicode})
|
||||
c = connection.execute(t, table=table_oid)
|
||||
fkeys = []
|
||||
for conname, condef in c.fetchall():
|
||||
for conname, condef, conschema in c.fetchall():
|
||||
m = re.search('FOREIGN KEY \((.*?)\) REFERENCES '
|
||||
'(?:(.*?)\.)?(.*?)\((.*?)\)', condef).groups()
|
||||
constrained_columns, referred_schema, \
|
||||
referred_table, referred_columns = m
|
||||
constrained_columns = [preparer._unquote_identifier(x)
|
||||
for x in re.split(r'\s*,\s*', constrained_columns)]
|
||||
|
||||
if referred_schema:
|
||||
referred_schema =\
|
||||
preparer._unquote_identifier(referred_schema)
|
||||
elif schema is not None and schema == self.default_schema_name:
|
||||
# no schema (i.e. its the default schema), and the table we're
|
||||
# reflecting has the default schema explicit, then use that.
|
||||
# i.e. try to use the user's conventions
|
||||
elif schema is not None and schema == conschema:
|
||||
# no schema was returned by pg_get_constraintdef(). This
|
||||
# means the schema is in the search path. We will leave
|
||||
# it as None, unless the actual schema, which we pull out
|
||||
# from pg_namespace even though pg_get_constraintdef() doesn't
|
||||
# want to give it to us, matches that of the referencing table,
|
||||
# and an explicit schema was given for the referencing table.
|
||||
referred_schema = schema
|
||||
referred_table = preparer._unquote_identifier(referred_table)
|
||||
referred_columns = [preparer._unquote_identifier(x)
|
||||
@@ -1261,16 +1532,31 @@ class PGDialect(default.DefaultDialect):
|
||||
def get_indexes(self, connection, table_name, schema, **kw):
|
||||
table_oid = self.get_table_oid(connection, table_name, schema,
|
||||
info_cache=kw.get('info_cache'))
|
||||
|
||||
IDX_SQL = """
|
||||
SELECT c.relname, i.indisunique, i.indexprs, i.indpred,
|
||||
a.attname
|
||||
FROM pg_index i, pg_class c, pg_attribute a
|
||||
WHERE i.indrelid = :table_oid AND i.indexrelid = c.oid
|
||||
AND a.attrelid = i.indexrelid AND i.indisprimary = 'f'
|
||||
ORDER BY c.relname, a.attnum
|
||||
SELECT
|
||||
i.relname as relname,
|
||||
ix.indisunique, ix.indexprs, ix.indpred,
|
||||
a.attname
|
||||
FROM
|
||||
pg_class t
|
||||
join pg_index ix on t.oid = ix.indrelid
|
||||
join pg_class i on i.oid=ix.indexrelid
|
||||
left outer join
|
||||
pg_attribute a
|
||||
on t.oid=a.attrelid and a.attnum=ANY(ix.indkey)
|
||||
WHERE
|
||||
t.relkind = 'r'
|
||||
and t.oid = :table_oid
|
||||
and ix.indisprimary = 'f'
|
||||
ORDER BY
|
||||
t.relname,
|
||||
i.relname
|
||||
"""
|
||||
|
||||
t = sql.text(IDX_SQL, typemap={'attname':sqltypes.Unicode})
|
||||
c = connection.execute(t, table_oid=table_oid)
|
||||
|
||||
index_names = {}
|
||||
indexes = []
|
||||
sv_idx_name = None
|
||||
@@ -1296,7 +1582,8 @@ class PGDialect(default.DefaultDialect):
|
||||
indexes.append(index_d)
|
||||
index_names[idx_name] = index_d
|
||||
index_d['name'] = idx_name
|
||||
index_d['column_names'].append(col)
|
||||
if col is not None:
|
||||
index_d['column_names'].append(col)
|
||||
index_d['unique'] = unique
|
||||
return indexes
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# postgresql/pg8000.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -27,10 +27,8 @@ Passing data from/to the Interval type is not supported as of
|
||||
yet.
|
||||
|
||||
"""
|
||||
import decimal
|
||||
|
||||
from sqlalchemy.engine import default
|
||||
from sqlalchemy import util, exc
|
||||
from sqlalchemy.util.compat import decimal
|
||||
from sqlalchemy import processors
|
||||
from sqlalchemy import types as sqltypes
|
||||
from sqlalchemy.dialects.postgresql.base import PGDialect, \
|
||||
@@ -58,6 +56,11 @@ class _PGNumeric(sqltypes.Numeric):
|
||||
raise exc.InvalidRequestError(
|
||||
"Unknown PG numeric type: %d" % coltype)
|
||||
|
||||
|
||||
class _PGNumericNoBind(_PGNumeric):
|
||||
def bind_processor(self, dialect):
|
||||
return None
|
||||
|
||||
class PGExecutionContext_pg8000(PGExecutionContext):
|
||||
pass
|
||||
|
||||
@@ -91,11 +94,13 @@ class PGDialect_pg8000(PGDialect):
|
||||
execution_ctx_cls = PGExecutionContext_pg8000
|
||||
statement_compiler = PGCompiler_pg8000
|
||||
preparer = PGIdentifierPreparer_pg8000
|
||||
description_encoding = 'use_encoding'
|
||||
|
||||
colspecs = util.update_copy(
|
||||
PGDialect.colspecs,
|
||||
{
|
||||
sqltypes.Numeric : _PGNumeric,
|
||||
sqltypes.Numeric : _PGNumericNoBind,
|
||||
sqltypes.Float : _PGNumeric
|
||||
}
|
||||
)
|
||||
|
||||
@@ -110,7 +115,7 @@ class PGDialect_pg8000(PGDialect):
|
||||
opts.update(url.query)
|
||||
return ([], opts)
|
||||
|
||||
def is_disconnect(self, e):
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
return "connection is closed" in str(e)
|
||||
|
||||
dialect = PGDialect_pg8000
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# postgresql/psycopg2.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -9,31 +9,12 @@
|
||||
Driver
|
||||
------
|
||||
|
||||
The psycopg2 driver is supported, available at http://pypi.python.org/pypi/psycopg2/ .
|
||||
The psycopg2 driver is available at http://pypi.python.org/pypi/psycopg2/ .
|
||||
The dialect has several behaviors which are specifically tailored towards compatibility
|
||||
with this module.
|
||||
|
||||
Note that psycopg1 is **not** supported.
|
||||
|
||||
Unicode
|
||||
-------
|
||||
|
||||
By default, the Psycopg2 driver uses the ``psycopg2.extensions.UNICODE``
|
||||
extension, such that the DBAPI receives and returns all strings as Python
|
||||
Unicode objects directly - SQLAlchemy passes these values through without
|
||||
change. Note that this setting requires that the PG client encoding be set to
|
||||
one which can accomodate the kind of character data being passed - typically
|
||||
``utf-8``. If the Postgresql database is configured for ``SQL_ASCII``
|
||||
encoding, which is often the default for PG installations, it may be necessary
|
||||
for non-ascii strings to be encoded into a specific encoding before being
|
||||
passed to the DBAPI. If changing the database's client encoding setting is not
|
||||
an option, specify ``use_native_unicode=False`` as a keyword argument to
|
||||
``create_engine()``, and take note of the ``encoding`` setting as well, which
|
||||
also defaults to ``utf-8``. Note that disabling "native unicode" mode has a
|
||||
slight performance penalty, as SQLAlchemy now must translate unicode strings
|
||||
to/from an encoding such as utf-8, a task that is handled more efficiently
|
||||
within the Psycopg2 driver natively.
|
||||
|
||||
Connecting
|
||||
----------
|
||||
|
||||
@@ -46,15 +27,71 @@ psycopg2-specific keyword arguments which are accepted by
|
||||
* *server_side_cursors* - Enable the usage of "server side cursors" for SQL
|
||||
statements which support this feature. What this essentially means from a
|
||||
psycopg2 point of view is that the cursor is created using a name, e.g.
|
||||
`connection.cursor('some name')`, which has the effect that result rows are
|
||||
``connection.cursor('some name')``, which has the effect that result rows are
|
||||
not immediately pre-fetched and buffered after statement execution, but are
|
||||
instead left on the server and only retrieved as needed. SQLAlchemy's
|
||||
:class:`~sqlalchemy.engine.base.ResultProxy` uses special row-buffering
|
||||
behavior when this feature is enabled, such that groups of 100 rows at a
|
||||
time are fetched over the wire to reduce conversational overhead.
|
||||
Note that the ``stream_results=True`` execution option is a more targeted
|
||||
way of enabling this mode on a per-execution basis.
|
||||
* *use_native_unicode* - Enable the usage of Psycopg2 "native unicode" mode
|
||||
per connection. True by default.
|
||||
|
||||
Per-Statement/Connection Execution Options
|
||||
-------------------------------------------
|
||||
|
||||
The following DBAPI-specific options are respected when used with
|
||||
:meth:`.Connection.execution_options`, :meth:`.Executable.execution_options`,
|
||||
:meth:`.Query.execution_options`, in addition to those not specific to DBAPIs:
|
||||
|
||||
* isolation_level - Set the transaction isolation level for the lifespan of a
|
||||
:class:`.Connection` (can only be set on a connection, not a statement or query).
|
||||
This includes the options ``SERIALIZABLE``, ``READ COMMITTED``,
|
||||
``READ UNCOMMITTED`` and ``REPEATABLE READ``.
|
||||
* stream_results - Enable or disable usage of server side cursors.
|
||||
If ``None`` or not set, the ``server_side_cursors`` option of the :class:`.Engine` is used.
|
||||
|
||||
Unicode
|
||||
-------
|
||||
|
||||
By default, the psycopg2 driver uses the ``psycopg2.extensions.UNICODE``
|
||||
extension, such that the DBAPI receives and returns all strings as Python
|
||||
Unicode objects directly - SQLAlchemy passes these values through without
|
||||
change. Psycopg2 here will encode/decode string values based on the
|
||||
current "client encoding" setting; by default this is the value in
|
||||
the ``postgresql.conf`` file, which often defaults to ``SQL_ASCII``.
|
||||
Typically, this can be changed to ``utf-8``, as a more useful default::
|
||||
|
||||
#client_encoding = sql_ascii # actually, defaults to database
|
||||
# encoding
|
||||
client_encoding = utf8
|
||||
|
||||
A second way to affect the client encoding is to set it within Psycopg2
|
||||
locally. SQLAlchemy will call psycopg2's ``set_client_encoding()``
|
||||
method (see: http://initd.org/psycopg/docs/connection.html#connection.set_client_encoding)
|
||||
on all new connections based on the value passed to
|
||||
:func:`.create_engine` using the ``client_encoding`` parameter::
|
||||
|
||||
engine = create_engine("postgresql://user:pass@host/dbname", client_encoding='utf8')
|
||||
|
||||
This overrides the encoding specified in the Postgresql client configuration.
|
||||
The psycopg2-specific ``client_encoding`` parameter to :func:`.create_engine` is new as of
|
||||
SQLAlchemy 0.7.3.
|
||||
|
||||
SQLAlchemy can also be instructed to skip the usage of the psycopg2
|
||||
``UNICODE`` extension and to instead utilize it's own unicode encode/decode
|
||||
services, which are normally reserved only for those DBAPIs that don't
|
||||
fully support unicode directly. Passing ``use_native_unicode=False``
|
||||
to :func:`.create_engine` will disable usage of ``psycopg2.extensions.UNICODE``.
|
||||
SQLAlchemy will instead encode data itself into Python bytestrings on the way
|
||||
in and coerce from bytes on the way back,
|
||||
using the value of the :func:`.create_engine` ``encoding`` parameter, which
|
||||
defaults to ``utf-8``.
|
||||
SQLAlchemy's own unicode encode/decode functionality is steadily becoming
|
||||
obsolete as more DBAPIs support unicode fully along with the approach of
|
||||
Python 3; in modern usage psycopg2 should be relied upon to handle unicode.
|
||||
|
||||
Transactions
|
||||
------------
|
||||
|
||||
@@ -79,27 +116,16 @@ The psycopg2 dialect will log Postgresql NOTICE messages via the
|
||||
logging.getLogger('sqlalchemy.dialects.postgresql').setLevel(logging.INFO)
|
||||
|
||||
|
||||
Per-Statement Execution Options
|
||||
-------------------------------
|
||||
|
||||
The following per-statement execution options are respected:
|
||||
|
||||
* *stream_results* - Enable or disable usage of server side cursors for the SELECT-statement.
|
||||
If *None* or not set, the *server_side_cursors* option of the connection is used. If
|
||||
auto-commit is enabled, the option is ignored.
|
||||
|
||||
"""
|
||||
|
||||
import random
|
||||
import re
|
||||
import decimal
|
||||
import logging
|
||||
|
||||
from sqlalchemy import util, exc
|
||||
from sqlalchemy.util.compat import decimal
|
||||
from sqlalchemy import processors
|
||||
from sqlalchemy.engine import base, default
|
||||
from sqlalchemy.engine import base
|
||||
from sqlalchemy.sql import expression
|
||||
from sqlalchemy.sql import operators as sql_operators
|
||||
from sqlalchemy import types as sqltypes
|
||||
from sqlalchemy.dialects.postgresql.base import PGDialect, PGCompiler, \
|
||||
PGIdentifierPreparer, PGExecutionContext, \
|
||||
@@ -137,17 +163,21 @@ class _PGNumeric(sqltypes.Numeric):
|
||||
class _PGEnum(ENUM):
|
||||
def __init__(self, *arg, **kw):
|
||||
super(_PGEnum, self).__init__(*arg, **kw)
|
||||
# Py2K
|
||||
if self.convert_unicode:
|
||||
self.convert_unicode = "force"
|
||||
# end Py2K
|
||||
|
||||
class _PGArray(ARRAY):
|
||||
def __init__(self, *arg, **kw):
|
||||
super(_PGArray, self).__init__(*arg, **kw)
|
||||
# Py2K
|
||||
# FIXME: this check won't work for setups that
|
||||
# have convert_unicode only on their create_engine().
|
||||
if isinstance(self.item_type, sqltypes.String) and \
|
||||
self.item_type.convert_unicode:
|
||||
self.item_type.convert_unicode = "force"
|
||||
# end Py2K
|
||||
|
||||
# When we're handed literal SQL, ensure it's a SELECT-query. Since
|
||||
# 8.3, combining cursors and "FOR UPDATE" has been fine.
|
||||
@@ -155,6 +185,8 @@ SERVER_SIDE_CURSOR_RE = re.compile(
|
||||
r'\s*SELECT',
|
||||
re.I | re.UNICODE)
|
||||
|
||||
_server_side_id = util.counter()
|
||||
|
||||
class PGExecutionContext_psycopg2(PGExecutionContext):
|
||||
def create_cursor(self):
|
||||
# TODO: coverage for server side cursors + select.for_update()
|
||||
@@ -177,12 +209,13 @@ class PGExecutionContext_psycopg2(PGExecutionContext):
|
||||
if is_server_side:
|
||||
# use server-side cursors:
|
||||
# http://lists.initd.org/pipermail/psycopg/2007-January/005251.html
|
||||
ident = "c_%s_%s" % (hex(id(self))[2:], hex(random.randint(0, 65535))[2:])
|
||||
return self._connection.connection.cursor(ident)
|
||||
ident = "c_%s_%s" % (hex(id(self))[2:], hex(_server_side_id())[2:])
|
||||
return self._dbapi_connection.cursor(ident)
|
||||
else:
|
||||
return self._connection.connection.cursor()
|
||||
return self._dbapi_connection.cursor()
|
||||
|
||||
def get_result_proxy(self):
|
||||
# TODO: ouch
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
self._log_notices(self.cursor)
|
||||
|
||||
@@ -215,12 +248,15 @@ class PGIdentifierPreparer_psycopg2(PGIdentifierPreparer):
|
||||
|
||||
class PGDialect_psycopg2(PGDialect):
|
||||
driver = 'psycopg2'
|
||||
# Py2K
|
||||
supports_unicode_statements = False
|
||||
# end Py2K
|
||||
default_paramstyle = 'pyformat'
|
||||
supports_sane_multi_rowcount = False
|
||||
execution_ctx_cls = PGExecutionContext_psycopg2
|
||||
statement_compiler = PGCompiler_psycopg2
|
||||
preparer = PGIdentifierPreparer_psycopg2
|
||||
psycopg2_version = (0, 0)
|
||||
|
||||
colspecs = util.update_copy(
|
||||
PGDialect.colspecs,
|
||||
@@ -232,46 +268,74 @@ class PGDialect_psycopg2(PGDialect):
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, server_side_cursors=False, use_native_unicode=True, **kwargs):
|
||||
def __init__(self, server_side_cursors=False, use_native_unicode=True,
|
||||
client_encoding=None, **kwargs):
|
||||
PGDialect.__init__(self, **kwargs)
|
||||
self.server_side_cursors = server_side_cursors
|
||||
self.use_native_unicode = use_native_unicode
|
||||
self.supports_unicode_binds = use_native_unicode
|
||||
self.client_encoding = client_encoding
|
||||
if self.dbapi and hasattr(self.dbapi, '__version__'):
|
||||
m = re.match(r'(\d+)\.(\d+)(?:\.(\d+))?',
|
||||
self.dbapi.__version__)
|
||||
if m:
|
||||
self.psycopg2_version = tuple(
|
||||
int(x)
|
||||
for x in m.group(1, 2, 3)
|
||||
if x is not None)
|
||||
|
||||
@classmethod
|
||||
def dbapi(cls):
|
||||
psycopg = __import__('psycopg2')
|
||||
return psycopg
|
||||
|
||||
def on_connect(self):
|
||||
if self.isolation_level is not None:
|
||||
extensions = __import__('psycopg2.extensions').extensions
|
||||
isol = {
|
||||
'READ_COMMITTED':extensions.ISOLATION_LEVEL_READ_COMMITTED,
|
||||
'READ_UNCOMMITTED':extensions.ISOLATION_LEVEL_READ_UNCOMMITTED,
|
||||
'REPEATABLE_READ':extensions.ISOLATION_LEVEL_REPEATABLE_READ,
|
||||
@util.memoized_property
|
||||
def _isolation_lookup(self):
|
||||
extensions = __import__('psycopg2.extensions').extensions
|
||||
return {
|
||||
'READ COMMITTED':extensions.ISOLATION_LEVEL_READ_COMMITTED,
|
||||
'READ UNCOMMITTED':extensions.ISOLATION_LEVEL_READ_UNCOMMITTED,
|
||||
'REPEATABLE READ':extensions.ISOLATION_LEVEL_REPEATABLE_READ,
|
||||
'SERIALIZABLE':extensions.ISOLATION_LEVEL_SERIALIZABLE
|
||||
}
|
||||
|
||||
}
|
||||
def base_on_connect(conn):
|
||||
try:
|
||||
conn.set_isolation_level(isol[self.isolation_level])
|
||||
except:
|
||||
raise exc.InvalidRequestError(
|
||||
"Invalid isolation level: '%s'" %
|
||||
self.isolation_level)
|
||||
else:
|
||||
base_on_connect = None
|
||||
def set_isolation_level(self, connection, level):
|
||||
try:
|
||||
level = self._isolation_lookup[level.replace('_', ' ')]
|
||||
except KeyError:
|
||||
raise exc.ArgumentError(
|
||||
"Invalid value '%s' for isolation_level. "
|
||||
"Valid isolation levels for %s are %s" %
|
||||
(level, self.name, ", ".join(self._isolation_lookup))
|
||||
)
|
||||
|
||||
connection.set_isolation_level(level)
|
||||
|
||||
def on_connect(self):
|
||||
fns = []
|
||||
if self.client_encoding is not None:
|
||||
def on_connect(conn):
|
||||
conn.set_client_encoding(self.client_encoding)
|
||||
fns.append(on_connect)
|
||||
|
||||
if self.isolation_level is not None:
|
||||
def on_connect(conn):
|
||||
self.set_isolation_level(conn, self.isolation_level)
|
||||
fns.append(on_connect)
|
||||
|
||||
if self.dbapi and self.use_native_unicode:
|
||||
extensions = __import__('psycopg2.extensions').extensions
|
||||
def connect(conn):
|
||||
def on_connect(conn):
|
||||
extensions.register_type(extensions.UNICODE, conn)
|
||||
if base_on_connect:
|
||||
base_on_connect(conn)
|
||||
return connect
|
||||
fns.append(on_connect)
|
||||
|
||||
if fns:
|
||||
def on_connect(conn):
|
||||
for fn in fns:
|
||||
fn(conn)
|
||||
return on_connect
|
||||
else:
|
||||
return base_on_connect
|
||||
return None
|
||||
|
||||
def create_connect_args(self, url):
|
||||
opts = url.translate_connect_args(username='user')
|
||||
@@ -280,13 +344,21 @@ class PGDialect_psycopg2(PGDialect):
|
||||
opts.update(url.query)
|
||||
return ([], opts)
|
||||
|
||||
def is_disconnect(self, e):
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
if isinstance(e, self.dbapi.OperationalError):
|
||||
return 'closed the connection' in str(e) or 'connection not open' in str(e)
|
||||
# these error messages from libpq: interfaces/libpq/fe-misc.c.
|
||||
# TODO: these are sent through gettext in libpq and we can't
|
||||
# check within other locales - consider using connection.closed
|
||||
return 'closed the connection' in str(e) or \
|
||||
'connection not open' in str(e) or \
|
||||
'could not receive data from server' in str(e)
|
||||
elif isinstance(e, self.dbapi.InterfaceError):
|
||||
return 'connection already closed' in str(e) or 'cursor already closed' in str(e)
|
||||
# psycopg2 client errors, psycopg2/conenction.h, psycopg2/cursor.h
|
||||
return 'connection already closed' in str(e) or \
|
||||
'cursor already closed' in str(e)
|
||||
elif isinstance(e, self.dbapi.ProgrammingError):
|
||||
# yes, it really says "losed", not "closed"
|
||||
# not sure where this path is originally from, it may
|
||||
# be obsolete. It really says "losed", not "closed".
|
||||
return "losed the connection unexpectedly" in str(e)
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# postgresql/pypostgresql.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -9,12 +9,10 @@
|
||||
Connecting
|
||||
----------
|
||||
|
||||
URLs are of the form ``postgresql+pypostgresql://user@password@host:port/dbname[?key=value&key=value...]``.
|
||||
URLs are of the form ``postgresql+pypostgresql://user:password@host:port/dbname[?key=value&key=value...]``.
|
||||
|
||||
|
||||
"""
|
||||
from sqlalchemy.engine import default
|
||||
import decimal
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy import types as sqltypes
|
||||
from sqlalchemy.dialects.postgresql.base import PGDialect, PGExecutionContext
|
||||
@@ -69,7 +67,7 @@ class PGDialect_pypostgresql(PGDialect):
|
||||
opts.update(url.query)
|
||||
return ([], opts)
|
||||
|
||||
def is_disconnect(self, e):
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
return "connection is closed" in str(e)
|
||||
|
||||
dialect = PGDialect_pypostgresql
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# postgresql/zxjdbc.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -13,12 +13,29 @@ The official Postgresql JDBC driver is at http://jdbc.postgresql.org/.
|
||||
|
||||
"""
|
||||
from sqlalchemy.connectors.zxJDBC import ZxJDBCConnector
|
||||
from sqlalchemy.dialects.postgresql.base import PGDialect
|
||||
from sqlalchemy.dialects.postgresql.base import PGDialect, PGExecutionContext
|
||||
|
||||
class PGExecutionContext_zxjdbc(PGExecutionContext):
|
||||
|
||||
def create_cursor(self):
|
||||
cursor = self._dbapi_connection.cursor()
|
||||
cursor.datahandler = self.dialect.DataHandler(cursor.datahandler)
|
||||
return cursor
|
||||
|
||||
|
||||
class PGDialect_zxjdbc(ZxJDBCConnector, PGDialect):
|
||||
jdbc_db_name = 'postgresql'
|
||||
jdbc_driver_name = 'org.postgresql.Driver'
|
||||
|
||||
execution_ctx_cls = PGExecutionContext_zxjdbc
|
||||
|
||||
supports_native_decimal = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PGDialect_zxjdbc, self).__init__(*args, **kwargs)
|
||||
from com.ziclix.python.sql.handler import PostgresqlDataHandler
|
||||
self.DataHandler = PostgresqlDataHandler
|
||||
|
||||
def _get_server_version_info(self, connection):
|
||||
return tuple(int(x) for x in connection.connection.dbversion.split('.'))
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# sqlite/__init__.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -11,10 +11,10 @@ base.dialect = pysqlite.dialect
|
||||
|
||||
|
||||
from sqlalchemy.dialects.sqlite.base import \
|
||||
BLOB, BOOLEAN, CHAR, DATE, DATETIME, DECIMAL, FLOAT, INTEGER,\
|
||||
BLOB, BOOLEAN, CHAR, DATE, DATETIME, DECIMAL, FLOAT, INTEGER, REAL,\
|
||||
NUMERIC, SMALLINT, TEXT, TIME, TIMESTAMP, VARCHAR, dialect
|
||||
|
||||
__all__ = (
|
||||
'BLOB', 'BOOLEAN', 'CHAR', 'DATE', 'DATETIME', 'DECIMAL', 'FLOAT', 'INTEGER',
|
||||
'NUMERIC', 'SMALLINT', 'TEXT', 'TIME', 'TIMESTAMP', 'VARCHAR', 'dialect'
|
||||
'NUMERIC', 'SMALLINT', 'TEXT', 'TIME', 'TIMESTAMP', 'VARCHAR', 'dialect', 'REAL'
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
# sqlite/base.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -16,7 +16,7 @@ SQLite does not have built-in DATE, TIME, or DATETIME types, and pysqlite does n
|
||||
out of the box functionality for translating values between Python `datetime` objects
|
||||
and a SQLite-supported format. SQLAlchemy's own :class:`~sqlalchemy.types.DateTime`
|
||||
and related types provide date formatting and parsing functionality when SQlite is used.
|
||||
The implementation classes are :class:`DATETIME`, :class:`DATE` and :class:`TIME`.
|
||||
The implementation classes are :class:`~.sqlite.DATETIME`, :class:`~.sqlite.DATE` and :class:`~.sqlite.TIME`.
|
||||
These types represent dates and times as ISO formatted strings, which also nicely
|
||||
support ordering. There's no reliance on typical "libc" internals for these functions
|
||||
so historical dates are fully supported.
|
||||
@@ -46,41 +46,74 @@ to the Table construct::
|
||||
Transaction Isolation Level
|
||||
---------------------------
|
||||
|
||||
:func:`create_engine` accepts an ``isolation_level`` parameter which results in
|
||||
:func:`.create_engine` accepts an ``isolation_level`` parameter which results in
|
||||
the command ``PRAGMA read_uncommitted <level>`` being invoked for every new
|
||||
connection. Valid values for this parameter are ``SERIALIZABLE`` and
|
||||
``READ UNCOMMITTED`` corresponding to a value of 0 and 1, respectively.
|
||||
See the section :ref:`pysqlite_serializable` for an important workaround
|
||||
when using serializable isolation with Pysqlite.
|
||||
|
||||
"""
|
||||
|
||||
import datetime, re, time
|
||||
import datetime, re
|
||||
|
||||
from sqlalchemy import schema as sa_schema
|
||||
from sqlalchemy import sql, exc, pool, DefaultClause
|
||||
from sqlalchemy.engine import default
|
||||
from sqlalchemy.engine import reflection
|
||||
from sqlalchemy import sql, exc
|
||||
from sqlalchemy.engine import default, base, reflection
|
||||
from sqlalchemy import types as sqltypes
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy.sql import compiler, functions as sql_functions
|
||||
from sqlalchemy.util import NoneType
|
||||
from sqlalchemy.sql import compiler
|
||||
from sqlalchemy import processors
|
||||
|
||||
from sqlalchemy.types import BLOB, BOOLEAN, CHAR, DATE, DATETIME, DECIMAL,\
|
||||
FLOAT, INTEGER, NUMERIC, SMALLINT, TEXT, TIME,\
|
||||
TIMESTAMP, VARCHAR
|
||||
|
||||
FLOAT, REAL, INTEGER, NUMERIC, SMALLINT, TEXT, TIME, TIMESTAMP, VARCHAR
|
||||
|
||||
class _DateTimeMixin(object):
|
||||
_reg = None
|
||||
_storage_format = None
|
||||
|
||||
def __init__(self, storage_format=None, regexp=None, **kwargs):
|
||||
def __init__(self, storage_format=None, regexp=None, **kw):
|
||||
super(_DateTimeMixin, self).__init__(**kw)
|
||||
if regexp is not None:
|
||||
self._reg = re.compile(regexp)
|
||||
if storage_format is not None:
|
||||
self._storage_format = storage_format
|
||||
|
||||
class DATETIME(_DateTimeMixin, sqltypes.DateTime):
|
||||
"""Represent a Python datetime object in SQLite using a string.
|
||||
|
||||
The default string storage format is::
|
||||
|
||||
"%04d-%02d-%02d %02d:%02d:%02d.%06d" % (value.year,
|
||||
value.month, value.day,
|
||||
value.hour, value.minute,
|
||||
value.second, value.microsecond)
|
||||
|
||||
e.g.::
|
||||
|
||||
2011-03-15 12:05:57.10558
|
||||
|
||||
The storage format can be customized to some degree using the
|
||||
``storage_format`` and ``regexp`` parameters, such as::
|
||||
|
||||
import re
|
||||
from sqlalchemy.dialects.sqlite import DATETIME
|
||||
|
||||
dt = DATETIME(
|
||||
storage_format="%04d/%02d/%02d %02d-%02d-%02d-%06d",
|
||||
regexp=re.compile("(\d+)/(\d+)/(\d+) (\d+)-(\d+)-(\d+)(?:-(\d+))?")
|
||||
)
|
||||
|
||||
:param storage_format: format string which will be appled to the
|
||||
tuple ``(value.year, value.month, value.day, value.hour,
|
||||
value.minute, value.second, value.microsecond)``, given a
|
||||
Python datetime.datetime() object.
|
||||
|
||||
:param regexp: regular expression which will be applied to
|
||||
incoming result rows. The resulting match object is appled to
|
||||
the Python datetime() constructor via ``*map(int,
|
||||
match_obj.groups(0))``.
|
||||
"""
|
||||
|
||||
_storage_format = "%04d-%02d-%02d %02d:%02d:%02d.%06d"
|
||||
|
||||
def bind_processor(self, dialect):
|
||||
@@ -110,6 +143,38 @@ class DATETIME(_DateTimeMixin, sqltypes.DateTime):
|
||||
return processors.str_to_datetime
|
||||
|
||||
class DATE(_DateTimeMixin, sqltypes.Date):
|
||||
"""Represent a Python date object in SQLite using a string.
|
||||
|
||||
The default string storage format is::
|
||||
|
||||
"%04d-%02d-%02d" % (value.year, value.month, value.day)
|
||||
|
||||
e.g.::
|
||||
|
||||
2011-03-15
|
||||
|
||||
The storage format can be customized to some degree using the
|
||||
``storage_format`` and ``regexp`` parameters, such as::
|
||||
|
||||
import re
|
||||
from sqlalchemy.dialects.sqlite import DATE
|
||||
|
||||
d = DATE(
|
||||
storage_format="%02d/%02d/%02d",
|
||||
regexp=re.compile("(\d+)/(\d+)/(\d+)")
|
||||
)
|
||||
|
||||
:param storage_format: format string which will be appled to the
|
||||
tuple ``(value.year, value.month, value.day)``,
|
||||
given a Python datetime.date() object.
|
||||
|
||||
:param regexp: regular expression which will be applied to
|
||||
incoming result rows. The resulting match object is appled to
|
||||
the Python date() constructor via ``*map(int,
|
||||
match_obj.groups(0))``.
|
||||
|
||||
"""
|
||||
|
||||
_storage_format = "%04d-%02d-%02d"
|
||||
|
||||
def bind_processor(self, dialect):
|
||||
@@ -133,6 +198,40 @@ class DATE(_DateTimeMixin, sqltypes.Date):
|
||||
return processors.str_to_date
|
||||
|
||||
class TIME(_DateTimeMixin, sqltypes.Time):
|
||||
"""Represent a Python time object in SQLite using a string.
|
||||
|
||||
The default string storage format is::
|
||||
|
||||
"%02d:%02d:%02d.%06d" % (value.hour, value.minute,
|
||||
value.second,
|
||||
value.microsecond)
|
||||
|
||||
e.g.::
|
||||
|
||||
12:05:57.10558
|
||||
|
||||
The storage format can be customized to some degree using the
|
||||
``storage_format`` and ``regexp`` parameters, such as::
|
||||
|
||||
import re
|
||||
from sqlalchemy.dialects.sqlite import TIME
|
||||
|
||||
t = TIME(
|
||||
storage_format="%02d-%02d-%02d-%06d",
|
||||
regexp=re.compile("(\d+)-(\d+)-(\d+)-(?:-(\d+))?")
|
||||
)
|
||||
|
||||
:param storage_format: format string which will be appled
|
||||
to the tuple ``(value.hour, value.minute, value.second,
|
||||
value.microsecond)``, given a Python datetime.time() object.
|
||||
|
||||
:param regexp: regular expression which will be applied to
|
||||
incoming result rows. The resulting match object is appled to
|
||||
the Python time() constructor via ``*map(int,
|
||||
match_obj.groups(0))``.
|
||||
|
||||
"""
|
||||
|
||||
_storage_format = "%02d:%02d:%02d.%06d"
|
||||
|
||||
def bind_processor(self, dialect):
|
||||
@@ -174,7 +273,7 @@ ischema_names = {
|
||||
'INT': sqltypes.INTEGER,
|
||||
'INTEGER': sqltypes.INTEGER,
|
||||
'NUMERIC': sqltypes.NUMERIC,
|
||||
'REAL': sqltypes.Numeric,
|
||||
'REAL': sqltypes.REAL,
|
||||
'SMALLINT': sqltypes.SMALLINT,
|
||||
'TEXT': sqltypes.TEXT,
|
||||
'TIME': sqltypes.TIME,
|
||||
@@ -203,6 +302,12 @@ class SQLiteCompiler(compiler.SQLCompiler):
|
||||
def visit_now_func(self, fn, **kw):
|
||||
return "CURRENT_TIMESTAMP"
|
||||
|
||||
def visit_true(self, expr, **kw):
|
||||
return '1'
|
||||
|
||||
def visit_false(self, expr, **kw):
|
||||
return '0'
|
||||
|
||||
def visit_char_length_func(self, fn, **kw):
|
||||
return "length%s" % self.function_argspec(fn)
|
||||
|
||||
@@ -217,19 +322,19 @@ class SQLiteCompiler(compiler.SQLCompiler):
|
||||
return "CAST(STRFTIME('%s', %s) AS INTEGER)" % (
|
||||
self.extract_map[extract.field], self.process(extract.expr, **kw))
|
||||
except KeyError:
|
||||
raise exc.ArgumentError(
|
||||
raise exc.CompileError(
|
||||
"%s is not a valid extract argument." % extract.field)
|
||||
|
||||
def limit_clause(self, select):
|
||||
text = ""
|
||||
if select._limit is not None:
|
||||
text += " \n LIMIT " + str(select._limit)
|
||||
text += "\n LIMIT " + self.process(sql.literal(select._limit))
|
||||
if select._offset is not None:
|
||||
if select._limit is None:
|
||||
text += " \n LIMIT -1"
|
||||
text += " OFFSET " + str(select._offset)
|
||||
text += "\n LIMIT " + self.process(sql.literal(-1))
|
||||
text += " OFFSET " + self.process(sql.literal(select._offset))
|
||||
else:
|
||||
text += " OFFSET 0"
|
||||
text += " OFFSET " + self.process(sql.literal(0))
|
||||
return text
|
||||
|
||||
def for_update_clause(self, select):
|
||||
@@ -251,7 +356,7 @@ class SQLiteDDLCompiler(compiler.DDLCompiler):
|
||||
if column.primary_key and \
|
||||
column.table.kwargs.get('sqlite_autoincrement', False) and \
|
||||
len(column.table.primary_key.columns) == 1 and \
|
||||
isinstance(column.type, sqltypes.Integer) and \
|
||||
issubclass(column.type._type_affinity, sqltypes.Integer) and \
|
||||
not column.foreign_keys:
|
||||
colspec += " PRIMARY KEY AUTOINCREMENT"
|
||||
|
||||
@@ -265,7 +370,7 @@ class SQLiteDDLCompiler(compiler.DDLCompiler):
|
||||
c = list(constraint)[0]
|
||||
if c.primary_key and \
|
||||
c.table.kwargs.get('sqlite_autoincrement', False) and \
|
||||
isinstance(c.type, sqltypes.Integer) and \
|
||||
issubclass(c.type._type_affinity, sqltypes.Integer) and \
|
||||
not c.foreign_keys:
|
||||
return None
|
||||
|
||||
@@ -336,6 +441,20 @@ class SQLiteIdentifierPreparer(compiler.IdentifierPreparer):
|
||||
result = self.quote_schema(index.table.schema, index.table.quote_schema) + "." + result
|
||||
return result
|
||||
|
||||
class SQLiteExecutionContext(default.DefaultExecutionContext):
|
||||
def get_result_proxy(self):
|
||||
rp = base.ResultProxy(self)
|
||||
if rp._metadata:
|
||||
# adjust for dotted column names. SQLite
|
||||
# in the case of UNION may store col names as
|
||||
# "tablename.colname"
|
||||
# in cursor.description
|
||||
for colname in rp._metadata.keys:
|
||||
if "." in colname:
|
||||
trunc_col = colname.split(".")[1]
|
||||
rp._metadata._set_keymap_synonym(trunc_col, colname)
|
||||
return rp
|
||||
|
||||
class SQLiteDialect(default.DefaultDialect):
|
||||
name = 'sqlite'
|
||||
supports_alter = False
|
||||
@@ -353,17 +472,13 @@ class SQLiteDialect(default.DefaultDialect):
|
||||
ischema_names = ischema_names
|
||||
colspecs = colspecs
|
||||
isolation_level = None
|
||||
execution_ctx_cls = SQLiteExecutionContext
|
||||
|
||||
supports_cast = True
|
||||
supports_default_values = True
|
||||
|
||||
def __init__(self, isolation_level=None, native_datetime=False, **kwargs):
|
||||
default.DefaultDialect.__init__(self, **kwargs)
|
||||
if isolation_level and isolation_level not in ('SERIALIZABLE',
|
||||
'READ UNCOMMITTED'):
|
||||
raise exc.ArgumentError("Invalid value for isolation_level. "
|
||||
"Valid isolation levels for sqlite are 'SERIALIZABLE' and "
|
||||
"'READ UNCOMMITTED'.")
|
||||
self.isolation_level = isolation_level
|
||||
|
||||
# this flag used by pysqlite dialect, and perhaps others in the
|
||||
@@ -378,18 +493,49 @@ class SQLiteDialect(default.DefaultDialect):
|
||||
self.supports_cast = \
|
||||
self.dbapi.sqlite_version_info >= (3, 2, 3)
|
||||
|
||||
_isolation_lookup = {
|
||||
'READ UNCOMMITTED':1,
|
||||
'SERIALIZABLE':0
|
||||
}
|
||||
def set_isolation_level(self, connection, level):
|
||||
try:
|
||||
isolation_level = self._isolation_lookup[level.replace('_', ' ')]
|
||||
except KeyError:
|
||||
raise exc.ArgumentError(
|
||||
"Invalid value '%s' for isolation_level. "
|
||||
"Valid isolation levels for %s are %s" %
|
||||
(level, self.name, ", ".join(self._isolation_lookup))
|
||||
)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute("PRAGMA read_uncommitted = %d" % isolation_level)
|
||||
cursor.close()
|
||||
|
||||
def get_isolation_level(self, connection):
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('PRAGMA read_uncommitted')
|
||||
res = cursor.fetchone()
|
||||
if res:
|
||||
value = res[0]
|
||||
else:
|
||||
# http://www.sqlite.org/changes.html#version_3_3_3
|
||||
# "Optional READ UNCOMMITTED isolation (instead of the
|
||||
# default isolation level of SERIALIZABLE) and
|
||||
# table level locking when database connections
|
||||
# share a common cache.""
|
||||
# pre-SQLite 3.3.0 default to 0
|
||||
value = 0
|
||||
cursor.close()
|
||||
if value == 0:
|
||||
return "SERIALIZABLE"
|
||||
elif value == 1:
|
||||
return "READ UNCOMMITTED"
|
||||
else:
|
||||
assert False, "Unknown isolation level %s" % value
|
||||
|
||||
def on_connect(self):
|
||||
if self.isolation_level is not None:
|
||||
if self.isolation_level == 'READ UNCOMMITTED':
|
||||
isolation_level = 1
|
||||
else:
|
||||
isolation_level = 0
|
||||
|
||||
def connect(conn):
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("PRAGMA read_uncommitted = %d" % isolation_level)
|
||||
cursor.close()
|
||||
self.set_isolation_level(conn, self.isolation_level)
|
||||
return connect
|
||||
else:
|
||||
return None
|
||||
@@ -410,7 +556,6 @@ class SQLiteDialect(default.DefaultDialect):
|
||||
"WHERE type='table' ORDER BY name")
|
||||
rs = connection.execute(s)
|
||||
except exc.DBAPIError:
|
||||
raise
|
||||
s = ("SELECT name FROM sqlite_master "
|
||||
"WHERE type='table' ORDER BY name")
|
||||
rs = connection.execute(s)
|
||||
@@ -429,7 +574,7 @@ class SQLiteDialect(default.DefaultDialect):
|
||||
|
||||
# consume remaining rows, to work around
|
||||
# http://www.sqlite.org/cvstrac/tktview?tn=1884
|
||||
while cursor.fetchone() is not None:
|
||||
while not cursor.closed and cursor.fetchone() is not None:
|
||||
pass
|
||||
|
||||
return (row is not None)
|
||||
@@ -450,7 +595,6 @@ class SQLiteDialect(default.DefaultDialect):
|
||||
"WHERE type='view' ORDER BY name")
|
||||
rs = connection.execute(s)
|
||||
except exc.DBAPIError:
|
||||
raise
|
||||
s = ("SELECT name FROM sqlite_master "
|
||||
"WHERE type='view' ORDER BY name")
|
||||
rs = connection.execute(s)
|
||||
@@ -475,7 +619,6 @@ class SQLiteDialect(default.DefaultDialect):
|
||||
"AND type='view'") % view_name
|
||||
rs = connection.execute(s)
|
||||
except exc.DBAPIError:
|
||||
raise
|
||||
s = ("SELECT sql FROM sqlite_master WHERE name = '%s' "
|
||||
"AND type='view'") % view_name
|
||||
rs = connection.execute(s)
|
||||
@@ -492,17 +635,19 @@ class SQLiteDialect(default.DefaultDialect):
|
||||
else:
|
||||
pragma = "PRAGMA "
|
||||
qtable = quote(table_name)
|
||||
c = _pragma_cursor(connection.execute("%stable_info(%s)" % (pragma, qtable)))
|
||||
c = _pragma_cursor(
|
||||
connection.execute("%stable_info(%s)" %
|
||||
(pragma, qtable)))
|
||||
found_table = False
|
||||
columns = []
|
||||
while True:
|
||||
row = c.fetchone()
|
||||
if row is None:
|
||||
break
|
||||
(name, type_, nullable, default, has_default, primary_key) = (row[1], row[2].upper(), not row[3], row[4], row[4] is not None, row[5])
|
||||
(name, type_, nullable, default, has_default, primary_key) = \
|
||||
(row[1], row[2].upper(), not row[3],
|
||||
row[4], row[4] is not None, row[5])
|
||||
name = re.sub(r'^\"|\"$', '', name)
|
||||
if default:
|
||||
default = re.sub(r"^\'|\'$", '', default)
|
||||
match = re.match(r'(\w+)(\(.*?\))?', type_)
|
||||
if match:
|
||||
coltype = match.group(1)
|
||||
@@ -512,19 +657,20 @@ class SQLiteDialect(default.DefaultDialect):
|
||||
args = ''
|
||||
try:
|
||||
coltype = self.ischema_names[coltype]
|
||||
if args is not None:
|
||||
args = re.findall(r'(\d+)', args)
|
||||
coltype = coltype(*[int(a) for a in args])
|
||||
except KeyError:
|
||||
util.warn("Did not recognize type '%s' of column '%s'" %
|
||||
(coltype, name))
|
||||
coltype = sqltypes.NullType
|
||||
if args is not None:
|
||||
args = re.findall(r'(\d+)', args)
|
||||
coltype = coltype(*[int(a) for a in args])
|
||||
coltype = sqltypes.NullType()
|
||||
|
||||
columns.append({
|
||||
'name' : name,
|
||||
'type' : coltype,
|
||||
'nullable' : nullable,
|
||||
'default' : default,
|
||||
'autoincrement':default is None,
|
||||
'primary_key': primary_key
|
||||
})
|
||||
return columns
|
||||
@@ -553,22 +699,26 @@ class SQLiteDialect(default.DefaultDialect):
|
||||
row = c.fetchone()
|
||||
if row is None:
|
||||
break
|
||||
(constraint_name, rtbl, lcol, rcol) = (row[0], row[2], row[3], row[4])
|
||||
(numerical_id, rtbl, lcol, rcol) = (row[0], row[2], row[3], row[4])
|
||||
# sqlite won't return rcol if the table
|
||||
# was created with REFERENCES <tablename>, no col
|
||||
if rcol is None:
|
||||
rcol = lcol
|
||||
rtbl = re.sub(r'^\"|\"$', '', rtbl)
|
||||
lcol = re.sub(r'^\"|\"$', '', lcol)
|
||||
rcol = re.sub(r'^\"|\"$', '', rcol)
|
||||
try:
|
||||
fk = fks[constraint_name]
|
||||
fk = fks[numerical_id]
|
||||
except KeyError:
|
||||
fk = {
|
||||
'name' : constraint_name,
|
||||
'name' : None,
|
||||
'constrained_columns' : [],
|
||||
'referred_schema' : None,
|
||||
'referred_table' : rtbl,
|
||||
'referred_columns' : []
|
||||
}
|
||||
fkeys.append(fk)
|
||||
fks[constraint_name] = fk
|
||||
fks[numerical_id] = fk
|
||||
|
||||
# look up the table based on the given table's engine, not 'self',
|
||||
# since it could be a ProxyEngine
|
||||
@@ -612,7 +762,8 @@ class SQLiteDialect(default.DefaultDialect):
|
||||
|
||||
|
||||
def _pragma_cursor(cursor):
|
||||
"""work around SQLite issue whereby cursor.description is blank when PRAGMA returns no rows."""
|
||||
"""work around SQLite issue whereby cursor.description
|
||||
is blank when PRAGMA returns no rows."""
|
||||
|
||||
if cursor.closed:
|
||||
cursor.fetchone = lambda: None
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# sqlite/pysqlite.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -93,61 +93,130 @@ processing. Execution of "func.current_date()" will return a string.
|
||||
"func.current_timestamp()" is registered as returning a DATETIME type in
|
||||
SQLAlchemy, so this function still receives SQLAlchemy-level result processing.
|
||||
|
||||
Threading Behavior
|
||||
------------------
|
||||
Threading/Pooling Behavior
|
||||
---------------------------
|
||||
|
||||
Pysqlite connections do not support being moved between threads, unless
|
||||
the ``check_same_thread`` Pysqlite flag is set to ``False``. In addition,
|
||||
when using an in-memory SQLite database, the full database exists only within
|
||||
the scope of a single connection. It is reported that an in-memory
|
||||
database does not support being shared between threads regardless of the
|
||||
``check_same_thread`` flag - which means that a multithreaded
|
||||
application **cannot** share data from a ``:memory:`` database across threads
|
||||
unless access to the connection is limited to a single worker thread which communicates
|
||||
through a queueing mechanism to concurrent threads.
|
||||
Pysqlite's default behavior is to prohibit the usage of a single connection
|
||||
in more than one thread. This is controlled by the ``check_same_thread``
|
||||
Pysqlite flag. This default is intended to work with older versions
|
||||
of SQLite that did not support multithreaded operation under
|
||||
various circumstances. In particular, older SQLite versions
|
||||
did not allow a ``:memory:`` database to be used in multiple threads
|
||||
under any circumstances.
|
||||
|
||||
To provide a default which accomodates SQLite's default threading capabilities
|
||||
somewhat reasonably, the SQLite dialect will specify that the :class:`~sqlalchemy.pool.SingletonThreadPool`
|
||||
be used by default. This pool maintains a single SQLite connection per thread
|
||||
that is held open up to a count of five concurrent threads. When more than five threads
|
||||
are used, a cleanup mechanism will dispose of excess unused connections.
|
||||
SQLAlchemy sets up pooling to work with Pysqlite's default behavior:
|
||||
|
||||
Two optional pool implementations that may be appropriate for particular SQLite usage scenarios:
|
||||
* When a ``:memory:`` SQLite database is specified, the dialect by default will use
|
||||
:class:`.SingletonThreadPool`. This pool maintains a single connection per
|
||||
thread, so that all access to the engine within the current thread use the
|
||||
same ``:memory:`` database - other threads would access a different
|
||||
``:memory:`` database.
|
||||
* When a file-based database is specified, the dialect will use :class:`.NullPool`
|
||||
as the source of connections. This pool closes and discards connections
|
||||
which are returned to the pool immediately. SQLite file-based connections
|
||||
have extremely low overhead, so pooling is not necessary. The scheme also
|
||||
prevents a connection from being used again in a different thread and works
|
||||
best with SQLite's coarse-grained file locking.
|
||||
|
||||
* the :class:`sqlalchemy.pool.StaticPool` might be appropriate for a multithreaded
|
||||
application using an in-memory database, assuming the threading issues inherent in
|
||||
pysqlite are somehow accomodated for. This pool holds persistently onto a single connection
|
||||
which is never closed, and is returned for all requests.
|
||||
.. note::
|
||||
|
||||
The default selection of :class:`.NullPool` for SQLite file-based databases
|
||||
is new in SQLAlchemy 0.7. Previous versions
|
||||
select :class:`.SingletonThreadPool` by
|
||||
default for all SQLite databases.
|
||||
|
||||
* the :class:`sqlalchemy.pool.NullPool` might be appropriate for an application that
|
||||
makes use of a file-based sqlite database. This pool disables any actual "pooling"
|
||||
behavior, and simply opens and closes real connections corresonding to the :func:`connect()`
|
||||
and :func:`close()` methods. SQLite can "connect" to a particular file with very high
|
||||
efficiency, so this option may actually perform better without the extra overhead
|
||||
of :class:`SingletonThreadPool`. NullPool will of course render a ``:memory:`` connection
|
||||
useless since the database would be lost as soon as the connection is "returned" to the pool.
|
||||
Modern versions of SQLite no longer have the threading restrictions, and assuming
|
||||
the sqlite3/pysqlite library was built with SQLite's default threading mode
|
||||
of "Serialized", even ``:memory:`` databases can be shared among threads.
|
||||
|
||||
Using a Memory Database in Multiple Threads
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
To use a ``:memory:`` database in a multithreaded scenario, the same connection
|
||||
object must be shared among threads, since the database exists
|
||||
only within the scope of that connection. The :class:`.StaticPool` implementation
|
||||
will maintain a single connection globally, and the ``check_same_thread`` flag
|
||||
can be passed to Pysqlite as ``False``::
|
||||
|
||||
from sqlalchemy.pool import StaticPool
|
||||
engine = create_engine('sqlite://',
|
||||
connect_args={'check_same_thread':False},
|
||||
poolclass=StaticPool)
|
||||
|
||||
Note that using a ``:memory:`` database in multiple threads requires a recent
|
||||
version of SQLite.
|
||||
|
||||
Using Temporary Tables with SQLite
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Due to the way SQLite deals with temporary tables, if you wish to use a temporary table
|
||||
in a file-based SQLite database across multiple checkouts from the connection pool, such
|
||||
as when using an ORM :class:`.Session` where the temporary table should continue to remain
|
||||
after :meth:`.commit` or :meth:`.rollback` is called,
|
||||
a pool which maintains a single connection must be used. Use :class:`.SingletonThreadPool`
|
||||
if the scope is only needed within the current thread, or :class:`.StaticPool` is scope is
|
||||
needed within multiple threads for this case::
|
||||
|
||||
# maintain the same connection per thread
|
||||
from sqlalchemy.pool import SingletonThreadPool
|
||||
engine = create_engine('sqlite:///mydb.db',
|
||||
poolclass=SingletonThreadPool)
|
||||
|
||||
|
||||
# maintain the same connection across all threads
|
||||
from sqlalchemy.pool import StaticPool
|
||||
engine = create_engine('sqlite:///mydb.db',
|
||||
poolclass=StaticPool)
|
||||
|
||||
Note that :class:`.SingletonThreadPool` should be configured for the number of threads
|
||||
that are to be used; beyond that number, connections will be closed out in a non deterministic
|
||||
way.
|
||||
|
||||
Unicode
|
||||
-------
|
||||
|
||||
In contrast to SQLAlchemy's active handling of date and time types for pysqlite, pysqlite's
|
||||
default behavior regarding Unicode is that all strings are returned as Python unicode objects
|
||||
in all cases. So even if the :class:`~sqlalchemy.types.Unicode` type is
|
||||
*not* used, you will still always receive unicode data back from a result set. It is
|
||||
**strongly** recommended that you do use the :class:`~sqlalchemy.types.Unicode` type
|
||||
to represent strings, since it will raise a warning if a non-unicode Python string is
|
||||
passed from the user application. Mixing the usage of non-unicode objects with returned unicode objects can
|
||||
quickly create confusion, particularly when using the ORM as internal data is not
|
||||
always represented by an actual database result string.
|
||||
The pysqlite driver only returns Python ``unicode`` objects in result sets, never
|
||||
plain strings, and accommodates ``unicode`` objects within bound parameter
|
||||
values in all cases. Regardless of the SQLAlchemy string type in use,
|
||||
string-based result values will by Python ``unicode`` in Python 2.
|
||||
The :class:`.Unicode` type should still be used to indicate those columns that
|
||||
require unicode, however, so that non-``unicode`` values passed inadvertently
|
||||
will emit a warning. Pysqlite will emit an error if a non-``unicode`` string
|
||||
is passed containing non-ASCII characters.
|
||||
|
||||
.. _pysqlite_serializable:
|
||||
|
||||
Serializable Transaction Isolation
|
||||
----------------------------------
|
||||
|
||||
The pysqlite DBAPI driver has a long-standing bug in which transactional
|
||||
state is not begun until the first DML statement, that is INSERT, UPDATE
|
||||
or DELETE, is emitted. A SELECT statement will not cause transactional
|
||||
state to begin. While this mode of usage is fine for typical situations
|
||||
and has the advantage that the SQLite database file is not prematurely
|
||||
locked, it breaks serializable transaction isolation, which requires
|
||||
that the database file be locked upon any SQL being emitted.
|
||||
|
||||
To work around this issue, the ``BEGIN`` keyword can be emitted
|
||||
at the start of each transaction. The following recipe establishes
|
||||
a :meth:`.ConnectionEvents.begin` handler to achieve this::
|
||||
|
||||
from sqlalchemy import create_engine, event
|
||||
|
||||
engine = create_engine("sqlite:///myfile.db", isolation_level='SERIALIZABLE')
|
||||
|
||||
@event.listens_for(engine, "begin")
|
||||
def do_begin(conn):
|
||||
conn.execute("BEGIN")
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy.dialects.sqlite.base import SQLiteDialect, DATETIME, DATE
|
||||
from sqlalchemy import schema, exc, pool
|
||||
from sqlalchemy.engine import default
|
||||
from sqlalchemy import exc, pool
|
||||
from sqlalchemy import types as sqltypes
|
||||
from sqlalchemy import util
|
||||
|
||||
import os
|
||||
|
||||
class _SQLite_pysqliteTimeStamp(DATETIME):
|
||||
def bind_processor(self, dialect):
|
||||
@@ -177,7 +246,6 @@ class _SQLite_pysqliteDate(DATE):
|
||||
|
||||
class SQLiteDialect_pysqlite(SQLiteDialect):
|
||||
default_paramstyle = 'qmark'
|
||||
poolclass = pool.SingletonThreadPool
|
||||
|
||||
colspecs = util.update_copy(
|
||||
SQLiteDialect.colspecs,
|
||||
@@ -215,6 +283,13 @@ class SQLiteDialect_pysqlite(SQLiteDialect):
|
||||
raise e
|
||||
return sqlite
|
||||
|
||||
@classmethod
|
||||
def get_pool_class(cls, url):
|
||||
if url.database and url.database != ':memory:':
|
||||
return pool.NullPool
|
||||
else:
|
||||
return pool.SingletonThreadPool
|
||||
|
||||
def _get_server_version_info(self, connection):
|
||||
return self.dbapi.sqlite_version_info
|
||||
|
||||
@@ -227,6 +302,8 @@ class SQLiteDialect_pysqlite(SQLiteDialect):
|
||||
" sqlite:///relative/path/to/file.db\n"
|
||||
" sqlite:////absolute/path/to/file.db" % (url,))
|
||||
filename = url.database or ':memory:'
|
||||
if filename != ':memory:':
|
||||
filename = os.path.abspath(filename)
|
||||
|
||||
opts = url.query.copy()
|
||||
util.coerce_kw_type(opts, 'timeout', float)
|
||||
@@ -237,7 +314,8 @@ class SQLiteDialect_pysqlite(SQLiteDialect):
|
||||
|
||||
return ([filename], opts)
|
||||
|
||||
def is_disconnect(self, e):
|
||||
return isinstance(e, self.dbapi.ProgrammingError) and "Cannot operate on a closed database." in str(e)
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
return isinstance(e, self.dbapi.ProgrammingError) and \
|
||||
"Cannot operate on a closed database." in str(e)
|
||||
|
||||
dialect = SQLiteDialect_pysqlite
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# sybase/__init__.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
@@ -271,6 +271,8 @@ class SybaseSQLCompiler(compiler.SQLCompiler):
|
||||
|
||||
def get_select_precolumns(self, select):
|
||||
s = select._distinct and "DISTINCT " or ""
|
||||
# TODO: don't think Sybase supports
|
||||
# bind params for FIRST / TOP
|
||||
if select._limit:
|
||||
#if select._limit == 1:
|
||||
#s += "FIRST "
|
||||
@@ -319,7 +321,7 @@ class SybaseDDLCompiler(compiler.DDLCompiler):
|
||||
self.dialect.type_compiler.process(column.type)
|
||||
|
||||
if column.table is None:
|
||||
raise exc.InvalidRequestError(
|
||||
raise exc.CompileError(
|
||||
"The Sybase dialect requires Table-bound "
|
||||
"columns in order to generate DDL")
|
||||
seq_col = column.table._autoincrement_column
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# sybase/mxodbc.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# sybase/pyodbc.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -37,8 +37,8 @@ Currently *not* supported are::
|
||||
from sqlalchemy.dialects.sybase.base import SybaseDialect,\
|
||||
SybaseExecutionContext
|
||||
from sqlalchemy.connectors.pyodbc import PyODBCConnector
|
||||
import decimal
|
||||
from sqlalchemy import types as sqltypes, util, processors
|
||||
from sqlalchemy.util.compat import decimal
|
||||
|
||||
class _SybNumeric_pyodbc(sqltypes.Numeric):
|
||||
"""Turns Decimals with adjusted() < -6 into floats.
|
||||
|
||||
@@ -87,7 +87,7 @@ class SybaseDialect_pysybase(SybaseDialect):
|
||||
# (12, 5, 0, 0)
|
||||
return (vers / 1000, vers % 1000 / 100, vers % 100 / 10, vers % 10)
|
||||
|
||||
def is_disconnect(self, e):
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
if isinstance(e, (self.dbapi.OperationalError,
|
||||
self.dbapi.ProgrammingError)):
|
||||
msg = str(e)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# engine/__init__.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -99,52 +99,69 @@ __all__ = (
|
||||
|
||||
default_strategy = 'plain'
|
||||
def create_engine(*args, **kwargs):
|
||||
"""Create a new Engine instance.
|
||||
"""Create a new :class:`.Engine` instance.
|
||||
|
||||
The standard method of specifying the engine is via URL as the
|
||||
first positional argument, to indicate the appropriate database
|
||||
dialect and connection arguments, with additional keyword
|
||||
arguments sent as options to the dialect and resulting Engine.
|
||||
The standard calling form is to send the URL as the
|
||||
first positional argument, usually a string
|
||||
that indicates database dialect and connection arguments.
|
||||
Additional keyword arguments may then follow it which
|
||||
establish various options on the resulting :class:`.Engine`
|
||||
and its underlying :class:`.Dialect` and :class:`.Pool`
|
||||
constructs.
|
||||
|
||||
The URL is a string in the form
|
||||
The string form of the URL is
|
||||
``dialect+driver://user:password@host/dbname[?key=value..]``, where
|
||||
``dialect`` is a database name such as ``mysql``, ``oracle``,
|
||||
``postgresql``, etc., and ``driver`` the name of a DBAPI, such as
|
||||
``psycopg2``, ``pyodbc``, ``cx_oracle``, etc. Alternatively,
|
||||
the URL can be an instance of :class:`~sqlalchemy.engine.url.URL`.
|
||||
|
||||
`**kwargs` takes a wide variety of options which are routed
|
||||
``**kwargs`` takes a wide variety of options which are routed
|
||||
towards their appropriate components. Arguments may be
|
||||
specific to the Engine, the underlying Dialect, as well as the
|
||||
Pool. Specific dialects also accept keyword arguments that
|
||||
specific to the :class:`.Engine`, the underlying :class:`.Dialect`, as well as the
|
||||
:class:`.Pool`. Specific dialects also accept keyword arguments that
|
||||
are unique to that dialect. Here, we describe the parameters
|
||||
that are common to most ``create_engine()`` usage.
|
||||
that are common to most :func:`.create_engine()` usage.
|
||||
|
||||
:param assert_unicode: Deprecated. A warning is raised in all cases when a non-Unicode
|
||||
object is passed when SQLAlchemy would coerce into an encoding
|
||||
(note: but **not** when the DBAPI handles unicode objects natively).
|
||||
To suppress or raise this warning to an
|
||||
error, use the Python warnings filter documented at:
|
||||
http://docs.python.org/library/warnings.html
|
||||
Once established, the newly resulting :class:`.Engine` will
|
||||
request a connection from the underlying :class:`.Pool` once
|
||||
:meth:`.Engine.connect` is called, or a method which depends on it
|
||||
such as :meth:`.Engine.execute` is invoked. The :class:`.Pool` in turn
|
||||
will establish the first actual DBAPI connection when this request
|
||||
is received. The :func:`.create_engine` call itself does **not**
|
||||
establish any actual DBAPI connections directly.
|
||||
|
||||
See also:
|
||||
|
||||
:ref:`engines_toplevel`
|
||||
|
||||
:ref:`connections_toplevel`
|
||||
|
||||
:param assert_unicode: Deprecated. This flag
|
||||
sets an engine-wide default value for
|
||||
the ``assert_unicode`` flag on the
|
||||
:class:`.String` type - see that
|
||||
type for further details.
|
||||
|
||||
:param connect_args: a dictionary of options which will be
|
||||
passed directly to the DBAPI's ``connect()`` method as
|
||||
additional keyword arguments.
|
||||
additional keyword arguments. See the example
|
||||
at :ref:`custom_dbapi_args`.
|
||||
|
||||
:param convert_unicode=False: if set to True, all
|
||||
String/character based types will convert Python Unicode values to raw
|
||||
byte values sent to the DBAPI as bind parameters, and all raw byte values to
|
||||
Python Unicode coming out in result sets. This is an
|
||||
engine-wide method to provide Unicode conversion across the
|
||||
board for those DBAPIs that do not accept Python Unicode objects
|
||||
as input. For Unicode conversion on a column-by-column level, use
|
||||
the ``Unicode`` column type instead, described in :ref:`types_toplevel`. Note that
|
||||
many DBAPIs have the ability to return Python Unicode objects in
|
||||
result sets directly - SQLAlchemy will use these modes of operation
|
||||
if possible and will also attempt to detect "Unicode returns"
|
||||
behavior by the DBAPI upon first connect by the
|
||||
:class:`.Engine`. When this is detected, string values in
|
||||
result sets are passed through without further processing.
|
||||
:param convert_unicode=False: if set to True, sets
|
||||
the default behavior of ``convert_unicode`` on the
|
||||
:class:`.String` type to ``True``, regardless
|
||||
of a setting of ``False`` on an individual
|
||||
:class:`.String` type, thus causing all :class:`.String`
|
||||
-based columns
|
||||
to accommodate Python ``unicode`` objects. This flag
|
||||
is useful as an engine-wide setting when using a
|
||||
DBAPI that does not natively support Python
|
||||
``unicode`` objects and raises an error when
|
||||
one is received (such as pyodbc with FreeTDS).
|
||||
|
||||
See :class:`.String` for further details on
|
||||
what this flag indicates.
|
||||
|
||||
:param creator: a callable which returns a DBAPI connection.
|
||||
This creation function will be passed to the underlying
|
||||
@@ -167,9 +184,50 @@ def create_engine(*args, **kwargs):
|
||||
:ref:`dbengine_logging` for information on how to configure logging
|
||||
directly.
|
||||
|
||||
:param encoding='utf-8': the encoding to use for all Unicode
|
||||
translations, both by engine-wide unicode conversion as well as
|
||||
the ``Unicode`` type object.
|
||||
:param encoding: Defaults to ``utf-8``. This is the string
|
||||
encoding used by SQLAlchemy for string encode/decode
|
||||
operations which occur within SQLAlchemy, **outside of
|
||||
the DBAPI.** Most modern DBAPIs feature some degree of
|
||||
direct support for Python ``unicode`` objects,
|
||||
what you see in Python 2 as a string of the form
|
||||
``u'some string'``. For those scenarios where the
|
||||
DBAPI is detected as not supporting a Python ``unicode``
|
||||
object, this encoding is used to determine the
|
||||
source/destination encoding. It is **not used**
|
||||
for those cases where the DBAPI handles unicode
|
||||
directly.
|
||||
|
||||
To properly configure a system to accommodate Python
|
||||
``unicode`` objects, the DBAPI should be
|
||||
configured to handle unicode to the greatest
|
||||
degree as is appropriate - see
|
||||
the notes on unicode pertaining to the specific
|
||||
target database in use at :ref:`dialect_toplevel`.
|
||||
|
||||
Areas where string encoding may need to be accommodated
|
||||
outside of the DBAPI include zero or more of:
|
||||
|
||||
* the values passed to bound parameters, corresponding to
|
||||
the :class:`.Unicode` type or the :class:`.String` type
|
||||
when ``convert_unicode`` is ``True``;
|
||||
* the values returned in result set columns corresponding
|
||||
to the :class:`.Unicode` type or the :class:`.String`
|
||||
type when ``convert_unicode`` is ``True``;
|
||||
* the string SQL statement passed to the DBAPI's
|
||||
``cursor.execute()`` method;
|
||||
* the string names of the keys in the bound parameter
|
||||
dictionary passed to the DBAPI's ``cursor.execute()``
|
||||
as well as ``cursor.setinputsizes()`` methods;
|
||||
* the string column names retrieved from the DBAPI's
|
||||
``cursor.description`` attribute.
|
||||
|
||||
When using Python 3, the DBAPI is required to support
|
||||
*all* of the above values as Python ``unicode`` objects,
|
||||
which in Python 3 are just known as ``str``. In Python 2,
|
||||
the DBAPI does not specify unicode behavior at all,
|
||||
so SQLAlchemy must make decisions for each of the above
|
||||
values on a per-DBAPI basis - implementations are
|
||||
completely inconsistent in their behavior.
|
||||
|
||||
:param execution_options: Dictionary execution options which will
|
||||
be applied to all connections. See
|
||||
|
||||
+927
-411
File diff suppressed because it is too large
Load Diff
@@ -21,35 +21,63 @@ class SchemaGenerator(DDLBase):
|
||||
self.tables = tables and set(tables) or None
|
||||
self.preparer = dialect.identifier_preparer
|
||||
self.dialect = dialect
|
||||
self.memo = {}
|
||||
|
||||
def _can_create(self, table):
|
||||
def _can_create_table(self, table):
|
||||
self.dialect.validate_identifier(table.name)
|
||||
if table.schema:
|
||||
self.dialect.validate_identifier(table.schema)
|
||||
return not self.checkfirst or not self.dialect.has_table(self.connection, table.name, schema=table.schema)
|
||||
return not self.checkfirst or \
|
||||
not self.dialect.has_table(self.connection,
|
||||
table.name, schema=table.schema)
|
||||
|
||||
def _can_create_sequence(self, sequence):
|
||||
return self.dialect.supports_sequences and \
|
||||
(
|
||||
(not self.dialect.sequences_optional or
|
||||
not sequence.optional) and
|
||||
(
|
||||
not self.checkfirst or
|
||||
not self.dialect.has_sequence(
|
||||
self.connection,
|
||||
sequence.name,
|
||||
schema=sequence.schema)
|
||||
)
|
||||
)
|
||||
|
||||
def visit_metadata(self, metadata):
|
||||
if self.tables:
|
||||
tables = self.tables
|
||||
else:
|
||||
tables = metadata.tables.values()
|
||||
collection = [t for t in sql_util.sort_tables(tables) if self._can_create(t)]
|
||||
collection = [t for t in sql_util.sort_tables(tables)
|
||||
if self._can_create_table(t)]
|
||||
seq_coll = [s for s in metadata._sequences.values()
|
||||
if s.column is None and self._can_create_sequence(s)]
|
||||
|
||||
for listener in metadata.ddl_listeners['before-create']:
|
||||
listener('before-create', metadata, self.connection, tables=collection)
|
||||
metadata.dispatch.before_create(metadata, self.connection,
|
||||
tables=collection,
|
||||
checkfirst=self.checkfirst,
|
||||
_ddl_runner=self)
|
||||
|
||||
for seq in seq_coll:
|
||||
self.traverse_single(seq, create_ok=True)
|
||||
|
||||
for table in collection:
|
||||
self.traverse_single(table, create_ok=True)
|
||||
|
||||
for listener in metadata.ddl_listeners['after-create']:
|
||||
listener('after-create', metadata, self.connection, tables=collection)
|
||||
metadata.dispatch.after_create(metadata, self.connection,
|
||||
tables=collection,
|
||||
checkfirst=self.checkfirst,
|
||||
_ddl_runner=self)
|
||||
|
||||
def visit_table(self, table, create_ok=False):
|
||||
if not create_ok and not self._can_create(table):
|
||||
if not create_ok and not self._can_create_table(table):
|
||||
return
|
||||
|
||||
for listener in table.ddl_listeners['before-create']:
|
||||
listener('before-create', table, self.connection)
|
||||
table.dispatch.before_create(table, self.connection,
|
||||
checkfirst=self.checkfirst,
|
||||
_ddl_runner=self)
|
||||
|
||||
for column in table.columns:
|
||||
if column.default is not None:
|
||||
@@ -61,16 +89,14 @@ class SchemaGenerator(DDLBase):
|
||||
for index in table.indexes:
|
||||
self.traverse_single(index)
|
||||
|
||||
for listener in table.ddl_listeners['after-create']:
|
||||
listener('after-create', table, self.connection)
|
||||
table.dispatch.after_create(table, self.connection,
|
||||
checkfirst=self.checkfirst,
|
||||
_ddl_runner=self)
|
||||
|
||||
def visit_sequence(self, sequence):
|
||||
if self.dialect.supports_sequences:
|
||||
if ((not self.dialect.sequences_optional or
|
||||
not sequence.optional) and
|
||||
(not self.checkfirst or
|
||||
not self.dialect.has_sequence(self.connection, sequence.name, schema=sequence.schema))):
|
||||
self.connection.execute(schema.CreateSequence(sequence))
|
||||
def visit_sequence(self, sequence, create_ok=False):
|
||||
if not create_ok and not self._can_create_sequence(sequence):
|
||||
return
|
||||
self.connection.execute(schema.CreateSequence(sequence))
|
||||
|
||||
def visit_index(self, index):
|
||||
self.connection.execute(schema.CreateIndex(index))
|
||||
@@ -83,38 +109,62 @@ class SchemaDropper(DDLBase):
|
||||
self.tables = tables
|
||||
self.preparer = dialect.identifier_preparer
|
||||
self.dialect = dialect
|
||||
self.memo = {}
|
||||
|
||||
def visit_metadata(self, metadata):
|
||||
if self.tables:
|
||||
tables = self.tables
|
||||
else:
|
||||
tables = metadata.tables.values()
|
||||
collection = [t for t in reversed(sql_util.sort_tables(tables)) if self._can_drop(t)]
|
||||
collection = [t for t in reversed(sql_util.sort_tables(tables))
|
||||
if self._can_drop_table(t)]
|
||||
seq_coll = [s for s in metadata._sequences.values()
|
||||
if s.column is None and self._can_drop_sequence(s)]
|
||||
|
||||
for listener in metadata.ddl_listeners['before-drop']:
|
||||
listener('before-drop', metadata, self.connection, tables=collection)
|
||||
metadata.dispatch.before_drop(metadata, self.connection,
|
||||
tables=collection,
|
||||
checkfirst=self.checkfirst,
|
||||
_ddl_runner=self)
|
||||
|
||||
for table in collection:
|
||||
self.traverse_single(table, drop_ok=True)
|
||||
|
||||
for listener in metadata.ddl_listeners['after-drop']:
|
||||
listener('after-drop', metadata, self.connection, tables=collection)
|
||||
for seq in seq_coll:
|
||||
self.traverse_single(seq, drop_ok=True)
|
||||
|
||||
def _can_drop(self, table):
|
||||
metadata.dispatch.after_drop(metadata, self.connection,
|
||||
tables=collection,
|
||||
checkfirst=self.checkfirst,
|
||||
_ddl_runner=self)
|
||||
|
||||
def _can_drop_table(self, table):
|
||||
self.dialect.validate_identifier(table.name)
|
||||
if table.schema:
|
||||
self.dialect.validate_identifier(table.schema)
|
||||
return not self.checkfirst or self.dialect.has_table(self.connection, table.name, schema=table.schema)
|
||||
return not self.checkfirst or self.dialect.has_table(self.connection,
|
||||
table.name, schema=table.schema)
|
||||
|
||||
def _can_drop_sequence(self, sequence):
|
||||
return self.dialect.supports_sequences and \
|
||||
((not self.dialect.sequences_optional or
|
||||
not sequence.optional) and
|
||||
(not self.checkfirst or
|
||||
self.dialect.has_sequence(
|
||||
self.connection,
|
||||
sequence.name,
|
||||
schema=sequence.schema))
|
||||
)
|
||||
|
||||
def visit_index(self, index):
|
||||
self.connection.execute(schema.DropIndex(index))
|
||||
|
||||
def visit_table(self, table, drop_ok=False):
|
||||
if not drop_ok and not self._can_drop(table):
|
||||
if not drop_ok and not self._can_drop_table(table):
|
||||
return
|
||||
|
||||
for listener in table.ddl_listeners['before-drop']:
|
||||
listener('before-drop', table, self.connection)
|
||||
table.dispatch.before_drop(table, self.connection,
|
||||
checkfirst=self.checkfirst,
|
||||
_ddl_runner=self)
|
||||
|
||||
for column in table.columns:
|
||||
if column.default is not None:
|
||||
@@ -122,13 +172,11 @@ class SchemaDropper(DDLBase):
|
||||
|
||||
self.connection.execute(schema.DropTable(table))
|
||||
|
||||
for listener in table.ddl_listeners['after-drop']:
|
||||
listener('after-drop', table, self.connection)
|
||||
table.dispatch.after_drop(table, self.connection,
|
||||
checkfirst=self.checkfirst,
|
||||
_ddl_runner=self)
|
||||
|
||||
def visit_sequence(self, sequence):
|
||||
if self.dialect.supports_sequences:
|
||||
if ((not self.dialect.sequences_optional or
|
||||
not sequence.optional) and
|
||||
(not self.checkfirst or
|
||||
self.dialect.has_sequence(self.connection, sequence.name, schema=sequence.schema))):
|
||||
self.connection.execute(schema.DropSequence(sequence))
|
||||
def visit_sequence(self, sequence, drop_ok=False):
|
||||
if not drop_ok and not self._can_drop_sequence(sequence):
|
||||
return
|
||||
self.connection.execute(schema.DropSequence(sequence))
|
||||
|
||||
+287
-219
@@ -1,5 +1,5 @@
|
||||
# engine/default.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -15,7 +15,9 @@ as the base class for their own corresponding classes.
|
||||
import re, random
|
||||
from sqlalchemy.engine import base, reflection
|
||||
from sqlalchemy.sql import compiler, expression
|
||||
from sqlalchemy import exc, types as sqltypes, util
|
||||
from sqlalchemy import exc, types as sqltypes, util, pool, processors
|
||||
import codecs
|
||||
import weakref
|
||||
|
||||
AUTOCOMMIT_REGEXP = re.compile(
|
||||
r'\s*(?:UPDATE|INSERT|CREATE|DELETE|DROP|ALTER)',
|
||||
@@ -35,6 +37,7 @@ class DefaultDialect(base.Dialect):
|
||||
# not cx_oracle.
|
||||
execute_sequence_format = tuple
|
||||
|
||||
supports_views = True
|
||||
supports_sequences = False
|
||||
sequences_optional = False
|
||||
preexecute_autoincrement_sequences = False
|
||||
@@ -52,12 +55,16 @@ class DefaultDialect(base.Dialect):
|
||||
# Py3K
|
||||
#supports_unicode_statements = True
|
||||
#supports_unicode_binds = True
|
||||
#returns_unicode_strings = True
|
||||
#description_encoding = None
|
||||
# Py2K
|
||||
supports_unicode_statements = False
|
||||
supports_unicode_binds = False
|
||||
returns_unicode_strings = False
|
||||
description_encoding = 'use_encoding'
|
||||
# end Py2K
|
||||
|
||||
|
||||
name = 'default'
|
||||
|
||||
# length at which to truncate
|
||||
@@ -97,7 +104,7 @@ class DefaultDialect(base.Dialect):
|
||||
|
||||
if not getattr(self, 'ported_sqla_06', True):
|
||||
util.warn(
|
||||
"The %s dialect is not yet ported to SQLAlchemy 0.6" %
|
||||
"The %s dialect is not yet ported to SQLAlchemy 0.6/0.7" %
|
||||
self.name)
|
||||
|
||||
self.convert_unicode = convert_unicode
|
||||
@@ -134,16 +141,29 @@ class DefaultDialect(base.Dialect):
|
||||
(label_length, self.max_identifier_length))
|
||||
self.label_length = label_length
|
||||
|
||||
if not hasattr(self, 'description_encoding'):
|
||||
self.description_encoding = getattr(
|
||||
self,
|
||||
'description_encoding',
|
||||
encoding)
|
||||
if self.description_encoding == 'use_encoding':
|
||||
self._description_decoder = processors.to_unicode_processor_factory(
|
||||
encoding
|
||||
)
|
||||
elif self.description_encoding is not None:
|
||||
self._description_decoder = processors.to_unicode_processor_factory(
|
||||
self.description_encoding
|
||||
)
|
||||
self._encoder = codecs.getencoder(self.encoding)
|
||||
self._decoder = processors.to_unicode_processor_factory(self.encoding)
|
||||
|
||||
@util.memoized_property
|
||||
def _type_memos(self):
|
||||
return weakref.WeakKeyDictionary()
|
||||
|
||||
@property
|
||||
def dialect_description(self):
|
||||
return self.name + "+" + self.driver
|
||||
|
||||
@classmethod
|
||||
def get_pool_class(cls, url):
|
||||
return getattr(cls, 'poolclass', pool.QueuePool)
|
||||
|
||||
def initialize(self, connection):
|
||||
try:
|
||||
self.server_version_info = \
|
||||
@@ -156,6 +176,12 @@ class DefaultDialect(base.Dialect):
|
||||
except NotImplementedError:
|
||||
self.default_schema_name = None
|
||||
|
||||
try:
|
||||
self.default_isolation_level = \
|
||||
self.get_isolation_level(connection.connection)
|
||||
except NotImplementedError:
|
||||
self.default_isolation_level = None
|
||||
|
||||
self.returns_unicode_strings = self._check_unicode_returns(connection)
|
||||
|
||||
self.do_rollback(connection.connection)
|
||||
@@ -183,29 +209,34 @@ class DefaultDialect(base.Dialect):
|
||||
# end Py2K
|
||||
# Py3K
|
||||
#cast_to = str
|
||||
def check_unicode(type_):
|
||||
def check_unicode(formatstr, type_):
|
||||
cursor = connection.connection.cursor()
|
||||
try:
|
||||
cursor.execute(
|
||||
cast_to(
|
||||
expression.select(
|
||||
[expression.cast(
|
||||
expression.literal_column(
|
||||
"'test unicode returns'"), type_)
|
||||
]).compile(dialect=self)
|
||||
try:
|
||||
cursor.execute(
|
||||
cast_to(
|
||||
expression.select(
|
||||
[expression.cast(
|
||||
expression.literal_column(
|
||||
"'test %s returns'" % formatstr), type_)
|
||||
]).compile(dialect=self)
|
||||
)
|
||||
)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
row = cursor.fetchone()
|
||||
|
||||
return isinstance(row[0], unicode)
|
||||
return isinstance(row[0], unicode)
|
||||
except self.dbapi.Error, de:
|
||||
util.warn("Exception attempting to "
|
||||
"detect unicode returns: %r" % de)
|
||||
return False
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
# detect plain VARCHAR
|
||||
unicode_for_varchar = check_unicode(sqltypes.VARCHAR(60))
|
||||
unicode_for_varchar = check_unicode("plain", sqltypes.VARCHAR(60))
|
||||
|
||||
# detect if there's an NVARCHAR type with different behavior available
|
||||
unicode_for_unicode = check_unicode(sqltypes.Unicode(60))
|
||||
unicode_for_unicode = check_unicode("unicode", sqltypes.Unicode(60))
|
||||
|
||||
if unicode_for_unicode and not unicode_for_varchar:
|
||||
return "conditional"
|
||||
@@ -223,9 +254,9 @@ class DefaultDialect(base.Dialect):
|
||||
"""
|
||||
return sqltypes.adapt_type(typeobj, self.colspecs)
|
||||
|
||||
def reflecttable(self, connection, table, include_columns):
|
||||
def reflecttable(self, connection, table, include_columns, exclude_columns=None):
|
||||
insp = reflection.Inspector.from_engine(connection)
|
||||
return insp.reflecttable(table, include_columns)
|
||||
return insp.reflecttable(table, include_columns, exclude_columns)
|
||||
|
||||
def get_pk_constraint(self, conn, table_name, schema=None, **kw):
|
||||
"""Compatiblity method, adapts the result of get_primary_keys()
|
||||
@@ -298,12 +329,16 @@ class DefaultDialect(base.Dialect):
|
||||
def do_execute(self, cursor, statement, parameters, context=None):
|
||||
cursor.execute(statement, parameters)
|
||||
|
||||
def is_disconnect(self, e):
|
||||
def is_disconnect(self, e, connection, cursor):
|
||||
return False
|
||||
|
||||
def reset_isolation_level(self, dbapi_conn):
|
||||
# default_isolation_level is read from the first conenction
|
||||
# after the initial set of 'isolation_level', if any, so is
|
||||
# the configured default of this dialect.
|
||||
self.set_isolation_level(dbapi_conn, self.default_isolation_level)
|
||||
|
||||
class DefaultExecutionContext(base.ExecutionContext):
|
||||
execution_options = util.frozendict()
|
||||
isinsert = False
|
||||
isupdate = False
|
||||
isdelete = False
|
||||
@@ -312,107 +347,190 @@ class DefaultExecutionContext(base.ExecutionContext):
|
||||
result_map = None
|
||||
compiled = None
|
||||
statement = None
|
||||
_is_implicit_returning = False
|
||||
_is_explicit_returning = False
|
||||
|
||||
def __init__(self,
|
||||
dialect,
|
||||
connection,
|
||||
compiled_sql=None,
|
||||
compiled_ddl=None,
|
||||
statement=None,
|
||||
parameters=None):
|
||||
@classmethod
|
||||
def _init_ddl(cls, dialect, connection, dbapi_connection, compiled_ddl):
|
||||
"""Initialize execution context for a DDLElement construct."""
|
||||
|
||||
self = cls.__new__(cls)
|
||||
self.dialect = dialect
|
||||
self._connection = self.root_connection = connection
|
||||
self.root_connection = connection
|
||||
self._dbapi_connection = dbapi_connection
|
||||
self.engine = connection.engine
|
||||
|
||||
if compiled_ddl is not None:
|
||||
self.compiled = compiled = compiled_ddl
|
||||
self.isddl = True
|
||||
self.compiled = compiled = compiled_ddl
|
||||
self.isddl = True
|
||||
|
||||
if compiled.statement._execution_options:
|
||||
self.execution_options = compiled.statement._execution_options
|
||||
if connection._execution_options:
|
||||
self.execution_options = self.execution_options.union(
|
||||
connection._execution_options
|
||||
)
|
||||
self.execution_options = compiled.statement._execution_options
|
||||
if connection._execution_options:
|
||||
self.execution_options = dict(self.execution_options)
|
||||
self.execution_options.update(connection._execution_options)
|
||||
|
||||
if not dialect.supports_unicode_statements:
|
||||
self.unicode_statement = unicode(compiled)
|
||||
self.statement = self.unicode_statement.encode(self.dialect.encoding)
|
||||
else:
|
||||
self.statement = self.unicode_statement = unicode(compiled)
|
||||
if not dialect.supports_unicode_statements:
|
||||
self.unicode_statement = unicode(compiled)
|
||||
self.statement = dialect._encoder(self.unicode_statement)[0]
|
||||
else:
|
||||
self.statement = self.unicode_statement = unicode(compiled)
|
||||
|
||||
self.cursor = self.create_cursor()
|
||||
self.compiled_parameters = []
|
||||
self.parameters = [self._default_params]
|
||||
self.cursor = self.create_cursor()
|
||||
self.compiled_parameters = []
|
||||
|
||||
elif compiled_sql is not None:
|
||||
self.compiled = compiled = compiled_sql
|
||||
if dialect.positional:
|
||||
self.parameters = [dialect.execute_sequence_format()]
|
||||
else:
|
||||
self.parameters = [{}]
|
||||
|
||||
if not compiled.can_execute:
|
||||
raise exc.ArgumentError("Not an executable clause: %s" % compiled)
|
||||
return self
|
||||
|
||||
if compiled.statement._execution_options:
|
||||
self.execution_options = compiled.statement._execution_options
|
||||
if connection._execution_options:
|
||||
self.execution_options = self.execution_options.union(
|
||||
connection._execution_options
|
||||
)
|
||||
@classmethod
|
||||
def _init_compiled(cls, dialect, connection, dbapi_connection, compiled, parameters):
|
||||
"""Initialize execution context for a Compiled construct."""
|
||||
|
||||
# compiled clauseelement. process bind params, process table defaults,
|
||||
# track collections used by ResultProxy to target and process results
|
||||
self = cls.__new__(cls)
|
||||
self.dialect = dialect
|
||||
self.root_connection = connection
|
||||
self._dbapi_connection = dbapi_connection
|
||||
self.engine = connection.engine
|
||||
|
||||
self.processors = dict(
|
||||
(key, value) for key, value in
|
||||
( (compiled.bind_names[bindparam],
|
||||
bindparam.bind_processor(self.dialect))
|
||||
for bindparam in compiled.bind_names )
|
||||
if value is not None)
|
||||
self.compiled = compiled
|
||||
|
||||
self.result_map = compiled.result_map
|
||||
if not compiled.can_execute:
|
||||
raise exc.ArgumentError("Not an executable clause")
|
||||
|
||||
if not dialect.supports_unicode_statements:
|
||||
self.unicode_statement = unicode(compiled)
|
||||
self.statement = self.unicode_statement.encode(self.dialect.encoding)
|
||||
else:
|
||||
self.statement = self.unicode_statement = unicode(compiled)
|
||||
self.execution_options = compiled.statement._execution_options
|
||||
if connection._execution_options:
|
||||
self.execution_options = dict(self.execution_options)
|
||||
self.execution_options.update(connection._execution_options)
|
||||
|
||||
self.isinsert = compiled.isinsert
|
||||
self.isupdate = compiled.isupdate
|
||||
self.isdelete = compiled.isdelete
|
||||
# compiled clauseelement. process bind params, process table defaults,
|
||||
# track collections used by ResultProxy to target and process results
|
||||
|
||||
if not parameters:
|
||||
self.compiled_parameters = [compiled.construct_params()]
|
||||
else:
|
||||
self.compiled_parameters = [compiled.construct_params(m, _group_number=grp) for
|
||||
grp,m in enumerate(parameters)]
|
||||
self.result_map = compiled.result_map
|
||||
|
||||
self.executemany = len(parameters) > 1
|
||||
self.unicode_statement = unicode(compiled)
|
||||
if not dialect.supports_unicode_statements:
|
||||
self.statement = self.unicode_statement.encode(self.dialect.encoding)
|
||||
else:
|
||||
self.statement = self.unicode_statement
|
||||
|
||||
self.cursor = self.create_cursor()
|
||||
if self.isinsert or self.isupdate:
|
||||
self.__process_defaults()
|
||||
self.parameters = self.__convert_compiled_params(self.compiled_parameters)
|
||||
self.isinsert = compiled.isinsert
|
||||
self.isupdate = compiled.isupdate
|
||||
self.isdelete = compiled.isdelete
|
||||
|
||||
if self.isinsert or self.isupdate or self.isdelete:
|
||||
self._is_explicit_returning = compiled.statement._returning
|
||||
self._is_implicit_returning = compiled.returning and \
|
||||
not compiled.statement._returning
|
||||
|
||||
if not parameters:
|
||||
self.compiled_parameters = [compiled.construct_params()]
|
||||
else:
|
||||
self.compiled_parameters = \
|
||||
[compiled.construct_params(m, _group_number=grp) for
|
||||
grp,m in enumerate(parameters)]
|
||||
|
||||
elif statement is not None:
|
||||
# plain text statement
|
||||
if connection._execution_options:
|
||||
self.execution_options = self.execution_options.union(connection._execution_options)
|
||||
self.parameters = self.__encode_param_keys(parameters)
|
||||
self.executemany = len(parameters) > 1
|
||||
|
||||
if isinstance(statement, unicode) and not dialect.supports_unicode_statements:
|
||||
self.unicode_statement = statement
|
||||
self.statement = statement.encode(self.dialect.encoding)
|
||||
else:
|
||||
self.statement = self.unicode_statement = statement
|
||||
self.cursor = self.create_cursor()
|
||||
if self.isinsert or self.isupdate:
|
||||
self.postfetch_cols = self.compiled.postfetch
|
||||
self.prefetch_cols = self.compiled.prefetch
|
||||
self.__process_defaults()
|
||||
|
||||
self.cursor = self.create_cursor()
|
||||
processors = compiled._bind_processors
|
||||
|
||||
# Convert the dictionary of bind parameter values
|
||||
# into a dict or list to be sent to the DBAPI's
|
||||
# execute() or executemany() method.
|
||||
parameters = []
|
||||
if dialect.positional:
|
||||
for compiled_params in self.compiled_parameters:
|
||||
param = []
|
||||
for key in self.compiled.positiontup:
|
||||
if key in processors:
|
||||
param.append(processors[key](compiled_params[key]))
|
||||
else:
|
||||
param.append(compiled_params[key])
|
||||
parameters.append(dialect.execute_sequence_format(param))
|
||||
else:
|
||||
# no statement. used for standalone ColumnDefault execution.
|
||||
if connection._execution_options:
|
||||
self.execution_options = self.execution_options.union(connection._execution_options)
|
||||
self.cursor = self.create_cursor()
|
||||
encode = not dialect.supports_unicode_statements
|
||||
for compiled_params in self.compiled_parameters:
|
||||
param = {}
|
||||
if encode:
|
||||
for key in compiled_params:
|
||||
if key in processors:
|
||||
param[dialect._encoder(key)[0]] = \
|
||||
processors[key](compiled_params[key])
|
||||
else:
|
||||
param[dialect._encoder(key)[0]] = compiled_params[key]
|
||||
else:
|
||||
for key in compiled_params:
|
||||
if key in processors:
|
||||
param[key] = processors[key](compiled_params[key])
|
||||
else:
|
||||
param[key] = compiled_params[key]
|
||||
parameters.append(param)
|
||||
self.parameters = dialect.execute_sequence_format(parameters)
|
||||
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def _init_statement(cls, dialect, connection, dbapi_connection, statement, parameters):
|
||||
"""Initialize execution context for a string SQL statement."""
|
||||
|
||||
self = cls.__new__(cls)
|
||||
self.dialect = dialect
|
||||
self.root_connection = connection
|
||||
self._dbapi_connection = dbapi_connection
|
||||
self.engine = connection.engine
|
||||
|
||||
# plain text statement
|
||||
self.execution_options = connection._execution_options
|
||||
|
||||
if not parameters:
|
||||
if self.dialect.positional:
|
||||
self.parameters = [dialect.execute_sequence_format()]
|
||||
else:
|
||||
self.parameters = [{}]
|
||||
elif isinstance(parameters[0], dialect.execute_sequence_format):
|
||||
self.parameters = parameters
|
||||
elif isinstance(parameters[0], dict):
|
||||
if dialect.supports_unicode_statements:
|
||||
self.parameters = parameters
|
||||
else:
|
||||
self.parameters= [
|
||||
dict((dialect._encoder(k)[0], d[k]) for k in d)
|
||||
for d in parameters
|
||||
] or [{}]
|
||||
else:
|
||||
self.parameters = [dialect.execute_sequence_format(p)
|
||||
for p in parameters]
|
||||
|
||||
self.executemany = len(parameters) > 1
|
||||
|
||||
if not dialect.supports_unicode_statements and isinstance(statement, unicode):
|
||||
self.unicode_statement = statement
|
||||
self.statement = dialect._encoder(statement)[0]
|
||||
else:
|
||||
self.statement = self.unicode_statement = statement
|
||||
|
||||
self.cursor = self.create_cursor()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def _init_default(cls, dialect, connection, dbapi_connection):
|
||||
"""Initialize execution context for a ColumnDefault construct."""
|
||||
|
||||
self = cls.__new__(cls)
|
||||
self.dialect = dialect
|
||||
self.root_connection = connection
|
||||
self._dbapi_connection = dbapi_connection
|
||||
self.engine = connection.engine
|
||||
self.execution_options = connection._execution_options
|
||||
self.cursor = self.create_cursor()
|
||||
return self
|
||||
|
||||
@util.memoized_property
|
||||
def is_crud(self):
|
||||
@@ -431,25 +549,7 @@ class DefaultExecutionContext(base.ExecutionContext):
|
||||
else:
|
||||
return autocommit
|
||||
|
||||
@util.memoized_property
|
||||
def _is_explicit_returning(self):
|
||||
return self.compiled and \
|
||||
getattr(self.compiled.statement, '_returning', False)
|
||||
|
||||
@util.memoized_property
|
||||
def _is_implicit_returning(self):
|
||||
return self.compiled and \
|
||||
bool(self.compiled.returning) and \
|
||||
not self.compiled.statement._returning
|
||||
|
||||
@util.memoized_property
|
||||
def _default_params(self):
|
||||
if self.dialect.positional:
|
||||
return self.dialect.execute_sequence_format()
|
||||
else:
|
||||
return {}
|
||||
|
||||
def _execute_scalar(self, stmt):
|
||||
def _execute_scalar(self, stmt, type_):
|
||||
"""Execute a string statement on the current cursor, returning a
|
||||
scalar result.
|
||||
|
||||
@@ -459,79 +559,37 @@ class DefaultExecutionContext(base.ExecutionContext):
|
||||
|
||||
"""
|
||||
|
||||
conn = self._connection
|
||||
if isinstance(stmt, unicode) and not self.dialect.supports_unicode_statements:
|
||||
stmt = stmt.encode(self.dialect.encoding)
|
||||
conn._cursor_execute(self.cursor, stmt, self._default_params)
|
||||
return self.cursor.fetchone()[0]
|
||||
conn = self.root_connection
|
||||
if isinstance(stmt, unicode) and \
|
||||
not self.dialect.supports_unicode_statements:
|
||||
stmt = self.dialect._encoder(stmt)[0]
|
||||
|
||||
if self.dialect.positional:
|
||||
default_params = self.dialect.execute_sequence_format()
|
||||
else:
|
||||
default_params = {}
|
||||
|
||||
conn._cursor_execute(self.cursor, stmt, default_params)
|
||||
r = self.cursor.fetchone()[0]
|
||||
if type_ is not None:
|
||||
# apply type post processors to the result
|
||||
proc = type_._cached_result_processor(
|
||||
self.dialect,
|
||||
self.cursor.description[0][1]
|
||||
)
|
||||
if proc:
|
||||
return proc(r)
|
||||
return r
|
||||
|
||||
@property
|
||||
def connection(self):
|
||||
return self._connection._branch()
|
||||
|
||||
def __encode_param_keys(self, params):
|
||||
"""Apply string encoding to the keys of dictionary-based bind parameters.
|
||||
|
||||
This is only used executing textual, non-compiled SQL expressions.
|
||||
|
||||
"""
|
||||
|
||||
if not params:
|
||||
return [self._default_params]
|
||||
elif isinstance(params[0], self.dialect.execute_sequence_format):
|
||||
return params
|
||||
elif isinstance(params[0], dict):
|
||||
if self.dialect.supports_unicode_statements:
|
||||
return params
|
||||
else:
|
||||
def proc(d):
|
||||
return dict((k.encode(self.dialect.encoding), d[k]) for k in d)
|
||||
return [proc(d) for d in params] or [{}]
|
||||
else:
|
||||
return [self.dialect.execute_sequence_format(p) for p in params]
|
||||
|
||||
|
||||
def __convert_compiled_params(self, compiled_parameters):
|
||||
"""Convert the dictionary of bind parameter values into a dict or list
|
||||
to be sent to the DBAPI's execute() or executemany() method.
|
||||
"""
|
||||
|
||||
processors = self.processors
|
||||
parameters = []
|
||||
if self.dialect.positional:
|
||||
for compiled_params in compiled_parameters:
|
||||
param = []
|
||||
for key in self.compiled.positiontup:
|
||||
if key in processors:
|
||||
param.append(processors[key](compiled_params[key]))
|
||||
else:
|
||||
param.append(compiled_params[key])
|
||||
parameters.append(self.dialect.execute_sequence_format(param))
|
||||
else:
|
||||
encode = not self.dialect.supports_unicode_statements
|
||||
for compiled_params in compiled_parameters:
|
||||
param = {}
|
||||
if encode:
|
||||
encoding = self.dialect.encoding
|
||||
for key in compiled_params:
|
||||
if key in processors:
|
||||
param[key.encode(encoding)] = processors[key](compiled_params[key])
|
||||
else:
|
||||
param[key.encode(encoding)] = compiled_params[key]
|
||||
else:
|
||||
for key in compiled_params:
|
||||
if key in processors:
|
||||
param[key] = processors[key](compiled_params[key])
|
||||
else:
|
||||
param[key] = compiled_params[key]
|
||||
parameters.append(param)
|
||||
return self.dialect.execute_sequence_format(parameters)
|
||||
return self.root_connection._branch()
|
||||
|
||||
def should_autocommit_text(self, statement):
|
||||
return AUTOCOMMIT_REGEXP.match(statement)
|
||||
|
||||
def create_cursor(self):
|
||||
return self._connection.connection.cursor()
|
||||
return self._dbapi_connection.cursor()
|
||||
|
||||
def pre_exec(self):
|
||||
pass
|
||||
@@ -584,14 +642,26 @@ class DefaultExecutionContext(base.ExecutionContext):
|
||||
return self.dialect.supports_sane_multi_rowcount
|
||||
|
||||
def post_insert(self):
|
||||
if self.dialect.postfetch_lastrowid and \
|
||||
(not len(self._inserted_primary_key) or \
|
||||
None in self._inserted_primary_key):
|
||||
if not self._is_implicit_returning and \
|
||||
self.dialect.postfetch_lastrowid and \
|
||||
(not self.inserted_primary_key or \
|
||||
None in self.inserted_primary_key):
|
||||
|
||||
table = self.compiled.statement.table
|
||||
lastrowid = self.get_lastrowid()
|
||||
self._inserted_primary_key = [c is table._autoincrement_column and lastrowid or v
|
||||
for c, v in zip(table.primary_key, self._inserted_primary_key)
|
||||
|
||||
autoinc_col = table._autoincrement_column
|
||||
if autoinc_col is not None:
|
||||
# apply type post processors to the lastrowid
|
||||
proc = autoinc_col.type._cached_result_processor(self.dialect, None)
|
||||
if proc is not None:
|
||||
lastrowid = proc(lastrowid)
|
||||
|
||||
self.inserted_primary_key = [
|
||||
c is autoinc_col and lastrowid or v
|
||||
for c, v in zip(
|
||||
table.primary_key,
|
||||
self.inserted_primary_key)
|
||||
]
|
||||
|
||||
def _fetch_implicit_returning(self, resultproxy):
|
||||
@@ -599,27 +669,26 @@ class DefaultExecutionContext(base.ExecutionContext):
|
||||
row = resultproxy.fetchone()
|
||||
|
||||
ipk = []
|
||||
for c, v in zip(table.primary_key, self._inserted_primary_key):
|
||||
for c, v in zip(table.primary_key, self.inserted_primary_key):
|
||||
if v is not None:
|
||||
ipk.append(v)
|
||||
else:
|
||||
ipk.append(row[c])
|
||||
|
||||
self._inserted_primary_key = ipk
|
||||
|
||||
def last_inserted_params(self):
|
||||
return self._last_inserted_params
|
||||
|
||||
def last_updated_params(self):
|
||||
return self._last_updated_params
|
||||
self.inserted_primary_key = ipk
|
||||
|
||||
def lastrow_has_defaults(self):
|
||||
return hasattr(self, 'postfetch_cols') and len(self.postfetch_cols)
|
||||
return (self.isinsert or self.isupdate) and \
|
||||
bool(self.postfetch_cols)
|
||||
|
||||
def set_input_sizes(self, translate=None, exclude_types=None):
|
||||
"""Given a cursor and ClauseParameters, call the appropriate
|
||||
style of ``setinputsizes()`` on the cursor, using DB-API types
|
||||
from the bind parameter's ``TypeEngine`` objects.
|
||||
|
||||
This method only called by those dialects which require it,
|
||||
currently cx_oracle.
|
||||
|
||||
"""
|
||||
|
||||
if not hasattr(self.compiled, 'bind_names'):
|
||||
@@ -639,7 +708,7 @@ class DefaultExecutionContext(base.ExecutionContext):
|
||||
try:
|
||||
self.cursor.setinputsizes(*inputsizes)
|
||||
except Exception, e:
|
||||
self._connection._handle_dbapi_exception(e, None, None, None, self)
|
||||
self.root_connection._handle_dbapi_exception(e, None, None, None, self)
|
||||
raise
|
||||
else:
|
||||
inputsizes = {}
|
||||
@@ -649,16 +718,16 @@ class DefaultExecutionContext(base.ExecutionContext):
|
||||
if dbtype is not None and (not exclude_types or dbtype not in exclude_types):
|
||||
if translate:
|
||||
key = translate.get(key, key)
|
||||
inputsizes[key.encode(self.dialect.encoding)] = dbtype
|
||||
inputsizes[self.dialect._encoder(key)[0]] = dbtype
|
||||
try:
|
||||
self.cursor.setinputsizes(**inputsizes)
|
||||
except Exception, e:
|
||||
self._connection._handle_dbapi_exception(e, None, None, None, self)
|
||||
self.root_connection._handle_dbapi_exception(e, None, None, None, self)
|
||||
raise
|
||||
|
||||
def _exec_default(self, default):
|
||||
def _exec_default(self, default, type_):
|
||||
if default.is_sequence:
|
||||
return self.fire_sequence(default)
|
||||
return self.fire_sequence(default, type_)
|
||||
elif default.is_callable:
|
||||
return default.arg(self)
|
||||
elif default.is_clause_element:
|
||||
@@ -674,13 +743,13 @@ class DefaultExecutionContext(base.ExecutionContext):
|
||||
if column.default is None:
|
||||
return None
|
||||
else:
|
||||
return self._exec_default(column.default)
|
||||
return self._exec_default(column.default, column.type)
|
||||
|
||||
def get_update_default(self, column):
|
||||
if column.onupdate is None:
|
||||
return None
|
||||
else:
|
||||
return self._exec_default(column.onupdate)
|
||||
return self._exec_default(column.onupdate, column.type)
|
||||
|
||||
def __process_defaults(self):
|
||||
"""Generate default values for compiled insert/update statements,
|
||||
@@ -692,8 +761,9 @@ class DefaultExecutionContext(base.ExecutionContext):
|
||||
scalar_defaults = {}
|
||||
|
||||
# pre-determine scalar Python-side defaults
|
||||
# to avoid many calls of get_insert_default()/get_update_default()
|
||||
for c in self.compiled.prefetch:
|
||||
# to avoid many calls of get_insert_default()/
|
||||
# get_update_default()
|
||||
for c in self.prefetch_cols:
|
||||
if self.isinsert and c.default and c.default.is_scalar:
|
||||
scalar_defaults[c] = c.default.arg
|
||||
elif self.isupdate and c.onupdate and c.onupdate.is_scalar:
|
||||
@@ -701,7 +771,7 @@ class DefaultExecutionContext(base.ExecutionContext):
|
||||
|
||||
for param in self.compiled_parameters:
|
||||
self.current_parameters = param
|
||||
for c in self.compiled.prefetch:
|
||||
for c in self.prefetch_cols:
|
||||
if c in scalar_defaults:
|
||||
val = scalar_defaults[c]
|
||||
elif self.isinsert:
|
||||
@@ -711,9 +781,9 @@ class DefaultExecutionContext(base.ExecutionContext):
|
||||
if val is not None:
|
||||
param[c.key] = val
|
||||
del self.current_parameters
|
||||
|
||||
else:
|
||||
self.current_parameters = compiled_parameters = self.compiled_parameters[0]
|
||||
self.current_parameters = compiled_parameters = \
|
||||
self.compiled_parameters[0]
|
||||
|
||||
for c in self.compiled.prefetch:
|
||||
if self.isinsert:
|
||||
@@ -726,13 +796,11 @@ class DefaultExecutionContext(base.ExecutionContext):
|
||||
del self.current_parameters
|
||||
|
||||
if self.isinsert:
|
||||
self._inserted_primary_key = [compiled_parameters.get(c.key, None)
|
||||
for c in self.compiled.statement.table.primary_key]
|
||||
self._last_inserted_params = compiled_parameters
|
||||
else:
|
||||
self._last_updated_params = compiled_parameters
|
||||
self.inserted_primary_key = [
|
||||
self.compiled_parameters[0].get(c.key, None)
|
||||
for c in self.compiled.\
|
||||
statement.table.primary_key
|
||||
]
|
||||
|
||||
self.postfetch_cols = self.compiled.postfetch
|
||||
self.prefetch_cols = self.compiled.prefetch
|
||||
|
||||
DefaultDialect.execution_ctx_cls = DefaultExecutionContext
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# engine/reflection.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -27,6 +27,7 @@ methods such as get_table_names, get_columns, etc.
|
||||
import sqlalchemy
|
||||
from sqlalchemy import exc, sql
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy.util import topological
|
||||
from sqlalchemy.types import TypeEngine
|
||||
from sqlalchemy import schema as sa_schema
|
||||
|
||||
@@ -80,10 +81,6 @@ class Inspector(object):
|
||||
:meth:`Inspector.from_engine`
|
||||
|
||||
"""
|
||||
|
||||
# ensure initialized
|
||||
bind.connect()
|
||||
|
||||
# this might not be a connection, it could be an engine.
|
||||
self.bind = bind
|
||||
|
||||
@@ -92,6 +89,11 @@ class Inspector(object):
|
||||
self.engine = bind.engine
|
||||
else:
|
||||
self.engine = bind
|
||||
|
||||
if self.engine is bind:
|
||||
# if engine, ensure initialized
|
||||
bind.connect().close()
|
||||
|
||||
self.dialect = self.engine.dialect
|
||||
self.info_cache = {}
|
||||
|
||||
@@ -149,27 +151,19 @@ class Inspector(object):
|
||||
|
||||
if hasattr(self.dialect, 'get_table_names'):
|
||||
tnames = self.dialect.get_table_names(self.bind,
|
||||
schema,
|
||||
info_cache=self.info_cache)
|
||||
schema, info_cache=self.info_cache)
|
||||
else:
|
||||
tnames = self.engine.table_names(schema)
|
||||
if order_by == 'foreign_key':
|
||||
ordered_tnames = tnames[:]
|
||||
# Order based on foreign key dependencies.
|
||||
import random
|
||||
random.shuffle(tnames)
|
||||
|
||||
tuples = []
|
||||
for tname in tnames:
|
||||
table_pos = tnames.index(tname)
|
||||
fkeys = self.get_foreign_keys(tname, schema)
|
||||
for fkey in fkeys:
|
||||
rtable = fkey['referred_table']
|
||||
if rtable in ordered_tnames:
|
||||
ref_pos = ordered_tnames.index(rtable)
|
||||
# Make sure it's lower in the list than anything it
|
||||
# references.
|
||||
if table_pos > ref_pos:
|
||||
ordered_tnames.pop(table_pos) # rtable moves up 1
|
||||
# insert just below rtable
|
||||
ordered_tnames.index(ref_pos, tname)
|
||||
tnames = ordered_tnames
|
||||
for fkey in self.get_foreign_keys(tname, schema):
|
||||
if tname != fkey['referred_table']:
|
||||
tuples.append((tname, fkey['referred_table']))
|
||||
tnames = list(topological.sort(tuples, tnames))
|
||||
return tnames
|
||||
|
||||
def get_table_options(self, table_name, schema=None, **kw):
|
||||
@@ -323,7 +317,7 @@ class Inspector(object):
|
||||
info_cache=self.info_cache, **kw)
|
||||
return indexes
|
||||
|
||||
def reflecttable(self, table, include_columns):
|
||||
def reflecttable(self, table, include_columns, exclude_columns=None):
|
||||
"""Given a Table object, load its internal constructs based on introspection.
|
||||
|
||||
This is the underlying method used by most dialects to produce
|
||||
@@ -345,12 +339,6 @@ class Inspector(object):
|
||||
"""
|
||||
dialect = self.bind.dialect
|
||||
|
||||
# MySQL dialect does this. Applicable with other dialects?
|
||||
if hasattr(dialect, '_connection_charset') \
|
||||
and hasattr(dialect, '_adjust_casing'):
|
||||
charset = dialect._connection_charset
|
||||
dialect._adjust_casing(table)
|
||||
|
||||
# table attributes we might need.
|
||||
reflection_options = dict(
|
||||
(k, table.kwargs.get(k)) for k in dialect.reflection_options if k in table.kwargs)
|
||||
@@ -381,24 +369,31 @@ class Inspector(object):
|
||||
found_table = False
|
||||
for col_d in self.get_columns(table_name, schema, **tblkw):
|
||||
found_table = True
|
||||
table.dispatch.column_reflect(table, col_d)
|
||||
|
||||
name = col_d['name']
|
||||
if include_columns and name not in include_columns:
|
||||
continue
|
||||
if exclude_columns and name in exclude_columns:
|
||||
continue
|
||||
|
||||
coltype = col_d['type']
|
||||
col_kw = {
|
||||
'nullable':col_d['nullable'],
|
||||
}
|
||||
if 'autoincrement' in col_d:
|
||||
col_kw['autoincrement'] = col_d['autoincrement']
|
||||
if 'quote' in col_d:
|
||||
col_kw['quote'] = col_d['quote']
|
||||
for k in ('autoincrement', 'quote', 'info', 'key'):
|
||||
if k in col_d:
|
||||
col_kw[k] = col_d[k]
|
||||
|
||||
colargs = []
|
||||
if col_d.get('default') is not None:
|
||||
# the "default" value is assumed to be a literal SQL expression,
|
||||
# so is wrapped in text() so that no quoting occurs on re-issuance.
|
||||
colargs.append(sa_schema.DefaultClause(sql.text(col_d['default'])))
|
||||
colargs.append(
|
||||
sa_schema.DefaultClause(
|
||||
sql.text(col_d['default']), _reflected=True
|
||||
)
|
||||
)
|
||||
|
||||
if 'sequence' in col_d:
|
||||
# TODO: mssql, maxdb and sybase are using this.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# engine/strategies.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -17,7 +17,7 @@ New strategies can be added via new ``EngineStrategy`` classes.
|
||||
from operator import attrgetter
|
||||
|
||||
from sqlalchemy.engine import base, threadlocal, url
|
||||
from sqlalchemy import util, exc
|
||||
from sqlalchemy import util, exc, event
|
||||
from sqlalchemy import pool as poollib
|
||||
|
||||
strategies = {}
|
||||
@@ -43,8 +43,6 @@ class EngineStrategy(object):
|
||||
class DefaultEngineStrategy(EngineStrategy):
|
||||
"""Base class for built-in stratgies."""
|
||||
|
||||
pool_threadlocal = False
|
||||
|
||||
def create(self, name_or_url, **kwargs):
|
||||
# create url.URL object
|
||||
u = url.make_url(name_or_url)
|
||||
@@ -82,16 +80,25 @@ class DefaultEngineStrategy(EngineStrategy):
|
||||
return dialect.connect(*cargs, **cparams)
|
||||
except Exception, e:
|
||||
# Py3K
|
||||
#raise exc.DBAPIError.instance(None, None, e) from e
|
||||
#raise exc.DBAPIError.instance(None, None,
|
||||
# e, dialect.dbapi.Error,
|
||||
# connection_invalidated=
|
||||
# dialect.is_disconnect(e, None, None)
|
||||
# ) from e
|
||||
# Py2K
|
||||
import sys
|
||||
raise exc.DBAPIError.instance(None, None, e), None, sys.exc_info()[2]
|
||||
raise exc.DBAPIError.instance(
|
||||
None, None, e, dialect.dbapi.Error,
|
||||
connection_invalidated=
|
||||
dialect.is_disconnect(e, None, None)), \
|
||||
None, sys.exc_info()[2]
|
||||
# end Py2K
|
||||
|
||||
creator = kwargs.pop('creator', connect)
|
||||
|
||||
poolclass = (kwargs.pop('poolclass', None) or
|
||||
getattr(dialect_cls, 'poolclass', poollib.QueuePool))
|
||||
poolclass = kwargs.pop('poolclass', None)
|
||||
if poolclass is None:
|
||||
poolclass = dialect_cls.get_pool_class(u)
|
||||
pool_args = {}
|
||||
|
||||
# consume pool arguments from kwargs, translating a few of
|
||||
@@ -100,12 +107,12 @@ class DefaultEngineStrategy(EngineStrategy):
|
||||
'echo': 'echo_pool',
|
||||
'timeout': 'pool_timeout',
|
||||
'recycle': 'pool_recycle',
|
||||
'events':'pool_events',
|
||||
'use_threadlocal':'pool_threadlocal'}
|
||||
for k in util.get_cls_kwargs(poolclass):
|
||||
tk = translate.get(k, k)
|
||||
if tk in kwargs:
|
||||
pool_args[k] = kwargs.pop(tk)
|
||||
pool_args.setdefault('use_threadlocal', self.pool_threadlocal)
|
||||
pool = poolclass(creator, **pool_args)
|
||||
else:
|
||||
if isinstance(pool, poollib._DBProxy):
|
||||
@@ -138,18 +145,26 @@ class DefaultEngineStrategy(EngineStrategy):
|
||||
if _initialize:
|
||||
do_on_connect = dialect.on_connect()
|
||||
if do_on_connect:
|
||||
def on_connect(conn, rec):
|
||||
conn = getattr(conn, '_sqla_unwrap', conn)
|
||||
def on_connect(dbapi_connection, connection_record):
|
||||
conn = getattr(dbapi_connection, '_sqla_unwrap', dbapi_connection)
|
||||
if conn is None:
|
||||
return
|
||||
do_on_connect(conn)
|
||||
|
||||
pool.add_listener({'first_connect': on_connect, 'connect':on_connect})
|
||||
event.listen(pool, 'first_connect', on_connect)
|
||||
event.listen(pool, 'connect', on_connect)
|
||||
|
||||
def first_connect(dbapi_connection, connection_record):
|
||||
c = base.Connection(engine, connection=dbapi_connection)
|
||||
|
||||
# TODO: removing this allows the on connect activities
|
||||
# to generate events. tests currently assume these aren't
|
||||
# sent. do we want users to get all the initial connect
|
||||
# activities as events ?
|
||||
c._has_events = False
|
||||
|
||||
def first_connect(conn, rec):
|
||||
c = base.Connection(engine, connection=conn)
|
||||
dialect.initialize(c)
|
||||
pool.add_listener({'first_connect':first_connect})
|
||||
event.listen(pool, 'first_connect', first_connect)
|
||||
|
||||
return engine
|
||||
|
||||
@@ -167,7 +182,6 @@ class ThreadLocalEngineStrategy(DefaultEngineStrategy):
|
||||
"""Strategy for configuring an Engine with thredlocal behavior."""
|
||||
|
||||
name = 'threadlocal'
|
||||
pool_threadlocal = True
|
||||
engine_cls = threadlocal.TLEngine
|
||||
|
||||
ThreadLocalEngineStrategy()
|
||||
@@ -220,12 +234,19 @@ class MockEngineStrategy(EngineStrategy):
|
||||
kwargs['checkfirst'] = False
|
||||
from sqlalchemy.engine import ddl
|
||||
|
||||
ddl.SchemaGenerator(self.dialect, self, **kwargs).traverse(entity)
|
||||
ddl.SchemaGenerator(self.dialect, self, **kwargs).traverse_single(entity)
|
||||
|
||||
def drop(self, entity, **kwargs):
|
||||
kwargs['checkfirst'] = False
|
||||
from sqlalchemy.engine import ddl
|
||||
ddl.SchemaDropper(self.dialect, self, **kwargs).traverse(entity)
|
||||
ddl.SchemaDropper(self.dialect, self, **kwargs).traverse_single(entity)
|
||||
|
||||
def _run_visitor(self, visitorcallable, element,
|
||||
connection=None,
|
||||
**kwargs):
|
||||
kwargs['checkfirst'] = False
|
||||
visitorcallable(self.dialect, self,
|
||||
**kwargs).traverse_single(element)
|
||||
|
||||
def execute(self, object, *multiparams, **params):
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# engine/threadlocal.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -11,7 +11,7 @@ with :func:`~sqlalchemy.engine.create_engine`. This module is semi-private and
|
||||
invoked automatically when the threadlocal engine strategy is used.
|
||||
"""
|
||||
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy import util, event
|
||||
from sqlalchemy.engine import base
|
||||
import weakref
|
||||
|
||||
@@ -36,16 +36,12 @@ class TLConnection(base.Connection):
|
||||
class TLEngine(base.Engine):
|
||||
"""An Engine that includes support for thread-local managed transactions."""
|
||||
|
||||
_tl_connection_cls = TLConnection
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TLEngine, self).__init__(*args, **kwargs)
|
||||
self._connections = util.threading.local()
|
||||
proxy = kwargs.get('proxy')
|
||||
if proxy:
|
||||
self.TLConnection = base._proxy_connection_cls(
|
||||
TLConnection, proxy)
|
||||
else:
|
||||
self.TLConnection = TLConnection
|
||||
|
||||
|
||||
def contextual_connect(self, **kw):
|
||||
if not hasattr(self._connections, 'conn'):
|
||||
@@ -56,7 +52,7 @@ class TLEngine(base.Engine):
|
||||
if connection is None or connection.closed:
|
||||
# guards against pool-level reapers, if desired.
|
||||
# or not connection.connection.is_valid:
|
||||
connection = self.TLConnection(self, self.pool.connect(), **kw)
|
||||
connection = self._tl_connection_cls(self, self.pool.connect(), **kw)
|
||||
self._connections.conn = conn = weakref.ref(connection)
|
||||
|
||||
return connection._increment_connect()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# engine/url.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -12,8 +12,8 @@ with a string argument; alternatively, the URL is a public-facing construct whic
|
||||
be used directly and is also accepted directly by ``create_engine()``.
|
||||
"""
|
||||
|
||||
import re, cgi, sys, urllib
|
||||
from sqlalchemy import exc
|
||||
import re, urllib
|
||||
from sqlalchemy import exc, util
|
||||
|
||||
|
||||
class URL(object):
|
||||
@@ -104,7 +104,14 @@ class URL(object):
|
||||
|
||||
module = __import__('sqlalchemy.dialects.%s' % (dialect, )).dialects
|
||||
module = getattr(module, dialect)
|
||||
module = getattr(module, driver)
|
||||
if hasattr(module, driver):
|
||||
module = getattr(module, driver)
|
||||
else:
|
||||
module = self._load_entry_point()
|
||||
if module is None:
|
||||
raise exc.ArgumentError(
|
||||
"Could not determine dialect for '%s'." %
|
||||
self.drivername)
|
||||
|
||||
return module.dialect
|
||||
except ImportError:
|
||||
@@ -112,7 +119,8 @@ class URL(object):
|
||||
if module is not None:
|
||||
return module
|
||||
else:
|
||||
raise
|
||||
raise exc.ArgumentError(
|
||||
"Could not determine dialect for '%s'." % self.drivername)
|
||||
|
||||
def _load_entry_point(self):
|
||||
"""attempt to load this url's dialect from entry points, or return None
|
||||
@@ -127,7 +135,7 @@ class URL(object):
|
||||
return None
|
||||
|
||||
for res in pkg_resources.iter_entry_points('sqlalchemy.dialects'):
|
||||
if res.name == self.drivername:
|
||||
if res.name == self.drivername.replace("+", "."):
|
||||
return res.load()
|
||||
else:
|
||||
return None
|
||||
@@ -192,7 +200,7 @@ def _parse_rfc1738_args(name):
|
||||
if components['database'] is not None:
|
||||
tokens = components['database'].split('?', 2)
|
||||
components['database'] = tokens[0]
|
||||
query = (len(tokens) > 1 and dict(cgi.parse_qsl(tokens[1]))) or None
|
||||
query = (len(tokens) > 1 and dict(util.parse_qsl(tokens[1]))) or None
|
||||
# Py2K
|
||||
if query is not None:
|
||||
query = dict((k.encode('ascii'), query[k]) for k in query)
|
||||
@@ -214,7 +222,7 @@ def _parse_keyvalue_args(name):
|
||||
m = re.match( r'(\w+)://(.*)', name)
|
||||
if m is not None:
|
||||
(name, args) = m.group(1, 2)
|
||||
opts = dict( cgi.parse_qsl( args ) )
|
||||
opts = dict( util.parse_qsl( args ) )
|
||||
return URL(name, *opts)
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
# sqlalchemy/event.py
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
"""Base event API."""
|
||||
|
||||
from sqlalchemy import util, exc
|
||||
|
||||
CANCEL = util.symbol('CANCEL')
|
||||
NO_RETVAL = util.symbol('NO_RETVAL')
|
||||
|
||||
def listen(target, identifier, fn, *args, **kw):
|
||||
"""Register a listener function for the given target.
|
||||
|
||||
e.g.::
|
||||
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.schema import UniqueConstraint
|
||||
|
||||
def unique_constraint_name(const, table):
|
||||
const.name = "uq_%s_%s" % (
|
||||
table.name,
|
||||
list(const.columns)[0].name
|
||||
)
|
||||
event.listen(
|
||||
UniqueConstraint,
|
||||
"after_parent_attach",
|
||||
unique_constraint_name)
|
||||
|
||||
"""
|
||||
|
||||
for evt_cls in _registrars[identifier]:
|
||||
tgt = evt_cls._accept_with(target)
|
||||
if tgt is not None:
|
||||
tgt.dispatch._listen(tgt, identifier, fn, *args, **kw)
|
||||
return
|
||||
raise exc.InvalidRequestError("No such event '%s' for target '%s'" %
|
||||
(identifier,target))
|
||||
|
||||
def listens_for(target, identifier, *args, **kw):
|
||||
"""Decorate a function as a listener for the given target + identifier.
|
||||
|
||||
e.g.::
|
||||
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.schema import UniqueConstraint
|
||||
|
||||
@event.listens_for(UniqueConstraint, "after_parent_attach")
|
||||
def unique_constraint_name(const, table):
|
||||
const.name = "uq_%s_%s" % (
|
||||
table.name,
|
||||
list(const.columns)[0].name
|
||||
)
|
||||
"""
|
||||
def decorate(fn):
|
||||
listen(target, identifier, fn, *args, **kw)
|
||||
return fn
|
||||
return decorate
|
||||
|
||||
def remove(target, identifier, fn):
|
||||
"""Remove an event listener.
|
||||
|
||||
Note that some event removals, particularly for those event dispatchers
|
||||
which create wrapper functions and secondary even listeners, may not yet
|
||||
be supported.
|
||||
|
||||
"""
|
||||
for evt_cls in _registrars[identifier]:
|
||||
for tgt in evt_cls._accept_with(target):
|
||||
tgt.dispatch._remove(identifier, tgt, fn, *args, **kw)
|
||||
return
|
||||
|
||||
_registrars = util.defaultdict(list)
|
||||
|
||||
def _is_event_name(name):
|
||||
return not name.startswith('_') and name != 'dispatch'
|
||||
|
||||
class _UnpickleDispatch(object):
|
||||
"""Serializable callable that re-generates an instance of :class:`_Dispatch`
|
||||
given a particular :class:`.Events` subclass.
|
||||
|
||||
"""
|
||||
def __call__(self, _parent_cls):
|
||||
for cls in _parent_cls.__mro__:
|
||||
if 'dispatch' in cls.__dict__:
|
||||
return cls.__dict__['dispatch'].dispatch_cls(_parent_cls)
|
||||
else:
|
||||
raise AttributeError("No class with a 'dispatch' member present.")
|
||||
|
||||
class _Dispatch(object):
|
||||
"""Mirror the event listening definitions of an Events class with
|
||||
listener collections.
|
||||
|
||||
Classes which define a "dispatch" member will return a
|
||||
non-instantiated :class:`._Dispatch` subclass when the member
|
||||
is accessed at the class level. When the "dispatch" member is
|
||||
accessed at the instance level of its owner, an instance
|
||||
of the :class:`._Dispatch` class is returned.
|
||||
|
||||
A :class:`._Dispatch` class is generated for each :class:`.Events`
|
||||
class defined, by the :func:`._create_dispatcher_class` function.
|
||||
The original :class:`.Events` classes remain untouched.
|
||||
This decouples the construction of :class:`.Events` subclasses from
|
||||
the implementation used by the event internals, and allows
|
||||
inspecting tools like Sphinx to work in an unsurprising
|
||||
way against the public API.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, _parent_cls):
|
||||
self._parent_cls = _parent_cls
|
||||
|
||||
def __reduce__(self):
|
||||
return _UnpickleDispatch(), (self._parent_cls, )
|
||||
|
||||
def _update(self, other, only_propagate=True):
|
||||
"""Populate from the listeners in another :class:`_Dispatch`
|
||||
object."""
|
||||
|
||||
for ls in _event_descriptors(other):
|
||||
getattr(self, ls.name)._update(ls, only_propagate=only_propagate)
|
||||
|
||||
def _event_descriptors(target):
|
||||
return [getattr(target, k) for k in dir(target) if _is_event_name(k)]
|
||||
|
||||
class _EventMeta(type):
|
||||
"""Intercept new Event subclasses and create
|
||||
associated _Dispatch classes."""
|
||||
|
||||
def __init__(cls, classname, bases, dict_):
|
||||
_create_dispatcher_class(cls, classname, bases, dict_)
|
||||
return type.__init__(cls, classname, bases, dict_)
|
||||
|
||||
def _create_dispatcher_class(cls, classname, bases, dict_):
|
||||
"""Create a :class:`._Dispatch` class corresponding to an
|
||||
:class:`.Events` class."""
|
||||
|
||||
# there's all kinds of ways to do this,
|
||||
# i.e. make a Dispatch class that shares the '_listen' method
|
||||
# of the Event class, this is the straight monkeypatch.
|
||||
dispatch_base = getattr(cls, 'dispatch', _Dispatch)
|
||||
cls.dispatch = dispatch_cls = type("%sDispatch" % classname,
|
||||
(dispatch_base, ), {})
|
||||
dispatch_cls._listen = cls._listen
|
||||
dispatch_cls._clear = cls._clear
|
||||
|
||||
for k in dict_:
|
||||
if _is_event_name(k):
|
||||
setattr(dispatch_cls, k, _DispatchDescriptor(dict_[k]))
|
||||
_registrars[k].append(cls)
|
||||
|
||||
def _remove_dispatcher(cls):
|
||||
for k in dir(cls):
|
||||
if _is_event_name(k):
|
||||
_registrars[k].remove(cls)
|
||||
if not _registrars[k]:
|
||||
del _registrars[k]
|
||||
|
||||
class Events(object):
|
||||
"""Define event listening functions for a particular target type."""
|
||||
|
||||
|
||||
__metaclass__ = _EventMeta
|
||||
|
||||
@classmethod
|
||||
def _accept_with(cls, target):
|
||||
# Mapper, ClassManager, Session override this to
|
||||
# also accept classes, scoped_sessions, sessionmakers, etc.
|
||||
if hasattr(target, 'dispatch') and (
|
||||
isinstance(target.dispatch, cls.dispatch) or \
|
||||
isinstance(target.dispatch, type) and \
|
||||
issubclass(target.dispatch, cls.dispatch)
|
||||
):
|
||||
return target
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _listen(cls, target, identifier, fn, propagate=False, insert=False):
|
||||
if insert:
|
||||
getattr(target.dispatch, identifier).insert(fn, target, propagate)
|
||||
else:
|
||||
getattr(target.dispatch, identifier).append(fn, target, propagate)
|
||||
|
||||
@classmethod
|
||||
def _remove(cls, target, identifier, fn):
|
||||
getattr(target.dispatch, identifier).remove(fn, target)
|
||||
|
||||
@classmethod
|
||||
def _clear(cls):
|
||||
for attr in dir(cls.dispatch):
|
||||
if _is_event_name(attr):
|
||||
getattr(cls.dispatch, attr).clear()
|
||||
|
||||
class _DispatchDescriptor(object):
|
||||
"""Class-level attributes on :class:`._Dispatch` classes."""
|
||||
|
||||
def __init__(self, fn):
|
||||
self.__name__ = fn.__name__
|
||||
self.__doc__ = fn.__doc__
|
||||
self._clslevel = util.defaultdict(list)
|
||||
|
||||
def insert(self, obj, target, propagate):
|
||||
assert isinstance(target, type), \
|
||||
"Class-level Event targets must be classes."
|
||||
|
||||
stack = [target]
|
||||
while stack:
|
||||
cls = stack.pop(0)
|
||||
stack.extend(cls.__subclasses__())
|
||||
self._clslevel[cls].insert(0, obj)
|
||||
|
||||
def append(self, obj, target, propagate):
|
||||
assert isinstance(target, type), \
|
||||
"Class-level Event targets must be classes."
|
||||
|
||||
stack = [target]
|
||||
while stack:
|
||||
cls = stack.pop(0)
|
||||
stack.extend(cls.__subclasses__())
|
||||
self._clslevel[cls].append(obj)
|
||||
|
||||
def remove(self, obj, target):
|
||||
stack = [target]
|
||||
while stack:
|
||||
cls = stack.pop(0)
|
||||
stack.extend(cls.__subclasses__())
|
||||
self._clslevel[cls].remove(obj)
|
||||
|
||||
def clear(self):
|
||||
"""Clear all class level listeners"""
|
||||
|
||||
for dispatcher in self._clslevel.values():
|
||||
dispatcher[:] = []
|
||||
|
||||
def __get__(self, obj, cls):
|
||||
if obj is None:
|
||||
return self
|
||||
obj.__dict__[self.__name__] = result = \
|
||||
_ListenerCollection(self, obj._parent_cls)
|
||||
return result
|
||||
|
||||
class _ListenerCollection(object):
|
||||
"""Instance-level attributes on instances of :class:`._Dispatch`.
|
||||
|
||||
Represents a collection of listeners.
|
||||
|
||||
"""
|
||||
|
||||
_exec_once = False
|
||||
|
||||
def __init__(self, parent, target_cls):
|
||||
self.parent_listeners = parent._clslevel[target_cls]
|
||||
self.name = parent.__name__
|
||||
self.listeners = []
|
||||
self.propagate = set()
|
||||
|
||||
def exec_once(self, *args, **kw):
|
||||
"""Execute this event, but only if it has not been
|
||||
executed already for this collection."""
|
||||
|
||||
if not self._exec_once:
|
||||
self(*args, **kw)
|
||||
self._exec_once = True
|
||||
|
||||
def __call__(self, *args, **kw):
|
||||
"""Execute this event."""
|
||||
|
||||
for fn in self.parent_listeners:
|
||||
fn(*args, **kw)
|
||||
for fn in self.listeners:
|
||||
fn(*args, **kw)
|
||||
|
||||
# I'm not entirely thrilled about the overhead here,
|
||||
# but this allows class-level listeners to be added
|
||||
# at any point.
|
||||
#
|
||||
# alternatively, _DispatchDescriptor could notify
|
||||
# all _ListenerCollection objects, but then we move
|
||||
# to a higher memory model, i.e.weakrefs to all _ListenerCollection
|
||||
# objects, the _DispatchDescriptor collection repeated
|
||||
# for all instances.
|
||||
|
||||
def __len__(self):
|
||||
return len(self.parent_listeners + self.listeners)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.parent_listeners + self.listeners)
|
||||
|
||||
def __getitem__(self, index):
|
||||
return (self.parent_listeners + self.listeners)[index]
|
||||
|
||||
def __nonzero__(self):
|
||||
return bool(self.listeners or self.parent_listeners)
|
||||
|
||||
def _update(self, other, only_propagate=True):
|
||||
"""Populate from the listeners in another :class:`_Dispatch`
|
||||
object."""
|
||||
|
||||
existing_listeners = self.listeners
|
||||
existing_listener_set = set(existing_listeners)
|
||||
self.propagate.update(other.propagate)
|
||||
existing_listeners.extend([l for l
|
||||
in other.listeners
|
||||
if l not in existing_listener_set
|
||||
and not only_propagate or l in self.propagate
|
||||
])
|
||||
|
||||
def insert(self, obj, target, propagate):
|
||||
if obj not in self.listeners:
|
||||
self.listeners.insert(0, obj)
|
||||
if propagate:
|
||||
self.propagate.add(obj)
|
||||
|
||||
def append(self, obj, target, propagate):
|
||||
if obj not in self.listeners:
|
||||
self.listeners.append(obj)
|
||||
if propagate:
|
||||
self.propagate.add(obj)
|
||||
|
||||
def remove(self, obj, target):
|
||||
if obj in self.listeners:
|
||||
self.listeners.remove(obj)
|
||||
self.propagate.discard(obj)
|
||||
|
||||
def clear(self):
|
||||
self.listeners[:] = []
|
||||
self.propagate.clear()
|
||||
|
||||
class dispatcher(object):
|
||||
"""Descriptor used by target classes to
|
||||
deliver the _Dispatch class at the class level
|
||||
and produce new _Dispatch instances for target
|
||||
instances.
|
||||
|
||||
"""
|
||||
def __init__(self, events):
|
||||
self.dispatch_cls = events.dispatch
|
||||
self.events = events
|
||||
|
||||
def __get__(self, obj, cls):
|
||||
if obj is None:
|
||||
return self.dispatch_cls
|
||||
obj.__dict__['dispatch'] = disp = self.dispatch_cls(cls)
|
||||
return disp
|
||||
@@ -0,0 +1,433 @@
|
||||
# sqlalchemy/events.py
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
"""Core event interfaces."""
|
||||
|
||||
from sqlalchemy import event, exc, util
|
||||
engine = util.importlater('sqlalchemy', 'engine')
|
||||
pool = util.importlater('sqlalchemy', 'pool')
|
||||
|
||||
|
||||
class DDLEvents(event.Events):
|
||||
"""
|
||||
Define event listeners for schema objects,
|
||||
that is, :class:`.SchemaItem` and :class:`.SchemaEvent`
|
||||
subclasses, including :class:`.MetaData`, :class:`.Table`,
|
||||
:class:`.Column`.
|
||||
|
||||
:class:`.MetaData` and :class:`.Table` support events
|
||||
specifically regarding when CREATE and DROP
|
||||
DDL is emitted to the database.
|
||||
|
||||
Attachment events are also provided to customize
|
||||
behavior whenever a child schema element is associated
|
||||
with a parent, such as, when a :class:`.Column` is associated
|
||||
with its :class:`.Table`, when a :class:`.ForeignKeyConstraint`
|
||||
is associated with a :class:`.Table`, etc.
|
||||
|
||||
Example using the ``after_create`` event::
|
||||
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy import Table, Column, Metadata, Integer
|
||||
|
||||
m = MetaData()
|
||||
some_table = Table('some_table', m, Column('data', Integer))
|
||||
|
||||
def after_create(target, connection, **kw):
|
||||
connection.execute("ALTER TABLE %s SET name=foo_%s" %
|
||||
(target.name, target.name))
|
||||
|
||||
event.listen(some_table, "after_create", after_create)
|
||||
|
||||
DDL events integrate closely with the
|
||||
:class:`.DDL` class and the :class:`.DDLElement` hierarchy
|
||||
of DDL clause constructs, which are themselves appropriate
|
||||
as listener callables::
|
||||
|
||||
from sqlalchemy import DDL
|
||||
event.listen(
|
||||
some_table,
|
||||
"after_create",
|
||||
DDL("ALTER TABLE %(table)s SET name=foo_%(table)s")
|
||||
)
|
||||
|
||||
The methods here define the name of an event as well
|
||||
as the names of members that are passed to listener
|
||||
functions.
|
||||
|
||||
See also:
|
||||
|
||||
:ref:`event_toplevel`
|
||||
|
||||
:class:`.DDLElement`
|
||||
|
||||
:class:`.DDL`
|
||||
|
||||
:ref:`schema_ddl_sequences`
|
||||
|
||||
"""
|
||||
|
||||
def before_create(self, target, connection, **kw):
|
||||
"""Called before CREATE statments are emitted.
|
||||
|
||||
:param target: the :class:`.MetaData` or :class:`.Table`
|
||||
object which is the target of the event.
|
||||
:param connection: the :class:`.Connection` where the
|
||||
CREATE statement or statements will be emitted.
|
||||
:param \**kw: additional keyword arguments relevant
|
||||
to the event. The contents of this dictionary
|
||||
may vary across releases, and include the
|
||||
list of tables being generated for a metadata-level
|
||||
event, the checkfirst flag, and other
|
||||
elements used by internal events.
|
||||
|
||||
"""
|
||||
|
||||
def after_create(self, target, connection, **kw):
|
||||
"""Called after CREATE statments are emitted.
|
||||
|
||||
:param target: the :class:`.MetaData` or :class:`.Table`
|
||||
object which is the target of the event.
|
||||
:param connection: the :class:`.Connection` where the
|
||||
CREATE statement or statements have been emitted.
|
||||
:param \**kw: additional keyword arguments relevant
|
||||
to the event. The contents of this dictionary
|
||||
may vary across releases, and include the
|
||||
list of tables being generated for a metadata-level
|
||||
event, the checkfirst flag, and other
|
||||
elements used by internal events.
|
||||
|
||||
"""
|
||||
|
||||
def before_drop(self, target, connection, **kw):
|
||||
"""Called before DROP statments are emitted.
|
||||
|
||||
:param target: the :class:`.MetaData` or :class:`.Table`
|
||||
object which is the target of the event.
|
||||
:param connection: the :class:`.Connection` where the
|
||||
DROP statement or statements will be emitted.
|
||||
:param \**kw: additional keyword arguments relevant
|
||||
to the event. The contents of this dictionary
|
||||
may vary across releases, and include the
|
||||
list of tables being generated for a metadata-level
|
||||
event, the checkfirst flag, and other
|
||||
elements used by internal events.
|
||||
|
||||
"""
|
||||
|
||||
def after_drop(self, target, connection, **kw):
|
||||
"""Called after DROP statments are emitted.
|
||||
|
||||
:param target: the :class:`.MetaData` or :class:`.Table`
|
||||
object which is the target of the event.
|
||||
:param connection: the :class:`.Connection` where the
|
||||
DROP statement or statements have been emitted.
|
||||
:param \**kw: additional keyword arguments relevant
|
||||
to the event. The contents of this dictionary
|
||||
may vary across releases, and include the
|
||||
list of tables being generated for a metadata-level
|
||||
event, the checkfirst flag, and other
|
||||
elements used by internal events.
|
||||
|
||||
"""
|
||||
|
||||
def before_parent_attach(self, target, parent):
|
||||
"""Called before a :class:`.SchemaItem` is associated with
|
||||
a parent :class:`.SchemaItem`.
|
||||
|
||||
:param target: the target object
|
||||
:param parent: the parent to which the target is being attached.
|
||||
|
||||
:func:`.event.listen` also accepts a modifier for this event:
|
||||
|
||||
:param propagate=False: When True, the listener function will
|
||||
be established for any copies made of the target object,
|
||||
i.e. those copies that are generated when
|
||||
:meth:`.Table.tometadata` is used.
|
||||
|
||||
"""
|
||||
|
||||
def after_parent_attach(self, target, parent):
|
||||
"""Called after a :class:`.SchemaItem` is associated with
|
||||
a parent :class:`.SchemaItem`.
|
||||
|
||||
:param target: the target object
|
||||
:param parent: the parent to which the target is being attached.
|
||||
|
||||
:func:`.event.listen` also accepts a modifier for this event:
|
||||
|
||||
:param propagate=False: When True, the listener function will
|
||||
be established for any copies made of the target object,
|
||||
i.e. those copies that are generated when
|
||||
:meth:`.Table.tometadata` is used.
|
||||
|
||||
"""
|
||||
|
||||
def column_reflect(self, table, column_info):
|
||||
"""Called for each unit of 'column info' retrieved when
|
||||
a :class:`.Table` is being reflected.
|
||||
|
||||
The dictionary of column information as returned by the
|
||||
dialect is passed, and can be modified. The dictionary
|
||||
is that returned in each element of the list returned
|
||||
by :meth:`.reflection.Inspector.get_columns`.
|
||||
|
||||
The event is called before any action is taken against
|
||||
this dictionary, and the contents can be modified.
|
||||
The :class:`.Column` specific arguments ``info``, ``key``,
|
||||
and ``quote`` can also be added to the dictionary and
|
||||
will be passed to the constructor of :class:`.Column`.
|
||||
|
||||
Note that this event is only meaningful if either
|
||||
associated with the :class:`.Table` class across the
|
||||
board, e.g.::
|
||||
|
||||
from sqlalchemy.schema import Table
|
||||
from sqlalchemy import event
|
||||
|
||||
def listen_for_reflect(table, column_info):
|
||||
"receive a column_reflect event"
|
||||
# ...
|
||||
|
||||
event.listen(
|
||||
Table,
|
||||
'column_reflect',
|
||||
listen_for_reflect)
|
||||
|
||||
...or with a specific :class:`.Table` instance using
|
||||
the ``listeners`` argument::
|
||||
|
||||
def listen_for_reflect(table, column_info):
|
||||
"receive a column_reflect event"
|
||||
# ...
|
||||
|
||||
t = Table(
|
||||
'sometable',
|
||||
autoload=True,
|
||||
listeners=[
|
||||
('column_reflect', listen_for_reflect)
|
||||
])
|
||||
|
||||
This because the reflection process initiated by ``autoload=True``
|
||||
completes within the scope of the constructor for :class:`.Table`.
|
||||
|
||||
"""
|
||||
|
||||
class SchemaEventTarget(object):
|
||||
"""Base class for elements that are the targets of :class:`.DDLEvents` events.
|
||||
|
||||
This includes :class:`.SchemaItem` as well as :class:`.SchemaType`.
|
||||
|
||||
"""
|
||||
dispatch = event.dispatcher(DDLEvents)
|
||||
|
||||
def _set_parent(self, parent):
|
||||
"""Associate with this SchemaEvent's parent object."""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def _set_parent_with_dispatch(self, parent):
|
||||
self.dispatch.before_parent_attach(self, parent)
|
||||
self._set_parent(parent)
|
||||
self.dispatch.after_parent_attach(self, parent)
|
||||
|
||||
class PoolEvents(event.Events):
|
||||
"""Available events for :class:`.Pool`.
|
||||
|
||||
The methods here define the name of an event as well
|
||||
as the names of members that are passed to listener
|
||||
functions.
|
||||
|
||||
e.g.::
|
||||
|
||||
from sqlalchemy import event
|
||||
|
||||
def my_on_checkout(dbapi_conn, connection_rec, connection_proxy):
|
||||
"handle an on checkout event"
|
||||
|
||||
event.listen(Pool, 'checkout', my_on_checkout)
|
||||
|
||||
In addition to accepting the :class:`.Pool` class and :class:`.Pool` instances,
|
||||
:class:`.PoolEvents` also accepts :class:`.Engine` objects and
|
||||
the :class:`.Engine` class as targets, which will be resolved
|
||||
to the ``.pool`` attribute of the given engine or the :class:`.Pool`
|
||||
class::
|
||||
|
||||
engine = create_engine("postgresql://scott:tiger@localhost/test")
|
||||
|
||||
# will associate with engine.pool
|
||||
event.listen(engine, 'checkout', my_on_checkout)
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _accept_with(cls, target):
|
||||
if isinstance(target, type):
|
||||
if issubclass(target, engine.Engine):
|
||||
return pool.Pool
|
||||
elif issubclass(target, pool.Pool):
|
||||
return target
|
||||
elif isinstance(target, engine.Engine):
|
||||
return target.pool
|
||||
else:
|
||||
return target
|
||||
|
||||
def connect(self, dbapi_connection, connection_record):
|
||||
"""Called once for each new DB-API connection or Pool's ``creator()``.
|
||||
|
||||
:param dbapi_con:
|
||||
A newly connected raw DB-API connection (not a SQLAlchemy
|
||||
``Connection`` wrapper).
|
||||
|
||||
:param con_record:
|
||||
The ``_ConnectionRecord`` that persistently manages the connection
|
||||
|
||||
"""
|
||||
|
||||
def first_connect(self, dbapi_connection, connection_record):
|
||||
"""Called exactly once for the first DB-API connection.
|
||||
|
||||
:param dbapi_con:
|
||||
A newly connected raw DB-API connection (not a SQLAlchemy
|
||||
``Connection`` wrapper).
|
||||
|
||||
:param con_record:
|
||||
The ``_ConnectionRecord`` that persistently manages the connection
|
||||
|
||||
"""
|
||||
|
||||
def checkout(self, dbapi_connection, connection_record, connection_proxy):
|
||||
"""Called when a connection is retrieved from the Pool.
|
||||
|
||||
:param dbapi_con:
|
||||
A raw DB-API connection
|
||||
|
||||
:param con_record:
|
||||
The ``_ConnectionRecord`` that persistently manages the connection
|
||||
|
||||
:param con_proxy:
|
||||
The ``_ConnectionFairy`` which manages the connection for the span of
|
||||
the current checkout.
|
||||
|
||||
If you raise a :class:`~sqlalchemy.exc.DisconnectionError`, the current
|
||||
connection will be disposed and a fresh connection retrieved.
|
||||
Processing of all checkout listeners will abort and restart
|
||||
using the new connection.
|
||||
"""
|
||||
|
||||
def checkin(self, dbapi_connection, connection_record):
|
||||
"""Called when a connection returns to the pool.
|
||||
|
||||
Note that the connection may be closed, and may be None if the
|
||||
connection has been invalidated. ``checkin`` will not be called
|
||||
for detached connections. (They do not return to the pool.)
|
||||
|
||||
:param dbapi_con:
|
||||
A raw DB-API connection
|
||||
|
||||
:param con_record:
|
||||
The ``_ConnectionRecord`` that persistently manages the connection
|
||||
|
||||
"""
|
||||
|
||||
class ConnectionEvents(event.Events):
|
||||
"""Available events for :class:`.Connection`.
|
||||
|
||||
The methods here define the name of an event as well as the names of members that are passed to listener functions.
|
||||
|
||||
e.g.::
|
||||
|
||||
from sqlalchemy import event, create_engine
|
||||
|
||||
def before_execute(conn, clauseelement, multiparams, params):
|
||||
log.info("Received statement: %s" % clauseelement)
|
||||
|
||||
engine = create_engine('postgresql://scott:tiger@localhost/test')
|
||||
event.listen(engine, "before_execute", before_execute)
|
||||
|
||||
Some events allow modifiers to the listen() function.
|
||||
|
||||
:param retval=False: Applies to the :meth:`.before_execute` and
|
||||
:meth:`.before_cursor_execute` events only. When True, the
|
||||
user-defined event function must have a return value, which
|
||||
is a tuple of parameters that replace the given statement
|
||||
and parameters. See those methods for a description of
|
||||
specific return arguments.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _listen(cls, target, identifier, fn, retval=False):
|
||||
target._has_events = True
|
||||
|
||||
if not retval:
|
||||
if identifier == 'before_execute':
|
||||
orig_fn = fn
|
||||
def wrap(conn, clauseelement, multiparams, params):
|
||||
orig_fn(conn, clauseelement, multiparams, params)
|
||||
return clauseelement, multiparams, params
|
||||
fn = wrap
|
||||
elif identifier == 'before_cursor_execute':
|
||||
orig_fn = fn
|
||||
def wrap(conn, cursor, statement,
|
||||
parameters, context, executemany):
|
||||
orig_fn(conn, cursor, statement,
|
||||
parameters, context, executemany)
|
||||
return statement, parameters
|
||||
fn = wrap
|
||||
|
||||
elif retval and identifier not in ('before_execute', 'before_cursor_execute'):
|
||||
raise exc.ArgumentError(
|
||||
"Only the 'before_execute' and "
|
||||
"'before_cursor_execute' engine "
|
||||
"event listeners accept the 'retval=True' "
|
||||
"argument.")
|
||||
event.Events._listen(target, identifier, fn)
|
||||
|
||||
def before_execute(self, conn, clauseelement, multiparams, params):
|
||||
"""Intercept high level execute() events."""
|
||||
|
||||
def after_execute(self, conn, clauseelement, multiparams, params, result):
|
||||
"""Intercept high level execute() events."""
|
||||
|
||||
def before_cursor_execute(self, conn, cursor, statement,
|
||||
parameters, context, executemany):
|
||||
"""Intercept low-level cursor execute() events."""
|
||||
|
||||
def after_cursor_execute(self, conn, cursor, statement,
|
||||
parameters, context, executemany):
|
||||
"""Intercept low-level cursor execute() events."""
|
||||
|
||||
def begin(self, conn):
|
||||
"""Intercept begin() events."""
|
||||
|
||||
def rollback(self, conn):
|
||||
"""Intercept rollback() events."""
|
||||
|
||||
def commit(self, conn):
|
||||
"""Intercept commit() events."""
|
||||
|
||||
def savepoint(self, conn, name=None):
|
||||
"""Intercept savepoint() events."""
|
||||
|
||||
def rollback_savepoint(self, conn, name, context):
|
||||
"""Intercept rollback_savepoint() events."""
|
||||
|
||||
def release_savepoint(self, conn, name, context):
|
||||
"""Intercept release_savepoint() events."""
|
||||
|
||||
def begin_twophase(self, conn, xid):
|
||||
"""Intercept begin_twophase() events."""
|
||||
|
||||
def prepare_twophase(self, conn, xid):
|
||||
"""Intercept prepare_twophase() events."""
|
||||
|
||||
def rollback_twophase(self, conn, xid, is_prepared):
|
||||
"""Intercept rollback_twophase() events."""
|
||||
|
||||
def commit_twophase(self, conn, xid, is_prepared):
|
||||
"""Intercept commit_twophase() events."""
|
||||
|
||||
+137
-33
@@ -1,5 +1,5 @@
|
||||
# sqlalchemy/exc.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -12,6 +12,7 @@ result of DBAPI exceptions are all subclasses of
|
||||
|
||||
"""
|
||||
|
||||
import traceback
|
||||
|
||||
class SQLAlchemyError(Exception):
|
||||
"""Generic error class."""
|
||||
@@ -26,32 +27,58 @@ class ArgumentError(SQLAlchemyError):
|
||||
|
||||
|
||||
class CircularDependencyError(SQLAlchemyError):
|
||||
"""Raised by topological sorts when a circular dependency is detected"""
|
||||
def __init__(self, message, cycles, edges):
|
||||
message += ": cycles: %r all edges: %r" % (cycles, edges)
|
||||
"""Raised by topological sorts when a circular dependency is detected.
|
||||
|
||||
There are two scenarios where this error occurs:
|
||||
|
||||
* In a Session flush operation, if two objects are mutually dependent
|
||||
on each other, they can not be inserted or deleted via INSERT or
|
||||
DELETE statements alone; an UPDATE will be needed to post-associate
|
||||
or pre-deassociate one of the foreign key constrained values.
|
||||
The ``post_update`` flag described at :ref:`post_update` can resolve
|
||||
this cycle.
|
||||
* In a :meth:`.MetaData.create_all`, :meth:`.MetaData.drop_all`,
|
||||
:attr:`.MetaData.sorted_tables` operation, two :class:`.ForeignKey`
|
||||
or :class:`.ForeignKeyConstraint` objects mutually refer to each
|
||||
other. Apply the ``use_alter=True`` flag to one or both,
|
||||
see :ref:`use_alter`.
|
||||
|
||||
"""
|
||||
def __init__(self, message, cycles, edges, msg=None):
|
||||
if msg is None:
|
||||
message += " Cycles: %r all edges: %r" % (cycles, edges)
|
||||
else:
|
||||
message = msg
|
||||
SQLAlchemyError.__init__(self, message)
|
||||
self.cycles = cycles
|
||||
self.edges = edges
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (None, self.cycles,
|
||||
self.edges, self.args[0])
|
||||
|
||||
class CompileError(SQLAlchemyError):
|
||||
"""Raised when an error occurs during SQL compilation"""
|
||||
|
||||
class IdentifierError(SQLAlchemyError):
|
||||
"""Raised when a schema name is beyond the max character limit"""
|
||||
|
||||
# Moved to orm.exc; compatability definition installed by orm import until 0.6
|
||||
# Moved to orm.exc; compatibility definition installed by orm import until 0.6
|
||||
ConcurrentModificationError = None
|
||||
|
||||
class DisconnectionError(SQLAlchemyError):
|
||||
"""A disconnect is detected on a raw DB-API connection.
|
||||
|
||||
This error is raised and consumed internally by a connection pool. It can
|
||||
be raised by a ``PoolListener`` so that the host pool forces a disconnect.
|
||||
be raised by the :meth:`.PoolEvents.checkout` event
|
||||
so that the host pool forces a retry; the exception will be caught
|
||||
three times in a row before the pool gives up and raises
|
||||
:class:`~sqlalchemy.exc.InvalidRequestError` regarding the connection attempt.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# Moved to orm.exc; compatability definition installed by orm import until 0.6
|
||||
# Moved to orm.exc; compatibility definition installed by orm import until 0.6
|
||||
FlushError = None
|
||||
|
||||
class TimeoutError(SQLAlchemyError):
|
||||
@@ -78,9 +105,25 @@ class NoReferenceError(InvalidRequestError):
|
||||
class NoReferencedTableError(NoReferenceError):
|
||||
"""Raised by ``ForeignKey`` when the referred ``Table`` cannot be located."""
|
||||
|
||||
def __init__(self, message, tname):
|
||||
NoReferenceError.__init__(self, message)
|
||||
self.table_name = tname
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (self.args[0], self.table_name)
|
||||
|
||||
class NoReferencedColumnError(NoReferenceError):
|
||||
"""Raised by ``ForeignKey`` when the referred ``Column`` cannot be located."""
|
||||
|
||||
def __init__(self, message, tname, cname):
|
||||
NoReferenceError.__init__(self, message)
|
||||
self.table_name = tname
|
||||
self.column_name = cname
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (self.args[0], self.table_name,
|
||||
self.column_name)
|
||||
|
||||
class NoSuchTableError(InvalidRequestError):
|
||||
"""Table does not exist or is not visible to a connection."""
|
||||
|
||||
@@ -89,10 +132,64 @@ class UnboundExecutionError(InvalidRequestError):
|
||||
"""SQL was attempted without a database connection to execute it on."""
|
||||
|
||||
|
||||
# Moved to orm.exc; compatability definition installed by orm import until 0.6
|
||||
class DontWrapMixin(object):
|
||||
"""A mixin class which, when applied to a user-defined Exception class,
|
||||
will not be wrapped inside of :class:`.StatementError` if the error is
|
||||
emitted within the process of executing a statement.
|
||||
|
||||
E.g.::
|
||||
from sqlalchemy.exc import DontWrapMixin
|
||||
|
||||
class MyCustomException(Exception, DontWrapMixin):
|
||||
pass
|
||||
|
||||
class MySpecialType(TypeDecorator):
|
||||
impl = String
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value == 'invalid':
|
||||
raise MyCustomException("invalid!")
|
||||
|
||||
"""
|
||||
import sys
|
||||
if sys.version_info < (2, 5):
|
||||
class DontWrapMixin:
|
||||
pass
|
||||
|
||||
# Moved to orm.exc; compatibility definition installed by orm import until 0.6
|
||||
UnmappedColumnError = None
|
||||
|
||||
class DBAPIError(SQLAlchemyError):
|
||||
class StatementError(SQLAlchemyError):
|
||||
"""An error occurred during execution of a SQL statement.
|
||||
|
||||
:class:`.StatementError` wraps the exception raised
|
||||
during execution, and features :attr:`.statement`
|
||||
and :attr:`.params` attributes which supply context regarding
|
||||
the specifics of the statement which had an issue.
|
||||
|
||||
The wrapped exception object is available in
|
||||
the :attr:`.orig` attribute.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, message, statement, params, orig):
|
||||
SQLAlchemyError.__init__(self, message)
|
||||
self.statement = statement
|
||||
self.params = params
|
||||
self.orig = orig
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (self.args[0], self.statement,
|
||||
self.params, self.orig)
|
||||
|
||||
def __str__(self):
|
||||
from sqlalchemy.sql import util
|
||||
params_repr = util._repr_params(self.params, 10)
|
||||
return ' '.join((SQLAlchemyError.__str__(self),
|
||||
repr(self.statement), repr(params_repr)))
|
||||
|
||||
|
||||
class DBAPIError(StatementError):
|
||||
"""Raised when the execution of a database operation fails.
|
||||
|
||||
``DBAPIError`` wraps exceptions raised by the DB-API underlying the
|
||||
@@ -103,29 +200,47 @@ class DBAPIError(SQLAlchemyError):
|
||||
that there is no guarantee that different DB-API implementations will
|
||||
raise the same exception type for any given error condition.
|
||||
|
||||
If the error-raising operation occured in the execution of a SQL
|
||||
statement, that statement and its parameters will be available on
|
||||
the exception object in the ``statement`` and ``params`` attributes.
|
||||
:class:`.DBAPIError` features :attr:`.statement`
|
||||
and :attr:`.params` attributes which supply context regarding
|
||||
the specifics of the statement which had an issue, for the
|
||||
typical case when the error was raised within the context of
|
||||
emitting a SQL statement.
|
||||
|
||||
The wrapped exception object is available in the ``orig`` attribute.
|
||||
The wrapped exception object is available in the :attr:`.orig` attribute.
|
||||
Its type and properties are DB-API implementation specific.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def instance(cls, statement, params, orig, connection_invalidated=False):
|
||||
def instance(cls, statement, params,
|
||||
orig,
|
||||
dbapi_base_err,
|
||||
connection_invalidated=False):
|
||||
# Don't ever wrap these, just return them directly as if
|
||||
# DBAPIError didn't exist.
|
||||
if isinstance(orig, (KeyboardInterrupt, SystemExit)):
|
||||
if isinstance(orig, (KeyboardInterrupt, SystemExit, DontWrapMixin)):
|
||||
return orig
|
||||
|
||||
if orig is not None:
|
||||
# not a DBAPI error, statement is present.
|
||||
# raise a StatementError
|
||||
if not isinstance(orig, dbapi_base_err) and statement:
|
||||
return StatementError(
|
||||
"%s (original cause: %s)" % (
|
||||
str(orig),
|
||||
traceback.format_exception_only(orig.__class__, orig)[-1].strip()
|
||||
), statement, params, orig)
|
||||
|
||||
name, glob = orig.__class__.__name__, globals()
|
||||
if name in glob and issubclass(glob[name], DBAPIError):
|
||||
cls = glob[name]
|
||||
|
||||
return cls(statement, params, orig, connection_invalidated)
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (self.statement, self.params,
|
||||
self.orig, self.connection_invalidated)
|
||||
|
||||
def __init__(self, statement, params, orig, connection_invalidated=False):
|
||||
try:
|
||||
text = str(orig)
|
||||
@@ -133,26 +248,15 @@ class DBAPIError(SQLAlchemyError):
|
||||
raise
|
||||
except Exception, e:
|
||||
text = 'Error in str() of DB-API-generated exception: ' + str(e)
|
||||
SQLAlchemyError.__init__(
|
||||
self, '(%s) %s' % (orig.__class__.__name__, text))
|
||||
self.statement = statement
|
||||
self.params = params
|
||||
self.orig = orig
|
||||
StatementError.__init__(
|
||||
self,
|
||||
'(%s) %s' % (orig.__class__.__name__, text),
|
||||
statement,
|
||||
params,
|
||||
orig
|
||||
)
|
||||
self.connection_invalidated = connection_invalidated
|
||||
|
||||
def __str__(self):
|
||||
if isinstance(self.params, (list, tuple)) and len(self.params) > 10 and isinstance(self.params[0], (list, dict, tuple)):
|
||||
return ' '.join((SQLAlchemyError.__str__(self),
|
||||
repr(self.statement),
|
||||
repr(self.params[:2]),
|
||||
'... and a total of %i bound parameter sets' % len(self.params)))
|
||||
return ' '.join((SQLAlchemyError.__str__(self),
|
||||
repr(self.statement), repr(self.params)))
|
||||
|
||||
|
||||
# As of 0.4, SQLError is now DBAPIError.
|
||||
# SQLError alias will be removed in 0.6.
|
||||
SQLError = DBAPIError
|
||||
|
||||
class InterfaceError(DBAPIError):
|
||||
"""Wraps a DB-API InterfaceError."""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ext/__init__.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ext/associationproxy.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -18,33 +18,28 @@ import weakref
|
||||
from sqlalchemy import exceptions
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy.orm import collections
|
||||
from sqlalchemy.orm import collections, ColumnProperty
|
||||
from sqlalchemy.sql import not_
|
||||
|
||||
|
||||
def association_proxy(target_collection, attr, **kw):
|
||||
"""Return a Python property implementing a view of *attr* over a collection.
|
||||
|
||||
Implements a read/write view over an instance's *target_collection*,
|
||||
extracting *attr* from each member of the collection. The property acts
|
||||
somewhat like this list comprehension::
|
||||
|
||||
[getattr(member, *attr*)
|
||||
for member in getattr(instance, *target_collection*)]
|
||||
|
||||
Unlike the list comprehension, the collection returned by the property is
|
||||
always in sync with *target_collection*, and mutations made to either
|
||||
collection will be reflected in both.
|
||||
|
||||
"""Return a Python property implementing a view of a target
|
||||
attribute which references an attribute on members of the
|
||||
target.
|
||||
|
||||
The returned value is an instance of :class:`.AssociationProxy`.
|
||||
|
||||
Implements a Python property representing a relationship as a collection of
|
||||
simpler values. The proxied property will mimic the collection type of
|
||||
simpler values, or a scalar value. The proxied property will mimic the collection type of
|
||||
the target (list, dict or set), or, in the case of a one to one relationship,
|
||||
a simple scalar value.
|
||||
|
||||
:param target_collection: Name of the relationship attribute we'll proxy to,
|
||||
usually created with :func:`~sqlalchemy.orm.relationship`.
|
||||
:param target_collection: Name of the attribute we'll proxy to.
|
||||
This attribute is typically mapped by
|
||||
:func:`~sqlalchemy.orm.relationship` to link to a target collection, but
|
||||
can also be a many-to-one or non-scalar relationship.
|
||||
|
||||
:param attr: Attribute on the associated instances we'll proxy for.
|
||||
:param attr: Attribute on the associated instance or instances we'll proxy for.
|
||||
|
||||
For example, given a target collection of [obj1, obj2], a list created
|
||||
by this proxy property would look like [getattr(obj1, *attr*),
|
||||
@@ -75,7 +70,7 @@ def association_proxy(target_collection, attr, **kw):
|
||||
situation.
|
||||
|
||||
:param \*\*kw: Passes along any other keyword arguments to
|
||||
:class:`AssociationProxy`.
|
||||
:class:`.AssociationProxy`.
|
||||
|
||||
"""
|
||||
return AssociationProxy(target_collection, attr, **kw)
|
||||
@@ -85,21 +80,23 @@ class AssociationProxy(object):
|
||||
"""A descriptor that presents a read/write view of an object attribute."""
|
||||
|
||||
def __init__(self, target_collection, attr, creator=None,
|
||||
getset_factory=None, proxy_factory=None, proxy_bulk_set=None):
|
||||
"""Arguments are:
|
||||
getset_factory=None, proxy_factory=None,
|
||||
proxy_bulk_set=None):
|
||||
"""Construct a new :class:`.AssociationProxy`.
|
||||
|
||||
The :func:`.association_proxy` function is provided as the usual
|
||||
entrypoint here, though :class:`.AssociationProxy` can be instantiated
|
||||
and/or subclassed directly.
|
||||
|
||||
target_collection
|
||||
Name of the collection we'll proxy to, usually created with
|
||||
'relationship()' in a mapper setup.
|
||||
:param target_collection: Name of the collection we'll proxy to,
|
||||
usually created with :func:`.relationship`.
|
||||
|
||||
attr
|
||||
Attribute on the collected instances we'll proxy for. For example,
|
||||
:param attr: Attribute on the collected instances we'll proxy for. For example,
|
||||
given a target collection of [obj1, obj2], a list created by this
|
||||
proxy property would look like [getattr(obj1, attr), getattr(obj2,
|
||||
attr)]
|
||||
|
||||
creator
|
||||
Optional. When new items are added to this proxied collection, new
|
||||
:param creator: Optional. When new items are added to this proxied collection, new
|
||||
instances of the class collected by the target collection will be
|
||||
created. For list and set collections, the target class constructor
|
||||
will be called with the 'value' for the new instance. For dict
|
||||
@@ -108,8 +105,7 @@ class AssociationProxy(object):
|
||||
If you want to construct instances differently, supply a 'creator'
|
||||
function that takes arguments as above and returns instances.
|
||||
|
||||
getset_factory
|
||||
Optional. Proxied attribute access is automatically handled by
|
||||
:param getset_factory: Optional. Proxied attribute access is automatically handled by
|
||||
routines that get and set values based on the `attr` argument for
|
||||
this proxy.
|
||||
|
||||
@@ -118,16 +114,14 @@ class AssociationProxy(object):
|
||||
`setter` functions. The factory is called with two arguments, the
|
||||
abstract type of the underlying collection and this proxy instance.
|
||||
|
||||
proxy_factory
|
||||
Optional. The type of collection to emulate is determined by
|
||||
:param proxy_factory: Optional. The type of collection to emulate is determined by
|
||||
sniffing the target collection. If your collection type can't be
|
||||
determined by duck typing or you'd like to use a different
|
||||
collection implementation, you may supply a factory function to
|
||||
produce those collections. Only applicable to non-scalar relationships.
|
||||
|
||||
proxy_bulk_set
|
||||
Optional, use with proxy_factory. See the _set() method for
|
||||
details.
|
||||
:param proxy_bulk_set: Optional, use with proxy_factory. See
|
||||
the _set() method for details.
|
||||
|
||||
"""
|
||||
self.target_collection = target_collection
|
||||
@@ -137,33 +131,97 @@ class AssociationProxy(object):
|
||||
self.proxy_factory = proxy_factory
|
||||
self.proxy_bulk_set = proxy_bulk_set
|
||||
|
||||
self.scalar = None
|
||||
self.owning_class = None
|
||||
self.key = '_%s_%s_%s' % (
|
||||
type(self).__name__, target_collection, id(self))
|
||||
self.collection_class = None
|
||||
|
||||
@property
|
||||
def remote_attr(self):
|
||||
"""The 'remote' :class:`.MapperProperty` referenced by this
|
||||
:class:`.AssociationProxy`.
|
||||
|
||||
New in 0.7.3.
|
||||
|
||||
See also:
|
||||
|
||||
:attr:`.AssociationProxy.attr`
|
||||
|
||||
:attr:`.AssociationProxy.local_attr`
|
||||
|
||||
"""
|
||||
return getattr(self.target_class, self.value_attr)
|
||||
|
||||
@property
|
||||
def local_attr(self):
|
||||
"""The 'local' :class:`.MapperProperty` referenced by this
|
||||
:class:`.AssociationProxy`.
|
||||
|
||||
New in 0.7.3.
|
||||
|
||||
See also:
|
||||
|
||||
:attr:`.AssociationProxy.attr`
|
||||
|
||||
:attr:`.AssociationProxy.remote_attr`
|
||||
|
||||
"""
|
||||
return getattr(self.owning_class, self.target_collection)
|
||||
|
||||
@property
|
||||
def attr(self):
|
||||
"""Return a tuple of ``(local_attr, remote_attr)``.
|
||||
|
||||
This attribute is convenient when specifying a join
|
||||
using :meth:`.Query.join` across two relationships::
|
||||
|
||||
sess.query(Parent).join(*Parent.proxied.attr)
|
||||
|
||||
New in 0.7.3.
|
||||
|
||||
See also:
|
||||
|
||||
:attr:`.AssociationProxy.local_attr`
|
||||
|
||||
:attr:`.AssociationProxy.remote_attr`
|
||||
|
||||
"""
|
||||
return (self.local_attr, self.remote_attr)
|
||||
|
||||
def _get_property(self):
|
||||
return (orm.class_mapper(self.owning_class).
|
||||
get_property(self.target_collection))
|
||||
|
||||
@property
|
||||
@util.memoized_property
|
||||
def target_class(self):
|
||||
"""The class the proxy is attached to."""
|
||||
"""The intermediary class handled by this :class:`.AssociationProxy`.
|
||||
|
||||
Intercepted append/set/assignment events will result
|
||||
in the generation of new instances of this class.
|
||||
|
||||
"""
|
||||
return self._get_property().mapper.class_
|
||||
|
||||
def _target_is_scalar(self):
|
||||
return not self._get_property().uselist
|
||||
@util.memoized_property
|
||||
def scalar(self):
|
||||
"""Return ``True`` if this :class:`.AssociationProxy` proxies a scalar
|
||||
relationship on the local side."""
|
||||
|
||||
scalar = not self._get_property().uselist
|
||||
if scalar:
|
||||
self._initialize_scalar_accessors()
|
||||
return scalar
|
||||
|
||||
@util.memoized_property
|
||||
def _value_is_scalar(self):
|
||||
return not self._get_property().\
|
||||
mapper.get_property(self.value_attr).uselist
|
||||
|
||||
def __get__(self, obj, class_):
|
||||
if self.owning_class is None:
|
||||
self.owning_class = class_ and class_ or type(obj)
|
||||
if obj is None:
|
||||
return self
|
||||
elif self.scalar is None:
|
||||
self.scalar = self._target_is_scalar()
|
||||
if self.scalar:
|
||||
self._initialize_scalar_accessors()
|
||||
|
||||
if self.scalar:
|
||||
return self._scalar_get(getattr(obj, self.target_collection))
|
||||
@@ -183,10 +241,6 @@ class AssociationProxy(object):
|
||||
def __set__(self, obj, values):
|
||||
if self.owning_class is None:
|
||||
self.owning_class = type(obj)
|
||||
if self.scalar is None:
|
||||
self.scalar = self._target_is_scalar()
|
||||
if self.scalar:
|
||||
self._initialize_scalar_accessors()
|
||||
|
||||
if self.scalar:
|
||||
creator = self.creator and self.creator or self.target_class
|
||||
@@ -278,13 +332,63 @@ class AssociationProxy(object):
|
||||
return self._get_property().comparator
|
||||
|
||||
def any(self, criterion=None, **kwargs):
|
||||
return self._comparator.any(getattr(self.target_class, self.value_attr).has(criterion, **kwargs))
|
||||
"""Produce a proxied 'any' expression using EXISTS.
|
||||
|
||||
This expression will be a composed product
|
||||
using the :meth:`.RelationshipProperty.Comparator.any`
|
||||
and/or :meth:`.RelationshipProperty.Comparator.has`
|
||||
operators of the underlying proxied attributes.
|
||||
|
||||
"""
|
||||
|
||||
if self._value_is_scalar:
|
||||
value_expr = getattr(self.target_class, self.value_attr).has(criterion, **kwargs)
|
||||
else:
|
||||
value_expr = getattr(self.target_class, self.value_attr).any(criterion, **kwargs)
|
||||
|
||||
# check _value_is_scalar here, otherwise
|
||||
# we're scalar->scalar - call .any() so that
|
||||
# the "can't call any() on a scalar" msg is raised.
|
||||
if self.scalar and not self._value_is_scalar:
|
||||
return self._comparator.has(
|
||||
value_expr
|
||||
)
|
||||
else:
|
||||
return self._comparator.any(
|
||||
value_expr
|
||||
)
|
||||
|
||||
def has(self, criterion=None, **kwargs):
|
||||
return self._comparator.has(getattr(self.target_class, self.value_attr).has(criterion, **kwargs))
|
||||
"""Produce a proxied 'has' expression using EXISTS.
|
||||
|
||||
This expression will be a composed product
|
||||
using the :meth:`.RelationshipProperty.Comparator.any`
|
||||
and/or :meth:`.RelationshipProperty.Comparator.has`
|
||||
operators of the underlying proxied attributes.
|
||||
|
||||
"""
|
||||
|
||||
return self._comparator.has(
|
||||
getattr(self.target_class, self.value_attr).\
|
||||
has(criterion, **kwargs)
|
||||
)
|
||||
|
||||
def contains(self, obj):
|
||||
return self._comparator.any(**{self.value_attr: obj})
|
||||
"""Produce a proxied 'contains' expression using EXISTS.
|
||||
|
||||
This expression will be a composed product
|
||||
using the :meth:`.RelationshipProperty.Comparator.any`
|
||||
, :meth:`.RelationshipProperty.Comparator.has`,
|
||||
and/or :meth:`.RelationshipProperty.Comparator.contains`
|
||||
operators of the underlying proxied attributes.
|
||||
"""
|
||||
|
||||
if self.scalar and not self._value_is_scalar:
|
||||
return self._comparator.has(
|
||||
getattr(self.target_class, self.value_attr).contains(obj)
|
||||
)
|
||||
else:
|
||||
return self._comparator.any(**{self.value_attr: obj})
|
||||
|
||||
def __eq__(self, obj):
|
||||
return self._comparator.has(**{self.value_attr: obj})
|
||||
@@ -664,11 +768,20 @@ class _AssociationDict(_AssociationCollection):
|
||||
len(a))
|
||||
elif len(a) == 1:
|
||||
seq_or_map = a[0]
|
||||
for item in seq_or_map:
|
||||
if isinstance(item, tuple):
|
||||
self[item[0]] = item[1]
|
||||
else:
|
||||
# discern dict from sequence - took the advice
|
||||
# from http://www.voidspace.org.uk/python/articles/duck_typing.shtml
|
||||
# still not perfect :(
|
||||
if hasattr(seq_or_map, 'keys'):
|
||||
for item in seq_or_map:
|
||||
self[item] = seq_or_map[item]
|
||||
else:
|
||||
try:
|
||||
for k, v in seq_or_map:
|
||||
self[k] = v
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
"dictionary update sequence "
|
||||
"requires 2-element tuples")
|
||||
|
||||
for key, value in kw:
|
||||
self[key] = value
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ext/compiler.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -91,6 +91,11 @@ Produces::
|
||||
|
||||
"INSERT INTO mytable (SELECT mytable.x, mytable.y, mytable.z FROM mytable WHERE mytable.x > :x_1)"
|
||||
|
||||
.. note::
|
||||
|
||||
The above ``InsertFromSelect`` construct probably wants to have "autocommit"
|
||||
enabled. See :ref:`enabling_compiled_autocommit` for this step.
|
||||
|
||||
Cross Compiling between SQL and DDL compilers
|
||||
---------------------------------------------
|
||||
|
||||
@@ -106,6 +111,50 @@ constraint that embeds a SQL expression::
|
||||
ddlcompiler.sql_compiler.process(constraint.expression)
|
||||
)
|
||||
|
||||
.. _enabling_compiled_autocommit:
|
||||
|
||||
Enabling Autocommit on a Construct
|
||||
==================================
|
||||
|
||||
Recall from the section :ref:`autocommit` that the :class:`.Engine`, when asked to execute
|
||||
a construct in the absence of a user-defined transaction, detects if the given
|
||||
construct represents DML or DDL, that is, a data modification or data definition statement, which
|
||||
requires (or may require, in the case of DDL) that the transaction generated by the DBAPI be committed
|
||||
(recall that DBAPI always has a transaction going on regardless of what SQLAlchemy does). Checking
|
||||
for this is actually accomplished
|
||||
by checking for the "autocommit" execution option on the construct. When building a construct like
|
||||
an INSERT derivation, a new DDL type, or perhaps a stored procedure that alters data, the "autocommit"
|
||||
option needs to be set in order for the statement to function with "connectionless" execution
|
||||
(as described in :ref:`dbengine_implicit`).
|
||||
|
||||
Currently a quick way to do this is to subclass :class:`.Executable`, then add the "autocommit" flag
|
||||
to the ``_execution_options`` dictionary (note this is a "frozen" dictionary which supplies a generative
|
||||
``union()`` method)::
|
||||
|
||||
from sqlalchemy.sql.expression import Executable, ClauseElement
|
||||
|
||||
class MyInsertThing(Executable, ClauseElement):
|
||||
_execution_options = \\
|
||||
Executable._execution_options.union({'autocommit': True})
|
||||
|
||||
More succinctly, if the construct is truly similar to an INSERT, UPDATE, or DELETE, :class:`.UpdateBase`
|
||||
can be used, which already is a subclass of :class:`.Executable`, :class:`.ClauseElement` and includes the
|
||||
``autocommit`` flag::
|
||||
|
||||
from sqlalchemy.sql.expression import UpdateBase
|
||||
|
||||
class MyInsertThing(UpdateBase):
|
||||
def __init__(self, ...):
|
||||
...
|
||||
|
||||
|
||||
|
||||
|
||||
DDL elements that subclass :class:`.DDLElement` already have the "autocommit" flag turned on.
|
||||
|
||||
|
||||
|
||||
|
||||
Changing the default compilation of existing constructs
|
||||
=======================================================
|
||||
|
||||
@@ -147,7 +196,10 @@ Changing Compilation of Types
|
||||
Subclassing Guidelines
|
||||
======================
|
||||
|
||||
A big part of using the compiler extension is subclassing SQLAlchemy expression constructs. To make this easier, the expression and schema packages feature a set of "bases" intended for common tasks. A synopsis is as follows:
|
||||
A big part of using the compiler extension is subclassing SQLAlchemy
|
||||
expression constructs. To make this easier, the expression and
|
||||
schema packages feature a set of "bases" intended for common tasks.
|
||||
A synopsis is as follows:
|
||||
|
||||
* :class:`~sqlalchemy.sql.expression.ClauseElement` - This is the root
|
||||
expression class. Any SQL expression can be derived from this base, and is
|
||||
@@ -201,7 +253,121 @@ A big part of using the compiler extension is subclassing SQLAlchemy expression
|
||||
can be passed directly to an ``execute()`` method. It is already implicit
|
||||
within ``DDLElement`` and ``FunctionElement``.
|
||||
|
||||
Further Examples
|
||||
================
|
||||
|
||||
"UTC timestamp" function
|
||||
-------------------------
|
||||
|
||||
A function that works like "CURRENT_TIMESTAMP" except applies the appropriate conversions
|
||||
so that the time is in UTC time. Timestamps are best stored in relational databases
|
||||
as UTC, without time zones. UTC so that your database doesn't think time has gone
|
||||
backwards in the hour when daylight savings ends, without timezones because timezones
|
||||
are like character encodings - they're best applied only at the endpoints of an
|
||||
application (i.e. convert to UTC upon user input, re-apply desired timezone upon display).
|
||||
|
||||
For Postgresql and Microsoft SQL Server::
|
||||
|
||||
from sqlalchemy.sql import expression
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
from sqlalchemy.types import DateTime
|
||||
|
||||
class utcnow(expression.FunctionElement):
|
||||
type = DateTime()
|
||||
|
||||
@compiles(utcnow, 'postgresql')
|
||||
def pg_utcnow(element, compiler, **kw):
|
||||
return "TIMEZONE('utc', CURRENT_TIMESTAMP)"
|
||||
|
||||
@compiles(utcnow, 'mssql')
|
||||
def ms_utcnow(element, compiler, **kw):
|
||||
return "GETUTCDATE()"
|
||||
|
||||
Example usage::
|
||||
|
||||
from sqlalchemy import (
|
||||
Table, Column, Integer, String, DateTime, MetaData
|
||||
)
|
||||
metadata = MetaData()
|
||||
event = Table("event", metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("description", String(50), nullable=False),
|
||||
Column("timestamp", DateTime, server_default=utcnow())
|
||||
)
|
||||
|
||||
"GREATEST" function
|
||||
-------------------
|
||||
|
||||
The "GREATEST" function is given any number of arguments and returns the one that is
|
||||
of the highest value - it's equivalent to Python's ``max`` function. A SQL
|
||||
standard version versus a CASE based version which only accommodates two
|
||||
arguments::
|
||||
|
||||
from sqlalchemy.sql import expression
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
from sqlalchemy.types import Numeric
|
||||
|
||||
class greatest(expression.FunctionElement):
|
||||
type = Numeric()
|
||||
name = 'greatest'
|
||||
|
||||
@compiles(greatest)
|
||||
def default_greatest(element, compiler, **kw):
|
||||
return compiler.visit_function(element)
|
||||
|
||||
@compiles(greatest, 'sqlite')
|
||||
@compiles(greatest, 'mssql')
|
||||
@compiles(greatest, 'oracle')
|
||||
def case_greatest(element, compiler, **kw):
|
||||
arg1, arg2 = list(element.clauses)
|
||||
return "CASE WHEN %s > %s THEN %s ELSE %s END" % (
|
||||
compiler.process(arg1),
|
||||
compiler.process(arg2),
|
||||
compiler.process(arg1),
|
||||
compiler.process(arg2),
|
||||
)
|
||||
|
||||
Example usage::
|
||||
|
||||
Session.query(Account).\\
|
||||
filter(
|
||||
greatest(
|
||||
Account.checking_balance,
|
||||
Account.savings_balance) > 10000
|
||||
)
|
||||
|
||||
"false" expression
|
||||
------------------
|
||||
|
||||
Render a "false" constant expression, rendering as "0" on platforms that don't have a "false" constant::
|
||||
|
||||
from sqlalchemy.sql import expression
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
|
||||
class sql_false(expression.ColumnElement):
|
||||
pass
|
||||
|
||||
@compiles(sql_false)
|
||||
def default_false(element, compiler, **kw):
|
||||
return "false"
|
||||
|
||||
@compiles(sql_false, 'mssql')
|
||||
@compiles(sql_false, 'mysql')
|
||||
@compiles(sql_false, 'oracle')
|
||||
def int_false(element, compiler, **kw):
|
||||
return "0"
|
||||
|
||||
Example usage::
|
||||
|
||||
from sqlalchemy import select, union_all
|
||||
|
||||
exp = union_all(
|
||||
select([users.c.name, sql_false().label("enrolled")]),
|
||||
select([customers.c.name, customers.c.enrolled])
|
||||
)
|
||||
|
||||
"""
|
||||
from sqlalchemy import exc
|
||||
|
||||
def compiles(class_, *specs):
|
||||
def decorate(fn):
|
||||
@@ -234,6 +400,11 @@ class _dispatcher(object):
|
||||
# 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']
|
||||
try:
|
||||
fn = self.specs['default']
|
||||
except KeyError:
|
||||
raise exc.CompileError(
|
||||
"%s construct has no default "
|
||||
"compilation handler." % type(element))
|
||||
return fn(element, compiler, **kw)
|
||||
|
||||
|
||||
Regular → Executable
+523
-217
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
# ext/horizontal_shard.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -14,16 +14,67 @@ the source distrbution.
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy.exceptions as sa_exc
|
||||
from sqlalchemy import exc as sa_exc
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy.orm.session import Session
|
||||
from sqlalchemy.orm.query import Query
|
||||
|
||||
__all__ = ['ShardedSession', 'ShardedQuery']
|
||||
|
||||
class ShardedQuery(Query):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ShardedQuery, self).__init__(*args, **kwargs)
|
||||
self.id_chooser = self.session.id_chooser
|
||||
self.query_chooser = self.session.query_chooser
|
||||
self._shard_id = None
|
||||
|
||||
def set_shard(self, shard_id):
|
||||
"""return a new query, limited to a single shard ID.
|
||||
|
||||
all subsequent operations with the returned query will
|
||||
be against the single shard regardless of other state.
|
||||
"""
|
||||
|
||||
q = self._clone()
|
||||
q._shard_id = shard_id
|
||||
return q
|
||||
|
||||
def _execute_and_instances(self, context):
|
||||
def iter_for_shard(shard_id):
|
||||
context.attributes['shard_id'] = shard_id
|
||||
result = self._connection_from_session(
|
||||
mapper=self._mapper_zero(),
|
||||
shard_id=shard_id).execute(
|
||||
context.statement,
|
||||
self._params)
|
||||
return self.instances(result, context)
|
||||
|
||||
if self._shard_id is not None:
|
||||
return iter_for_shard(self._shard_id)
|
||||
else:
|
||||
partial = []
|
||||
for shard_id in self.query_chooser(self):
|
||||
partial.extend(iter_for_shard(shard_id))
|
||||
|
||||
# if some kind of in memory 'sorting'
|
||||
# were done, this is where it would happen
|
||||
return iter(partial)
|
||||
|
||||
def get(self, ident, **kwargs):
|
||||
if self._shard_id is not None:
|
||||
return super(ShardedQuery, self).get(ident)
|
||||
else:
|
||||
ident = util.to_list(ident)
|
||||
for shard_id in self.id_chooser(self, ident):
|
||||
o = self.set_shard(shard_id).get(ident, **kwargs)
|
||||
if o is not None:
|
||||
return o
|
||||
else:
|
||||
return None
|
||||
|
||||
class ShardedSession(Session):
|
||||
def __init__(self, shard_chooser, id_chooser, query_chooser, shards=None, **kwargs):
|
||||
def __init__(self, shard_chooser, id_chooser, query_chooser, shards=None,
|
||||
query_cls=ShardedQuery, **kwargs):
|
||||
"""Construct a ShardedSession.
|
||||
|
||||
:param shard_chooser: A callable which, passed a Mapper, a mapped instance, and possibly a
|
||||
@@ -45,13 +96,12 @@ class ShardedSession(Session):
|
||||
objects.
|
||||
|
||||
"""
|
||||
super(ShardedSession, self).__init__(**kwargs)
|
||||
super(ShardedSession, self).__init__(query_cls=query_cls, **kwargs)
|
||||
self.shard_chooser = shard_chooser
|
||||
self.id_chooser = id_chooser
|
||||
self.query_chooser = query_chooser
|
||||
self.__binds = {}
|
||||
self._mapper_flush_opts = {'connection_callable':self.connection}
|
||||
self._query_cls = ShardedQuery
|
||||
self.connection_callable = self.connection
|
||||
if shards is not None:
|
||||
for k in shards:
|
||||
self.bind_shard(k, shards[k])
|
||||
@@ -75,51 +125,4 @@ class ShardedSession(Session):
|
||||
def bind_shard(self, shard_id, bind):
|
||||
self.__binds[shard_id] = bind
|
||||
|
||||
class ShardedQuery(Query):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ShardedQuery, self).__init__(*args, **kwargs)
|
||||
self.id_chooser = self.session.id_chooser
|
||||
self.query_chooser = self.session.query_chooser
|
||||
self._shard_id = None
|
||||
|
||||
def set_shard(self, shard_id):
|
||||
"""return a new query, limited to a single shard ID.
|
||||
|
||||
all subsequent operations with the returned query will
|
||||
be against the single shard regardless of other state.
|
||||
"""
|
||||
|
||||
q = self._clone()
|
||||
q._shard_id = shard_id
|
||||
return q
|
||||
|
||||
def _execute_and_instances(self, context):
|
||||
if self._shard_id is not None:
|
||||
result = self.session.connection(
|
||||
mapper=self._mapper_zero(),
|
||||
shard_id=self._shard_id).execute(context.statement, self._params)
|
||||
return self.instances(result, context)
|
||||
else:
|
||||
partial = []
|
||||
for shard_id in self.query_chooser(self):
|
||||
result = self.session.connection(
|
||||
mapper=self._mapper_zero(),
|
||||
shard_id=shard_id).execute(context.statement, self._params)
|
||||
partial = partial + list(self.instances(result, context))
|
||||
|
||||
# if some kind of in memory 'sorting'
|
||||
# were done, this is where it would happen
|
||||
return iter(partial)
|
||||
|
||||
def get(self, ident, **kwargs):
|
||||
if self._shard_id is not None:
|
||||
return super(ShardedQuery, self).get(ident)
|
||||
else:
|
||||
ident = util.to_list(ident)
|
||||
for shard_id in self.id_chooser(self, ident):
|
||||
o = self.set_shard(shard_id).get(ident, **kwargs)
|
||||
if o is not None:
|
||||
return o
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@@ -0,0 +1,677 @@
|
||||
# ext/hybrid.py
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
"""Define attributes on ORM-mapped classes that have "hybrid" behavior.
|
||||
|
||||
"hybrid" means the attribute has distinct behaviors defined at the
|
||||
class level and at the instance level.
|
||||
|
||||
The :mod:`~sqlalchemy.ext.hybrid` extension provides a special form of method
|
||||
decorator, is around 50 lines of code and has almost no dependencies on the rest
|
||||
of SQLAlchemy. It can in theory work with any class-level expression generator.
|
||||
|
||||
Consider a table ``interval`` as below::
|
||||
|
||||
from sqlalchemy import MetaData, Table, Column, Integer
|
||||
|
||||
metadata = MetaData()
|
||||
|
||||
interval_table = Table('interval', metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('start', Integer, nullable=False),
|
||||
Column('end', Integer, nullable=False)
|
||||
)
|
||||
|
||||
We can define higher level functions on mapped classes that produce SQL
|
||||
expressions at the class level, and Python expression evaluation at the
|
||||
instance level. Below, each function decorated with :func:`.hybrid_method`
|
||||
or :func:`.hybrid_property` may receive ``self`` as an instance of the class,
|
||||
or as the class itself::
|
||||
|
||||
from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
|
||||
from sqlalchemy.orm import mapper, Session, aliased
|
||||
|
||||
class Interval(object):
|
||||
def __init__(self, start, end):
|
||||
self.start = start
|
||||
self.end = end
|
||||
|
||||
@hybrid_property
|
||||
def length(self):
|
||||
return self.end - self.start
|
||||
|
||||
@hybrid_method
|
||||
def contains(self,point):
|
||||
return (self.start <= point) & (point < self.end)
|
||||
|
||||
@hybrid_method
|
||||
def intersects(self, other):
|
||||
return self.contains(other.start) | self.contains(other.end)
|
||||
|
||||
mapper(Interval, interval_table)
|
||||
|
||||
Above, the ``length`` property returns the difference between the ``end`` and
|
||||
``start`` attributes. With an instance of ``Interval``, this subtraction occurs
|
||||
in Python, using normal Python descriptor mechanics::
|
||||
|
||||
>>> i1 = Interval(5, 10)
|
||||
>>> i1.length
|
||||
5
|
||||
|
||||
At the class level, the usual descriptor behavior of returning the descriptor
|
||||
itself is modified by :class:`.hybrid_property`, to instead evaluate the function
|
||||
body given the ``Interval`` class as the argument::
|
||||
|
||||
>>> print Interval.length
|
||||
interval."end" - interval.start
|
||||
|
||||
>>> print Session().query(Interval).filter(Interval.length > 10)
|
||||
SELECT interval.id AS interval_id, interval.start AS interval_start,
|
||||
interval."end" AS interval_end
|
||||
FROM interval
|
||||
WHERE interval."end" - interval.start > :param_1
|
||||
|
||||
ORM methods such as :meth:`~.Query.filter_by` generally use ``getattr()`` to
|
||||
locate attributes, so can also be used with hybrid attributes::
|
||||
|
||||
>>> print Session().query(Interval).filter_by(length=5)
|
||||
SELECT interval.id AS interval_id, interval.start AS interval_start,
|
||||
interval."end" AS interval_end
|
||||
FROM interval
|
||||
WHERE interval."end" - interval.start = :param_1
|
||||
|
||||
The ``contains()`` and ``intersects()`` methods are decorated with :class:`.hybrid_method`.
|
||||
This decorator applies the same idea to methods which accept
|
||||
zero or more arguments. The above methods return boolean values, and take advantage
|
||||
of the Python ``|`` and ``&`` bitwise operators to produce equivalent instance-level and
|
||||
SQL expression-level boolean behavior::
|
||||
|
||||
>>> i1.contains(6)
|
||||
True
|
||||
>>> i1.contains(15)
|
||||
False
|
||||
>>> i1.intersects(Interval(7, 18))
|
||||
True
|
||||
>>> i1.intersects(Interval(25, 29))
|
||||
False
|
||||
|
||||
>>> print Session().query(Interval).filter(Interval.contains(15))
|
||||
SELECT interval.id AS interval_id, interval.start AS interval_start,
|
||||
interval."end" AS interval_end
|
||||
FROM interval
|
||||
WHERE interval.start <= :start_1 AND interval."end" > :end_1
|
||||
|
||||
>>> ia = aliased(Interval)
|
||||
>>> print Session().query(Interval, ia).filter(Interval.intersects(ia))
|
||||
SELECT interval.id AS interval_id, interval.start AS interval_start,
|
||||
interval."end" AS interval_end, interval_1.id AS interval_1_id,
|
||||
interval_1.start AS interval_1_start, interval_1."end" AS interval_1_end
|
||||
FROM interval, interval AS interval_1
|
||||
WHERE interval.start <= interval_1.start
|
||||
AND interval."end" > interval_1.start
|
||||
OR interval.start <= interval_1."end"
|
||||
AND interval."end" > interval_1."end"
|
||||
|
||||
Defining Expression Behavior Distinct from Attribute Behavior
|
||||
--------------------------------------------------------------
|
||||
|
||||
Our usage of the ``&`` and ``|`` bitwise operators above was fortunate, considering
|
||||
our functions operated on two boolean values to return a new one. In many cases, the construction
|
||||
of an in-Python function and a SQLAlchemy SQL expression have enough differences that two
|
||||
separate Python expressions should be defined. The :mod:`~sqlalchemy.ext.hybrid` decorators
|
||||
define the :meth:`.hybrid_property.expression` modifier for this purpose. As an example we'll
|
||||
define the radius of the interval, which requires the usage of the absolute value function::
|
||||
|
||||
from sqlalchemy import func
|
||||
|
||||
class Interval(object):
|
||||
# ...
|
||||
|
||||
@hybrid_property
|
||||
def radius(self):
|
||||
return abs(self.length) / 2
|
||||
|
||||
@radius.expression
|
||||
def radius(cls):
|
||||
return func.abs(cls.length) / 2
|
||||
|
||||
Above the Python function ``abs()`` is used for instance-level operations, the SQL function
|
||||
``ABS()`` is used via the :attr:`.func` object for class-level expressions::
|
||||
|
||||
>>> i1.radius
|
||||
2
|
||||
|
||||
>>> print Session().query(Interval).filter(Interval.radius > 5)
|
||||
SELECT interval.id AS interval_id, interval.start AS interval_start,
|
||||
interval."end" AS interval_end
|
||||
FROM interval
|
||||
WHERE abs(interval."end" - interval.start) / :abs_1 > :param_1
|
||||
|
||||
Defining Setters
|
||||
----------------
|
||||
|
||||
Hybrid properties can also define setter methods. If we wanted ``length`` above, when
|
||||
set, to modify the endpoint value::
|
||||
|
||||
class Interval(object):
|
||||
# ...
|
||||
|
||||
@hybrid_property
|
||||
def length(self):
|
||||
return self.end - self.start
|
||||
|
||||
@length.setter
|
||||
def length(self, value):
|
||||
self.end = self.start + value
|
||||
|
||||
The ``length(self, value)`` method is now called upon set::
|
||||
|
||||
>>> i1 = Interval(5, 10)
|
||||
>>> i1.length
|
||||
5
|
||||
>>> i1.length = 12
|
||||
>>> i1.end
|
||||
17
|
||||
|
||||
Working with Relationships
|
||||
--------------------------
|
||||
|
||||
There's no essential difference when creating hybrids that work with related objects as
|
||||
opposed to column-based data. The need for distinct expressions tends to be greater.
|
||||
Consider the following declarative mapping which relates a ``User`` to a ``SavingsAccount``::
|
||||
|
||||
from sqlalchemy import Column, Integer, ForeignKey, Numeric, String
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class SavingsAccount(Base):
|
||||
__tablename__ = 'account'
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey('user.id'), nullable=False)
|
||||
balance = Column(Numeric(15, 5))
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = 'user'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
|
||||
accounts = relationship("SavingsAccount", backref="owner")
|
||||
|
||||
@hybrid_property
|
||||
def balance(self):
|
||||
if self.accounts:
|
||||
return self.accounts[0].balance
|
||||
else:
|
||||
return None
|
||||
|
||||
@balance.setter
|
||||
def balance(self, value):
|
||||
if not self.accounts:
|
||||
account = Account(owner=self)
|
||||
else:
|
||||
account = self.accounts[0]
|
||||
account.balance = balance
|
||||
|
||||
@balance.expression
|
||||
def balance(cls):
|
||||
return SavingsAccount.balance
|
||||
|
||||
The above hybrid property ``balance`` works with the first ``SavingsAccount`` entry in the list of
|
||||
accounts for this user. The in-Python getter/setter methods can treat ``accounts`` as a Python
|
||||
list available on ``self``.
|
||||
|
||||
However, at the expression level, we can't travel along relationships to column attributes
|
||||
directly since SQLAlchemy is explicit about joins. So here, it's expected that the ``User`` class will be
|
||||
used in an appropriate context such that an appropriate join to ``SavingsAccount`` will be present::
|
||||
|
||||
>>> print Session().query(User, User.balance).join(User.accounts).filter(User.balance > 5000)
|
||||
SELECT "user".id AS user_id, "user".name AS user_name, account.balance AS account_balance
|
||||
FROM "user" JOIN account ON "user".id = account.user_id
|
||||
WHERE account.balance > :balance_1
|
||||
|
||||
Note however, that while the instance level accessors need to worry about whether ``self.accounts``
|
||||
is even present, this issue expresses itself differently at the SQL expression level, where we basically
|
||||
would use an outer join::
|
||||
|
||||
>>> from sqlalchemy import or_
|
||||
>>> print (Session().query(User, User.balance).outerjoin(User.accounts).
|
||||
... filter(or_(User.balance < 5000, User.balance == None)))
|
||||
SELECT "user".id AS user_id, "user".name AS user_name, account.balance AS account_balance
|
||||
FROM "user" LEFT OUTER JOIN account ON "user".id = account.user_id
|
||||
WHERE account.balance < :balance_1 OR account.balance IS NULL
|
||||
|
||||
.. _hybrid_custom_comparators:
|
||||
|
||||
Building Custom Comparators
|
||||
---------------------------
|
||||
|
||||
The hybrid property also includes a helper that allows construction of custom comparators.
|
||||
A comparator object allows one to customize the behavior of each SQLAlchemy expression
|
||||
operator individually. They are useful when creating custom types that have
|
||||
some highly idiosyncratic behavior on the SQL side.
|
||||
|
||||
The example class below allows case-insensitive comparisons on the attribute
|
||||
named ``word_insensitive``::
|
||||
|
||||
from sqlalchemy.ext.hybrid import Comparator, hybrid_property
|
||||
from sqlalchemy import func, Column, Integer, String
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class CaseInsensitiveComparator(Comparator):
|
||||
def __eq__(self, other):
|
||||
return func.lower(self.__clause_element__()) == func.lower(other)
|
||||
|
||||
class SearchWord(Base):
|
||||
__tablename__ = 'searchword'
|
||||
id = Column(Integer, primary_key=True)
|
||||
word = Column(String(255), nullable=False)
|
||||
|
||||
@hybrid_property
|
||||
def word_insensitive(self):
|
||||
return self.word.lower()
|
||||
|
||||
@word_insensitive.comparator
|
||||
def word_insensitive(cls):
|
||||
return CaseInsensitiveComparator(cls.word)
|
||||
|
||||
Above, SQL expressions against ``word_insensitive`` will apply the ``LOWER()``
|
||||
SQL function to both sides::
|
||||
|
||||
>>> print Session().query(SearchWord).filter_by(word_insensitive="Trucks")
|
||||
SELECT searchword.id AS searchword_id, searchword.word AS searchword_word
|
||||
FROM searchword
|
||||
WHERE lower(searchword.word) = lower(:lower_1)
|
||||
|
||||
The ``CaseInsensitiveComparator`` above implements part of the :class:`.ColumnOperators`
|
||||
interface. A "coercion" operation like lowercasing can be applied to all comparison operations
|
||||
(i.e. ``eq``, ``lt``, ``gt``, etc.) using :meth:`.Operators.operate`::
|
||||
|
||||
class CaseInsensitiveComparator(Comparator):
|
||||
def operate(self, op, other):
|
||||
return op(func.lower(self.__clause_element__()), func.lower(other))
|
||||
|
||||
Hybrid Value Objects
|
||||
--------------------
|
||||
|
||||
Note in our previous example, if we were to compare the ``word_insensitive`` attribute of
|
||||
a ``SearchWord`` instance to a plain Python string, the plain Python string would not
|
||||
be coerced to lower case - the ``CaseInsensitiveComparator`` we built, being returned
|
||||
by ``@word_insensitive.comparator``, only applies to the SQL side.
|
||||
|
||||
A more comprehensive form of the custom comparator is to construct a *Hybrid Value Object*.
|
||||
This technique applies the target value or expression to a value object which is then
|
||||
returned by the accessor in all cases. The value object allows control
|
||||
of all operations upon the value as well as how compared values are treated, both
|
||||
on the SQL expression side as well as the Python value side. Replacing the
|
||||
previous ``CaseInsensitiveComparator`` class with a new ``CaseInsensitiveWord`` class::
|
||||
|
||||
class CaseInsensitiveWord(Comparator):
|
||||
"Hybrid value representing a lower case representation of a word."
|
||||
|
||||
def __init__(self, word):
|
||||
if isinstance(word, basestring):
|
||||
self.word = word.lower()
|
||||
elif isinstance(word, CaseInsensitiveWord):
|
||||
self.word = word.word
|
||||
else:
|
||||
self.word = func.lower(word)
|
||||
|
||||
def operate(self, op, other):
|
||||
if not isinstance(other, CaseInsensitiveWord):
|
||||
other = CaseInsensitiveWord(other)
|
||||
return op(self.word, other.word)
|
||||
|
||||
def __clause_element__(self):
|
||||
return self.word
|
||||
|
||||
def __str__(self):
|
||||
return self.word
|
||||
|
||||
key = 'word'
|
||||
"Label to apply to Query tuple results"
|
||||
|
||||
Above, the ``CaseInsensitiveWord`` object represents ``self.word``, which may be a SQL function,
|
||||
or may be a Python native. By overriding ``operate()`` and ``__clause_element__()``
|
||||
to work in terms of ``self.word``, all comparison operations will work against the
|
||||
"converted" form of ``word``, whether it be SQL side or Python side.
|
||||
Our ``SearchWord`` class can now deliver the ``CaseInsensitiveWord`` object unconditionally
|
||||
from a single hybrid call::
|
||||
|
||||
class SearchWord(Base):
|
||||
__tablename__ = 'searchword'
|
||||
id = Column(Integer, primary_key=True)
|
||||
word = Column(String(255), nullable=False)
|
||||
|
||||
@hybrid_property
|
||||
def word_insensitive(self):
|
||||
return CaseInsensitiveWord(self.word)
|
||||
|
||||
The ``word_insensitive`` attribute now has case-insensitive comparison behavior
|
||||
universally, including SQL expression vs. Python expression (note the Python value is
|
||||
converted to lower case on the Python side here)::
|
||||
|
||||
>>> print Session().query(SearchWord).filter_by(word_insensitive="Trucks")
|
||||
SELECT searchword.id AS searchword_id, searchword.word AS searchword_word
|
||||
FROM searchword
|
||||
WHERE lower(searchword.word) = :lower_1
|
||||
|
||||
SQL expression versus SQL expression::
|
||||
|
||||
>>> sw1 = aliased(SearchWord)
|
||||
>>> sw2 = aliased(SearchWord)
|
||||
>>> print Session().query(sw1.word_insensitive, sw2.word_insensitive).filter(sw1.word_insensitive > sw2.word_insensitive)
|
||||
SELECT lower(searchword_1.word) AS lower_1, lower(searchword_2.word) AS lower_2
|
||||
FROM searchword AS searchword_1, searchword AS searchword_2
|
||||
WHERE lower(searchword_1.word) > lower(searchword_2.word)
|
||||
|
||||
Python only expression::
|
||||
|
||||
>>> ws1 = SearchWord(word="SomeWord")
|
||||
>>> ws1.word_insensitive == "sOmEwOrD"
|
||||
True
|
||||
>>> ws1.word_insensitive == "XOmEwOrX"
|
||||
False
|
||||
>>> print ws1.word_insensitive
|
||||
someword
|
||||
|
||||
The Hybrid Value pattern is very useful for any kind of value that may have multiple representations,
|
||||
such as timestamps, time deltas, units of measurement, currencies and encrypted passwords.
|
||||
|
||||
See Also:
|
||||
|
||||
`Hybrids and Value Agnostic Types <http://techspot.zzzeek.org/2011/10/21/hybrids-and-value-agnostic-types/>`_ - on the techspot.zzzeek.org blog
|
||||
|
||||
`Value Agnostic Types, Part II <http://techspot.zzzeek.org/2011/10/29/value-agnostic-types-part-ii/>`_ - on the techspot.zzzeek.org blog
|
||||
|
||||
.. _hybrid_transformers:
|
||||
|
||||
Building Transformers
|
||||
----------------------
|
||||
|
||||
A *transformer* is an object which can receive a :class:`.Query` object and return a
|
||||
new one. The :class:`.Query` object includes a method :meth:`.with_transformation`
|
||||
that simply returns a new :class:`.Query` transformed by the given function.
|
||||
|
||||
We can combine this with the :class:`.Comparator` class to produce one type
|
||||
of recipe which can both set up the FROM clause of a query as well as assign
|
||||
filtering criterion.
|
||||
|
||||
Consider a mapped class ``Node``, which assembles using adjacency list into a hierarchical
|
||||
tree pattern::
|
||||
|
||||
from sqlalchemy import Column, Integer, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
Base = declarative_base()
|
||||
|
||||
class Node(Base):
|
||||
__tablename__ = 'node'
|
||||
id =Column(Integer, primary_key=True)
|
||||
parent_id = Column(Integer, ForeignKey('node.id'))
|
||||
parent = relationship("Node", remote_side=id)
|
||||
|
||||
Suppose we wanted to add an accessor ``grandparent``. This would return the ``parent`` of
|
||||
``Node.parent``. When we have an instance of ``Node``, this is simple::
|
||||
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
|
||||
class Node(Base):
|
||||
# ...
|
||||
|
||||
@hybrid_property
|
||||
def grandparent(self):
|
||||
return self.parent.parent
|
||||
|
||||
For the expression, things are not so clear. We'd need to construct a :class:`.Query` where we
|
||||
:meth:`~.Query.join` twice along ``Node.parent`` to get to the ``grandparent``. We can instead
|
||||
return a transforming callable that we'll combine with the :class:`.Comparator` class
|
||||
to receive any :class:`.Query` object, and return a new one that's joined to the ``Node.parent``
|
||||
attribute and filtered based on the given criterion::
|
||||
|
||||
from sqlalchemy.ext.hybrid import Comparator
|
||||
|
||||
class GrandparentTransformer(Comparator):
|
||||
def operate(self, op, other):
|
||||
def transform(q):
|
||||
cls = self.__clause_element__()
|
||||
parent_alias = aliased(cls)
|
||||
return q.join(parent_alias, cls.parent).\\
|
||||
filter(op(parent_alias.parent, other))
|
||||
return transform
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class Node(Base):
|
||||
__tablename__ = 'node'
|
||||
id =Column(Integer, primary_key=True)
|
||||
parent_id = Column(Integer, ForeignKey('node.id'))
|
||||
parent = relationship("Node", remote_side=id)
|
||||
|
||||
@hybrid_property
|
||||
def grandparent(self):
|
||||
return self.parent.parent
|
||||
|
||||
@grandparent.comparator
|
||||
def grandparent(cls):
|
||||
return GrandparentTransformer(cls)
|
||||
|
||||
The ``GrandparentTransformer`` overrides the core :meth:`.Operators.operate` method
|
||||
at the base of the :class:`.Comparator` hierarchy to return a query-transforming
|
||||
callable, which then runs the given comparison operation in a particular context.
|
||||
Such as, in the example above, the ``operate`` method is called, given the
|
||||
:attr:`.Operators.eq` callable as well as the right side of the comparison
|
||||
``Node(id=5)``. A function ``transform`` is then returned which will transform
|
||||
a :class:`.Query` first to join to ``Node.parent``, then to compare ``parent_alias``
|
||||
using :attr:`.Operators.eq` against the left and right sides, passing into
|
||||
:class:`.Query.filter`:
|
||||
|
||||
.. sourcecode:: pycon+sql
|
||||
|
||||
>>> from sqlalchemy.orm import Session
|
||||
>>> session = Session()
|
||||
{sql}>>> session.query(Node).\\
|
||||
... with_transformation(Node.grandparent==Node(id=5)).\\
|
||||
... all()
|
||||
SELECT node.id AS node_id, node.parent_id AS node_parent_id
|
||||
FROM node JOIN node AS node_1 ON node_1.id = node.parent_id
|
||||
WHERE :param_1 = node_1.parent_id
|
||||
{stop}
|
||||
|
||||
We can modify the pattern to be more verbose but flexible by separating
|
||||
the "join" step from the "filter" step. The tricky part here is ensuring
|
||||
that successive instances of ``GrandparentTransformer`` use the same
|
||||
:class:`.AliasedClass` object against ``Node``. Below we use a simple
|
||||
memoizing approach that associates a ``GrandparentTransformer``
|
||||
with each class::
|
||||
|
||||
class Node(Base):
|
||||
|
||||
# ...
|
||||
|
||||
@grandparent.comparator
|
||||
def grandparent(cls):
|
||||
# memoize a GrandparentTransformer
|
||||
# per class
|
||||
if '_gp' not in cls.__dict__:
|
||||
cls._gp = GrandparentTransformer(cls)
|
||||
return cls._gp
|
||||
|
||||
class GrandparentTransformer(Comparator):
|
||||
|
||||
def __init__(self, cls):
|
||||
self.parent_alias = aliased(cls)
|
||||
|
||||
@property
|
||||
def join(self):
|
||||
def go(q):
|
||||
return q.join(self.parent_alias, Node.parent)
|
||||
return go
|
||||
|
||||
def operate(self, op, other):
|
||||
return op(self.parent_alias.parent, other)
|
||||
|
||||
.. sourcecode:: pycon+sql
|
||||
|
||||
{sql}>>> session.query(Node).\\
|
||||
... with_transformation(Node.grandparent.join).\\
|
||||
... filter(Node.grandparent==Node(id=5))
|
||||
SELECT node.id AS node_id, node.parent_id AS node_parent_id
|
||||
FROM node JOIN node AS node_1 ON node_1.id = node.parent_id
|
||||
WHERE :param_1 = node_1.parent_id
|
||||
{stop}
|
||||
|
||||
The "transformer" pattern is an experimental pattern that starts
|
||||
to make usage of some functional programming paradigms.
|
||||
While it's only recommended for advanced and/or patient developers,
|
||||
there's probably a whole lot of amazing things it can be used for.
|
||||
|
||||
"""
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy.orm import attributes, interfaces
|
||||
|
||||
class hybrid_method(object):
|
||||
"""A decorator which allows definition of a Python object method with both
|
||||
instance-level and class-level behavior.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, func, expr=None):
|
||||
"""Create a new :class:`.hybrid_method`.
|
||||
|
||||
Usage is typically via decorator::
|
||||
|
||||
from sqlalchemy.ext.hybrid import hybrid_method
|
||||
|
||||
class SomeClass(object):
|
||||
@hybrid_method
|
||||
def value(self, x, y):
|
||||
return self._value + x + y
|
||||
|
||||
@value.expression
|
||||
def value(self, x, y):
|
||||
return func.some_function(self._value, x, y)
|
||||
|
||||
"""
|
||||
self.func = func
|
||||
self.expr = expr or func
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self.expr.__get__(owner, owner.__class__)
|
||||
else:
|
||||
return self.func.__get__(instance, owner)
|
||||
|
||||
def expression(self, expr):
|
||||
"""Provide a modifying decorator that defines a SQL-expression producing method."""
|
||||
|
||||
self.expr = expr
|
||||
return self
|
||||
|
||||
class hybrid_property(object):
|
||||
"""A decorator which allows definition of a Python descriptor with both
|
||||
instance-level and class-level behavior.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, fget, fset=None, fdel=None, expr=None):
|
||||
"""Create a new :class:`.hybrid_property`.
|
||||
|
||||
Usage is typically via decorator::
|
||||
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
|
||||
class SomeClass(object):
|
||||
@hybrid_property
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
@value.setter
|
||||
def value(self, value):
|
||||
self._value = value
|
||||
|
||||
"""
|
||||
self.fget = fget
|
||||
self.fset = fset
|
||||
self.fdel = fdel
|
||||
self.expr = expr or fget
|
||||
util.update_wrapper(self, fget)
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self.expr(owner)
|
||||
else:
|
||||
return self.fget(instance)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
if self.fset is None:
|
||||
raise AttributeError("can't set attribute")
|
||||
self.fset(instance, value)
|
||||
|
||||
def __delete__(self, instance):
|
||||
if self.fdel is None:
|
||||
raise AttributeError("can't delete attribute")
|
||||
self.fdel(instance)
|
||||
|
||||
def setter(self, fset):
|
||||
"""Provide a modifying decorator that defines a value-setter method."""
|
||||
|
||||
self.fset = fset
|
||||
return self
|
||||
|
||||
def deleter(self, fdel):
|
||||
"""Provide a modifying decorator that defines a value-deletion method."""
|
||||
|
||||
self.fdel = fdel
|
||||
return self
|
||||
|
||||
def expression(self, expr):
|
||||
"""Provide a modifying decorator that defines a SQL-expression producing method."""
|
||||
|
||||
self.expr = expr
|
||||
return self
|
||||
|
||||
def comparator(self, comparator):
|
||||
"""Provide a modifying decorator that defines a custom comparator producing method.
|
||||
|
||||
The return value of the decorated method should be an instance of
|
||||
:class:`~.hybrid.Comparator`.
|
||||
|
||||
"""
|
||||
|
||||
proxy_attr = attributes.\
|
||||
create_proxied_attribute(self)
|
||||
def expr(owner):
|
||||
return proxy_attr(owner, self.__name__, self, comparator(owner))
|
||||
self.expr = expr
|
||||
return self
|
||||
|
||||
|
||||
class Comparator(interfaces.PropComparator):
|
||||
"""A helper class that allows easy construction of custom :class:`~.orm.interfaces.PropComparator`
|
||||
classes for usage with hybrids."""
|
||||
|
||||
|
||||
def __init__(self, expression):
|
||||
self.expression = expression
|
||||
|
||||
def __clause_element__(self):
|
||||
expr = self.expression
|
||||
while hasattr(expr, '__clause_element__'):
|
||||
expr = expr.__clause_element__()
|
||||
return expr
|
||||
|
||||
def adapted(self, adapter):
|
||||
# interesting....
|
||||
return self
|
||||
|
||||
|
||||
@@ -0,0 +1,563 @@
|
||||
# ext/mutable.py
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
"""Provide support for tracking of in-place changes to scalar values,
|
||||
which are propagated into ORM change events on owning parent objects.
|
||||
|
||||
The :mod:`sqlalchemy.ext.mutable` extension replaces SQLAlchemy's legacy approach to in-place
|
||||
mutations of scalar values, established by the :class:`.types.MutableType`
|
||||
class as well as the ``mutable=True`` type flag, with a system that allows
|
||||
change events to be propagated from the value to the owning parent, thereby
|
||||
removing the need for the ORM to maintain copies of values as well as the very
|
||||
expensive requirement of scanning through all "mutable" values on each flush
|
||||
call, looking for changes.
|
||||
|
||||
.. _mutable_scalars:
|
||||
|
||||
Establishing Mutability on Scalar Column Values
|
||||
===============================================
|
||||
|
||||
A typical example of a "mutable" structure is a Python dictionary.
|
||||
Following the example introduced in :ref:`types_toplevel`, we
|
||||
begin with a custom type that marshals Python dictionaries into
|
||||
JSON strings before being persisted::
|
||||
|
||||
from sqlalchemy.types import TypeDecorator, VARCHAR
|
||||
import json
|
||||
|
||||
class JSONEncodedDict(TypeDecorator):
|
||||
"Represents an immutable structure as a json-encoded string."
|
||||
|
||||
impl = VARCHAR
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.dumps(value)
|
||||
return value
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.loads(value)
|
||||
return value
|
||||
|
||||
The usage of ``json`` is only for the purposes of example. The :mod:`sqlalchemy.ext.mutable`
|
||||
extension can be used
|
||||
with any type whose target Python type may be mutable, including
|
||||
:class:`.PickleType`, :class:`.postgresql.ARRAY`, etc.
|
||||
|
||||
When using the :mod:`sqlalchemy.ext.mutable` extension, the value itself
|
||||
tracks all parents which reference it. Here we will replace the usage
|
||||
of plain Python dictionaries with a dict subclass that implements
|
||||
the :class:`.Mutable` mixin::
|
||||
|
||||
import collections
|
||||
from sqlalchemy.ext.mutable import Mutable
|
||||
|
||||
class MutationDict(Mutable, dict):
|
||||
@classmethod
|
||||
def coerce(cls, key, value):
|
||||
"Convert plain dictionaries to MutationDict."
|
||||
|
||||
if not isinstance(value, MutationDict):
|
||||
if isinstance(value, dict):
|
||||
return MutationDict(value)
|
||||
|
||||
# this call will raise ValueError
|
||||
return Mutable.coerce(key, value)
|
||||
else:
|
||||
return value
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"Detect dictionary set events and emit change events."
|
||||
|
||||
dict.__setitem__(self, key, value)
|
||||
self.changed()
|
||||
|
||||
def __delitem__(self, key):
|
||||
"Detect dictionary del events and emit change events."
|
||||
|
||||
dict.__delitem__(self, key)
|
||||
self.changed()
|
||||
|
||||
The above dictionary class takes the approach of subclassing the Python
|
||||
built-in ``dict`` to produce a dict
|
||||
subclass which routes all mutation events through ``__setitem__``. There are
|
||||
many variants on this approach, such as subclassing ``UserDict.UserDict``,
|
||||
the newer ``collections.MutableMapping``, etc. The part that's important to this
|
||||
example is that the :meth:`.Mutable.changed` method is called whenever an in-place change to the
|
||||
datastructure takes place.
|
||||
|
||||
We also redefine the :meth:`.Mutable.coerce` method which will be used to
|
||||
convert any values that are not instances of ``MutationDict``, such
|
||||
as the plain dictionaries returned by the ``json`` module, into the
|
||||
appropriate type. Defining this method is optional; we could just as well created our
|
||||
``JSONEncodedDict`` such that it always returns an instance of ``MutationDict``,
|
||||
and additionally ensured that all calling code uses ``MutationDict``
|
||||
explicitly. When :meth:`.Mutable.coerce` is not overridden, any values
|
||||
applied to a parent object which are not instances of the mutable type
|
||||
will raise a ``ValueError``.
|
||||
|
||||
Our new ``MutationDict`` type offers a class method
|
||||
:meth:`~.Mutable.as_mutable` which we can use within column metadata
|
||||
to associate with types. This method grabs the given type object or
|
||||
class and associates a listener that will detect all future mappings
|
||||
of this type, applying event listening instrumentation to the mapped
|
||||
attribute. Such as, with classical table metadata::
|
||||
|
||||
from sqlalchemy import Table, Column, Integer
|
||||
|
||||
my_data = Table('my_data', metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('data', MutationDict.as_mutable(JSONEncodedDict))
|
||||
)
|
||||
|
||||
Above, :meth:`~.Mutable.as_mutable` returns an instance of ``JSONEncodedDict``
|
||||
(if the type object was not an instance already), which will intercept any
|
||||
attributes which are mapped against this type. Below we establish a simple
|
||||
mapping against the ``my_data`` table::
|
||||
|
||||
from sqlalchemy import mapper
|
||||
|
||||
class MyDataClass(object):
|
||||
pass
|
||||
|
||||
# associates mutation listeners with MyDataClass.data
|
||||
mapper(MyDataClass, my_data)
|
||||
|
||||
The ``MyDataClass.data`` member will now be notified of in place changes
|
||||
to its value.
|
||||
|
||||
There's no difference in usage when using declarative::
|
||||
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class MyDataClass(Base):
|
||||
__tablename__ = 'my_data'
|
||||
id = Column(Integer, primary_key=True)
|
||||
data = Column(MutationDict.as_mutable(JSONEncodedDict))
|
||||
|
||||
Any in-place changes to the ``MyDataClass.data`` member
|
||||
will flag the attribute as "dirty" on the parent object::
|
||||
|
||||
>>> from sqlalchemy.orm import Session
|
||||
|
||||
>>> sess = Session()
|
||||
>>> m1 = MyDataClass(data={'value1':'foo'})
|
||||
>>> sess.add(m1)
|
||||
>>> sess.commit()
|
||||
|
||||
>>> m1.data['value1'] = 'bar'
|
||||
>>> assert m1 in sess.dirty
|
||||
True
|
||||
|
||||
The ``MutationDict`` can be associated with all future instances
|
||||
of ``JSONEncodedDict`` in one step, using :meth:`~.Mutable.associate_with`. This
|
||||
is similar to :meth:`~.Mutable.as_mutable` except it will intercept
|
||||
all occurrences of ``MutationDict`` in all mappings unconditionally, without
|
||||
the need to declare it individually::
|
||||
|
||||
MutationDict.associate_with(JSONEncodedDict)
|
||||
|
||||
class MyDataClass(Base):
|
||||
__tablename__ = 'my_data'
|
||||
id = Column(Integer, primary_key=True)
|
||||
data = Column(JSONEncodedDict)
|
||||
|
||||
|
||||
Supporting Pickling
|
||||
--------------------
|
||||
|
||||
The key to the :mod:`sqlalchemy.ext.mutable` extension relies upon the
|
||||
placement of a ``weakref.WeakKeyDictionary`` upon the value object, which
|
||||
stores a mapping of parent mapped objects keyed to the attribute name under
|
||||
which they are associated with this value. ``WeakKeyDictionary`` objects are
|
||||
not picklable, due to the fact that they contain weakrefs and function
|
||||
callbacks. In our case, this is a good thing, since if this dictionary were
|
||||
picklable, it could lead to an excessively large pickle size for our value
|
||||
objects that are pickled by themselves outside of the context of the parent.
|
||||
The developer responsiblity here is only to provide a ``__getstate__`` method
|
||||
that excludes the :meth:`~.MutableBase._parents` collection from the pickle
|
||||
stream::
|
||||
|
||||
class MyMutableType(Mutable):
|
||||
def __getstate__(self):
|
||||
d = self.__dict__.copy()
|
||||
d.pop('_parents', None)
|
||||
return d
|
||||
|
||||
With our dictionary example, we need to return the contents of the dict itself
|
||||
(and also restore them on __setstate__)::
|
||||
|
||||
class MutationDict(Mutable, dict):
|
||||
# ....
|
||||
|
||||
def __getstate__(self):
|
||||
return dict(self)
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.update(state)
|
||||
|
||||
In the case that our mutable value object is pickled as it is attached to one
|
||||
or more parent objects that are also part of the pickle, the :class:`.Mutable`
|
||||
mixin will re-establish the :attr:`.Mutable._parents` collection on each value
|
||||
object as the owning parents themselves are unpickled.
|
||||
|
||||
.. _mutable_composites:
|
||||
|
||||
Establishing Mutability on Composites
|
||||
=====================================
|
||||
|
||||
Composites are a special ORM feature which allow a single scalar attribute to
|
||||
be assigned an object value which represents information "composed" from one
|
||||
or more columns from the underlying mapped table. The usual example is that of
|
||||
a geometric "point", and is introduced in :ref:`mapper_composite`.
|
||||
|
||||
As of SQLAlchemy 0.7, the internals of :func:`.orm.composite` have been
|
||||
greatly simplified and in-place mutation detection is no longer enabled by
|
||||
default; instead, the user-defined value must detect changes on its own and
|
||||
propagate them to all owning parents. The :mod:`sqlalchemy.ext.mutable`
|
||||
extension provides the helper class :class:`.MutableComposite`, which is a
|
||||
slight variant on the :class:`.Mutable` class.
|
||||
|
||||
As is the case with :class:`.Mutable`, the user-defined composite class
|
||||
subclasses :class:`.MutableComposite` as a mixin, and detects and delivers
|
||||
change events to its parents via the :meth:`.MutableComposite.changed` method.
|
||||
In the case of a composite class, the detection is usually via the usage of
|
||||
Python descriptors (i.e. ``@property``), or alternatively via the special
|
||||
Python method ``__setattr__()``. Below we expand upon the ``Point`` class
|
||||
introduced in :ref:`mapper_composite` to subclass :class:`.MutableComposite`
|
||||
and to also route attribute set events via ``__setattr__`` to the
|
||||
:meth:`.MutableComposite.changed` method::
|
||||
|
||||
from sqlalchemy.ext.mutable import MutableComposite
|
||||
|
||||
class Point(MutableComposite):
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
"Intercept set events"
|
||||
|
||||
# set the attribute
|
||||
object.__setattr__(self, key, value)
|
||||
|
||||
# alert all parents to the change
|
||||
self.changed()
|
||||
|
||||
def __composite_values__(self):
|
||||
return self.x, self.y
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Point) and \\
|
||||
other.x == self.x and \\
|
||||
other.y == self.y
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
The :class:`.MutableComposite` class uses a Python metaclass to automatically
|
||||
establish listeners for any usage of :func:`.orm.composite` that specifies our
|
||||
``Point`` type. Below, when ``Point`` is mapped to the ``Vertex`` class,
|
||||
listeners are established which will route change events from ``Point``
|
||||
objects to each of the ``Vertex.start`` and ``Vertex.end`` attributes::
|
||||
|
||||
from sqlalchemy.orm import composite, mapper
|
||||
from sqlalchemy import Table, Column
|
||||
|
||||
vertices = Table('vertices', metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('x1', Integer),
|
||||
Column('y1', Integer),
|
||||
Column('x2', Integer),
|
||||
Column('y2', Integer),
|
||||
)
|
||||
|
||||
class Vertex(object):
|
||||
pass
|
||||
|
||||
mapper(Vertex, vertices, properties={
|
||||
'start': composite(Point, vertices.c.x1, vertices.c.y1),
|
||||
'end': composite(Point, vertices.c.x2, vertices.c.y2)
|
||||
})
|
||||
|
||||
Any in-place changes to the ``Vertex.start`` or ``Vertex.end`` members
|
||||
will flag the attribute as "dirty" on the parent object::
|
||||
|
||||
>>> from sqlalchemy.orm import Session
|
||||
|
||||
>>> sess = Session()
|
||||
>>> v1 = Vertex(start=Point(3, 4), end=Point(12, 15))
|
||||
>>> sess.add(v1)
|
||||
>>> sess.commit()
|
||||
|
||||
>>> v1.end.x = 8
|
||||
>>> assert v1 in sess.dirty
|
||||
True
|
||||
|
||||
Supporting Pickling
|
||||
--------------------
|
||||
|
||||
As is the case with :class:`.Mutable`, the :class:`.MutableComposite` helper
|
||||
class uses a ``weakref.WeakKeyDictionary`` available via the
|
||||
:meth:`.MutableBase._parents` attribute which isn't picklable. If we need to
|
||||
pickle instances of ``Point`` or its owning class ``Vertex``, we at least need
|
||||
to define a ``__getstate__`` that doesn't include the ``_parents`` dictionary.
|
||||
Below we define both a ``__getstate__`` and a ``__setstate__`` that package up
|
||||
the minimal form of our ``Point`` class::
|
||||
|
||||
class Point(MutableComposite):
|
||||
# ...
|
||||
|
||||
def __getstate__(self):
|
||||
return self.x, self.y
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.x, self.y = state
|
||||
|
||||
As with :class:`.Mutable`, the :class:`.MutableComposite` augments the
|
||||
pickling process of the parent's object-relational state so that the
|
||||
:meth:`.MutableBase._parents` collection is restored to all ``Point`` objects.
|
||||
|
||||
"""
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from sqlalchemy import event, types
|
||||
from sqlalchemy.orm import mapper, object_mapper
|
||||
from sqlalchemy.util import memoized_property
|
||||
import weakref
|
||||
|
||||
class MutableBase(object):
|
||||
"""Common base class to :class:`.Mutable` and :class:`.MutableComposite`."""
|
||||
|
||||
@memoized_property
|
||||
def _parents(self):
|
||||
"""Dictionary of parent object->attribute name on the parent.
|
||||
|
||||
This attribute is a so-called "memoized" property. It initializes
|
||||
itself with a new ``weakref.WeakKeyDictionary`` the first time
|
||||
it is accessed, returning the same object upon subsequent access.
|
||||
|
||||
"""
|
||||
|
||||
return weakref.WeakKeyDictionary()
|
||||
|
||||
@classmethod
|
||||
def coerce(cls, key, value):
|
||||
"""Given a value, coerce it into this type.
|
||||
|
||||
By default raises ValueError.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
raise ValueError("Attribute '%s' does not accept objects of type %s" % (key, type(value)))
|
||||
|
||||
@classmethod
|
||||
def _listen_on_attribute(cls, attribute, coerce, parent_cls):
|
||||
"""Establish this type as a mutation listener for the given
|
||||
mapped descriptor.
|
||||
|
||||
"""
|
||||
key = attribute.key
|
||||
if parent_cls is not attribute.class_:
|
||||
return
|
||||
|
||||
# rely on "propagate" here
|
||||
parent_cls = attribute.class_
|
||||
|
||||
def load(state, *args):
|
||||
"""Listen for objects loaded or refreshed.
|
||||
|
||||
Wrap the target data member's value with
|
||||
``Mutable``.
|
||||
|
||||
"""
|
||||
val = state.dict.get(key, None)
|
||||
if val is not None:
|
||||
if coerce:
|
||||
val = cls.coerce(key, val)
|
||||
state.dict[key] = val
|
||||
val._parents[state.obj()] = key
|
||||
|
||||
def set(target, value, oldvalue, initiator):
|
||||
"""Listen for set/replace events on the target
|
||||
data member.
|
||||
|
||||
Establish a weak reference to the parent object
|
||||
on the incoming value, remove it for the one
|
||||
outgoing.
|
||||
|
||||
"""
|
||||
if not isinstance(value, cls):
|
||||
value = cls.coerce(key, value)
|
||||
if value is not None:
|
||||
value._parents[target.obj()] = key
|
||||
if isinstance(oldvalue, cls):
|
||||
oldvalue._parents.pop(target.obj(), None)
|
||||
return value
|
||||
|
||||
def pickle(state, state_dict):
|
||||
val = state.dict.get(key, None)
|
||||
if val is not None:
|
||||
if 'ext.mutable.values' not in state_dict:
|
||||
state_dict['ext.mutable.values'] = []
|
||||
state_dict['ext.mutable.values'].append(val)
|
||||
|
||||
def unpickle(state, state_dict):
|
||||
if 'ext.mutable.values' in state_dict:
|
||||
for val in state_dict['ext.mutable.values']:
|
||||
val._parents[state.obj()] = key
|
||||
|
||||
|
||||
event.listen(parent_cls, 'load', load, raw=True, propagate=True)
|
||||
event.listen(parent_cls, 'refresh', load, raw=True, propagate=True)
|
||||
event.listen(attribute, 'set', set, raw=True, retval=True, propagate=True)
|
||||
event.listen(parent_cls, 'pickle', pickle, raw=True, propagate=True)
|
||||
event.listen(parent_cls, 'unpickle', unpickle, raw=True, propagate=True)
|
||||
|
||||
class Mutable(MutableBase):
|
||||
"""Mixin that defines transparent propagation of change
|
||||
events to a parent object.
|
||||
|
||||
See the example in :ref:`mutable_scalars` for usage information.
|
||||
|
||||
"""
|
||||
|
||||
def changed(self):
|
||||
"""Subclasses should call this method whenever change events occur."""
|
||||
|
||||
for parent, key in self._parents.items():
|
||||
flag_modified(parent, key)
|
||||
|
||||
@classmethod
|
||||
def associate_with_attribute(cls, attribute):
|
||||
"""Establish this type as a mutation listener for the given
|
||||
mapped descriptor.
|
||||
|
||||
"""
|
||||
cls._listen_on_attribute(attribute, True, attribute.class_)
|
||||
|
||||
@classmethod
|
||||
def associate_with(cls, sqltype):
|
||||
"""Associate this wrapper with all future mapped columns
|
||||
of the given type.
|
||||
|
||||
This is a convenience method that calls ``associate_with_attribute`` automatically.
|
||||
|
||||
.. warning::
|
||||
|
||||
The listeners established by this method are *global*
|
||||
to all mappers, and are *not* garbage collected. Only use
|
||||
:meth:`.associate_with` for types that are permanent to an application,
|
||||
not with ad-hoc types else this will cause unbounded growth
|
||||
in memory usage.
|
||||
|
||||
"""
|
||||
|
||||
def listen_for_type(mapper, class_):
|
||||
for prop in mapper.iterate_properties:
|
||||
if hasattr(prop, 'columns'):
|
||||
if isinstance(prop.columns[0].type, sqltype):
|
||||
cls.associate_with_attribute(getattr(class_, prop.key))
|
||||
|
||||
event.listen(mapper, 'mapper_configured', listen_for_type)
|
||||
|
||||
@classmethod
|
||||
def as_mutable(cls, sqltype):
|
||||
"""Associate a SQL type with this mutable Python type.
|
||||
|
||||
This establishes listeners that will detect ORM mappings against
|
||||
the given type, adding mutation event trackers to those mappings.
|
||||
|
||||
The type is returned, unconditionally as an instance, so that
|
||||
:meth:`.as_mutable` can be used inline::
|
||||
|
||||
Table('mytable', metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('data', MyMutableType.as_mutable(PickleType))
|
||||
)
|
||||
|
||||
Note that the returned type is always an instance, even if a class
|
||||
is given, and that only columns which are declared specifically with that
|
||||
type instance receive additional instrumentation.
|
||||
|
||||
To associate a particular mutable type with all occurrences of a
|
||||
particular type, use the :meth:`.Mutable.associate_with` classmethod
|
||||
of the particular :meth:`.Mutable` subclass to establish a global
|
||||
association.
|
||||
|
||||
.. warning::
|
||||
|
||||
The listeners established by this method are *global*
|
||||
to all mappers, and are *not* garbage collected. Only use
|
||||
:meth:`.as_mutable` for types that are permanent to an application,
|
||||
not with ad-hoc types else this will cause unbounded growth
|
||||
in memory usage.
|
||||
|
||||
"""
|
||||
sqltype = types.to_instance(sqltype)
|
||||
|
||||
def listen_for_type(mapper, class_):
|
||||
for prop in mapper.iterate_properties:
|
||||
if hasattr(prop, 'columns'):
|
||||
if prop.columns[0].type is sqltype:
|
||||
cls.associate_with_attribute(getattr(class_, prop.key))
|
||||
|
||||
event.listen(mapper, 'mapper_configured', listen_for_type)
|
||||
|
||||
return sqltype
|
||||
|
||||
class _MutableCompositeMeta(type):
|
||||
def __init__(cls, classname, bases, dict_):
|
||||
cls._setup_listeners()
|
||||
return type.__init__(cls, classname, bases, dict_)
|
||||
|
||||
class MutableComposite(MutableBase):
|
||||
"""Mixin that defines transparent propagation of change
|
||||
events on a SQLAlchemy "composite" object to its
|
||||
owning parent or parents.
|
||||
|
||||
See the example in :ref:`mutable_composites` for usage information.
|
||||
|
||||
.. warning::
|
||||
|
||||
The listeners established by the :class:`.MutableComposite`
|
||||
class are *global* to all mappers, and are *not* garbage collected. Only use
|
||||
:class:`.MutableComposite` for types that are permanent to an application,
|
||||
not with ad-hoc types else this will cause unbounded growth
|
||||
in memory usage.
|
||||
|
||||
"""
|
||||
__metaclass__ = _MutableCompositeMeta
|
||||
|
||||
def changed(self):
|
||||
"""Subclasses should call this method whenever change events occur."""
|
||||
|
||||
for parent, key in self._parents.items():
|
||||
|
||||
prop = object_mapper(parent).get_property(key)
|
||||
for value, attr_name in zip(
|
||||
self.__composite_values__(),
|
||||
prop._attribute_keys):
|
||||
setattr(parent, attr_name, value)
|
||||
|
||||
@classmethod
|
||||
def _setup_listeners(cls):
|
||||
"""Associate this wrapper with all future mapped composites
|
||||
of the given type.
|
||||
|
||||
This is a convenience method that calls ``associate_with_attribute`` automatically.
|
||||
|
||||
"""
|
||||
|
||||
def listen_for_type(mapper, class_):
|
||||
for prop in mapper.iterate_properties:
|
||||
if hasattr(prop, 'composite_class') and issubclass(prop.composite_class, cls):
|
||||
cls._listen_on_attribute(getattr(class_, prop.key), False, class_)
|
||||
|
||||
event.listen(mapper, 'mapper_configured', listen_for_type)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ext/orderinglist.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -73,7 +73,9 @@ Use the ``ordering_list`` function to set up the ``collection_class`` on relatio
|
||||
(as in the mapper example above). This implementation depends on the list
|
||||
starting in the proper order, so be SURE to put an order_by on your relationship.
|
||||
|
||||
.. warning:: ``ordering_list`` only provides limited functionality when a primary
|
||||
.. warning::
|
||||
|
||||
``ordering_list`` only provides limited functionality when a primary
|
||||
key column or unique column is the target of the sort. Since changing the order of
|
||||
entries often means that two rows must trade values, this is not possible when
|
||||
the value is constrained by a primary key or unique constraint, since one of the rows
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ext/serializer.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
# ext/sqlsoup.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
"""
|
||||
|
||||
.. note::
|
||||
|
||||
SQLSoup is now its own project. Documentation
|
||||
and project status are available at:
|
||||
|
||||
http://pypi.python.org/pypi/sqlsoup
|
||||
|
||||
http://readthedocs.org/docs/sqlsoup
|
||||
|
||||
SQLSoup will no longer be included with SQLAlchemy as of
|
||||
version 0.8.
|
||||
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
@@ -152,7 +166,7 @@ construction rules apply here as to the select methods::
|
||||
You can similarly update multiple rows at once. This will change the
|
||||
book_id to 1 in all loans whose book_id is 2::
|
||||
|
||||
>>> db.loans.update(db.loans.book_id==2, book_id=1)
|
||||
>>> db.loans.filter_by(db.loans.book_id==2).update({'book_id':1})
|
||||
>>> db.loans.filter_by(book_id=1).all()
|
||||
[MappedLoans(book_id=1,user_name=u'Joe Student',
|
||||
loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
|
||||
@@ -245,8 +259,10 @@ Advanced Use
|
||||
Sessions, Transations and Application Integration
|
||||
-------------------------------------------------
|
||||
|
||||
**Note:** please read and understand this section thoroughly
|
||||
before using SqlSoup in any web application.
|
||||
.. note::
|
||||
|
||||
Please read and understand this section thoroughly
|
||||
before using SqlSoup in any web application.
|
||||
|
||||
SqlSoup uses a ScopedSession to provide thread-local sessions.
|
||||
You can get a reference to the current one like this::
|
||||
@@ -365,9 +381,9 @@ from sqlalchemy import schema, sql, util
|
||||
from sqlalchemy.engine.base import Engine
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker, mapper, \
|
||||
class_mapper, relationship, session,\
|
||||
object_session
|
||||
object_session, attributes
|
||||
from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE
|
||||
from sqlalchemy.exceptions import SQLAlchemyError, InvalidRequestError, ArgumentError
|
||||
from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, ArgumentError
|
||||
from sqlalchemy.sql import expression
|
||||
|
||||
|
||||
@@ -390,7 +406,8 @@ class AutoAdd(MapperExtension):
|
||||
|
||||
def init_instance(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
session = self.scoped_session()
|
||||
session._save_without_cascade(instance)
|
||||
state = attributes.instance_state(instance)
|
||||
session._save_impl(state)
|
||||
return EXT_CONTINUE
|
||||
|
||||
def init_failed(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
@@ -619,7 +636,7 @@ class SqlSoup(object):
|
||||
self.session.expunge_all()
|
||||
|
||||
def map_to(self, attrname, tablename=None, selectable=None,
|
||||
schema=None, base=None, mapper_args=util.frozendict()):
|
||||
schema=None, base=None, mapper_args=util.immutabledict()):
|
||||
"""Configure a mapping to the given attrname.
|
||||
|
||||
This is the "master" method that can be used to create any
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
# sqlalchemy/interfaces.py
|
||||
# Copyright (C) 2007-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2007-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2007 Jason Kirtland jek@discorporate.us
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
"""Interfaces and abstract types."""
|
||||
"""Interfaces and abstract types.
|
||||
|
||||
This module is **deprecated** and is superseded by the
|
||||
event system.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy import event, util
|
||||
|
||||
class PoolListener(object):
|
||||
"""Hooks into the lifecycle of connections in a ``Pool``.
|
||||
"""Hooks into the lifecycle of connections in a :class:`.Pool`.
|
||||
|
||||
.. note::
|
||||
|
||||
:class:`.PoolListener` is deprecated. Please
|
||||
refer to :class:`.PoolEvents`.
|
||||
|
||||
Usage::
|
||||
|
||||
@@ -60,6 +71,25 @@ class PoolListener(object):
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _adapt_listener(cls, self, listener):
|
||||
"""Adapt a :class:`.PoolListener` to individual
|
||||
:class:`event.Dispatch` events.
|
||||
|
||||
"""
|
||||
|
||||
listener = util.as_interface(listener, methods=('connect',
|
||||
'first_connect', 'checkout', 'checkin'))
|
||||
if hasattr(listener, 'connect'):
|
||||
event.listen(self, 'connect', listener.connect)
|
||||
if hasattr(listener, 'first_connect'):
|
||||
event.listen(self, 'first_connect', listener.first_connect)
|
||||
if hasattr(listener, 'checkout'):
|
||||
event.listen(self, 'checkout', listener.checkout)
|
||||
if hasattr(listener, 'checkin'):
|
||||
event.listen(self, 'checkin', listener.checkin)
|
||||
|
||||
|
||||
def connect(self, dbapi_con, con_record):
|
||||
"""Called once for each new DB-API connection or Pool's ``creator()``.
|
||||
|
||||
@@ -121,6 +151,11 @@ class PoolListener(object):
|
||||
class ConnectionProxy(object):
|
||||
"""Allows interception of statement execution by Connections.
|
||||
|
||||
.. note::
|
||||
|
||||
:class:`.ConnectionProxy` is deprecated. Please
|
||||
refer to :class:`.ConnectionEvents`.
|
||||
|
||||
Either or both of the ``execute()`` and ``cursor_execute()``
|
||||
may be implemented to intercept compiled statement and
|
||||
cursor level executions, e.g.::
|
||||
@@ -144,9 +179,77 @@ class ConnectionProxy(object):
|
||||
e = create_engine('someurl://', proxy=MyProxy())
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _adapt_listener(cls, self, listener):
|
||||
|
||||
def adapt_execute(conn, clauseelement, multiparams, params):
|
||||
|
||||
def execute_wrapper(clauseelement, *multiparams, **params):
|
||||
return clauseelement, multiparams, params
|
||||
|
||||
return listener.execute(conn, execute_wrapper,
|
||||
clauseelement, *multiparams,
|
||||
**params)
|
||||
|
||||
event.listen(self, 'before_execute', adapt_execute)
|
||||
|
||||
def adapt_cursor_execute(conn, cursor, statement,
|
||||
parameters,context, executemany, ):
|
||||
|
||||
def execute_wrapper(
|
||||
cursor,
|
||||
statement,
|
||||
parameters,
|
||||
context,
|
||||
):
|
||||
return statement, parameters
|
||||
|
||||
return listener.cursor_execute(
|
||||
execute_wrapper,
|
||||
cursor,
|
||||
statement,
|
||||
parameters,
|
||||
context,
|
||||
executemany,
|
||||
)
|
||||
|
||||
event.listen(self, 'before_cursor_execute', adapt_cursor_execute)
|
||||
|
||||
def do_nothing_callback(*arg, **kw):
|
||||
pass
|
||||
|
||||
def adapt_listener(fn):
|
||||
|
||||
def go(conn, *arg, **kw):
|
||||
fn(conn, do_nothing_callback, *arg, **kw)
|
||||
|
||||
return util.update_wrapper(go, fn)
|
||||
|
||||
event.listen(self, 'begin', adapt_listener(listener.begin))
|
||||
event.listen(self, 'rollback',
|
||||
adapt_listener(listener.rollback))
|
||||
event.listen(self, 'commit', adapt_listener(listener.commit))
|
||||
event.listen(self, 'savepoint',
|
||||
adapt_listener(listener.savepoint))
|
||||
event.listen(self, 'rollback_savepoint',
|
||||
adapt_listener(listener.rollback_savepoint))
|
||||
event.listen(self, 'release_savepoint',
|
||||
adapt_listener(listener.release_savepoint))
|
||||
event.listen(self, 'begin_twophase',
|
||||
adapt_listener(listener.begin_twophase))
|
||||
event.listen(self, 'prepare_twophase',
|
||||
adapt_listener(listener.prepare_twophase))
|
||||
event.listen(self, 'rollback_twophase',
|
||||
adapt_listener(listener.rollback_twophase))
|
||||
event.listen(self, 'commit_twophase',
|
||||
adapt_listener(listener.commit_twophase))
|
||||
|
||||
|
||||
def execute(self, conn, execute, clauseelement, *multiparams, **params):
|
||||
"""Intercept high level execute() events."""
|
||||
|
||||
|
||||
return execute(clauseelement, *multiparams, **params)
|
||||
|
||||
def cursor_execute(self, execute, cursor, statement, parameters, context, executemany):
|
||||
|
||||
+146
-53
@@ -1,5 +1,6 @@
|
||||
# sqlalchemy/log.py
|
||||
# Copyright (C) 2006-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Includes alterations by Vinay Sajip vinay_sajip@yahoo.co.uk
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -10,41 +11,28 @@ Control of logging for SA can be performed from the regular python logging
|
||||
module. The regular dotted module namespace is used, starting at
|
||||
'sqlalchemy'. For class-level logging, the class name is appended.
|
||||
|
||||
The "echo" keyword parameter which is available on SQLA ``Engine``
|
||||
and ``Pool`` objects corresponds to a logger specific to that
|
||||
The "echo" keyword parameter, available on SQLA :class:`.Engine`
|
||||
and :class:`.Pool` objects, corresponds to a logger specific to that
|
||||
instance only.
|
||||
|
||||
E.g.::
|
||||
|
||||
engine.echo = True
|
||||
|
||||
is equivalent to::
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger('sqlalchemy.engine.Engine.%s' % hex(id(engine)))
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from sqlalchemy import util
|
||||
|
||||
# set initial level to WARN. This so that
|
||||
# log statements don't occur in the absense of explicit
|
||||
# logging being enabled for 'sqlalchemy'.
|
||||
rootlogger = logging.getLogger('sqlalchemy')
|
||||
if rootlogger.level == logging.NOTSET:
|
||||
rootlogger.setLevel(logging.WARN)
|
||||
|
||||
default_enabled = False
|
||||
def default_logging(name):
|
||||
global default_enabled
|
||||
if logging.getLogger(name).getEffectiveLevel() < logging.WARN:
|
||||
default_enabled = True
|
||||
if not default_enabled:
|
||||
default_enabled = True
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setFormatter(logging.Formatter(
|
||||
'%(asctime)s %(levelname)s %(name)s %(message)s'))
|
||||
rootlogger.addHandler(handler)
|
||||
def _add_default_handler(logger):
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setFormatter(logging.Formatter(
|
||||
'%(asctime)s %(levelname)s %(name)s %(message)s'))
|
||||
logger.addHandler(handler)
|
||||
|
||||
_logged_classes = set()
|
||||
def class_logger(cls, enable=False):
|
||||
@@ -60,42 +48,148 @@ def class_logger(cls, enable=False):
|
||||
|
||||
|
||||
class Identified(object):
|
||||
@util.memoized_property
|
||||
def logging_name(self):
|
||||
# limit the number of loggers by chopping off the hex(id).
|
||||
# some novice users unfortunately create an unlimited number
|
||||
# of Engines in their applications which would otherwise
|
||||
# cause the app to run out of memory.
|
||||
return "0x...%s" % hex(id(self))[-4:]
|
||||
logging_name = None
|
||||
|
||||
def _should_log_debug(self):
|
||||
return self.logger.isEnabledFor(logging.DEBUG)
|
||||
|
||||
def instance_logger(instance, echoflag=None):
|
||||
"""create a logger for an instance that implements :class:`Identified`.
|
||||
def _should_log_info(self):
|
||||
return self.logger.isEnabledFor(logging.INFO)
|
||||
|
||||
Warning: this is an expensive call which also results in a permanent
|
||||
increase in memory overhead for each call. Use only for
|
||||
low-volume, long-time-spanning objects.
|
||||
class InstanceLogger(object):
|
||||
"""A logger adapter (wrapper) for :class:`.Identified` subclasses.
|
||||
|
||||
This allows multiple instances (e.g. Engine or Pool instances)
|
||||
to share a logger, but have its verbosity controlled on a
|
||||
per-instance basis.
|
||||
|
||||
The basic functionality is to return a logging level
|
||||
which is based on an instance's echo setting.
|
||||
|
||||
Default implementation is:
|
||||
|
||||
'debug' -> logging.DEBUG
|
||||
True -> logging.INFO
|
||||
False -> Effective level of underlying logger
|
||||
(logging.WARNING by default)
|
||||
None -> same as False
|
||||
"""
|
||||
|
||||
name = "%s.%s.%s" % (instance.__class__.__module__,
|
||||
instance.__class__.__name__, instance.logging_name)
|
||||
# Map echo settings to logger levels
|
||||
_echo_map = {
|
||||
None: logging.NOTSET,
|
||||
False: logging.NOTSET,
|
||||
True: logging.INFO,
|
||||
'debug': logging.DEBUG,
|
||||
}
|
||||
|
||||
if echoflag is not None:
|
||||
l = logging.getLogger(name)
|
||||
if echoflag == 'debug':
|
||||
default_logging(name)
|
||||
l.setLevel(logging.DEBUG)
|
||||
elif echoflag is True:
|
||||
default_logging(name)
|
||||
l.setLevel(logging.INFO)
|
||||
elif echoflag is False:
|
||||
l.setLevel(logging.WARN)
|
||||
def __init__(self, echo, name):
|
||||
self.echo = echo
|
||||
self.logger = logging.getLogger(name)
|
||||
|
||||
# if echo flag is enabled and no handlers,
|
||||
# add a handler to the list
|
||||
if self._echo_map[echo] <= logging.INFO \
|
||||
and not self.logger.handlers:
|
||||
_add_default_handler(self.logger)
|
||||
|
||||
#
|
||||
# Boilerplate convenience methods
|
||||
#
|
||||
def debug(self, msg, *args, **kwargs):
|
||||
"""Delegate a debug call to the underlying logger."""
|
||||
|
||||
self.log(logging.DEBUG, msg, *args, **kwargs)
|
||||
|
||||
def info(self, msg, *args, **kwargs):
|
||||
"""Delegate an info call to the underlying logger."""
|
||||
|
||||
self.log(logging.INFO, msg, *args, **kwargs)
|
||||
|
||||
def warning(self, msg, *args, **kwargs):
|
||||
"""Delegate a warning call to the underlying logger."""
|
||||
|
||||
self.log(logging.WARNING, msg, *args, **kwargs)
|
||||
|
||||
warn = warning
|
||||
|
||||
def error(self, msg, *args, **kwargs):
|
||||
"""
|
||||
Delegate an error call to the underlying logger.
|
||||
"""
|
||||
self.log(logging.ERROR, msg, *args, **kwargs)
|
||||
|
||||
def exception(self, msg, *args, **kwargs):
|
||||
"""Delegate an exception call to the underlying logger."""
|
||||
|
||||
kwargs["exc_info"] = 1
|
||||
self.log(logging.ERROR, msg, *args, **kwargs)
|
||||
|
||||
def critical(self, msg, *args, **kwargs):
|
||||
"""Delegate a critical call to the underlying logger."""
|
||||
|
||||
self.log(logging.CRITICAL, msg, *args, **kwargs)
|
||||
|
||||
def log(self, level, msg, *args, **kwargs):
|
||||
"""Delegate a log call to the underlying logger.
|
||||
|
||||
The level here is determined by the echo
|
||||
flag as well as that of the underlying logger, and
|
||||
logger._log() is called directly.
|
||||
|
||||
"""
|
||||
|
||||
# inline the logic from isEnabledFor(),
|
||||
# getEffectiveLevel(), to avoid overhead.
|
||||
|
||||
if self.logger.manager.disable >= level:
|
||||
return
|
||||
|
||||
selected_level = self._echo_map[self.echo]
|
||||
if selected_level == logging.NOTSET:
|
||||
selected_level = self.logger.getEffectiveLevel()
|
||||
|
||||
if level >= selected_level:
|
||||
self.logger._log(level, msg, args, **kwargs)
|
||||
|
||||
def isEnabledFor(self, level):
|
||||
"""Is this logger enabled for level 'level'?"""
|
||||
|
||||
if self.logger.manager.disable >= level:
|
||||
return False
|
||||
return level >= self.getEffectiveLevel()
|
||||
|
||||
def getEffectiveLevel(self):
|
||||
"""What's the effective level for this logger?"""
|
||||
|
||||
level = self._echo_map[self.echo]
|
||||
if level == logging.NOTSET:
|
||||
level = self.logger.getEffectiveLevel()
|
||||
return level
|
||||
|
||||
def instance_logger(instance, echoflag=None):
|
||||
"""create a logger for an instance that implements :class:`.Identified`."""
|
||||
|
||||
if instance.logging_name:
|
||||
name = "%s.%s.%s" % (instance.__class__.__module__,
|
||||
instance.__class__.__name__, instance.logging_name)
|
||||
else:
|
||||
l = logging.getLogger(name)
|
||||
instance._should_log_debug = lambda: l.isEnabledFor(logging.DEBUG)
|
||||
instance._should_log_info = lambda: l.isEnabledFor(logging.INFO)
|
||||
return l
|
||||
name = "%s.%s" % (instance.__class__.__module__,
|
||||
instance.__class__.__name__)
|
||||
|
||||
instance._echo = echoflag
|
||||
|
||||
if echoflag in (False, None):
|
||||
# if no echo setting or False, return a Logger directly,
|
||||
# avoiding overhead of filtering
|
||||
logger = logging.getLogger(name)
|
||||
else:
|
||||
# if a specified echo flag, return an EchoLogger,
|
||||
# which checks the flag, overrides normal log
|
||||
# levels by calling logger._log()
|
||||
logger = InstanceLogger(echoflag, name)
|
||||
|
||||
instance.logger = logger
|
||||
|
||||
class echo_property(object):
|
||||
__doc__ = """\
|
||||
@@ -112,8 +206,7 @@ class echo_property(object):
|
||||
if instance is None:
|
||||
return self
|
||||
else:
|
||||
return instance._should_log_debug() and 'debug' or \
|
||||
(instance._should_log_info() and True or False)
|
||||
return instance._echo
|
||||
|
||||
def __set__(self, instance, value):
|
||||
instance_logger(instance, echoflag=value)
|
||||
|
||||
+709
-328
File diff suppressed because it is too large
Load Diff
+520
-945
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
# orm/collections.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -108,9 +108,8 @@ import operator
|
||||
import sys
|
||||
import weakref
|
||||
|
||||
import sqlalchemy.exceptions as sa_exc
|
||||
from sqlalchemy.sql import expression
|
||||
from sqlalchemy import schema, util
|
||||
from sqlalchemy import schema, util, exc as sa_exc
|
||||
|
||||
|
||||
__all__ = ['collection', 'collection_adapter',
|
||||
@@ -123,7 +122,7 @@ __instrumentation_mutex = util.threading.Lock()
|
||||
def column_mapped_collection(mapping_spec):
|
||||
"""A dictionary-based collection type with column-based keying.
|
||||
|
||||
Returns a MappedCollection factory with a keying function generated
|
||||
Returns a :class:`.MappedCollection` factory with a keying function generated
|
||||
from mapping_spec, which may be a Column or a sequence of Columns.
|
||||
|
||||
The key value must be immutable for the lifetime of the object. You
|
||||
@@ -154,8 +153,9 @@ def column_mapped_collection(mapping_spec):
|
||||
def attribute_mapped_collection(attr_name):
|
||||
"""A dictionary-based collection type with attribute-based keying.
|
||||
|
||||
Returns a MappedCollection factory with a keying based on the
|
||||
'attr_name' attribute of entities in the collection.
|
||||
Returns a :class:`.MappedCollection` factory with a keying based on the
|
||||
'attr_name' attribute of entities in the collection, where ``attr_name``
|
||||
is the string name of the attribute.
|
||||
|
||||
The key value must be immutable for the lifetime of the object. You
|
||||
can not, for example, map on foreign key values if those key values will
|
||||
@@ -169,7 +169,7 @@ def attribute_mapped_collection(attr_name):
|
||||
def mapped_collection(keyfunc):
|
||||
"""A dictionary-based collection type with arbitrary keying.
|
||||
|
||||
Returns a MappedCollection factory with a keying function generated
|
||||
Returns a :class:`.MappedCollection` factory with a keying function generated
|
||||
from keyfunc, a callable that takes an entity and returns a key value.
|
||||
|
||||
The key value must be immutable for the lifetime of the object. You
|
||||
@@ -186,7 +186,7 @@ class collection(object):
|
||||
The decorators fall into two groups: annotations and interception recipes.
|
||||
|
||||
The annotating decorators (appender, remover, iterator,
|
||||
internally_instrumented, on_link) indicate the method's purpose and take no
|
||||
internally_instrumented, link) indicate the method's purpose and take no
|
||||
arguments. They are not written with parens::
|
||||
|
||||
@collection.appender
|
||||
@@ -201,10 +201,6 @@ class collection(object):
|
||||
@collection.removes_return()
|
||||
def popitem(self): ...
|
||||
|
||||
Decorators can be specified in long-hand for Python 2.3, or with
|
||||
the class-level dict attribute '__instrumentation__'- see the source
|
||||
for details.
|
||||
|
||||
"""
|
||||
# Bundled as a class solely for ease of use: packaging, doc strings,
|
||||
# importability.
|
||||
@@ -315,7 +311,7 @@ class collection(object):
|
||||
return fn
|
||||
|
||||
@staticmethod
|
||||
def on_link(fn):
|
||||
def link(fn):
|
||||
"""Tag the method as a the "linked to attribute" event handler.
|
||||
|
||||
This optional event handler will be called when the collection class
|
||||
@@ -325,7 +321,7 @@ class collection(object):
|
||||
that has been linked, or None if unlinking.
|
||||
|
||||
"""
|
||||
setattr(fn, '_sa_instrument_role', 'on_link')
|
||||
setattr(fn, '_sa_instrument_role', 'link')
|
||||
return fn
|
||||
|
||||
@staticmethod
|
||||
@@ -474,9 +470,12 @@ class CollectionAdapter(object):
|
||||
to the underlying Python collection, and emits add/remove events for
|
||||
entities entering or leaving the collection.
|
||||
|
||||
The ORM uses an CollectionAdapter exclusively for interaction with
|
||||
The ORM uses :class:`.CollectionAdapter` exclusively for interaction with
|
||||
entity collections.
|
||||
|
||||
The usage of getattr()/setattr() is currently to allow injection
|
||||
of custom methods, such as to unwrap Zope security proxies.
|
||||
|
||||
"""
|
||||
def __init__(self, attr, owner_state, data):
|
||||
self._key = attr.key
|
||||
@@ -559,6 +558,12 @@ class CollectionAdapter(object):
|
||||
"""Add or restore an entity to the collection, firing no events."""
|
||||
getattr(self._data(), '_sa_appender')(item, _sa_initiator=False)
|
||||
|
||||
def append_multiple_without_event(self, items):
|
||||
"""Add or restore an entity to the collection, firing no events."""
|
||||
appender = getattr(self._data(), '_sa_appender')
|
||||
for item in items:
|
||||
appender(item, _sa_initiator=False)
|
||||
|
||||
def remove_with_event(self, item, initiator=None):
|
||||
"""Remove an entity from the collection, firing mutation events."""
|
||||
getattr(self._data(), '_sa_remover')(item, _sa_initiator=initiator)
|
||||
@@ -569,13 +574,17 @@ class CollectionAdapter(object):
|
||||
|
||||
def clear_with_event(self, initiator=None):
|
||||
"""Empty the collection, firing a mutation event for each entity."""
|
||||
|
||||
remover = getattr(self._data(), '_sa_remover')
|
||||
for item in list(self):
|
||||
self.remove_with_event(item, initiator)
|
||||
remover(item, _sa_initiator=initiator)
|
||||
|
||||
def clear_without_event(self):
|
||||
"""Empty the collection, firing no events."""
|
||||
|
||||
remover = getattr(self._data(), '_sa_remover')
|
||||
for item in list(self):
|
||||
self.remove_without_event(item)
|
||||
remover(item, _sa_initiator=False)
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over entities in the collection."""
|
||||
@@ -651,14 +660,11 @@ def bulk_replace(values, existing_adapter, new_adapter):
|
||||
instances in ``existing_adapter`` not present in ``values`` will have
|
||||
remove events fired upon them.
|
||||
|
||||
values
|
||||
An iterable of collection member instances
|
||||
:param values: An iterable of collection member instances
|
||||
|
||||
existing_adapter
|
||||
A CollectionAdapter of instances to be replaced
|
||||
:param existing_adapter: A :class:`.CollectionAdapter` of instances to be replaced
|
||||
|
||||
new_adapter
|
||||
An empty CollectionAdapter to load with ``values``
|
||||
:param new_adapter: An empty :class:`.CollectionAdapter` to load with ``values``
|
||||
|
||||
|
||||
"""
|
||||
@@ -788,7 +794,7 @@ def _instrument_class(cls):
|
||||
if hasattr(method, '_sa_instrument_role'):
|
||||
role = method._sa_instrument_role
|
||||
assert role in ('appender', 'remover', 'iterator',
|
||||
'on_link', 'converter')
|
||||
'link', 'converter')
|
||||
roles[role] = name
|
||||
|
||||
# transfer instrumentation requests from decorated function
|
||||
@@ -1160,7 +1166,7 @@ def _dict_decorators():
|
||||
l.pop('Unspecified')
|
||||
return l
|
||||
|
||||
if util.py3k:
|
||||
if util.py3k_warning:
|
||||
_set_binop_bases = (set, frozenset)
|
||||
else:
|
||||
import sets
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# orm/dependency.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -8,8 +8,7 @@
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy import sql, util
|
||||
import sqlalchemy.exceptions as sa_exc
|
||||
from sqlalchemy import sql, util, exc as sa_exc
|
||||
from sqlalchemy.orm import attributes, exc, sync, unitofwork, \
|
||||
util as mapperutil
|
||||
from sqlalchemy.orm.interfaces import ONETOMANY, MANYTOONE, MANYTOMANY
|
||||
@@ -26,9 +25,15 @@ class DependencyProcessor(object):
|
||||
self.passive_deletes = prop.passive_deletes
|
||||
self.passive_updates = prop.passive_updates
|
||||
self.enable_typechecks = prop.enable_typechecks
|
||||
self._passive_delete_flag = self.passive_deletes and \
|
||||
attributes.PASSIVE_NO_INITIALIZE or \
|
||||
attributes.PASSIVE_OFF
|
||||
if self.passive_deletes:
|
||||
self._passive_delete_flag = attributes.PASSIVE_NO_INITIALIZE
|
||||
else:
|
||||
self._passive_delete_flag = attributes.PASSIVE_OFF
|
||||
if self.passive_updates:
|
||||
self._passive_update_flag = attributes.PASSIVE_NO_INITIALIZE
|
||||
else:
|
||||
self._passive_update_flag= attributes.PASSIVE_OFF
|
||||
|
||||
self.key = prop.key
|
||||
if not self.prop.synchronize_pairs:
|
||||
raise sa_exc.ArgumentError(
|
||||
@@ -53,7 +58,7 @@ class DependencyProcessor(object):
|
||||
"""establish actions and dependencies related to a flush.
|
||||
|
||||
These actions will operate on all relevant states in
|
||||
the aggreagte.
|
||||
the aggregate.
|
||||
|
||||
"""
|
||||
uow.register_preprocessor(self, True)
|
||||
@@ -154,10 +159,8 @@ class DependencyProcessor(object):
|
||||
# detect if there's anything changed or loaded
|
||||
# by a preprocessor on this state/attribute. if not,
|
||||
# we should be able to skip it entirely.
|
||||
sum_ = uow.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=True).sum()
|
||||
sum_ = state.manager[self.key].impl.get_all_pending(state, state.dict)
|
||||
|
||||
if not sum_:
|
||||
continue
|
||||
|
||||
@@ -179,9 +182,7 @@ class DependencyProcessor(object):
|
||||
|
||||
if child_in_cycles:
|
||||
child_actions = []
|
||||
for child_state in sum_:
|
||||
if child_state is None:
|
||||
continue
|
||||
for child_state, child in sum_:
|
||||
if child_state not in uow.states:
|
||||
child_action = (None, None)
|
||||
else:
|
||||
@@ -223,7 +224,12 @@ class DependencyProcessor(object):
|
||||
pass
|
||||
|
||||
def prop_has_changes(self, uowcommit, states, isdelete):
|
||||
passive = not isdelete or self.passive_deletes
|
||||
if not isdelete or self.passive_deletes:
|
||||
passive = attributes.PASSIVE_NO_INITIALIZE
|
||||
elif self.direction is MANYTOONE:
|
||||
passive = attributes.PASSIVE_NO_FETCH_RELATED
|
||||
else:
|
||||
passive = attributes.PASSIVE_OFF
|
||||
|
||||
for s in states:
|
||||
# TODO: add a high speed method
|
||||
@@ -232,30 +238,45 @@ class DependencyProcessor(object):
|
||||
history = uowcommit.get_attribute_history(
|
||||
s,
|
||||
self.key,
|
||||
passive=passive)
|
||||
passive)
|
||||
if history and not history.empty():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return states and \
|
||||
not self.prop._is_self_referential and \
|
||||
self.mapper in uowcommit.mappers
|
||||
|
||||
def _verify_canload(self, state):
|
||||
if state is not None and \
|
||||
not self.mapper._canload(state,
|
||||
allow_subtypes=not self.enable_typechecks):
|
||||
if self.mapper._canload(state, allow_subtypes=True):
|
||||
raise exc.FlushError(
|
||||
"Attempting to flush an item of type %s on collection '%s', "
|
||||
"which is not the expected type %s. Configure mapper '%s' "
|
||||
"to load this subtype polymorphically, or set "
|
||||
"enable_typechecks=False to allow subtypes. "
|
||||
"Mismatched typeloading may cause bi-directional "
|
||||
"relationships (backrefs) to not function properly." %
|
||||
(state.class_, self.prop, self.mapper.class_, self.mapper))
|
||||
raise exc.FlushError('Attempting to flush an item of type '
|
||||
'%(x)s as a member of collection '
|
||||
'"%(y)s". Expected an object of type '
|
||||
'%(z)s or a polymorphic subclass of '
|
||||
'this type. If %(x)s is a subclass of '
|
||||
'%(z)s, configure mapper "%(zm)s" to '
|
||||
'load this subtype polymorphically, or '
|
||||
'set enable_typechecks=False to allow '
|
||||
'any subtype to be accepted for flush. '
|
||||
% {
|
||||
'x': state.class_,
|
||||
'y': self.prop,
|
||||
'z': self.mapper.class_,
|
||||
'zm': self.mapper,
|
||||
})
|
||||
else:
|
||||
raise exc.FlushError(
|
||||
"Attempting to flush an item of type %s on collection '%s', "
|
||||
"whose mapper does not inherit from that of %s." %
|
||||
(state.class_, self.prop, self.mapper.class_))
|
||||
'Attempting to flush an item of type '
|
||||
'%(x)s as a member of collection '
|
||||
'"%(y)s". Expected an object of type '
|
||||
'%(z)s or a polymorphic subclass of '
|
||||
'this type.' % {
|
||||
'x': state.class_,
|
||||
'y': self.prop,
|
||||
'z': self.mapper.class_,
|
||||
})
|
||||
|
||||
def _synchronize(self, state, child, associationrow,
|
||||
clearkeys, uowcommit):
|
||||
@@ -397,7 +418,7 @@ class OneToManyDP(DependencyProcessor):
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=self._passive_delete_flag)
|
||||
self._passive_delete_flag)
|
||||
if history:
|
||||
for child in history.deleted:
|
||||
if child is not None and self.hasparent(child) is False:
|
||||
@@ -409,7 +430,9 @@ class OneToManyDP(DependencyProcessor):
|
||||
if should_null_fks:
|
||||
for child in history.unchanged:
|
||||
if child is not None:
|
||||
uowcommit.register_object(child)
|
||||
uowcommit.register_object(child,
|
||||
operation="delete", prop=self.prop)
|
||||
|
||||
|
||||
|
||||
def presort_saves(self, uowcommit, states):
|
||||
@@ -418,27 +441,36 @@ class OneToManyDP(DependencyProcessor):
|
||||
for state in states:
|
||||
pks_changed = self._pks_changed(uowcommit, state)
|
||||
|
||||
if not pks_changed or self.passive_updates:
|
||||
passive = attributes.PASSIVE_NO_INITIALIZE
|
||||
else:
|
||||
passive = attributes.PASSIVE_OFF
|
||||
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=not pks_changed
|
||||
or self.passive_updates)
|
||||
passive)
|
||||
if history:
|
||||
for child in history.added:
|
||||
if child is not None:
|
||||
uowcommit.register_object(child, cancel_delete=True)
|
||||
uowcommit.register_object(child, cancel_delete=True,
|
||||
operation="add",
|
||||
prop=self.prop)
|
||||
|
||||
children_added.update(history.added)
|
||||
|
||||
for child in history.deleted:
|
||||
if not self.cascade.delete_orphan:
|
||||
uowcommit.register_object(child, isdelete=False)
|
||||
uowcommit.register_object(child, isdelete=False,
|
||||
operation='delete',
|
||||
prop=self.prop)
|
||||
elif self.hasparent(child) is False:
|
||||
uowcommit.register_object(child, isdelete=True)
|
||||
for c, m in self.mapper.cascade_iterator(
|
||||
uowcommit.register_object(child, isdelete=True,
|
||||
operation="delete", prop=self.prop)
|
||||
for c, m, st_, dct_ in self.mapper.cascade_iterator(
|
||||
'delete', child):
|
||||
uowcommit.register_object(
|
||||
attributes.instance_state(c),
|
||||
st_,
|
||||
isdelete=True)
|
||||
|
||||
if pks_changed:
|
||||
@@ -448,7 +480,9 @@ class OneToManyDP(DependencyProcessor):
|
||||
uowcommit.register_object(
|
||||
child,
|
||||
False,
|
||||
self.passive_updates)
|
||||
self.passive_updates,
|
||||
operation="pk change",
|
||||
prop=self.prop)
|
||||
|
||||
def process_deletes(self, uowcommit, states):
|
||||
# head object is being deleted, and we manage its list of
|
||||
@@ -464,7 +498,7 @@ class OneToManyDP(DependencyProcessor):
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=self._passive_delete_flag)
|
||||
self._passive_delete_flag)
|
||||
if history:
|
||||
for child in history.deleted:
|
||||
if child is not None and \
|
||||
@@ -498,9 +532,10 @@ class OneToManyDP(DependencyProcessor):
|
||||
|
||||
def process_saves(self, uowcommit, states):
|
||||
for state in states:
|
||||
history = uowcommit.get_attribute_history(state,
|
||||
self.key,
|
||||
passive=True)
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
attributes.PASSIVE_NO_INITIALIZE)
|
||||
if history:
|
||||
for child in history.added:
|
||||
self._synchronize(state, child, None,
|
||||
@@ -644,7 +679,7 @@ class ManyToOneDP(DependencyProcessor):
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=self._passive_delete_flag)
|
||||
self._passive_delete_flag)
|
||||
if history:
|
||||
if self.cascade.delete_orphan:
|
||||
todelete = history.sum()
|
||||
@@ -653,29 +688,32 @@ class ManyToOneDP(DependencyProcessor):
|
||||
for child in todelete:
|
||||
if child is None:
|
||||
continue
|
||||
uowcommit.register_object(child, isdelete=True)
|
||||
for c, m in self.mapper.cascade_iterator(
|
||||
uowcommit.register_object(child, isdelete=True,
|
||||
operation="delete", prop=self.prop)
|
||||
for c, m, st_, dct_ in self.mapper.cascade_iterator(
|
||||
'delete', child):
|
||||
uowcommit.register_object(
|
||||
attributes.instance_state(c), isdelete=True)
|
||||
st_, isdelete=True)
|
||||
|
||||
def presort_saves(self, uowcommit, states):
|
||||
for state in states:
|
||||
uowcommit.register_object(state)
|
||||
uowcommit.register_object(state, operation="add", prop=self.prop)
|
||||
if self.cascade.delete_orphan:
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=self._passive_delete_flag)
|
||||
self._passive_delete_flag)
|
||||
if history:
|
||||
ret = True
|
||||
for child in history.deleted:
|
||||
if self.hasparent(child) is False:
|
||||
uowcommit.register_object(child, isdelete=True)
|
||||
for c, m in self.mapper.cascade_iterator(
|
||||
uowcommit.register_object(child, isdelete=True,
|
||||
operation="delete", prop=self.prop)
|
||||
|
||||
for c, m, st_, dct_ in self.mapper.cascade_iterator(
|
||||
'delete', child):
|
||||
uowcommit.register_object(
|
||||
attributes.instance_state(c),
|
||||
st_,
|
||||
isdelete=True)
|
||||
|
||||
def process_deletes(self, uowcommit, states):
|
||||
@@ -692,28 +730,39 @@ class ManyToOneDP(DependencyProcessor):
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=self._passive_delete_flag)
|
||||
self._passive_delete_flag)
|
||||
if history:
|
||||
self._post_update(state, uowcommit, history.sum())
|
||||
|
||||
def process_saves(self, uowcommit, states):
|
||||
for state in states:
|
||||
history = uowcommit.get_attribute_history(state,
|
||||
self.key,
|
||||
passive=True)
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
attributes.PASSIVE_NO_INITIALIZE)
|
||||
if history:
|
||||
for child in history.added:
|
||||
self._synchronize(state, child, None, False, uowcommit)
|
||||
self._synchronize(state, child, None, False,
|
||||
uowcommit, "add")
|
||||
|
||||
if self.post_update:
|
||||
self._post_update(state, uowcommit, history.sum())
|
||||
|
||||
def _synchronize(self, state, child, associationrow,
|
||||
clearkeys, uowcommit):
|
||||
def _synchronize(self, state, child, associationrow,
|
||||
clearkeys, uowcommit, operation=None):
|
||||
if state is None or \
|
||||
(not self.post_update and uowcommit.is_deleted(state)):
|
||||
return
|
||||
|
||||
if operation is not None and \
|
||||
child is not None and \
|
||||
not uowcommit.session._contains_state(child):
|
||||
util.warn(
|
||||
"Object of type %s not in session, %s "
|
||||
"operation along '%s' won't proceed" %
|
||||
(mapperutil.state_class_str(child), operation, self.prop))
|
||||
return
|
||||
|
||||
if clearkeys or child is None:
|
||||
sync.clear(state, self.parent, self.prop.synchronize_pairs)
|
||||
else:
|
||||
@@ -811,7 +860,7 @@ class DetectKeySwitch(DependencyProcessor):
|
||||
continue
|
||||
dict_ = state.dict
|
||||
related = state.get_impl(self.key).get(state, dict_,
|
||||
passive=self.passive_updates)
|
||||
passive=self._passive_update_flag)
|
||||
if related is not attributes.PASSIVE_NO_RESULT and \
|
||||
related is not None:
|
||||
related_state = attributes.instance_state(dict_[self.key])
|
||||
@@ -826,7 +875,7 @@ class DetectKeySwitch(DependencyProcessor):
|
||||
uowcommit, self.passive_updates)
|
||||
|
||||
def _pks_changed(self, uowcommit, state):
|
||||
return state.has_identity and sync.source_modified(uowcommit,
|
||||
return bool(state.key) and sync.source_modified(uowcommit,
|
||||
state,
|
||||
self.mapper,
|
||||
self.prop.synchronize_pairs)
|
||||
@@ -891,7 +940,7 @@ class ManyToManyDP(DependencyProcessor):
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=self._passive_delete_flag)
|
||||
self._passive_delete_flag)
|
||||
|
||||
def presort_saves(self, uowcommit, states):
|
||||
if not self.passive_updates:
|
||||
@@ -903,7 +952,7 @@ class ManyToManyDP(DependencyProcessor):
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
False)
|
||||
attributes.PASSIVE_OFF)
|
||||
|
||||
if not self.cascade.delete_orphan:
|
||||
return
|
||||
@@ -914,16 +963,17 @@ class ManyToManyDP(DependencyProcessor):
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=True)
|
||||
attributes.PASSIVE_NO_INITIALIZE)
|
||||
if history:
|
||||
for child in history.deleted:
|
||||
if self.hasparent(child) is False:
|
||||
uowcommit.register_object(child, isdelete=True)
|
||||
for c, m in self.mapper.cascade_iterator(
|
||||
uowcommit.register_object(child, isdelete=True,
|
||||
operation="delete", prop=self.prop)
|
||||
for c, m, st_, dct_ in self.mapper.cascade_iterator(
|
||||
'delete',
|
||||
child):
|
||||
uowcommit.register_object(
|
||||
attributes.instance_state(c), isdelete=True)
|
||||
st_, isdelete=True)
|
||||
|
||||
def process_deletes(self, uowcommit, states):
|
||||
secondary_delete = []
|
||||
@@ -938,20 +988,20 @@ class ManyToManyDP(DependencyProcessor):
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=self._passive_delete_flag)
|
||||
self._passive_delete_flag)
|
||||
if history:
|
||||
for child in history.non_added():
|
||||
if child is None or \
|
||||
(processed is not None and
|
||||
(state, child) in processed) or \
|
||||
not uowcommit.session._contains_state(child):
|
||||
(state, child) in processed):
|
||||
continue
|
||||
associationrow = {}
|
||||
self._synchronize(
|
||||
if not self._synchronize(
|
||||
state,
|
||||
child,
|
||||
associationrow,
|
||||
False, uowcommit)
|
||||
False, uowcommit, "delete"):
|
||||
continue
|
||||
secondary_delete.append(associationrow)
|
||||
|
||||
tmp.update((c, state) for c in history.non_added())
|
||||
@@ -973,8 +1023,12 @@ class ManyToManyDP(DependencyProcessor):
|
||||
for state in states:
|
||||
need_cascade_pks = not self.passive_updates and \
|
||||
self._pks_changed(uowcommit, state)
|
||||
if need_cascade_pks:
|
||||
passive = attributes.PASSIVE_OFF
|
||||
else:
|
||||
passive = attributes.PASSIVE_NO_INITIALIZE
|
||||
history = uowcommit.get_attribute_history(state, self.key,
|
||||
passive=not need_cascade_pks)
|
||||
passive)
|
||||
if history:
|
||||
for child in history.added:
|
||||
if child is None or \
|
||||
@@ -982,22 +1036,23 @@ class ManyToManyDP(DependencyProcessor):
|
||||
(state, child) in processed):
|
||||
continue
|
||||
associationrow = {}
|
||||
self._synchronize(state,
|
||||
if not self._synchronize(state,
|
||||
child,
|
||||
associationrow,
|
||||
False, uowcommit)
|
||||
False, uowcommit, "add"):
|
||||
continue
|
||||
secondary_insert.append(associationrow)
|
||||
for child in history.deleted:
|
||||
if child is None or \
|
||||
(processed is not None and
|
||||
(state, child) in processed) or \
|
||||
not uowcommit.session._contains_state(child):
|
||||
(state, child) in processed):
|
||||
continue
|
||||
associationrow = {}
|
||||
self._synchronize(state,
|
||||
if not self._synchronize(state,
|
||||
child,
|
||||
associationrow,
|
||||
False, uowcommit)
|
||||
False, uowcommit, "delete"):
|
||||
continue
|
||||
secondary_delete.append(associationrow)
|
||||
|
||||
tmp.update((c, state)
|
||||
@@ -1070,9 +1125,18 @@ class ManyToManyDP(DependencyProcessor):
|
||||
connection.execute(statement, secondary_insert)
|
||||
|
||||
def _synchronize(self, state, child, associationrow,
|
||||
clearkeys, uowcommit):
|
||||
clearkeys, uowcommit, operation):
|
||||
if associationrow is None:
|
||||
return
|
||||
|
||||
if child is not None and not uowcommit.session._contains_state(child):
|
||||
if not child.deleted:
|
||||
util.warn(
|
||||
"Object of type %s not in session, %s "
|
||||
"operation along '%s' won't proceed" %
|
||||
(mapperutil.state_class_str(child), operation, self.prop))
|
||||
return False
|
||||
|
||||
self._verify_canload(child)
|
||||
|
||||
sync.populate_dict(state, self.parent, associationrow,
|
||||
@@ -1080,6 +1144,8 @@ class ManyToManyDP(DependencyProcessor):
|
||||
sync.populate_dict(child, self.mapper, associationrow,
|
||||
self.prop.secondary_synchronize_pairs)
|
||||
|
||||
return True
|
||||
|
||||
def _pks_changed(self, uowcommit, state):
|
||||
return sync.source_modified(
|
||||
uowcommit,
|
||||
|
||||
@@ -0,0 +1,589 @@
|
||||
# orm/deprecated_interfaces.py
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
from sqlalchemy import event, util
|
||||
from interfaces import EXT_CONTINUE
|
||||
|
||||
|
||||
class MapperExtension(object):
|
||||
"""Base implementation for :class:`.Mapper` event hooks.
|
||||
|
||||
.. note::
|
||||
|
||||
:class:`.MapperExtension` is deprecated. Please
|
||||
refer to :func:`.event.listen` as well as
|
||||
:class:`.MapperEvents`.
|
||||
|
||||
New extension classes subclass :class:`.MapperExtension` and are specified
|
||||
using the ``extension`` mapper() argument, which is a single
|
||||
:class:`.MapperExtension` or a list of such::
|
||||
|
||||
from sqlalchemy.orm.interfaces import MapperExtension
|
||||
|
||||
class MyExtension(MapperExtension):
|
||||
def before_insert(self, mapper, connection, instance):
|
||||
print "instance %s before insert !" % instance
|
||||
|
||||
m = mapper(User, users_table, extension=MyExtension())
|
||||
|
||||
A single mapper can maintain a chain of ``MapperExtension``
|
||||
objects. When a particular mapping event occurs, the
|
||||
corresponding method on each ``MapperExtension`` is invoked
|
||||
serially, and each method has the ability to halt the chain
|
||||
from proceeding further::
|
||||
|
||||
m = mapper(User, users_table, extension=[ext1, ext2, ext3])
|
||||
|
||||
Each ``MapperExtension`` method returns the symbol
|
||||
EXT_CONTINUE by default. This symbol generally means "move
|
||||
to the next ``MapperExtension`` for processing". For methods
|
||||
that return objects like translated rows or new object
|
||||
instances, EXT_CONTINUE means the result of the method
|
||||
should be ignored. In some cases it's required for a
|
||||
default mapper activity to be performed, such as adding a
|
||||
new instance to a result list.
|
||||
|
||||
The symbol EXT_STOP has significance within a chain
|
||||
of ``MapperExtension`` objects that the chain will be stopped
|
||||
when this symbol is returned. Like EXT_CONTINUE, it also
|
||||
has additional significance in some cases that a default
|
||||
mapper activity will not be performed.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _adapt_instrument_class(cls, self, listener):
|
||||
cls._adapt_listener_methods(self, listener, ('instrument_class',))
|
||||
|
||||
@classmethod
|
||||
def _adapt_listener(cls, self, listener):
|
||||
cls._adapt_listener_methods(
|
||||
self, listener,
|
||||
(
|
||||
'init_instance',
|
||||
'init_failed',
|
||||
'translate_row',
|
||||
'create_instance',
|
||||
'append_result',
|
||||
'populate_instance',
|
||||
'reconstruct_instance',
|
||||
'before_insert',
|
||||
'after_insert',
|
||||
'before_update',
|
||||
'after_update',
|
||||
'before_delete',
|
||||
'after_delete'
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def _adapt_listener_methods(cls, self, listener, methods):
|
||||
|
||||
for meth in methods:
|
||||
me_meth = getattr(MapperExtension, meth)
|
||||
ls_meth = getattr(listener, meth)
|
||||
|
||||
if not util.methods_equivalent(me_meth, ls_meth):
|
||||
if meth == 'reconstruct_instance':
|
||||
def go(ls_meth):
|
||||
def reconstruct(instance, ctx):
|
||||
ls_meth(self, instance)
|
||||
return reconstruct
|
||||
event.listen(self.class_manager, 'load',
|
||||
go(ls_meth), raw=False, propagate=True)
|
||||
elif meth == 'init_instance':
|
||||
def go(ls_meth):
|
||||
def init_instance(instance, args, kwargs):
|
||||
ls_meth(self, self.class_,
|
||||
self.class_manager.original_init,
|
||||
instance, args, kwargs)
|
||||
return init_instance
|
||||
event.listen(self.class_manager, 'init',
|
||||
go(ls_meth), raw=False, propagate=True)
|
||||
elif meth == 'init_failed':
|
||||
def go(ls_meth):
|
||||
def init_failed(instance, args, kwargs):
|
||||
util.warn_exception(ls_meth, self, self.class_,
|
||||
self.class_manager.original_init,
|
||||
instance, args, kwargs)
|
||||
|
||||
return init_failed
|
||||
event.listen(self.class_manager, 'init_failure',
|
||||
go(ls_meth), raw=False, propagate=True)
|
||||
else:
|
||||
event.listen(self, "%s" % meth, ls_meth,
|
||||
raw=False, retval=True, propagate=True)
|
||||
|
||||
|
||||
def instrument_class(self, mapper, class_):
|
||||
"""Receive a class when the mapper is first constructed, and has
|
||||
applied instrumentation to the mapped class.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def init_instance(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
"""Receive an instance when it's constructor is called.
|
||||
|
||||
This method is only called during a userland construction of
|
||||
an object. It is not called when an object is loaded from the
|
||||
database.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def init_failed(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
"""Receive an instance when it's constructor has been called,
|
||||
and raised an exception.
|
||||
|
||||
This method is only called during a userland construction of
|
||||
an object. It is not called when an object is loaded from the
|
||||
database.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def translate_row(self, mapper, context, row):
|
||||
"""Perform pre-processing on the given result row and return a
|
||||
new row instance.
|
||||
|
||||
This is called when the mapper first receives a row, before
|
||||
the object identity or the instance itself has been derived
|
||||
from that row. The given row may or may not be a
|
||||
``RowProxy`` object - it will always be a dictionary-like
|
||||
object which contains mapped columns as keys. The
|
||||
returned object should also be a dictionary-like object
|
||||
which recognizes mapped columns as keys.
|
||||
|
||||
If the ultimate return value is EXT_CONTINUE, the row
|
||||
is not translated.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def create_instance(self, mapper, selectcontext, row, class_):
|
||||
"""Receive a row when a new object instance is about to be
|
||||
created from that row.
|
||||
|
||||
The method can choose to create the instance itself, or it can return
|
||||
EXT_CONTINUE to indicate normal object creation should take place.
|
||||
|
||||
mapper
|
||||
The mapper doing the operation
|
||||
|
||||
selectcontext
|
||||
The QueryContext generated from the Query.
|
||||
|
||||
row
|
||||
The result row from the database
|
||||
|
||||
class\_
|
||||
The class we are mapping.
|
||||
|
||||
return value
|
||||
A new object instance, or EXT_CONTINUE
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def append_result(self, mapper, selectcontext, row, instance,
|
||||
result, **flags):
|
||||
"""Receive an object instance before that instance is appended
|
||||
to a result list.
|
||||
|
||||
If this method returns EXT_CONTINUE, result appending will proceed
|
||||
normally. if this method returns any other value or None,
|
||||
result appending will not proceed for this instance, giving
|
||||
this extension an opportunity to do the appending itself, if
|
||||
desired.
|
||||
|
||||
mapper
|
||||
The mapper doing the operation.
|
||||
|
||||
selectcontext
|
||||
The QueryContext generated from the Query.
|
||||
|
||||
row
|
||||
The result row from the database.
|
||||
|
||||
instance
|
||||
The object instance to be appended to the result.
|
||||
|
||||
result
|
||||
List to which results are being appended.
|
||||
|
||||
\**flags
|
||||
extra information about the row, same as criterion in
|
||||
``create_row_processor()`` method of
|
||||
:class:`~sqlalchemy.orm.interfaces.MapperProperty`
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def populate_instance(self, mapper, selectcontext, row,
|
||||
instance, **flags):
|
||||
"""Receive an instance before that instance has
|
||||
its attributes populated.
|
||||
|
||||
This usually corresponds to a newly loaded instance but may
|
||||
also correspond to an already-loaded instance which has
|
||||
unloaded attributes to be populated. The method may be called
|
||||
many times for a single instance, as multiple result rows are
|
||||
used to populate eagerly loaded collections.
|
||||
|
||||
If this method returns EXT_CONTINUE, instance population will
|
||||
proceed normally. If any other value or None is returned,
|
||||
instance population will not proceed, giving this extension an
|
||||
opportunity to populate the instance itself, if desired.
|
||||
|
||||
As of 0.5, most usages of this hook are obsolete. For a
|
||||
generic "object has been newly created from a row" hook, use
|
||||
``reconstruct_instance()``, or the ``@orm.reconstructor``
|
||||
decorator.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def reconstruct_instance(self, mapper, instance):
|
||||
"""Receive an object instance after it has been created via
|
||||
``__new__``, and after initial attribute population has
|
||||
occurred.
|
||||
|
||||
This typically occurs when the instance is created based on
|
||||
incoming result rows, and is only called once for that
|
||||
instance's lifetime.
|
||||
|
||||
Note that during a result-row load, this method is called upon
|
||||
the first row received for this instance. Note that some
|
||||
attributes and collections may or may not be loaded or even
|
||||
initialized, depending on what's present in the result rows.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_insert(self, mapper, connection, instance):
|
||||
"""Receive an object instance before that instance is inserted
|
||||
into its table.
|
||||
|
||||
This is a good place to set up primary key values and such
|
||||
that aren't handled otherwise.
|
||||
|
||||
Column-based attributes can be modified within this method
|
||||
which will result in the new value being inserted. However
|
||||
*no* changes to the overall flush plan can be made, and
|
||||
manipulation of the ``Session`` will not have the desired effect.
|
||||
To manipulate the ``Session`` within an extension, use
|
||||
``SessionExtension``.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def after_insert(self, mapper, connection, instance):
|
||||
"""Receive an object instance after that instance is inserted.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_update(self, mapper, connection, instance):
|
||||
"""Receive an object instance before that instance is updated.
|
||||
|
||||
Note that this method is called for all instances that are marked as
|
||||
"dirty", even those which have no net changes to their column-based
|
||||
attributes. An object is marked as dirty when any of its column-based
|
||||
attributes have a "set attribute" operation called or when any of its
|
||||
collections are modified. If, at update time, no column-based
|
||||
attributes have any net changes, no UPDATE statement will be issued.
|
||||
This means that an instance being sent to before_update is *not* a
|
||||
guarantee that an UPDATE statement will be issued (although you can
|
||||
affect the outcome here).
|
||||
|
||||
To detect if the column-based attributes on the object have net
|
||||
changes, and will therefore generate an UPDATE statement, use
|
||||
``object_session(instance).is_modified(instance,
|
||||
include_collections=False)``.
|
||||
|
||||
Column-based attributes can be modified within this method
|
||||
which will result in the new value being updated. However
|
||||
*no* changes to the overall flush plan can be made, and
|
||||
manipulation of the ``Session`` will not have the desired effect.
|
||||
To manipulate the ``Session`` within an extension, use
|
||||
``SessionExtension``.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def after_update(self, mapper, connection, instance):
|
||||
"""Receive an object instance after that instance is updated.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_delete(self, mapper, connection, instance):
|
||||
"""Receive an object instance before that instance is deleted.
|
||||
|
||||
Note that *no* changes to the overall flush plan can be made
|
||||
here; and manipulation of the ``Session`` will not have the
|
||||
desired effect. To manipulate the ``Session`` within an
|
||||
extension, use ``SessionExtension``.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def after_delete(self, mapper, connection, instance):
|
||||
"""Receive an object instance after that instance is deleted.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
class SessionExtension(object):
|
||||
|
||||
"""Base implementation for :class:`.Session` event hooks.
|
||||
|
||||
.. note::
|
||||
|
||||
:class:`.SessionExtension` is deprecated. Please
|
||||
refer to :func:`.event.listen` as well as
|
||||
:class:`.SessionEvents`.
|
||||
|
||||
Subclasses may be installed into a :class:`.Session` (or
|
||||
:func:`.sessionmaker`) using the ``extension`` keyword
|
||||
argument::
|
||||
|
||||
from sqlalchemy.orm.interfaces import SessionExtension
|
||||
|
||||
class MySessionExtension(SessionExtension):
|
||||
def before_commit(self, session):
|
||||
print "before commit!"
|
||||
|
||||
Session = sessionmaker(extension=MySessionExtension())
|
||||
|
||||
The same :class:`.SessionExtension` instance can be used
|
||||
with any number of sessions.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _adapt_listener(cls, self, listener):
|
||||
for meth in [
|
||||
'before_commit',
|
||||
'after_commit',
|
||||
'after_rollback',
|
||||
'before_flush',
|
||||
'after_flush',
|
||||
'after_flush_postexec',
|
||||
'after_begin',
|
||||
'after_attach',
|
||||
'after_bulk_update',
|
||||
'after_bulk_delete',
|
||||
]:
|
||||
me_meth = getattr(SessionExtension, meth)
|
||||
ls_meth = getattr(listener, meth)
|
||||
|
||||
if not util.methods_equivalent(me_meth, ls_meth):
|
||||
event.listen(self, meth, getattr(listener, meth))
|
||||
|
||||
def before_commit(self, session):
|
||||
"""Execute right before commit is called.
|
||||
|
||||
Note that this may not be per-flush if a longer running
|
||||
transaction is ongoing."""
|
||||
|
||||
def after_commit(self, session):
|
||||
"""Execute after a commit has occurred.
|
||||
|
||||
Note that this may not be per-flush if a longer running
|
||||
transaction is ongoing."""
|
||||
|
||||
def after_rollback(self, session):
|
||||
"""Execute after a rollback has occurred.
|
||||
|
||||
Note that this may not be per-flush if a longer running
|
||||
transaction is ongoing."""
|
||||
|
||||
def before_flush( self, session, flush_context, instances):
|
||||
"""Execute before flush process has started.
|
||||
|
||||
`instances` is an optional list of objects which were passed to
|
||||
the ``flush()`` method. """
|
||||
|
||||
def after_flush(self, session, flush_context):
|
||||
"""Execute after flush has completed, but before commit has been
|
||||
called.
|
||||
|
||||
Note that the session's state is still in pre-flush, i.e. 'new',
|
||||
'dirty', and 'deleted' lists still show pre-flush state as well
|
||||
as the history settings on instance attributes."""
|
||||
|
||||
def after_flush_postexec(self, session, flush_context):
|
||||
"""Execute after flush has completed, and after the post-exec
|
||||
state occurs.
|
||||
|
||||
This will be when the 'new', 'dirty', and 'deleted' lists are in
|
||||
their final state. An actual commit() may or may not have
|
||||
occurred, depending on whether or not the flush started its own
|
||||
transaction or participated in a larger transaction. """
|
||||
|
||||
def after_begin( self, session, transaction, connection):
|
||||
"""Execute after a transaction is begun on a connection
|
||||
|
||||
`transaction` is the SessionTransaction. This method is called
|
||||
after an engine level transaction is begun on a connection. """
|
||||
|
||||
def after_attach(self, session, instance):
|
||||
"""Execute after an instance is attached to a session.
|
||||
|
||||
This is called after an add, delete or merge. """
|
||||
|
||||
def after_bulk_update( self, session, query, query_context, result):
|
||||
"""Execute after a bulk update operation to the session.
|
||||
|
||||
This is called after a session.query(...).update()
|
||||
|
||||
`query` is the query object that this update operation was
|
||||
called on. `query_context` was the query context object.
|
||||
`result` is the result object returned from the bulk operation.
|
||||
"""
|
||||
|
||||
def after_bulk_delete( self, session, query, query_context, result):
|
||||
"""Execute after a bulk delete operation to the session.
|
||||
|
||||
This is called after a session.query(...).delete()
|
||||
|
||||
`query` is the query object that this delete operation was
|
||||
called on. `query_context` was the query context object.
|
||||
`result` is the result object returned from the bulk operation.
|
||||
"""
|
||||
|
||||
|
||||
class AttributeExtension(object):
|
||||
"""Base implementation for :class:`.AttributeImpl` event hooks, events
|
||||
that fire upon attribute mutations in user code.
|
||||
|
||||
.. note::
|
||||
|
||||
:class:`.AttributeExtension` is deprecated. Please
|
||||
refer to :func:`.event.listen` as well as
|
||||
:class:`.AttributeEvents`.
|
||||
|
||||
:class:`.AttributeExtension` is used to listen for set,
|
||||
remove, and append events on individual mapped attributes.
|
||||
It is established on an individual mapped attribute using
|
||||
the `extension` argument, available on
|
||||
:func:`.column_property`, :func:`.relationship`, and
|
||||
others::
|
||||
|
||||
from sqlalchemy.orm.interfaces import AttributeExtension
|
||||
from sqlalchemy.orm import mapper, relationship, column_property
|
||||
|
||||
class MyAttrExt(AttributeExtension):
|
||||
def append(self, state, value, initiator):
|
||||
print "append event !"
|
||||
return value
|
||||
|
||||
def set(self, state, value, oldvalue, initiator):
|
||||
print "set event !"
|
||||
return value
|
||||
|
||||
mapper(SomeClass, sometable, properties={
|
||||
'foo':column_property(sometable.c.foo, extension=MyAttrExt()),
|
||||
'bar':relationship(Bar, extension=MyAttrExt())
|
||||
})
|
||||
|
||||
Note that the :class:`.AttributeExtension` methods
|
||||
:meth:`~.AttributeExtension.append` and
|
||||
:meth:`~.AttributeExtension.set` need to return the
|
||||
``value`` parameter. The returned value is used as the
|
||||
effective value, and allows the extension to change what is
|
||||
ultimately persisted.
|
||||
|
||||
AttributeExtension is assembled within the descriptors associated
|
||||
with a mapped class.
|
||||
|
||||
"""
|
||||
|
||||
active_history = True
|
||||
"""indicates that the set() method would like to receive the 'old' value,
|
||||
even if it means firing lazy callables.
|
||||
|
||||
Note that ``active_history`` can also be set directly via
|
||||
:func:`.column_property` and :func:`.relationship`.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _adapt_listener(cls, self, listener):
|
||||
event.listen(self, 'append', listener.append,
|
||||
active_history=listener.active_history,
|
||||
raw=True, retval=True)
|
||||
event.listen(self, 'remove', listener.remove,
|
||||
active_history=listener.active_history,
|
||||
raw=True, retval=True)
|
||||
event.listen(self, 'set', listener.set,
|
||||
active_history=listener.active_history,
|
||||
raw=True, retval=True)
|
||||
|
||||
def append(self, state, value, initiator):
|
||||
"""Receive a collection append event.
|
||||
|
||||
The returned value will be used as the actual value to be
|
||||
appended.
|
||||
|
||||
"""
|
||||
return value
|
||||
|
||||
def remove(self, state, value, initiator):
|
||||
"""Receive a remove event.
|
||||
|
||||
No return value is defined.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def set(self, state, value, oldvalue, initiator):
|
||||
"""Receive a set event.
|
||||
|
||||
The returned value will be used as the actual value to be
|
||||
set.
|
||||
|
||||
"""
|
||||
return value
|
||||
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
# orm/descriptor_props.py
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
"""Descriptor properties are more "auxiliary" properties
|
||||
that exist as configurational elements, but don't participate
|
||||
as actively in the load/persist ORM loop.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm.interfaces import \
|
||||
MapperProperty, PropComparator, StrategizedProperty
|
||||
from sqlalchemy.orm.mapper import _none_set
|
||||
from sqlalchemy.orm import attributes, strategies
|
||||
from sqlalchemy import util, sql, exc as sa_exc, event, schema
|
||||
from sqlalchemy.sql import expression
|
||||
properties = util.importlater('sqlalchemy.orm', 'properties')
|
||||
|
||||
class DescriptorProperty(MapperProperty):
|
||||
""":class:`.MapperProperty` which proxies access to a
|
||||
user-defined descriptor."""
|
||||
|
||||
doc = None
|
||||
|
||||
def instrument_class(self, mapper):
|
||||
prop = self
|
||||
|
||||
class _ProxyImpl(object):
|
||||
accepts_scalar_loader = False
|
||||
expire_missing = True
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
if hasattr(prop, 'get_history'):
|
||||
def get_history(self, state, dict_,
|
||||
passive=attributes.PASSIVE_OFF):
|
||||
return prop.get_history(state, dict_, passive)
|
||||
|
||||
if self.descriptor is None:
|
||||
desc = getattr(mapper.class_, self.key, None)
|
||||
if mapper._is_userland_descriptor(desc):
|
||||
self.descriptor = desc
|
||||
|
||||
if self.descriptor is None:
|
||||
def fset(obj, value):
|
||||
setattr(obj, self.name, value)
|
||||
def fdel(obj):
|
||||
delattr(obj, self.name)
|
||||
def fget(obj):
|
||||
return getattr(obj, self.name)
|
||||
|
||||
self.descriptor = property(
|
||||
fget=fget,
|
||||
fset=fset,
|
||||
fdel=fdel,
|
||||
)
|
||||
|
||||
proxy_attr = attributes.\
|
||||
create_proxied_attribute(self.descriptor)\
|
||||
(
|
||||
self.parent.class_,
|
||||
self.key,
|
||||
self.descriptor,
|
||||
lambda: self._comparator_factory(mapper),
|
||||
doc=self.doc
|
||||
)
|
||||
proxy_attr.impl = _ProxyImpl(self.key)
|
||||
mapper.class_manager.instrument_attribute(self.key, proxy_attr)
|
||||
|
||||
|
||||
class CompositeProperty(DescriptorProperty):
|
||||
|
||||
def __init__(self, class_, *attrs, **kwargs):
|
||||
self.attrs = attrs
|
||||
self.composite_class = class_
|
||||
self.active_history = kwargs.get('active_history', False)
|
||||
self.deferred = kwargs.get('deferred', False)
|
||||
self.group = kwargs.get('group', None)
|
||||
self.comparator_factory = kwargs.pop('comparator_factory',
|
||||
self.__class__.Comparator)
|
||||
util.set_creation_order(self)
|
||||
self._create_descriptor()
|
||||
|
||||
def instrument_class(self, mapper):
|
||||
super(CompositeProperty, self).instrument_class(mapper)
|
||||
self._setup_event_handlers()
|
||||
|
||||
def do_init(self):
|
||||
"""Initialization which occurs after the :class:`.CompositeProperty`
|
||||
has been associated with its parent mapper.
|
||||
|
||||
"""
|
||||
self._init_props()
|
||||
self._setup_arguments_on_columns()
|
||||
|
||||
def _create_descriptor(self):
|
||||
"""Create the Python descriptor that will serve as
|
||||
the access point on instances of the mapped class.
|
||||
|
||||
"""
|
||||
|
||||
def fget(instance):
|
||||
dict_ = attributes.instance_dict(instance)
|
||||
state = attributes.instance_state(instance)
|
||||
|
||||
if self.key not in dict_:
|
||||
# key not present. Iterate through related
|
||||
# attributes, retrieve their values. This
|
||||
# ensures they all load.
|
||||
values = [getattr(instance, key) for key in self._attribute_keys]
|
||||
|
||||
# current expected behavior here is that the composite is
|
||||
# created on access if the object is persistent or if
|
||||
# col attributes have non-None. This would be better
|
||||
# if the composite were created unconditionally,
|
||||
# but that would be a behavioral change.
|
||||
if self.key not in dict_ and (
|
||||
state.key is not None or
|
||||
not _none_set.issuperset(values)
|
||||
):
|
||||
dict_[self.key] = self.composite_class(*values)
|
||||
state.manager.dispatch.refresh(state, None, [self.key])
|
||||
|
||||
return dict_.get(self.key, None)
|
||||
|
||||
def fset(instance, value):
|
||||
dict_ = attributes.instance_dict(instance)
|
||||
state = attributes.instance_state(instance)
|
||||
attr = state.manager[self.key]
|
||||
previous = dict_.get(self.key, attributes.NO_VALUE)
|
||||
for fn in attr.dispatch.set:
|
||||
value = fn(state, value, previous, attr.impl)
|
||||
dict_[self.key] = value
|
||||
if value is None:
|
||||
for key in self._attribute_keys:
|
||||
setattr(instance, key, None)
|
||||
else:
|
||||
for key, value in zip(
|
||||
self._attribute_keys,
|
||||
value.__composite_values__()):
|
||||
setattr(instance, key, value)
|
||||
|
||||
def fdel(instance):
|
||||
state = attributes.instance_state(instance)
|
||||
dict_ = attributes.instance_dict(instance)
|
||||
previous = dict_.pop(self.key, attributes.NO_VALUE)
|
||||
attr = state.manager[self.key]
|
||||
attr.dispatch.remove(state, previous, attr.impl)
|
||||
for key in self._attribute_keys:
|
||||
setattr(instance, key, None)
|
||||
|
||||
self.descriptor = property(fget, fset, fdel)
|
||||
|
||||
@util.memoized_property
|
||||
def _comparable_elements(self):
|
||||
return [
|
||||
getattr(self.parent.class_, prop.key)
|
||||
for prop in self.props
|
||||
]
|
||||
|
||||
def _init_props(self):
|
||||
self.props = props = []
|
||||
for attr in self.attrs:
|
||||
if isinstance(attr, basestring):
|
||||
prop = self.parent.get_property(attr)
|
||||
elif isinstance(attr, schema.Column):
|
||||
prop = self.parent._columntoproperty[attr]
|
||||
elif isinstance(attr, attributes.InstrumentedAttribute):
|
||||
prop = attr.property
|
||||
props.append(prop)
|
||||
|
||||
@property
|
||||
def columns(self):
|
||||
return [a for a in self.attrs if isinstance(a, schema.Column)]
|
||||
|
||||
def _setup_arguments_on_columns(self):
|
||||
"""Propagate configuration arguments made on this composite
|
||||
to the target columns, for those that apply.
|
||||
|
||||
"""
|
||||
for prop in self.props:
|
||||
prop.active_history = self.active_history
|
||||
if self.deferred:
|
||||
prop.deferred = self.deferred
|
||||
prop.strategy_class = strategies.DeferredColumnLoader
|
||||
prop.group = self.group
|
||||
|
||||
def _setup_event_handlers(self):
|
||||
"""Establish events that populate/expire the composite attribute."""
|
||||
|
||||
def load_handler(state, *args):
|
||||
dict_ = state.dict
|
||||
|
||||
if self.key in dict_:
|
||||
return
|
||||
|
||||
# if column elements aren't loaded, skip.
|
||||
# __get__() will initiate a load for those
|
||||
# columns
|
||||
for k in self._attribute_keys:
|
||||
if k not in dict_:
|
||||
return
|
||||
|
||||
#assert self.key not in dict_
|
||||
dict_[self.key] = self.composite_class(
|
||||
*[state.dict[key] for key in
|
||||
self._attribute_keys]
|
||||
)
|
||||
|
||||
def expire_handler(state, keys):
|
||||
if keys is None or set(self._attribute_keys).intersection(keys):
|
||||
state.dict.pop(self.key, None)
|
||||
|
||||
def insert_update_handler(mapper, connection, state):
|
||||
"""After an insert or update, some columns may be expired due
|
||||
to server side defaults, or re-populated due to client side
|
||||
defaults. Pop out the composite value here so that it
|
||||
recreates.
|
||||
|
||||
"""
|
||||
|
||||
state.dict.pop(self.key, None)
|
||||
|
||||
event.listen(self.parent, 'after_insert',
|
||||
insert_update_handler, raw=True)
|
||||
event.listen(self.parent, 'after_update',
|
||||
insert_update_handler, raw=True)
|
||||
event.listen(self.parent, 'load', load_handler, raw=True, propagate=True)
|
||||
event.listen(self.parent, 'refresh', load_handler, raw=True, propagate=True)
|
||||
event.listen(self.parent, "expire", expire_handler, raw=True, propagate=True)
|
||||
|
||||
# TODO: need a deserialize hook here
|
||||
|
||||
@util.memoized_property
|
||||
def _attribute_keys(self):
|
||||
return [
|
||||
prop.key for prop in self.props
|
||||
]
|
||||
|
||||
def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF):
|
||||
"""Provided for userland code that uses attributes.get_history()."""
|
||||
|
||||
added = []
|
||||
deleted = []
|
||||
|
||||
has_history = False
|
||||
for prop in self.props:
|
||||
key = prop.key
|
||||
hist = state.manager[key].impl.get_history(state, dict_)
|
||||
if hist.has_changes():
|
||||
has_history = True
|
||||
|
||||
non_deleted = hist.non_deleted()
|
||||
if non_deleted:
|
||||
added.extend(non_deleted)
|
||||
else:
|
||||
added.append(None)
|
||||
if hist.deleted:
|
||||
deleted.extend(hist.deleted)
|
||||
else:
|
||||
deleted.append(None)
|
||||
|
||||
if has_history:
|
||||
return attributes.History(
|
||||
[self.composite_class(*added)],
|
||||
(),
|
||||
[self.composite_class(*deleted)]
|
||||
)
|
||||
else:
|
||||
return attributes.History(
|
||||
(),[self.composite_class(*added)], ()
|
||||
)
|
||||
|
||||
def _comparator_factory(self, mapper):
|
||||
return self.comparator_factory(self)
|
||||
|
||||
class Comparator(PropComparator):
|
||||
def __init__(self, prop, adapter=None):
|
||||
self.prop = self.property = prop
|
||||
self.adapter = adapter
|
||||
|
||||
def __clause_element__(self):
|
||||
if self.adapter:
|
||||
# TODO: test coverage for adapted composite comparison
|
||||
return expression.ClauseList(
|
||||
*[self.adapter(x) for x in self.prop._comparable_elements])
|
||||
else:
|
||||
return expression.ClauseList(*self.prop._comparable_elements)
|
||||
|
||||
__hash__ = None
|
||||
|
||||
def __eq__(self, other):
|
||||
if other is None:
|
||||
values = [None] * len(self.prop._comparable_elements)
|
||||
else:
|
||||
values = other.__composite_values__()
|
||||
return sql.and_(
|
||||
*[a==b for a, b in zip(self.prop._comparable_elements, values)])
|
||||
|
||||
def __ne__(self, other):
|
||||
return sql.not_(self.__eq__(other))
|
||||
|
||||
def __str__(self):
|
||||
return str(self.parent.class_.__name__) + "." + self.key
|
||||
|
||||
class ConcreteInheritedProperty(DescriptorProperty):
|
||||
"""A 'do nothing' :class:`.MapperProperty` that disables
|
||||
an attribute on a concrete subclass that is only present
|
||||
on the inherited mapper, not the concrete classes' mapper.
|
||||
|
||||
Cases where this occurs include:
|
||||
|
||||
* When the superclass mapper is mapped against a
|
||||
"polymorphic union", which includes all attributes from
|
||||
all subclasses.
|
||||
* When a relationship() is configured on an inherited mapper,
|
||||
but not on the subclass mapper. Concrete mappers require
|
||||
that relationship() is configured explicitly on each
|
||||
subclass.
|
||||
|
||||
"""
|
||||
|
||||
def _comparator_factory(self, mapper):
|
||||
comparator_callable = None
|
||||
|
||||
for m in self.parent.iterate_to_root():
|
||||
p = m._props[self.key]
|
||||
if not isinstance(p, ConcreteInheritedProperty):
|
||||
comparator_callable = p.comparator_factory
|
||||
break
|
||||
return comparator_callable
|
||||
|
||||
def __init__(self):
|
||||
def warn():
|
||||
raise AttributeError("Concrete %s does not implement "
|
||||
"attribute %r at the instance level. Add this "
|
||||
"property explicitly to %s." %
|
||||
(self.parent, self.key, self.parent))
|
||||
|
||||
class NoninheritedConcreteProp(object):
|
||||
def __set__(s, obj, value):
|
||||
warn()
|
||||
def __delete__(s, obj):
|
||||
warn()
|
||||
def __get__(s, obj, owner):
|
||||
if obj is None:
|
||||
return self.descriptor
|
||||
warn()
|
||||
self.descriptor = NoninheritedConcreteProp()
|
||||
|
||||
|
||||
class SynonymProperty(DescriptorProperty):
|
||||
|
||||
def __init__(self, name, map_column=None,
|
||||
descriptor=None, comparator_factory=None,
|
||||
doc=None):
|
||||
self.name = name
|
||||
self.map_column = map_column
|
||||
self.descriptor = descriptor
|
||||
self.comparator_factory = comparator_factory
|
||||
self.doc = doc or (descriptor and descriptor.__doc__) or None
|
||||
|
||||
util.set_creation_order(self)
|
||||
|
||||
# TODO: when initialized, check _proxied_property,
|
||||
# emit a warning if its not a column-based property
|
||||
|
||||
@util.memoized_property
|
||||
def _proxied_property(self):
|
||||
return getattr(self.parent.class_, self.name).property
|
||||
|
||||
def _comparator_factory(self, mapper):
|
||||
prop = self._proxied_property
|
||||
|
||||
if self.comparator_factory:
|
||||
comp = self.comparator_factory(prop, mapper)
|
||||
else:
|
||||
comp = prop.comparator_factory(prop, mapper)
|
||||
return comp
|
||||
|
||||
def set_parent(self, parent, init):
|
||||
if self.map_column:
|
||||
# implement the 'map_column' option.
|
||||
if self.key not in parent.mapped_table.c:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Can't compile synonym '%s': no column on table "
|
||||
"'%s' named '%s'"
|
||||
% (self.name, parent.mapped_table.description, self.key))
|
||||
elif parent.mapped_table.c[self.key] in \
|
||||
parent._columntoproperty and \
|
||||
parent._columntoproperty[
|
||||
parent.mapped_table.c[self.key]
|
||||
].key == self.name:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Can't call map_column=True for synonym %r=%r, "
|
||||
"a ColumnProperty already exists keyed to the name "
|
||||
"%r for column %r" %
|
||||
(self.key, self.name, self.name, self.key)
|
||||
)
|
||||
p = properties.ColumnProperty(parent.mapped_table.c[self.key])
|
||||
parent._configure_property(
|
||||
self.name, p,
|
||||
init=init,
|
||||
setparent=True)
|
||||
p._mapped_by_synonym = self.key
|
||||
|
||||
self.parent = parent
|
||||
|
||||
class ComparableProperty(DescriptorProperty):
|
||||
"""Instruments a Python property for use in query expressions."""
|
||||
|
||||
def __init__(self, comparator_factory, descriptor=None, doc=None):
|
||||
self.descriptor = descriptor
|
||||
self.comparator_factory = comparator_factory
|
||||
self.doc = doc or (descriptor and descriptor.__doc__) or None
|
||||
util.set_creation_order(self)
|
||||
|
||||
def _comparator_factory(self, mapper):
|
||||
return self.comparator_factory(self, mapper)
|
||||
@@ -1,5 +1,5 @@
|
||||
# orm/dynamic.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -35,9 +35,6 @@ class DynaLoader(strategies.AbstractRelationshipLoader):
|
||||
query_class=self.parent_property.query_class
|
||||
)
|
||||
|
||||
def create_row_processor(self, selectcontext, path, mapper, row, adapter):
|
||||
return None, None, None
|
||||
|
||||
log.class_logger(DynaLoader)
|
||||
|
||||
class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
@@ -46,9 +43,10 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
supports_population = False
|
||||
|
||||
def __init__(self, class_, key, typecallable,
|
||||
target_mapper, order_by, query_class=None, **kwargs):
|
||||
dispatch,
|
||||
target_mapper, order_by, query_class=None, **kw):
|
||||
super(DynamicAttributeImpl, self).\
|
||||
__init__(class_, key, typecallable, **kwargs)
|
||||
__init__(class_, key, typecallable, dispatch, **kw)
|
||||
self.target_mapper = target_mapper
|
||||
self.order_by = order_by
|
||||
if not query_class:
|
||||
@@ -58,41 +56,41 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
else:
|
||||
self.query_class = mixin_user_query(query_class)
|
||||
|
||||
def get(self, state, dict_, passive=False):
|
||||
if passive:
|
||||
def get(self, state, dict_, passive=attributes.PASSIVE_OFF):
|
||||
if passive is not attributes.PASSIVE_OFF:
|
||||
return self._get_collection_history(state,
|
||||
passive=True).added_items
|
||||
attributes.PASSIVE_NO_INITIALIZE).added_items
|
||||
else:
|
||||
return self.query_class(self, state)
|
||||
|
||||
def get_collection(self, state, dict_, user_data=None, passive=True):
|
||||
if passive:
|
||||
def get_collection(self, state, dict_, user_data=None,
|
||||
passive=attributes.PASSIVE_NO_INITIALIZE):
|
||||
if passive is not attributes.PASSIVE_OFF:
|
||||
return self._get_collection_history(state,
|
||||
passive=passive).added_items
|
||||
passive).added_items
|
||||
else:
|
||||
history = self._get_collection_history(state,
|
||||
passive=passive)
|
||||
history = self._get_collection_history(state, passive)
|
||||
return history.added_items + history.unchanged_items
|
||||
|
||||
def fire_append_event(self, state, dict_, value, initiator):
|
||||
collection_history = self._modified_event(state, dict_)
|
||||
collection_history.added_items.append(value)
|
||||
|
||||
for ext in self.extensions:
|
||||
ext.append(state, value, initiator or self)
|
||||
for fn in self.dispatch.append:
|
||||
value = fn(state, value, initiator or self)
|
||||
|
||||
if self.trackparent and value is not None:
|
||||
self.sethasparent(attributes.instance_state(value), True)
|
||||
self.sethasparent(attributes.instance_state(value), state, True)
|
||||
|
||||
def fire_remove_event(self, state, dict_, value, initiator):
|
||||
collection_history = self._modified_event(state, dict_)
|
||||
collection_history.deleted_items.append(value)
|
||||
|
||||
if self.trackparent and value is not None:
|
||||
self.sethasparent(attributes.instance_state(value), False)
|
||||
self.sethasparent(attributes.instance_state(value), state, False)
|
||||
|
||||
for ext in self.extensions:
|
||||
ext.remove(state, value, initiator or self)
|
||||
for fn in self.dispatch.remove:
|
||||
fn(state, value, initiator or self)
|
||||
|
||||
def _modified_event(self, state, dict_):
|
||||
|
||||
@@ -100,23 +98,25 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
state.committed_state[self.key] = CollectionHistory(self, state)
|
||||
|
||||
state.modified_event(dict_,
|
||||
self,
|
||||
False,
|
||||
attributes.NEVER_SET,
|
||||
passive=attributes.PASSIVE_NO_INITIALIZE)
|
||||
self,
|
||||
attributes.NEVER_SET)
|
||||
|
||||
# this is a hack to allow the _base.ComparableEntity fixture
|
||||
# this is a hack to allow the fixtures.ComparableEntity fixture
|
||||
# to work
|
||||
dict_[self.key] = True
|
||||
return state.committed_state[self.key]
|
||||
|
||||
def set(self, state, dict_, value, initiator,
|
||||
passive=attributes.PASSIVE_OFF):
|
||||
passive=attributes.PASSIVE_OFF,
|
||||
check_old=None, pop=False):
|
||||
if initiator and initiator.parent_token is self.parent_token:
|
||||
return
|
||||
|
||||
if pop and value is None:
|
||||
return
|
||||
self._set_iterable(state, dict_, value)
|
||||
|
||||
|
||||
def _set_iterable(self, state, dict_, iterable, adapter=None):
|
||||
collection_history = self._modified_event(state, dict_)
|
||||
new_values = list(iterable)
|
||||
@@ -136,27 +136,37 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
raise NotImplementedError("Dynamic attributes don't support "
|
||||
"collection population.")
|
||||
|
||||
def get_history(self, state, dict_, passive=False):
|
||||
def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF):
|
||||
c = self._get_collection_history(state, passive)
|
||||
return attributes.History(c.added_items, c.unchanged_items,
|
||||
c.deleted_items)
|
||||
|
||||
def _get_collection_history(self, state, passive=False):
|
||||
def get_all_pending(self, state, dict_):
|
||||
c = self._get_collection_history(state, True)
|
||||
return [
|
||||
(attributes.instance_state(x), x)
|
||||
for x in
|
||||
c.added_items + c.unchanged_items + c.deleted_items
|
||||
]
|
||||
|
||||
def _get_collection_history(self, state, passive=attributes.PASSIVE_OFF):
|
||||
if self.key in state.committed_state:
|
||||
c = state.committed_state[self.key]
|
||||
else:
|
||||
c = CollectionHistory(self, state)
|
||||
|
||||
if not passive:
|
||||
if passive is attributes.PASSIVE_OFF:
|
||||
return CollectionHistory(self, state, apply_to=c)
|
||||
else:
|
||||
return c
|
||||
|
||||
def append(self, state, dict_, value, initiator, passive=False):
|
||||
def append(self, state, dict_, value, initiator,
|
||||
passive=attributes.PASSIVE_OFF):
|
||||
if initiator is not self:
|
||||
self.fire_append_event(state, dict_, value, initiator)
|
||||
|
||||
def remove(self, state, dict_, value, initiator, passive=False):
|
||||
def remove(self, state, dict_, value, initiator,
|
||||
passive=attributes.PASSIVE_OFF):
|
||||
if initiator is not self:
|
||||
self.fire_remove_event(state, dict_, value, initiator)
|
||||
|
||||
@@ -192,7 +202,7 @@ class AppenderMixin(object):
|
||||
self.attr = attr
|
||||
|
||||
mapper = object_mapper(instance)
|
||||
prop = mapper.get_property(self.attr.key, resolve_synonyms=True)
|
||||
prop = mapper._props[self.attr.key]
|
||||
self._criterion = prop.compare(
|
||||
operators.eq,
|
||||
instance,
|
||||
@@ -221,7 +231,7 @@ class AppenderMixin(object):
|
||||
if sess is None:
|
||||
return iter(self.attr._get_collection_history(
|
||||
attributes.instance_state(self.instance),
|
||||
passive=True).added_items)
|
||||
attributes.PASSIVE_NO_INITIALIZE).added_items)
|
||||
else:
|
||||
return iter(self._clone(sess))
|
||||
|
||||
@@ -230,7 +240,8 @@ class AppenderMixin(object):
|
||||
if sess is None:
|
||||
return self.attr._get_collection_history(
|
||||
attributes.instance_state(self.instance),
|
||||
passive=True).added_items.__getitem__(index)
|
||||
attributes.PASSIVE_NO_INITIALIZE).added_items.\
|
||||
__getitem__(index)
|
||||
else:
|
||||
return self._clone(sess).__getitem__(index)
|
||||
|
||||
@@ -239,7 +250,7 @@ class AppenderMixin(object):
|
||||
if sess is None:
|
||||
return len(self.attr._get_collection_history(
|
||||
attributes.instance_state(self.instance),
|
||||
passive=True).added_items)
|
||||
attributes.PASSIVE_NO_INITIALIZE).added_items)
|
||||
else:
|
||||
return self._clone(sess).count()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# orm/evaluator.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
# orm/exc.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
@@ -7,7 +7,7 @@
|
||||
"""SQLAlchemy ORM exceptions."""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
orm_util = sa.util.importlater('sqlalchemy.orm', 'util')
|
||||
|
||||
NO_STATE = (AttributeError, KeyError)
|
||||
"""Exception types that may be raised by instrumentation implementations."""
|
||||
@@ -15,7 +15,7 @@ NO_STATE = (AttributeError, KeyError)
|
||||
class StaleDataError(sa.exc.SQLAlchemyError):
|
||||
"""An operation encountered database state that is unaccounted for.
|
||||
|
||||
Two conditions cause this to happen:
|
||||
Conditions which cause this to happen include:
|
||||
|
||||
* A flush may have attempted to update or delete rows
|
||||
and an unexpected number of rows were matched during
|
||||
@@ -27,6 +27,12 @@ class StaleDataError(sa.exc.SQLAlchemyError):
|
||||
* A mapped object with version_id_col was refreshed,
|
||||
and the version number coming back from the database does
|
||||
not match that of the object itself.
|
||||
|
||||
* A object is detached from its parent object, however
|
||||
the object was previously attached to a different parent
|
||||
identity which was garbage collected, and a decision
|
||||
cannot be made if the new parent was really the most
|
||||
recent "parent" (new in 0.7.4).
|
||||
|
||||
"""
|
||||
|
||||
@@ -40,6 +46,9 @@ class FlushError(sa.exc.SQLAlchemyError):
|
||||
class UnmappedError(sa.exc.InvalidRequestError):
|
||||
"""Base for exceptions that involve expected mappings not present."""
|
||||
|
||||
class ObjectDereferencedError(sa.exc.SQLAlchemyError):
|
||||
"""An operation cannot complete due to an object being garbage collected."""
|
||||
|
||||
class DetachedInstanceError(sa.exc.SQLAlchemyError):
|
||||
"""An attempt to access unloaded attributes on a
|
||||
mapped instance that is detached."""
|
||||
@@ -63,6 +72,8 @@ class UnmappedInstanceError(UnmappedError):
|
||||
'required?' % _safe_cls_name(obj))
|
||||
UnmappedError.__init__(self, msg)
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (None, self.args[0])
|
||||
|
||||
class UnmappedClassError(UnmappedError):
|
||||
"""An mapping operation was requested for an unknown class."""
|
||||
@@ -72,10 +83,37 @@ class UnmappedClassError(UnmappedError):
|
||||
msg = _default_unmapped(cls)
|
||||
UnmappedError.__init__(self, msg)
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (None, self.args[0])
|
||||
|
||||
class ObjectDeletedError(sa.exc.InvalidRequestError):
|
||||
"""An refresh() operation failed to re-retrieve an object's row."""
|
||||
"""A refresh operation failed to retrieve the database
|
||||
row corresponding to an object's known primary key identity.
|
||||
|
||||
A refresh operation proceeds when an expired attribute is
|
||||
accessed on an object, or when :meth:`.Query.get` is
|
||||
used to retrieve an object which is, upon retrieval, detected
|
||||
as expired. A SELECT is emitted for the target row
|
||||
based on primary key; if no row is returned, this
|
||||
exception is raised.
|
||||
|
||||
The true meaning of this exception is simply that
|
||||
no row exists for the primary key identifier associated
|
||||
with a persistent object. The row may have been
|
||||
deleted, or in some cases the primary key updated
|
||||
to a new value, outside of the ORM's management of the target
|
||||
object.
|
||||
|
||||
"""
|
||||
def __init__(self, state, msg=None):
|
||||
if not msg:
|
||||
msg = "Instance '%s' has been deleted, or its "\
|
||||
"row is otherwise not present." % orm_util.state_str(state)
|
||||
|
||||
sa.exc.InvalidRequestError.__init__(self, msg)
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (None, self.args[0])
|
||||
|
||||
class UnmappedColumnError(sa.exc.InvalidRequestError):
|
||||
"""Mapping operation was requested on an unknown column."""
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
# orm/identity.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
import weakref
|
||||
|
||||
from sqlalchemy import util as base_util
|
||||
from sqlalchemy.orm import attributes
|
||||
|
||||
|
||||
@@ -22,9 +20,6 @@ class IdentityMap(dict):
|
||||
def add(self, state):
|
||||
raise NotImplementedError()
|
||||
|
||||
def remove(self, state):
|
||||
raise NotImplementedError()
|
||||
|
||||
def update(self, dict):
|
||||
raise NotImplementedError("IdentityMap uses add() to insert data")
|
||||
|
||||
@@ -83,7 +78,6 @@ class IdentityMap(dict):
|
||||
class WeakInstanceDict(IdentityMap):
|
||||
def __init__(self):
|
||||
IdentityMap.__init__(self)
|
||||
self._remove_mutex = base_util.threading.Lock()
|
||||
|
||||
def __getitem__(self, key):
|
||||
state = dict.__getitem__(self, key)
|
||||
@@ -123,33 +117,25 @@ class WeakInstanceDict(IdentityMap):
|
||||
self._manage_incoming_state(state)
|
||||
|
||||
def add(self, state):
|
||||
if state.key in self:
|
||||
if dict.__getitem__(self, state.key) is not state:
|
||||
raise AssertionError("A conflicting state is already "
|
||||
"present in the identity map for key %r"
|
||||
% (state.key, ))
|
||||
else:
|
||||
dict.__setitem__(self, state.key, state)
|
||||
self._manage_incoming_state(state)
|
||||
|
||||
def remove_key(self, key):
|
||||
state = dict.__getitem__(self, key)
|
||||
self.remove(state)
|
||||
|
||||
def remove(self, state):
|
||||
self._remove_mutex.acquire()
|
||||
try:
|
||||
if dict.pop(self, state.key) is not state:
|
||||
raise AssertionError("State %s is not present in this identity map" % state)
|
||||
finally:
|
||||
self._remove_mutex.release()
|
||||
|
||||
self._manage_removed_state(state)
|
||||
|
||||
def discard(self, state):
|
||||
if self.contains_state(state):
|
||||
dict.__delitem__(self, state.key)
|
||||
self._manage_removed_state(state)
|
||||
key = state.key
|
||||
# inline of self.__contains__
|
||||
if dict.__contains__(self, key):
|
||||
try:
|
||||
existing_state = dict.__getitem__(self, key)
|
||||
if existing_state is not state:
|
||||
o = existing_state.obj()
|
||||
if o is None:
|
||||
o = existing_state._is_really_none()
|
||||
if o is not None:
|
||||
raise AssertionError("A conflicting state is already "
|
||||
"present in the identity map for key %r"
|
||||
% (key, ))
|
||||
else:
|
||||
return
|
||||
except KeyError:
|
||||
pass
|
||||
dict.__setitem__(self, key, state)
|
||||
self._manage_incoming_state(state)
|
||||
|
||||
def get(self, key, default=None):
|
||||
state = dict.get(self, key, default)
|
||||
@@ -158,58 +144,57 @@ class WeakInstanceDict(IdentityMap):
|
||||
o = state.obj()
|
||||
if o is None:
|
||||
o = state._is_really_none()
|
||||
if o is None:
|
||||
return default
|
||||
if o is None:
|
||||
return default
|
||||
return o
|
||||
|
||||
def _items(self):
|
||||
values = self.all_states()
|
||||
result = []
|
||||
for state in values:
|
||||
value = state.obj()
|
||||
if value is not None:
|
||||
result.append((state.key, value))
|
||||
return result
|
||||
|
||||
def items(self):
|
||||
def _values(self):
|
||||
values = self.all_states()
|
||||
result = []
|
||||
for state in values:
|
||||
value = state.obj()
|
||||
if value is not None:
|
||||
result.append(value)
|
||||
|
||||
return result
|
||||
|
||||
# Py3K
|
||||
#def items(self):
|
||||
# return iter(self._items())
|
||||
#
|
||||
#def values(self):
|
||||
# return iter(self._values())
|
||||
# Py2K
|
||||
return list(self.iteritems())
|
||||
|
||||
items = _items
|
||||
def iteritems(self):
|
||||
# end Py2K
|
||||
self._remove_mutex.acquire()
|
||||
try:
|
||||
result = []
|
||||
for state in dict.values(self):
|
||||
value = state.obj()
|
||||
if value is not None:
|
||||
result.append((state.key, value))
|
||||
|
||||
return iter(result)
|
||||
finally:
|
||||
self._remove_mutex.release()
|
||||
|
||||
def values(self):
|
||||
# Py2K
|
||||
return list(self.itervalues())
|
||||
return iter(self.items())
|
||||
|
||||
values = _values
|
||||
def itervalues(self):
|
||||
return iter(self.values())
|
||||
# end Py2K
|
||||
self._remove_mutex.acquire()
|
||||
try:
|
||||
result = []
|
||||
for state in dict.values(self):
|
||||
value = state.obj()
|
||||
if value is not None:
|
||||
result.append(value)
|
||||
|
||||
return iter(result)
|
||||
finally:
|
||||
self._remove_mutex.release()
|
||||
|
||||
def all_states(self):
|
||||
self._remove_mutex.acquire()
|
||||
try:
|
||||
# Py3K
|
||||
# return list(dict.values(self))
|
||||
# Py3K
|
||||
# return list(dict.values(self))
|
||||
# Py2K
|
||||
return dict.values(self)
|
||||
# end Py2K
|
||||
|
||||
# Py2K
|
||||
return dict.values(self)
|
||||
# end Py2K
|
||||
finally:
|
||||
self._remove_mutex.release()
|
||||
def discard(self, state):
|
||||
st = dict.get(self, state.key, None)
|
||||
if st is state:
|
||||
dict.pop(self, state.key, None)
|
||||
self._manage_removed_state(state)
|
||||
|
||||
def prune(self):
|
||||
return 0
|
||||
@@ -235,25 +220,22 @@ class StrongInstanceDict(IdentityMap):
|
||||
|
||||
def add(self, state):
|
||||
if state.key in self:
|
||||
if attributes.instance_state(dict.__getitem__(self, state.key)) is not state:
|
||||
raise AssertionError("A conflicting state is already present in the identity map for key %r" % (state.key, ))
|
||||
if attributes.instance_state(dict.__getitem__(self,
|
||||
state.key)) is not state:
|
||||
raise AssertionError('A conflicting state is already '
|
||||
'present in the identity map for key %r'
|
||||
% (state.key, ))
|
||||
else:
|
||||
dict.__setitem__(self, state.key, state.obj())
|
||||
self._manage_incoming_state(state)
|
||||
|
||||
def remove(self, state):
|
||||
if attributes.instance_state(dict.pop(self, state.key)) is not state:
|
||||
raise AssertionError("State %s is not present in this identity map" % state)
|
||||
self._manage_removed_state(state)
|
||||
|
||||
def discard(self, state):
|
||||
if self.contains_state(state):
|
||||
dict.__delitem__(self, state.key)
|
||||
self._manage_removed_state(state)
|
||||
|
||||
def remove_key(self, key):
|
||||
state = attributes.instance_state(dict.__getitem__(self, key))
|
||||
self.remove(state)
|
||||
obj = dict.get(self, state.key, None)
|
||||
if obj is not None:
|
||||
st = attributes.instance_state(obj)
|
||||
if st is state:
|
||||
dict.pop(self, state.key, None)
|
||||
self._manage_removed_state(state)
|
||||
|
||||
def prune(self):
|
||||
"""prune unreferenced, non-dirty states."""
|
||||
|
||||
@@ -0,0 +1,674 @@
|
||||
# orm/instrumentation.py
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
"""Defines SQLAlchemy's system of class instrumentation.
|
||||
|
||||
This module is usually not directly visible to user applications, but
|
||||
defines a large part of the ORM's interactivity.
|
||||
|
||||
instrumentation.py deals with registration of end-user classes
|
||||
for state tracking. It interacts closely with state.py
|
||||
and attributes.py which establish per-instance and per-class-attribute
|
||||
instrumentation, respectively.
|
||||
|
||||
SQLA's instrumentation system is completely customizable, in which
|
||||
case an understanding of the general mechanics of this module is helpful.
|
||||
An example of full customization is in /examples/custom_attributes.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from sqlalchemy.orm import exc, collections, events
|
||||
from operator import attrgetter, itemgetter
|
||||
from sqlalchemy import event, util
|
||||
import weakref
|
||||
from sqlalchemy.orm import state, attributes
|
||||
|
||||
|
||||
INSTRUMENTATION_MANAGER = '__sa_instrumentation_manager__'
|
||||
"""Attribute, elects custom instrumentation when present on a mapped class.
|
||||
|
||||
Allows a class to specify a slightly or wildly different technique for
|
||||
tracking changes made to mapped attributes and collections.
|
||||
|
||||
Only one instrumentation implementation is allowed in a given object
|
||||
inheritance hierarchy.
|
||||
|
||||
The value of this attribute must be a callable and will be passed a class
|
||||
object. The callable must return one of:
|
||||
|
||||
- An instance of an interfaces.InstrumentationManager or subclass
|
||||
- An object implementing all or some of InstrumentationManager (TODO)
|
||||
- A dictionary of callables, implementing all or some of the above (TODO)
|
||||
- An instance of a ClassManager or subclass
|
||||
|
||||
interfaces.InstrumentationManager is public API and will remain stable
|
||||
between releases. ClassManager is not public and no guarantees are made
|
||||
about stability. Caveat emptor.
|
||||
|
||||
This attribute is consulted by the default SQLAlchemy instrumentation
|
||||
resolution code. If custom finders are installed in the global
|
||||
instrumentation_finders list, they may or may not choose to honor this
|
||||
attribute.
|
||||
|
||||
"""
|
||||
|
||||
instrumentation_finders = []
|
||||
"""An extensible sequence of instrumentation implementation finding callables.
|
||||
|
||||
Finders callables will be passed a class object. If None is returned, the
|
||||
next finder in the sequence is consulted. Otherwise the return must be an
|
||||
instrumentation factory that follows the same guidelines as
|
||||
INSTRUMENTATION_MANAGER.
|
||||
|
||||
By default, the only finder is find_native_user_instrumentation_hook, which
|
||||
searches for INSTRUMENTATION_MANAGER. If all finders return None, standard
|
||||
ClassManager instrumentation is used.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ClassManager(dict):
|
||||
"""tracks state information at the class level."""
|
||||
|
||||
MANAGER_ATTR = '_sa_class_manager'
|
||||
STATE_ATTR = '_sa_instance_state'
|
||||
|
||||
deferred_scalar_loader = None
|
||||
|
||||
original_init = object.__init__
|
||||
|
||||
def __init__(self, class_):
|
||||
self.class_ = class_
|
||||
self.factory = None # where we came from, for inheritance bookkeeping
|
||||
self.info = {}
|
||||
self.new_init = None
|
||||
self.mutable_attributes = set()
|
||||
self.local_attrs = {}
|
||||
self.originals = {}
|
||||
|
||||
self._bases = [mgr for mgr in [
|
||||
manager_of_class(base)
|
||||
for base in self.class_.__bases__
|
||||
if isinstance(base, type)
|
||||
] if mgr is not None]
|
||||
|
||||
for base in self._bases:
|
||||
self.update(base)
|
||||
|
||||
self.manage()
|
||||
self._instrument_init()
|
||||
|
||||
dispatch = event.dispatcher(events.InstanceEvents)
|
||||
|
||||
@property
|
||||
def is_mapped(self):
|
||||
return 'mapper' in self.__dict__
|
||||
|
||||
@util.memoized_property
|
||||
def mapper(self):
|
||||
# raises unless self.mapper has been assigned
|
||||
raise exc.UnmappedClassError(self.class_)
|
||||
|
||||
def _attr_has_impl(self, key):
|
||||
"""Return True if the given attribute is fully initialized.
|
||||
|
||||
i.e. has an impl.
|
||||
"""
|
||||
|
||||
return key in self and self[key].impl is not None
|
||||
|
||||
def _subclass_manager(self, cls):
|
||||
"""Create a new ClassManager for a subclass of this ClassManager's
|
||||
class.
|
||||
|
||||
This is called automatically when attributes are instrumented so that
|
||||
the attributes can be propagated to subclasses against their own
|
||||
class-local manager, without the need for mappers etc. to have already
|
||||
pre-configured managers for the full class hierarchy. Mappers
|
||||
can post-configure the auto-generated ClassManager when needed.
|
||||
|
||||
"""
|
||||
manager = manager_of_class(cls)
|
||||
if manager is None:
|
||||
manager = _create_manager_for_cls(cls, _source=self)
|
||||
return manager
|
||||
|
||||
def _instrument_init(self):
|
||||
# TODO: self.class_.__init__ is often the already-instrumented
|
||||
# __init__ from an instrumented superclass. We still need to make
|
||||
# our own wrapper, but it would
|
||||
# be nice to wrap the original __init__ and not our existing wrapper
|
||||
# of such, since this adds method overhead.
|
||||
self.original_init = self.class_.__init__
|
||||
self.new_init = _generate_init(self.class_, self)
|
||||
self.install_member('__init__', self.new_init)
|
||||
|
||||
def _uninstrument_init(self):
|
||||
if self.new_init:
|
||||
self.uninstall_member('__init__')
|
||||
self.new_init = None
|
||||
|
||||
@util.memoized_property
|
||||
def _state_constructor(self):
|
||||
self.dispatch.first_init(self, self.class_)
|
||||
if self.mutable_attributes:
|
||||
return state.MutableAttrInstanceState
|
||||
else:
|
||||
return state.InstanceState
|
||||
|
||||
def manage(self):
|
||||
"""Mark this instance as the manager for its class."""
|
||||
|
||||
setattr(self.class_, self.MANAGER_ATTR, self)
|
||||
|
||||
def dispose(self):
|
||||
"""Dissasociate this manager from its class."""
|
||||
|
||||
delattr(self.class_, self.MANAGER_ATTR)
|
||||
|
||||
def manager_getter(self):
|
||||
return attrgetter(self.MANAGER_ATTR)
|
||||
|
||||
def instrument_attribute(self, key, inst, propagated=False):
|
||||
if propagated:
|
||||
if key in self.local_attrs:
|
||||
return # don't override local attr with inherited attr
|
||||
else:
|
||||
self.local_attrs[key] = inst
|
||||
self.install_descriptor(key, inst)
|
||||
self[key] = inst
|
||||
|
||||
for cls in self.class_.__subclasses__():
|
||||
manager = self._subclass_manager(cls)
|
||||
manager.instrument_attribute(key, inst, True)
|
||||
|
||||
def subclass_managers(self, recursive):
|
||||
for cls in self.class_.__subclasses__():
|
||||
mgr = manager_of_class(cls)
|
||||
if mgr is not None and mgr is not self:
|
||||
yield mgr
|
||||
if recursive:
|
||||
for m in mgr.subclass_managers(True):
|
||||
yield m
|
||||
|
||||
def post_configure_attribute(self, key):
|
||||
instrumentation_registry.dispatch.\
|
||||
attribute_instrument(self.class_, key, self[key])
|
||||
|
||||
def uninstrument_attribute(self, key, propagated=False):
|
||||
if key not in self:
|
||||
return
|
||||
if propagated:
|
||||
if key in self.local_attrs:
|
||||
return # don't get rid of local attr
|
||||
else:
|
||||
del self.local_attrs[key]
|
||||
self.uninstall_descriptor(key)
|
||||
del self[key]
|
||||
if key in self.mutable_attributes:
|
||||
self.mutable_attributes.remove(key)
|
||||
for cls in self.class_.__subclasses__():
|
||||
manager = manager_of_class(cls)
|
||||
if manager:
|
||||
manager.uninstrument_attribute(key, True)
|
||||
|
||||
def unregister(self):
|
||||
"""remove all instrumentation established by this ClassManager."""
|
||||
|
||||
self._uninstrument_init()
|
||||
|
||||
self.mapper = self.dispatch = None
|
||||
self.info.clear()
|
||||
|
||||
for key in list(self):
|
||||
if key in self.local_attrs:
|
||||
self.uninstrument_attribute(key)
|
||||
|
||||
def install_descriptor(self, key, inst):
|
||||
if key in (self.STATE_ATTR, self.MANAGER_ATTR):
|
||||
raise KeyError("%r: requested attribute name conflicts with "
|
||||
"instrumentation attribute of the same name." %
|
||||
key)
|
||||
setattr(self.class_, key, inst)
|
||||
|
||||
def uninstall_descriptor(self, key):
|
||||
delattr(self.class_, key)
|
||||
|
||||
def install_member(self, key, implementation):
|
||||
if key in (self.STATE_ATTR, self.MANAGER_ATTR):
|
||||
raise KeyError("%r: requested attribute name conflicts with "
|
||||
"instrumentation attribute of the same name." %
|
||||
key)
|
||||
self.originals.setdefault(key, getattr(self.class_, key, None))
|
||||
setattr(self.class_, key, implementation)
|
||||
|
||||
def uninstall_member(self, key):
|
||||
original = self.originals.pop(key, None)
|
||||
if original is not None:
|
||||
setattr(self.class_, key, original)
|
||||
|
||||
def instrument_collection_class(self, key, collection_class):
|
||||
return collections.prepare_instrumentation(collection_class)
|
||||
|
||||
def initialize_collection(self, key, state, factory):
|
||||
user_data = factory()
|
||||
adapter = collections.CollectionAdapter(
|
||||
self.get_impl(key), state, user_data)
|
||||
return adapter, user_data
|
||||
|
||||
def is_instrumented(self, key, search=False):
|
||||
if search:
|
||||
return key in self
|
||||
else:
|
||||
return key in self.local_attrs
|
||||
|
||||
def get_impl(self, key):
|
||||
return self[key].impl
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
return self.itervalues()
|
||||
|
||||
## InstanceState management
|
||||
|
||||
def new_instance(self, state=None):
|
||||
instance = self.class_.__new__(self.class_)
|
||||
setattr(instance, self.STATE_ATTR,
|
||||
state or self._state_constructor(instance, self))
|
||||
return instance
|
||||
|
||||
def setup_instance(self, instance, state=None):
|
||||
setattr(instance, self.STATE_ATTR,
|
||||
state or self._state_constructor(instance, self))
|
||||
|
||||
def teardown_instance(self, instance):
|
||||
delattr(instance, self.STATE_ATTR)
|
||||
|
||||
def _new_state_if_none(self, instance):
|
||||
"""Install a default InstanceState if none is present.
|
||||
|
||||
A private convenience method used by the __init__ decorator.
|
||||
|
||||
"""
|
||||
if hasattr(instance, self.STATE_ATTR):
|
||||
return False
|
||||
elif self.class_ is not instance.__class__ and \
|
||||
self.is_mapped:
|
||||
# this will create a new ClassManager for the
|
||||
# subclass, without a mapper. This is likely a
|
||||
# user error situation but allow the object
|
||||
# to be constructed, so that it is usable
|
||||
# in a non-ORM context at least.
|
||||
return self._subclass_manager(instance.__class__).\
|
||||
_new_state_if_none(instance)
|
||||
else:
|
||||
state = self._state_constructor(instance, self)
|
||||
setattr(instance, self.STATE_ATTR, state)
|
||||
return state
|
||||
|
||||
def state_getter(self):
|
||||
"""Return a (instance) -> InstanceState callable.
|
||||
|
||||
"state getter" callables should raise either KeyError or
|
||||
AttributeError if no InstanceState could be found for the
|
||||
instance.
|
||||
"""
|
||||
|
||||
return attrgetter(self.STATE_ATTR)
|
||||
|
||||
def dict_getter(self):
|
||||
return attrgetter('__dict__')
|
||||
|
||||
def has_state(self, instance):
|
||||
return hasattr(instance, self.STATE_ATTR)
|
||||
|
||||
def has_parent(self, state, key, optimistic=False):
|
||||
"""TODO"""
|
||||
return self.get_impl(key).hasparent(state, optimistic=optimistic)
|
||||
|
||||
def __nonzero__(self):
|
||||
"""All ClassManagers are non-zero regardless of attribute state."""
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s of %r at %x>' % (
|
||||
self.__class__.__name__, self.class_, id(self))
|
||||
|
||||
class _ClassInstrumentationAdapter(ClassManager):
|
||||
"""Adapts a user-defined InstrumentationManager to a ClassManager."""
|
||||
|
||||
def __init__(self, class_, override, **kw):
|
||||
self._adapted = override
|
||||
self._get_state = self._adapted.state_getter(class_)
|
||||
self._get_dict = self._adapted.dict_getter(class_)
|
||||
|
||||
ClassManager.__init__(self, class_, **kw)
|
||||
|
||||
def manage(self):
|
||||
self._adapted.manage(self.class_, self)
|
||||
|
||||
def dispose(self):
|
||||
self._adapted.dispose(self.class_)
|
||||
|
||||
def manager_getter(self):
|
||||
return self._adapted.manager_getter(self.class_)
|
||||
|
||||
def instrument_attribute(self, key, inst, propagated=False):
|
||||
ClassManager.instrument_attribute(self, key, inst, propagated)
|
||||
if not propagated:
|
||||
self._adapted.instrument_attribute(self.class_, key, inst)
|
||||
|
||||
def post_configure_attribute(self, key):
|
||||
super(_ClassInstrumentationAdapter, self).post_configure_attribute(key)
|
||||
self._adapted.post_configure_attribute(self.class_, key, self[key])
|
||||
|
||||
def install_descriptor(self, key, inst):
|
||||
self._adapted.install_descriptor(self.class_, key, inst)
|
||||
|
||||
def uninstall_descriptor(self, key):
|
||||
self._adapted.uninstall_descriptor(self.class_, key)
|
||||
|
||||
def install_member(self, key, implementation):
|
||||
self._adapted.install_member(self.class_, key, implementation)
|
||||
|
||||
def uninstall_member(self, key):
|
||||
self._adapted.uninstall_member(self.class_, key)
|
||||
|
||||
def instrument_collection_class(self, key, collection_class):
|
||||
return self._adapted.instrument_collection_class(
|
||||
self.class_, key, collection_class)
|
||||
|
||||
def initialize_collection(self, key, state, factory):
|
||||
delegate = getattr(self._adapted, 'initialize_collection', None)
|
||||
if delegate:
|
||||
return delegate(key, state, factory)
|
||||
else:
|
||||
return ClassManager.initialize_collection(self, key,
|
||||
state, factory)
|
||||
|
||||
def new_instance(self, state=None):
|
||||
instance = self.class_.__new__(self.class_)
|
||||
self.setup_instance(instance, state)
|
||||
return instance
|
||||
|
||||
def _new_state_if_none(self, instance):
|
||||
"""Install a default InstanceState if none is present.
|
||||
|
||||
A private convenience method used by the __init__ decorator.
|
||||
"""
|
||||
if self.has_state(instance):
|
||||
return False
|
||||
else:
|
||||
return self.setup_instance(instance)
|
||||
|
||||
def setup_instance(self, instance, state=None):
|
||||
self._adapted.initialize_instance_dict(self.class_, instance)
|
||||
|
||||
if state is None:
|
||||
state = self._state_constructor(instance, self)
|
||||
|
||||
# the given instance is assumed to have no state
|
||||
self._adapted.install_state(self.class_, instance, state)
|
||||
return state
|
||||
|
||||
def teardown_instance(self, instance):
|
||||
self._adapted.remove_state(self.class_, instance)
|
||||
|
||||
def has_state(self, instance):
|
||||
try:
|
||||
state = self._get_state(instance)
|
||||
except exc.NO_STATE:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def state_getter(self):
|
||||
return self._get_state
|
||||
|
||||
def dict_getter(self):
|
||||
return self._get_dict
|
||||
|
||||
def register_class(class_, **kw):
|
||||
"""Register class instrumentation.
|
||||
|
||||
Returns the existing or newly created class manager.
|
||||
"""
|
||||
|
||||
manager = manager_of_class(class_)
|
||||
if manager is None:
|
||||
manager = _create_manager_for_cls(class_, **kw)
|
||||
return manager
|
||||
|
||||
def unregister_class(class_):
|
||||
"""Unregister class instrumentation."""
|
||||
|
||||
instrumentation_registry.unregister(class_)
|
||||
|
||||
|
||||
def is_instrumented(instance, key):
|
||||
"""Return True if the given attribute on the given instance is
|
||||
instrumented by the attributes package.
|
||||
|
||||
This function may be used regardless of instrumentation
|
||||
applied directly to the class, i.e. no descriptors are required.
|
||||
|
||||
"""
|
||||
return manager_of_class(instance.__class__).\
|
||||
is_instrumented(key, search=True)
|
||||
|
||||
class InstrumentationRegistry(object):
|
||||
"""Private instrumentation registration singleton.
|
||||
|
||||
All classes are routed through this registry
|
||||
when first instrumented, however the InstrumentationRegistry
|
||||
is not actually needed unless custom ClassManagers are in use.
|
||||
|
||||
"""
|
||||
|
||||
_manager_finders = weakref.WeakKeyDictionary()
|
||||
_state_finders = util.WeakIdentityMapping()
|
||||
_dict_finders = util.WeakIdentityMapping()
|
||||
_extended = False
|
||||
|
||||
dispatch = event.dispatcher(events.InstrumentationEvents)
|
||||
|
||||
def create_manager_for_cls(self, class_, **kw):
|
||||
assert class_ is not None
|
||||
assert manager_of_class(class_) is None
|
||||
|
||||
for finder in instrumentation_finders:
|
||||
factory = finder(class_)
|
||||
if factory is not None:
|
||||
break
|
||||
else:
|
||||
factory = ClassManager
|
||||
|
||||
existing_factories = self._collect_management_factories_for(class_).\
|
||||
difference([factory])
|
||||
if existing_factories:
|
||||
raise TypeError(
|
||||
"multiple instrumentation implementations specified "
|
||||
"in %s inheritance hierarchy: %r" % (
|
||||
class_.__name__, list(existing_factories)))
|
||||
|
||||
manager = factory(class_)
|
||||
if not isinstance(manager, ClassManager):
|
||||
manager = _ClassInstrumentationAdapter(class_, manager)
|
||||
|
||||
if factory != ClassManager and not self._extended:
|
||||
# somebody invoked a custom ClassManager.
|
||||
# reinstall global "getter" functions with the more
|
||||
# expensive ones.
|
||||
self._extended = True
|
||||
_install_lookup_strategy(self)
|
||||
|
||||
manager.factory = factory
|
||||
self._manager_finders[class_] = manager.manager_getter()
|
||||
self._state_finders[class_] = manager.state_getter()
|
||||
self._dict_finders[class_] = manager.dict_getter()
|
||||
|
||||
self.dispatch.class_instrument(class_)
|
||||
|
||||
return manager
|
||||
|
||||
def _collect_management_factories_for(self, cls):
|
||||
"""Return a collection of factories in play or specified for a
|
||||
hierarchy.
|
||||
|
||||
Traverses the entire inheritance graph of a cls and returns a
|
||||
collection of instrumentation factories for those classes. Factories
|
||||
are extracted from active ClassManagers, if available, otherwise
|
||||
instrumentation_finders is consulted.
|
||||
|
||||
"""
|
||||
hierarchy = util.class_hierarchy(cls)
|
||||
factories = set()
|
||||
for member in hierarchy:
|
||||
manager = manager_of_class(member)
|
||||
if manager is not None:
|
||||
factories.add(manager.factory)
|
||||
else:
|
||||
for finder in instrumentation_finders:
|
||||
factory = finder(member)
|
||||
if factory is not None:
|
||||
break
|
||||
else:
|
||||
factory = None
|
||||
factories.add(factory)
|
||||
factories.discard(None)
|
||||
return factories
|
||||
|
||||
def manager_of_class(self, cls):
|
||||
# this is only called when alternate instrumentation
|
||||
# has been established
|
||||
if cls is None:
|
||||
return None
|
||||
try:
|
||||
finder = self._manager_finders[cls]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return finder(cls)
|
||||
|
||||
def state_of(self, instance):
|
||||
# this is only called when alternate instrumentation
|
||||
# has been established
|
||||
if instance is None:
|
||||
raise AttributeError("None has no persistent state.")
|
||||
try:
|
||||
return self._state_finders[instance.__class__](instance)
|
||||
except KeyError:
|
||||
raise AttributeError("%r is not instrumented" %
|
||||
instance.__class__)
|
||||
|
||||
def dict_of(self, instance):
|
||||
# this is only called when alternate instrumentation
|
||||
# has been established
|
||||
if instance is None:
|
||||
raise AttributeError("None has no persistent state.")
|
||||
try:
|
||||
return self._dict_finders[instance.__class__](instance)
|
||||
except KeyError:
|
||||
raise AttributeError("%r is not instrumented" %
|
||||
instance.__class__)
|
||||
|
||||
def unregister(self, class_):
|
||||
if class_ in self._manager_finders:
|
||||
manager = self.manager_of_class(class_)
|
||||
self.dispatch.class_uninstrument(class_)
|
||||
manager.unregister()
|
||||
manager.dispose()
|
||||
del self._manager_finders[class_]
|
||||
del self._state_finders[class_]
|
||||
del self._dict_finders[class_]
|
||||
if ClassManager.MANAGER_ATTR in class_.__dict__:
|
||||
delattr(class_, ClassManager.MANAGER_ATTR)
|
||||
|
||||
instrumentation_registry = InstrumentationRegistry()
|
||||
|
||||
|
||||
def _install_lookup_strategy(implementation):
|
||||
"""Replace global class/object management functions
|
||||
with either faster or more comprehensive implementations,
|
||||
based on whether or not extended class instrumentation
|
||||
has been detected.
|
||||
|
||||
This function is called only by InstrumentationRegistry()
|
||||
and unit tests specific to this behavior.
|
||||
|
||||
"""
|
||||
global instance_state, instance_dict, manager_of_class
|
||||
if implementation is util.symbol('native'):
|
||||
instance_state = attrgetter(ClassManager.STATE_ATTR)
|
||||
instance_dict = attrgetter("__dict__")
|
||||
def manager_of_class(cls):
|
||||
return cls.__dict__.get(ClassManager.MANAGER_ATTR, None)
|
||||
else:
|
||||
instance_state = instrumentation_registry.state_of
|
||||
instance_dict = instrumentation_registry.dict_of
|
||||
manager_of_class = instrumentation_registry.manager_of_class
|
||||
attributes.instance_state = instance_state
|
||||
attributes.instance_dict = instance_dict
|
||||
attributes.manager_of_class = manager_of_class
|
||||
|
||||
_create_manager_for_cls = instrumentation_registry.create_manager_for_cls
|
||||
|
||||
# Install default "lookup" strategies. These are basically
|
||||
# very fast attrgetters for key attributes.
|
||||
# When a custom ClassManager is installed, more expensive per-class
|
||||
# strategies are copied over these.
|
||||
_install_lookup_strategy(util.symbol('native'))
|
||||
|
||||
|
||||
def find_native_user_instrumentation_hook(cls):
|
||||
"""Find user-specified instrumentation management for a class."""
|
||||
return getattr(cls, INSTRUMENTATION_MANAGER, None)
|
||||
instrumentation_finders.append(find_native_user_instrumentation_hook)
|
||||
|
||||
def _generate_init(class_, class_manager):
|
||||
"""Build an __init__ decorator that triggers ClassManager events."""
|
||||
|
||||
# TODO: we should use the ClassManager's notion of the
|
||||
# original '__init__' method, once ClassManager is fixed
|
||||
# to always reference that.
|
||||
original__init__ = class_.__init__
|
||||
assert original__init__
|
||||
|
||||
# Go through some effort here and don't change the user's __init__
|
||||
# calling signature, including the unlikely case that it has
|
||||
# a return value.
|
||||
# FIXME: need to juggle local names to avoid constructor argument
|
||||
# clashes.
|
||||
func_body = """\
|
||||
def __init__(%(apply_pos)s):
|
||||
new_state = class_manager._new_state_if_none(%(self_arg)s)
|
||||
if new_state:
|
||||
return new_state.initialize_instance(%(apply_kw)s)
|
||||
else:
|
||||
return original__init__(%(apply_kw)s)
|
||||
"""
|
||||
func_vars = util.format_argspec_init(original__init__, grouped=False)
|
||||
func_text = func_body % func_vars
|
||||
|
||||
# Py3K
|
||||
#func_defaults = getattr(original__init__, '__defaults__', None)
|
||||
#func_kw_defaults = getattr(original__init__, '__kwdefaults__', None)
|
||||
# Py2K
|
||||
func = getattr(original__init__, 'im_func', original__init__)
|
||||
func_defaults = getattr(func, 'func_defaults', None)
|
||||
# end Py2K
|
||||
|
||||
env = locals().copy()
|
||||
exec func_text in env
|
||||
__init__ = env['__init__']
|
||||
__init__.__doc__ = original__init__.__doc__
|
||||
if func_defaults:
|
||||
__init__.func_defaults = func_defaults
|
||||
# Py3K
|
||||
#if func_kw_defaults:
|
||||
# __init__.__kwdefaults__ = func_kw_defaults
|
||||
return __init__
|
||||
+216
-527
@@ -1,26 +1,30 @@
|
||||
# orm/interfaces.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
"""
|
||||
|
||||
Semi-private module containing various base classes used throughout the ORM.
|
||||
Contains various base classes used throughout the ORM.
|
||||
|
||||
Defines the extension classes :class:`MapperExtension`,
|
||||
:class:`SessionExtension`, and :class:`AttributeExtension` as
|
||||
well as other user-subclassable extension objects.
|
||||
Defines the now deprecated ORM extension classes as well
|
||||
as ORM internals.
|
||||
|
||||
Other than the deprecated extensions, this module and the
|
||||
classes within should be considered mostly private.
|
||||
|
||||
"""
|
||||
|
||||
from itertools import chain
|
||||
|
||||
import sqlalchemy.exceptions as sa_exc
|
||||
from sqlalchemy import log, util
|
||||
from sqlalchemy.sql import expression
|
||||
from sqlalchemy import exc as sa_exc
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy.sql import operators
|
||||
deque = __import__('collections').deque
|
||||
|
||||
mapperutil = util.importlater('sqlalchemy.orm', 'util')
|
||||
|
||||
class_mapper = None
|
||||
collections = None
|
||||
|
||||
__all__ = (
|
||||
@@ -48,373 +52,22 @@ ONETOMANY = util.symbol('ONETOMANY')
|
||||
MANYTOONE = util.symbol('MANYTOONE')
|
||||
MANYTOMANY = util.symbol('MANYTOMANY')
|
||||
|
||||
class MapperExtension(object):
|
||||
"""Base implementation for customizing ``Mapper`` behavior.
|
||||
from deprecated_interfaces import AttributeExtension, SessionExtension, \
|
||||
MapperExtension
|
||||
|
||||
New extension classes subclass ``MapperExtension`` and are specified
|
||||
using the ``extension`` mapper() argument, which is a single
|
||||
``MapperExtension`` or a list of such. A single mapper
|
||||
can maintain a chain of ``MapperExtension`` objects. When a
|
||||
particular mapping event occurs, the corresponding method
|
||||
on each ``MapperExtension`` is invoked serially, and each method
|
||||
has the ability to halt the chain from proceeding further.
|
||||
|
||||
Each ``MapperExtension`` method returns the symbol
|
||||
EXT_CONTINUE by default. This symbol generally means "move
|
||||
to the next ``MapperExtension`` for processing". For methods
|
||||
that return objects like translated rows or new object
|
||||
instances, EXT_CONTINUE means the result of the method
|
||||
should be ignored. In some cases it's required for a
|
||||
default mapper activity to be performed, such as adding a
|
||||
new instance to a result list.
|
||||
|
||||
The symbol EXT_STOP has significance within a chain
|
||||
of ``MapperExtension`` objects that the chain will be stopped
|
||||
when this symbol is returned. Like EXT_CONTINUE, it also
|
||||
has additional significance in some cases that a default
|
||||
mapper activity will not be performed.
|
||||
|
||||
"""
|
||||
|
||||
def instrument_class(self, mapper, class_):
|
||||
"""Receive a class when the mapper is first constructed, and has
|
||||
applied instrumentation to the mapped class.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def init_instance(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
"""Receive an instance when it's constructor is called.
|
||||
|
||||
This method is only called during a userland construction of
|
||||
an object. It is not called when an object is loaded from the
|
||||
database.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def init_failed(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
"""Receive an instance when it's constructor has been called,
|
||||
and raised an exception.
|
||||
|
||||
This method is only called during a userland construction of
|
||||
an object. It is not called when an object is loaded from the
|
||||
database.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def translate_row(self, mapper, context, row):
|
||||
"""Perform pre-processing on the given result row and return a
|
||||
new row instance.
|
||||
|
||||
This is called when the mapper first receives a row, before
|
||||
the object identity or the instance itself has been derived
|
||||
from that row. The given row may or may not be a
|
||||
``RowProxy`` object - it will always be a dictionary-like
|
||||
object which contains mapped columns as keys. The
|
||||
returned object should also be a dictionary-like object
|
||||
which recognizes mapped columns as keys.
|
||||
|
||||
If the ultimate return value is EXT_CONTINUE, the row
|
||||
is not translated.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def create_instance(self, mapper, selectcontext, row, class_):
|
||||
"""Receive a row when a new object instance is about to be
|
||||
created from that row.
|
||||
|
||||
The method can choose to create the instance itself, or it can return
|
||||
EXT_CONTINUE to indicate normal object creation should take place.
|
||||
|
||||
mapper
|
||||
The mapper doing the operation
|
||||
|
||||
selectcontext
|
||||
The QueryContext generated from the Query.
|
||||
|
||||
row
|
||||
The result row from the database
|
||||
|
||||
class\_
|
||||
The class we are mapping.
|
||||
|
||||
return value
|
||||
A new object instance, or EXT_CONTINUE
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def append_result(self, mapper, selectcontext, row, instance,
|
||||
result, **flags):
|
||||
"""Receive an object instance before that instance is appended
|
||||
to a result list.
|
||||
|
||||
If this method returns EXT_CONTINUE, result appending will proceed
|
||||
normally. if this method returns any other value or None,
|
||||
result appending will not proceed for this instance, giving
|
||||
this extension an opportunity to do the appending itself, if
|
||||
desired.
|
||||
|
||||
mapper
|
||||
The mapper doing the operation.
|
||||
|
||||
selectcontext
|
||||
The QueryContext generated from the Query.
|
||||
|
||||
row
|
||||
The result row from the database.
|
||||
|
||||
instance
|
||||
The object instance to be appended to the result.
|
||||
|
||||
result
|
||||
List to which results are being appended.
|
||||
|
||||
\**flags
|
||||
extra information about the row, same as criterion in
|
||||
``create_row_processor()`` method of
|
||||
:class:`~sqlalchemy.orm.interfaces.MapperProperty`
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def populate_instance(self, mapper, selectcontext, row,
|
||||
instance, **flags):
|
||||
"""Receive an instance before that instance has
|
||||
its attributes populated.
|
||||
|
||||
This usually corresponds to a newly loaded instance but may
|
||||
also correspond to an already-loaded instance which has
|
||||
unloaded attributes to be populated. The method may be called
|
||||
many times for a single instance, as multiple result rows are
|
||||
used to populate eagerly loaded collections.
|
||||
|
||||
If this method returns EXT_CONTINUE, instance population will
|
||||
proceed normally. If any other value or None is returned,
|
||||
instance population will not proceed, giving this extension an
|
||||
opportunity to populate the instance itself, if desired.
|
||||
|
||||
As of 0.5, most usages of this hook are obsolete. For a
|
||||
generic "object has been newly created from a row" hook, use
|
||||
``reconstruct_instance()``, or the ``@orm.reconstructor``
|
||||
decorator.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def reconstruct_instance(self, mapper, instance):
|
||||
"""Receive an object instance after it has been created via
|
||||
``__new__``, and after initial attribute population has
|
||||
occurred.
|
||||
|
||||
This typically occurs when the instance is created based on
|
||||
incoming result rows, and is only called once for that
|
||||
instance's lifetime.
|
||||
|
||||
Note that during a result-row load, this method is called upon
|
||||
the first row received for this instance. Note that some
|
||||
attributes and collections may or may not be loaded or even
|
||||
initialized, depending on what's present in the result rows.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_insert(self, mapper, connection, instance):
|
||||
"""Receive an object instance before that instance is inserted
|
||||
into its table.
|
||||
|
||||
This is a good place to set up primary key values and such
|
||||
that aren't handled otherwise.
|
||||
|
||||
Column-based attributes can be modified within this method
|
||||
which will result in the new value being inserted. However
|
||||
*no* changes to the overall flush plan can be made, and
|
||||
manipulation of the ``Session`` will not have the desired effect.
|
||||
To manipulate the ``Session`` within an extension, use
|
||||
``SessionExtension``.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def after_insert(self, mapper, connection, instance):
|
||||
"""Receive an object instance after that instance is inserted.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_update(self, mapper, connection, instance):
|
||||
"""Receive an object instance before that instance is updated.
|
||||
|
||||
Note that this method is called for all instances that are marked as
|
||||
"dirty", even those which have no net changes to their column-based
|
||||
attributes. An object is marked as dirty when any of its column-based
|
||||
attributes have a "set attribute" operation called or when any of its
|
||||
collections are modified. If, at update time, no column-based
|
||||
attributes have any net changes, no UPDATE statement will be issued.
|
||||
This means that an instance being sent to before_update is *not* a
|
||||
guarantee that an UPDATE statement will be issued (although you can
|
||||
affect the outcome here).
|
||||
|
||||
To detect if the column-based attributes on the object have net
|
||||
changes, and will therefore generate an UPDATE statement, use
|
||||
``object_session(instance).is_modified(instance,
|
||||
include_collections=False)``.
|
||||
|
||||
Column-based attributes can be modified within this method
|
||||
which will result in the new value being updated. However
|
||||
*no* changes to the overall flush plan can be made, and
|
||||
manipulation of the ``Session`` will not have the desired effect.
|
||||
To manipulate the ``Session`` within an extension, use
|
||||
``SessionExtension``.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def after_update(self, mapper, connection, instance):
|
||||
"""Receive an object instance after that instance is updated.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_delete(self, mapper, connection, instance):
|
||||
"""Receive an object instance before that instance is deleted.
|
||||
|
||||
Note that *no* changes to the overall flush plan can be made
|
||||
here; and manipulation of the ``Session`` will not have the
|
||||
desired effect. To manipulate the ``Session`` within an
|
||||
extension, use ``SessionExtension``.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def after_delete(self, mapper, connection, instance):
|
||||
"""Receive an object instance after that instance is deleted.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
class SessionExtension(object):
|
||||
|
||||
"""An extension hook object for Sessions. Subclasses may be
|
||||
installed into a Session (or sessionmaker) using the ``extension``
|
||||
keyword argument. """
|
||||
|
||||
def before_commit(self, session):
|
||||
"""Execute right before commit is called.
|
||||
|
||||
Note that this may not be per-flush if a longer running
|
||||
transaction is ongoing."""
|
||||
|
||||
def after_commit(self, session):
|
||||
"""Execute after a commit has occured.
|
||||
|
||||
Note that this may not be per-flush if a longer running
|
||||
transaction is ongoing."""
|
||||
|
||||
def after_rollback(self, session):
|
||||
"""Execute after a rollback has occured.
|
||||
|
||||
Note that this may not be per-flush if a longer running
|
||||
transaction is ongoing."""
|
||||
|
||||
def before_flush( self, session, flush_context, instances):
|
||||
"""Execute before flush process has started.
|
||||
|
||||
`instances` is an optional list of objects which were passed to
|
||||
the ``flush()`` method. """
|
||||
|
||||
def after_flush(self, session, flush_context):
|
||||
"""Execute after flush has completed, but before commit has been
|
||||
called.
|
||||
|
||||
Note that the session's state is still in pre-flush, i.e. 'new',
|
||||
'dirty', and 'deleted' lists still show pre-flush state as well
|
||||
as the history settings on instance attributes."""
|
||||
|
||||
def after_flush_postexec(self, session, flush_context):
|
||||
"""Execute after flush has completed, and after the post-exec
|
||||
state occurs.
|
||||
|
||||
This will be when the 'new', 'dirty', and 'deleted' lists are in
|
||||
their final state. An actual commit() may or may not have
|
||||
occured, depending on whether or not the flush started its own
|
||||
transaction or participated in a larger transaction. """
|
||||
|
||||
def after_begin( self, session, transaction, connection):
|
||||
"""Execute after a transaction is begun on a connection
|
||||
|
||||
`transaction` is the SessionTransaction. This method is called
|
||||
after an engine level transaction is begun on a connection. """
|
||||
|
||||
def after_attach(self, session, instance):
|
||||
"""Execute after an instance is attached to a session.
|
||||
|
||||
This is called after an add, delete or merge. """
|
||||
|
||||
def after_bulk_update( self, session, query, query_context, result):
|
||||
"""Execute after a bulk update operation to the session.
|
||||
|
||||
This is called after a session.query(...).update()
|
||||
|
||||
`query` is the query object that this update operation was
|
||||
called on. `query_context` was the query context object.
|
||||
`result` is the result object returned from the bulk operation.
|
||||
"""
|
||||
|
||||
def after_bulk_delete( self, session, query, query_context, result):
|
||||
"""Execute after a bulk delete operation to the session.
|
||||
|
||||
This is called after a session.query(...).delete()
|
||||
|
||||
`query` is the query object that this delete operation was
|
||||
called on. `query_context` was the query context object.
|
||||
`result` is the result object returned from the bulk operation.
|
||||
"""
|
||||
|
||||
class MapperProperty(object):
|
||||
"""Manage the relationship of a ``Mapper`` to a single class
|
||||
attribute, as well as that attribute as it appears on individual
|
||||
instances of the class, including attribute instrumentation,
|
||||
attribute access, loading behavior, and dependency calculations.
|
||||
|
||||
The most common occurrences of :class:`.MapperProperty` are the
|
||||
mapped :class:`.Column`, which is represented in a mapping as
|
||||
an instance of :class:`.ColumnProperty`,
|
||||
and a reference to another class produced by :func:`.relationship`,
|
||||
represented in the mapping as an instance of :class:`.RelationshipProperty`.
|
||||
|
||||
"""
|
||||
|
||||
cascade = ()
|
||||
@@ -424,7 +77,7 @@ class MapperProperty(object):
|
||||
|
||||
"""
|
||||
|
||||
def setup(self, context, entity, path, adapter, **kwargs):
|
||||
def setup(self, context, entity, path, reduced_path, adapter, **kwargs):
|
||||
"""Called by Query for the purposes of constructing a SQL statement.
|
||||
|
||||
Each MapperProperty associated with the target mapper processes the
|
||||
@@ -434,12 +87,12 @@ class MapperProperty(object):
|
||||
|
||||
pass
|
||||
|
||||
def create_row_processor(self, selectcontext, path, mapper, row, adapter):
|
||||
def create_row_processor(self, context, path, reduced_path,
|
||||
mapper, row, adapter):
|
||||
"""Return a 3-tuple consisting of three row processing functions.
|
||||
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
return None, None, None
|
||||
|
||||
def cascade_iterator(self, type_, state, visited_instances=None,
|
||||
halt_on=None):
|
||||
@@ -519,9 +172,9 @@ class MapperProperty(object):
|
||||
"""Merge the attribute represented by this ``MapperProperty``
|
||||
from source to destination object"""
|
||||
|
||||
raise NotImplementedError()
|
||||
pass
|
||||
|
||||
def compare(self, operator, value):
|
||||
def compare(self, operator, value, **kw):
|
||||
"""Return a compare operation for the columns represented by
|
||||
this ``MapperProperty`` to the given value, which may be a
|
||||
column value or an instance. 'operator' is an operator from
|
||||
@@ -533,13 +186,13 @@ class MapperProperty(object):
|
||||
|
||||
return operator(self.comparator, value)
|
||||
|
||||
class PropComparator(expression.ColumnOperators):
|
||||
class PropComparator(operators.ColumnOperators):
|
||||
"""Defines comparison operations for MapperProperty objects.
|
||||
|
||||
User-defined subclasses of :class:`.PropComparator` may be created. The
|
||||
built-in Python comparison and math operator methods, such as
|
||||
``__eq__()``, ``__lt__()``, ``__add__()``, can be overridden to provide
|
||||
new operator behaivor. The custom :class:`.PropComparator` is passed to
|
||||
new operator behavior. The custom :class:`.PropComparator` is passed to
|
||||
the mapper property via the ``comparator_factory`` argument. In each case,
|
||||
the appropriate subclass of :class:`.PropComparator` should be used::
|
||||
|
||||
@@ -598,8 +251,7 @@ class PropComparator(expression.ColumnOperators):
|
||||
query.join(Company.employees.of_type(Engineer)).\\
|
||||
filter(Engineer.name=='foo')
|
||||
|
||||
\class_
|
||||
a class or mapper indicating that criterion will be against
|
||||
:param \class_: a class or mapper indicating that criterion will be against
|
||||
this specific subclass.
|
||||
|
||||
|
||||
@@ -611,13 +263,16 @@ class PropComparator(expression.ColumnOperators):
|
||||
"""Return true if this collection contains any member that meets the
|
||||
given criterion.
|
||||
|
||||
criterion
|
||||
an optional ClauseElement formulated against the member class' table
|
||||
or attributes.
|
||||
The usual implementation of ``any()`` is
|
||||
:meth:`.RelationshipProperty.Comparator.any`.
|
||||
|
||||
:param criterion: an optional ClauseElement formulated against the
|
||||
member class' table or attributes.
|
||||
|
||||
:param \**kwargs: key/value pairs corresponding to member class attribute
|
||||
names which will be compared via equality to the corresponding
|
||||
values.
|
||||
|
||||
\**kwargs
|
||||
key/value pairs corresponding to member class attribute names which
|
||||
will be compared via equality to the corresponding values.
|
||||
"""
|
||||
|
||||
return self.operate(PropComparator.any_op, criterion, **kwargs)
|
||||
@@ -626,13 +281,16 @@ class PropComparator(expression.ColumnOperators):
|
||||
"""Return true if this element references a member which meets the
|
||||
given criterion.
|
||||
|
||||
criterion
|
||||
an optional ClauseElement formulated against the member class' table
|
||||
or attributes.
|
||||
The usual implementation of ``has()`` is
|
||||
:meth:`.RelationshipProperty.Comparator.has`.
|
||||
|
||||
:param criterion: an optional ClauseElement formulated against the
|
||||
member class' table or attributes.
|
||||
|
||||
:param \**kwargs: key/value pairs corresponding to member class attribute
|
||||
names which will be compared via equality to the corresponding
|
||||
values.
|
||||
|
||||
\**kwargs
|
||||
key/value pairs corresponding to member class attribute names which
|
||||
will be compared via equality to the corresponding values.
|
||||
"""
|
||||
|
||||
return self.operate(PropComparator.has_op, criterion, **kwargs)
|
||||
@@ -648,38 +306,47 @@ class StrategizedProperty(MapperProperty):
|
||||
|
||||
"""
|
||||
|
||||
def _get_context_strategy(self, context, path):
|
||||
cls = context.attributes.get(('loaderstrategy',
|
||||
_reduce_path(path)), None)
|
||||
strategy_wildcard_key = None
|
||||
|
||||
def _get_context_strategy(self, context, reduced_path):
|
||||
key = ('loaderstrategy', reduced_path)
|
||||
cls = None
|
||||
if key in context.attributes:
|
||||
cls = context.attributes[key]
|
||||
elif self.strategy_wildcard_key:
|
||||
key = ('loaderstrategy', (self.strategy_wildcard_key,))
|
||||
if key in context.attributes:
|
||||
cls = context.attributes[key]
|
||||
|
||||
if cls:
|
||||
try:
|
||||
return self.__all_strategies[cls]
|
||||
return self._strategies[cls]
|
||||
except KeyError:
|
||||
return self.__init_strategy(cls)
|
||||
else:
|
||||
return self.strategy
|
||||
return self.strategy
|
||||
|
||||
def _get_strategy(self, cls):
|
||||
try:
|
||||
return self.__all_strategies[cls]
|
||||
return self._strategies[cls]
|
||||
except KeyError:
|
||||
return self.__init_strategy(cls)
|
||||
|
||||
def __init_strategy(self, cls):
|
||||
self.__all_strategies[cls] = strategy = cls(self)
|
||||
strategy.init()
|
||||
self._strategies[cls] = strategy = cls(self)
|
||||
return strategy
|
||||
|
||||
def setup(self, context, entity, path, adapter, **kwargs):
|
||||
self._get_context_strategy(context, path + (self.key,)).\
|
||||
setup_query(context, entity, path, adapter, **kwargs)
|
||||
def setup(self, context, entity, path, reduced_path, adapter, **kwargs):
|
||||
self._get_context_strategy(context, reduced_path + (self.key,)).\
|
||||
setup_query(context, entity, path,
|
||||
reduced_path, adapter, **kwargs)
|
||||
|
||||
def create_row_processor(self, context, path, mapper, row, adapter):
|
||||
return self._get_context_strategy(context, path + (self.key,)).\
|
||||
create_row_processor(context, path, mapper, row, adapter)
|
||||
def create_row_processor(self, context, path, reduced_path, mapper, row, adapter):
|
||||
return self._get_context_strategy(context, reduced_path + (self.key,)).\
|
||||
create_row_processor(context, path,
|
||||
reduced_path, mapper, row, adapter)
|
||||
|
||||
def do_init(self):
|
||||
self.__all_strategies = {}
|
||||
self._strategies = {}
|
||||
self.strategy = self.__init_strategy(self.strategy_class)
|
||||
|
||||
def post_instrument_class(self, mapper):
|
||||
@@ -706,11 +373,7 @@ def deserialize_path(path):
|
||||
if path is None:
|
||||
return None
|
||||
|
||||
global class_mapper
|
||||
if class_mapper is None:
|
||||
from sqlalchemy.orm import class_mapper
|
||||
|
||||
p = tuple(chain(*[(class_mapper(cls), key) for cls, key in path]))
|
||||
p = tuple(chain(*[(mapperutil.class_mapper(cls), key) for cls, key in path]))
|
||||
if p and p[-1] is None:
|
||||
p = p[0:-1]
|
||||
return p
|
||||
@@ -735,22 +398,10 @@ class MapperOption(object):
|
||||
|
||||
self.process_query(query)
|
||||
|
||||
class ExtensionOption(MapperOption):
|
||||
|
||||
"""a MapperOption that applies a MapperExtension to a query
|
||||
operation."""
|
||||
|
||||
def __init__(self, ext):
|
||||
self.ext = ext
|
||||
|
||||
def process_query(self, query):
|
||||
entity = query._generate_mapper_zero()
|
||||
entity.extension = entity.extension.copy()
|
||||
entity.extension.push(self.ext)
|
||||
|
||||
class PropertyOption(MapperOption):
|
||||
"""A MapperOption that is applied to a property off the mapper or
|
||||
one of its child mappers, identified by a dot-separated key. """
|
||||
one of its child mappers, identified by a dot-separated key
|
||||
or list of class-bound attributes. """
|
||||
|
||||
def __init__(self, key, mapper=None):
|
||||
self.key = key
|
||||
@@ -791,14 +442,12 @@ class PropertyOption(MapperOption):
|
||||
state['key'] = tuple(ret)
|
||||
self.__dict__ = state
|
||||
|
||||
def _find_entity( self, query, mapper, raiseerr):
|
||||
from sqlalchemy.orm.util import _class_to_mapper, \
|
||||
_is_aliased_class
|
||||
if _is_aliased_class(mapper):
|
||||
def _find_entity_prop_comparator(self, query, token, mapper, raiseerr):
|
||||
if mapperutil._is_aliased_class(mapper):
|
||||
searchfor = mapper
|
||||
isa = False
|
||||
else:
|
||||
searchfor = _class_to_mapper(mapper)
|
||||
searchfor = mapperutil._class_to_mapper(mapper)
|
||||
isa = True
|
||||
for ent in query._mapper_entities:
|
||||
if searchfor is ent.path_entity or isa \
|
||||
@@ -806,9 +455,36 @@ class PropertyOption(MapperOption):
|
||||
return ent
|
||||
else:
|
||||
if raiseerr:
|
||||
raise sa_exc.ArgumentError("Can't find entity %s in "
|
||||
"Query. Current list: %r" % (searchfor,
|
||||
[str(m.path_entity) for m in query._entities]))
|
||||
if not list(query._mapper_entities):
|
||||
raise sa_exc.ArgumentError(
|
||||
"Query has only expression-based entities - "
|
||||
"can't find property named '%s'."
|
||||
% (token, )
|
||||
)
|
||||
else:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Can't find property '%s' on any entity "
|
||||
"specified in this Query. Note the full path "
|
||||
"from root (%s) to target entity must be specified."
|
||||
% (token, ",".join(str(x) for
|
||||
x in query._mapper_entities))
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
def _find_entity_basestring(self, query, token, raiseerr):
|
||||
for ent in query._mapper_entities:
|
||||
# return only the first _MapperEntity when searching
|
||||
# based on string prop name. Ideally object
|
||||
# attributes are used to specify more exactly.
|
||||
return ent
|
||||
else:
|
||||
if raiseerr:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Query has only expression-based entities - "
|
||||
"can't find property named '%s'."
|
||||
% (token, )
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -818,114 +494,112 @@ class PropertyOption(MapperOption):
|
||||
l = []
|
||||
mappers = []
|
||||
|
||||
# _current_path implies we're in a secondary load with an
|
||||
# existing path
|
||||
|
||||
# _current_path implies we're in a
|
||||
# secondary load with an existing path
|
||||
current_path = list(query._current_path)
|
||||
tokens = []
|
||||
for key in util.to_list(self.key):
|
||||
if isinstance(key, basestring):
|
||||
tokens += key.split('.')
|
||||
else:
|
||||
tokens += [key]
|
||||
for token in tokens:
|
||||
|
||||
tokens = deque(self.key)
|
||||
while tokens:
|
||||
token = tokens.popleft()
|
||||
if isinstance(token, basestring):
|
||||
# wildcard token
|
||||
if token.endswith(':*'):
|
||||
return [(token,)], []
|
||||
sub_tokens = token.split(".", 1)
|
||||
token = sub_tokens[0]
|
||||
tokens.extendleft(sub_tokens[1:])
|
||||
|
||||
# exhaust current_path before
|
||||
# matching tokens to entities
|
||||
if current_path:
|
||||
if current_path[1] == token:
|
||||
current_path = current_path[2:]
|
||||
continue
|
||||
else:
|
||||
return [], []
|
||||
|
||||
if not entity:
|
||||
if current_path:
|
||||
if current_path[1] == token:
|
||||
current_path = current_path[2:]
|
||||
continue
|
||||
entity = query._entity_zero()
|
||||
entity = self._find_entity_basestring(
|
||||
query,
|
||||
token,
|
||||
raiseerr)
|
||||
if entity is None:
|
||||
return [], []
|
||||
path_element = entity.path_entity
|
||||
mapper = entity.mapper
|
||||
mappers.append(mapper)
|
||||
prop = mapper.get_property(token,
|
||||
resolve_synonyms=True, raiseerr=raiseerr)
|
||||
key = token
|
||||
if hasattr(mapper.class_, token):
|
||||
prop = getattr(mapper.class_, token).property
|
||||
else:
|
||||
if raiseerr:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Can't find property named '%s' on the "
|
||||
"mapped entity %s in this Query. " % (
|
||||
token, mapper)
|
||||
)
|
||||
else:
|
||||
return [], []
|
||||
elif isinstance(token, PropComparator):
|
||||
prop = token.property
|
||||
|
||||
# exhaust current_path before
|
||||
# matching tokens to entities
|
||||
if current_path:
|
||||
if current_path[0:2] == \
|
||||
[token.parententity, prop.key]:
|
||||
current_path = current_path[2:]
|
||||
continue
|
||||
else:
|
||||
return [], []
|
||||
|
||||
if not entity:
|
||||
if current_path:
|
||||
if current_path[0:2] == [token.parententity,
|
||||
prop.key]:
|
||||
current_path = current_path[2:]
|
||||
continue
|
||||
entity = self._find_entity(query,
|
||||
token.parententity, raiseerr)
|
||||
entity = self._find_entity_prop_comparator(
|
||||
query,
|
||||
prop.key,
|
||||
token.parententity,
|
||||
raiseerr)
|
||||
if not entity:
|
||||
return [], []
|
||||
path_element = entity.path_entity
|
||||
mapper = entity.mapper
|
||||
mappers.append(prop.parent)
|
||||
key = prop.key
|
||||
else:
|
||||
raise sa_exc.ArgumentError('mapper option expects '
|
||||
'string key or list of attributes')
|
||||
if prop is None:
|
||||
return [], []
|
||||
raise sa_exc.ArgumentError(
|
||||
"mapper option expects "
|
||||
"string key or list of attributes")
|
||||
assert prop is not None
|
||||
if raiseerr and not prop.parent.common_parent(mapper):
|
||||
raise sa_exc.ArgumentError("Attribute '%s' does not "
|
||||
"link from element '%s'" % (token, path_element))
|
||||
|
||||
path = build_path(path_element, prop.key, path)
|
||||
|
||||
l.append(path)
|
||||
if getattr(token, '_of_type', None):
|
||||
path_element = mapper = token._of_type
|
||||
else:
|
||||
path_element = mapper = getattr(prop, 'mapper', None)
|
||||
if path_element:
|
||||
path_element = path_element
|
||||
if mapper is None and tokens:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Attribute '%s' of entity '%s' does not "
|
||||
"refer to a mapped entity" %
|
||||
(token, entity)
|
||||
)
|
||||
|
||||
if current_path:
|
||||
# ran out of tokens before
|
||||
# current_path was exhausted.
|
||||
assert not tokens
|
||||
return [], []
|
||||
|
||||
return l, mappers
|
||||
|
||||
class AttributeExtension(object):
|
||||
"""An event handler for individual attribute change events.
|
||||
|
||||
AttributeExtension is assembled within the descriptors associated
|
||||
with a mapped class.
|
||||
|
||||
"""
|
||||
|
||||
active_history = True
|
||||
"""indicates that the set() method would like to receive the 'old' value,
|
||||
even if it means firing lazy callables.
|
||||
|
||||
Note that ``active_history`` can also be set directly via
|
||||
:func:`.column_property` and :func:`.relationship`.
|
||||
|
||||
"""
|
||||
|
||||
def append(self, state, value, initiator):
|
||||
"""Receive a collection append event.
|
||||
|
||||
The returned value will be used as the actual value to be
|
||||
appended.
|
||||
|
||||
"""
|
||||
return value
|
||||
|
||||
def remove(self, state, value, initiator):
|
||||
"""Receive a remove event.
|
||||
|
||||
No return value is defined.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def set(self, state, value, oldvalue, initiator):
|
||||
"""Receive a set event.
|
||||
|
||||
The returned value will be used as the actual value to be
|
||||
set.
|
||||
|
||||
"""
|
||||
return value
|
||||
|
||||
|
||||
class StrategizedOption(PropertyOption):
|
||||
"""A MapperOption that affects which LoaderStrategy will be used
|
||||
for an operation by a StrategizedProperty.
|
||||
"""
|
||||
|
||||
is_chained = False
|
||||
chained = False
|
||||
|
||||
def process_query_property(self, query, paths, mappers):
|
||||
|
||||
@@ -935,7 +609,7 @@ class StrategizedOption(PropertyOption):
|
||||
# "(Person, 'machines')" in the path due to the mechanics of how
|
||||
# the eager strategy builds up the path
|
||||
|
||||
if self.is_chained:
|
||||
if self.chained:
|
||||
for path in paths:
|
||||
query._attributes[('loaderstrategy',
|
||||
_reduce_path(path))] = \
|
||||
@@ -953,13 +627,13 @@ def _reduce_path(path):
|
||||
|
||||
This is used to allow more open ended selection of loader strategies, i.e.
|
||||
Mapper -> prop1 -> Subclass -> prop2, where Subclass is a sub-mapper
|
||||
of the mapper referened by Mapper.prop1.
|
||||
of the mapper referenced by Mapper.prop1.
|
||||
|
||||
"""
|
||||
return tuple([i % 2 != 0 and
|
||||
path[i] or
|
||||
getattr(path[i], 'base_mapper', path[i])
|
||||
for i in xrange(len(path))])
|
||||
element or
|
||||
getattr(element, 'base_mapper', element)
|
||||
for i, element in enumerate(path)])
|
||||
|
||||
class LoaderStrategy(object):
|
||||
"""Describe the loading behavior of a StrategizedProperty object.
|
||||
@@ -975,22 +649,25 @@ class LoaderStrategy(object):
|
||||
|
||||
* it processes the ``QueryContext`` at statement construction time,
|
||||
where it can modify the SQL statement that is being produced.
|
||||
simple column attributes may add their represented column to the
|
||||
Simple column attributes may add their represented column to the
|
||||
list of selected columns, *eager loading* properties may add
|
||||
``LEFT OUTER JOIN`` clauses to the statement.
|
||||
|
||||
* it processes the ``SelectionContext`` at row-processing time. This
|
||||
includes straight population of attributes corresponding to rows,
|
||||
setting instance-level lazyloader callables on newly
|
||||
constructed instances, and appending child items to scalar/collection
|
||||
attributes in response to eagerly-loaded relations.
|
||||
"""
|
||||
* It produces "row processor" functions at result fetching time.
|
||||
These "row processor" functions populate a particular attribute
|
||||
on a particular mapped instance.
|
||||
|
||||
"""
|
||||
def __init__(self, parent):
|
||||
self.parent_property = parent
|
||||
self.is_class_level = False
|
||||
self.parent = self.parent_property.parent
|
||||
self.key = self.parent_property.key
|
||||
# TODO: there's no particular reason we need
|
||||
# the separate .init() method at this point.
|
||||
# It's possible someone has written their
|
||||
# own LS object.
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
raise NotImplementedError("LoaderStrategy")
|
||||
@@ -998,10 +675,10 @@ class LoaderStrategy(object):
|
||||
def init_class_attribute(self, mapper):
|
||||
pass
|
||||
|
||||
def setup_query(self, context, entity, path, adapter, **kwargs):
|
||||
def setup_query(self, context, entity, path, reduced_path, adapter, **kwargs):
|
||||
pass
|
||||
|
||||
def create_row_processor(self, selectcontext, path, mapper,
|
||||
def create_row_processor(self, context, path, reduced_path, mapper,
|
||||
row, adapter):
|
||||
"""Return row processing functions which fulfill the contract
|
||||
specified by MapperProperty.create_row_processor.
|
||||
@@ -1009,7 +686,7 @@ class LoaderStrategy(object):
|
||||
StrategizedProperty delegates its create_row_processor method
|
||||
directly to this method. """
|
||||
|
||||
raise NotImplementedError()
|
||||
return None, None, None
|
||||
|
||||
def __str__(self):
|
||||
return str(self.parent_property)
|
||||
@@ -1028,6 +705,18 @@ class LoaderStrategy(object):
|
||||
class InstrumentationManager(object):
|
||||
"""User-defined class instrumentation extension.
|
||||
|
||||
:class:`.InstrumentationManager` can be subclassed in order
|
||||
to change
|
||||
how class instrumentation proceeds. This class exists for
|
||||
the purposes of integration with other object management
|
||||
frameworks which would like to entirely modify the
|
||||
instrumentation methodology of the ORM, and is not intended
|
||||
for regular usage. For interception of class instrumentation
|
||||
events, see :class:`.InstrumentationEvents`.
|
||||
|
||||
For an example of :class:`.InstrumentationManager`, see the
|
||||
example :ref:`examples_instrumentation`.
|
||||
|
||||
The API for this class should be considered as semi-stable,
|
||||
and may change slightly with new releases.
|
||||
|
||||
@@ -1085,7 +774,7 @@ class InstrumentationManager(object):
|
||||
setattr(instance, '_default_state', state)
|
||||
|
||||
def remove_state(self, class_, instance):
|
||||
delattr(instance, '_default_state', state)
|
||||
delattr(instance, '_default_state')
|
||||
|
||||
def state_getter(self, class_):
|
||||
return lambda instance: getattr(instance, '_default_state')
|
||||
|
||||
+1054
-585
File diff suppressed because it is too large
Load Diff
+659
-592
File diff suppressed because it is too large
Load Diff
+837
-426
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,12 @@
|
||||
# orm/scoping.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
import sqlalchemy.exceptions as sa_exc
|
||||
from sqlalchemy.util import ScopedRegistry, ThreadLocalRegistry, \
|
||||
to_list, get_cls_kwargs, deprecated,\
|
||||
warn
|
||||
from sqlalchemy.orm import (
|
||||
EXT_CONTINUE, MapperExtension, class_mapper, object_session
|
||||
)
|
||||
from sqlalchemy import exc as sa_exc
|
||||
from sqlalchemy.util import ScopedRegistry, ThreadLocalRegistry, warn
|
||||
from sqlalchemy.orm import class_mapper
|
||||
from sqlalchemy.orm import exc as orm_exc
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
@@ -20,16 +16,16 @@ __all__ = ['ScopedSession']
|
||||
|
||||
class ScopedSession(object):
|
||||
"""Provides thread-local management of Sessions.
|
||||
|
||||
Usage::
|
||||
|
||||
|
||||
Typical invocation is via the :func:`.scoped_session`
|
||||
function::
|
||||
|
||||
Session = scoped_session(sessionmaker())
|
||||
|
||||
... use Session normally.
|
||||
|
||||
The internal registry is accessible as well,
|
||||
The internal registry is accessible,
|
||||
and by default is an instance of :class:`.ThreadLocalRegistry`.
|
||||
|
||||
See also: :ref:`unitofwork_contextual`.
|
||||
|
||||
"""
|
||||
|
||||
@@ -39,7 +35,6 @@ class ScopedSession(object):
|
||||
self.registry = ScopedRegistry(session_factory, scopefunc)
|
||||
else:
|
||||
self.registry = ThreadLocalRegistry(session_factory)
|
||||
self.extension = _ScopedExt(self)
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
if kwargs:
|
||||
@@ -64,27 +59,6 @@ class ScopedSession(object):
|
||||
self.registry().close()
|
||||
self.registry.clear()
|
||||
|
||||
@deprecated("0.5", ":meth:`.ScopedSession.mapper` is deprecated. "
|
||||
"Please see http://www.sqlalchemy.org/trac/wiki/UsageRecipes/SessionAwareMapper "
|
||||
"for information on how to replicate its behavior.")
|
||||
def mapper(self, *args, **kwargs):
|
||||
"""return a :func:`.mapper` function which associates this ScopedSession with the Mapper.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import mapper
|
||||
|
||||
extension_args = dict((arg, kwargs.pop(arg))
|
||||
for arg in get_cls_kwargs(_ScopedExt)
|
||||
if arg in kwargs)
|
||||
|
||||
kwargs['extension'] = extension = to_list(kwargs.get('extension', []))
|
||||
if extension_args:
|
||||
extension.append(self.extension.configure(**extension_args))
|
||||
else:
|
||||
extension.append(self.extension)
|
||||
return mapper(*args, **kwargs)
|
||||
|
||||
def configure(self, **kwargs):
|
||||
"""reconfigure the sessionmaker used by this ScopedSession."""
|
||||
|
||||
@@ -157,59 +131,3 @@ def clslevel(name):
|
||||
for prop in ('close_all', 'object_session', 'identity_key'):
|
||||
setattr(ScopedSession, prop, clslevel(prop))
|
||||
|
||||
class _ScopedExt(MapperExtension):
|
||||
def __init__(self, context, validate=False, save_on_init=True):
|
||||
self.context = context
|
||||
self.validate = validate
|
||||
self.save_on_init = save_on_init
|
||||
self.set_kwargs_on_init = True
|
||||
|
||||
def validating(self):
|
||||
return _ScopedExt(self.context, validate=True)
|
||||
|
||||
def configure(self, **kwargs):
|
||||
return _ScopedExt(self.context, **kwargs)
|
||||
|
||||
def instrument_class(self, mapper, class_):
|
||||
class query(object):
|
||||
def __getattr__(s, key):
|
||||
return getattr(self.context.registry().query(class_), key)
|
||||
def __call__(s):
|
||||
return self.context.registry().query(class_)
|
||||
def __get__(self, instance, cls):
|
||||
return self
|
||||
|
||||
if not 'query' in class_.__dict__:
|
||||
class_.query = query()
|
||||
|
||||
if self.set_kwargs_on_init and class_.__init__ is object.__init__:
|
||||
class_.__init__ = self._default__init__(mapper)
|
||||
|
||||
def _default__init__(ext, mapper):
|
||||
def __init__(self, **kwargs):
|
||||
for key, value in kwargs.iteritems():
|
||||
if ext.validate:
|
||||
if not mapper.get_property(key, resolve_synonyms=False,
|
||||
raiseerr=False):
|
||||
raise sa_exc.ArgumentError(
|
||||
"Invalid __init__ argument: '%s'" % key)
|
||||
setattr(self, key, value)
|
||||
return __init__
|
||||
|
||||
def init_instance(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
if self.save_on_init:
|
||||
session = kwargs.pop('_sa_session', None)
|
||||
if session is None:
|
||||
session = self.context.registry()
|
||||
session._save_without_cascade(instance)
|
||||
return EXT_CONTINUE
|
||||
|
||||
def init_failed(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
sess = object_session(instance)
|
||||
if sess:
|
||||
sess.expunge(instance)
|
||||
return EXT_CONTINUE
|
||||
|
||||
def dispose_class(self, mapper, class_):
|
||||
if hasattr(class_, 'query'):
|
||||
delattr(class_, 'query')
|
||||
|
||||
+523
-343
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
# orm/shard.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user