- [feature] the MS Access dialect has been

moved to its own project on Bitbucket,
    taking advantage of the new SQLAlchemy
    dialect compliance suite.   The dialect is
    still in very rough shape and probably not
    ready for general use yet, however
    it does have *extremely* rudimental
    functionality now.
This commit is contained in:
Mike Bayer
2012-09-30 20:17:49 -04:00
parent 68fb10cae5
commit fab9be5d0e
10 changed files with 22 additions and 495 deletions
+10
View File
@@ -607,6 +607,16 @@ underneath "0.7.xx".
constructed to refer to multiple remote tables
is first used. [ticket:2455]
- access
- [feature] the MS Access dialect has been
moved to its own project on Bitbucket,
taking advantage of the new SQLAlchemy
dialect compliance suite. The dialect is
still in very rough shape and probably not
ready for general use yet, however
it does have *extremely* rudimental
functionality now.
- sqlite
- [feature] the SQLite date and time types
have been overhauled to support a more open
+10 -6
View File
@@ -46,10 +46,13 @@ Supported Databases
====================
SQLAlchemy includes many :class:`~sqlalchemy.engine.base.Dialect` implementations for various
backends; each is described as its own package in the :ref:`sqlalchemy.dialects_toplevel` package. A
SQLAlchemy dialect always requires that an appropriate DBAPI driver is installed.
backends. Dialects for the most common databases are included with SQLAlchemy; a handful
of others require an additional install of a separate dialect.
The table below summarizes the state of DBAPI support in SQLAlchemy 0.7. The values
Those dialects which are included with SQLAlchemy are described under the section :ref:`dialect_toplevel`.
All dialects additionally require that an appropriate DBAPI driver is installed.
The table below summarizes the state of DBAPI support in this version of SQLAlchemy. The values
translate as:
* yes / Python platform - The SQLAlchemy dialect is mostly or fully operational on the target platform.
@@ -66,7 +69,7 @@ translate as:
Driver Connect string Py2K Py3K Jython Unix Windows
=============================================================== =========================== =========== =========== =========== ================= ============
**DB2/Informix IDS**
ibm-db_ thirdparty thirdparty thirdparty thirdparty thirdparty thirdparty
ibm-db_ See `ibm-db`_ unknown unknown unknown unknown unknown
**Drizzle** :ref:`(docs) <drizzle_toplevel>`
mysql-python_ ``drizzle+mysqldb``\* yes development no yes yes
**Firebird / Interbase** :ref:`(docs) <firebird_toplevel>`
@@ -75,8 +78,8 @@ kinterbasdb_ ``firebird+kint
informixdb_ ``informix+informixdb``\* yes development no unknown unknown
**MaxDB** :ref:`(docs) <maxdb_toplevel>`
sapdb_ ``maxdb+sapdb``\* development development no yes unknown
**Microsoft Access** :ref:`(docs) <access_toplevel>`
pyodbc_ ``access+pyodbc``\* development development no unknown yes
**Microsoft Access**
pyodbc_ See `sqlalchemy-access`_ development development no unknown yes
**Microsoft SQL Server** :ref:`(docs) <mssql_toplevel>`
adodbapi_ ``mssql+adodbapi`` development development no no yes
`jTDS JDBC Driver`_ ``mssql+zxjdbc`` no no development yes yes
@@ -110,6 +113,7 @@ python-sybase_ ``sybase+pysyba
.. [1] The Sybase dialect currently lacks the ability to reflect tables.
.. _psycopg2: http://www.initd.org/
.. _pg8000: http://pybrary.net/pg8000/
.. _sqlalchemy-access: https://bitbucket.org/zzzeek/sqlalchemy-access
.. _pypostgresql: http://python.projects.postgresql.org/
.. _mysql-python: http://sourceforge.net/projects/mysql-python
.. _MySQL Connector/Python: https://launchpad.net/myconnpy
-6
View File
@@ -1,6 +0,0 @@
.. _access_toplevel:
Microsoft Access
================
.. automodule:: sqlalchemy.dialects.access.base
+1 -2
View File
@@ -4,7 +4,7 @@ Dialects
========
The **dialect** is the system SQLAlchemy uses to communicate with various types of DBAPIs and databases.
A compatibility chart of supported backends can be found at :ref:`supported_dbapis`. The sections that
A compatibility chart of supported backends can be found at :ref:`supported_dbapis`. The sections that
follow contain reference documentation and notes specific to the usage of each backend, as well as notes
for the various DBAPIs.
@@ -20,7 +20,6 @@ should be consulted to check for current support level.
firebird
informix
maxdb
access
mssql
mysql
oracle
-2
View File
@@ -18,12 +18,10 @@ from ..dialects.firebird import base as firebird
from ..dialects.maxdb import base as maxdb
from ..dialects.informix import base as informix
from ..dialects.mssql import base as mssql
from ..dialects.access import base as access
from ..dialects.sybase import base as sybase
__all__ = (
'access',
'drizzle',
'firebird',
'informix',
-1
View File
@@ -5,7 +5,6 @@
# the MIT License: http://www.opensource.org/licenses/mit-license.php
__all__ = (
# 'access',
'drizzle',
'firebird',
# 'informix',
-444
View File
@@ -1,444 +0,0 @@
# access/base.py
# Copyright (C) 2007-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
# Copyright (C) 2007 Paul Johnston, paj@pajhome.org.uk
# Portions derived from jet2sql.py by Matt Keranen, mksql@yahoo.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 Microsoft Access database.
.. note::
The Access dialect is **non-functional as of SQLAlchemy 0.6**,
pending development efforts to bring it up-to-date.
"""
from sqlalchemy import sql, schema, types, exc, pool
from sqlalchemy.sql import compiler, expression
from sqlalchemy.engine import default, base, reflection
from sqlalchemy import processors
class AcNumeric(types.Numeric):
def get_col_spec(self):
return "NUMERIC"
def bind_processor(self, dialect):
return processors.to_str
def result_processor(self, dialect, coltype):
return None
class AcFloat(types.Float):
def get_col_spec(self):
return "FLOAT"
def bind_processor(self, dialect):
"""By converting to string, we can use Decimal types round-trip."""
return processors.to_str
class AcInteger(types.Integer):
def get_col_spec(self):
return "INTEGER"
class AcTinyInteger(types.Integer):
def get_col_spec(self):
return "TINYINT"
class AcSmallInteger(types.SmallInteger):
def get_col_spec(self):
return "SMALLINT"
class AcDateTime(types.DateTime):
def get_col_spec(self):
return "DATETIME"
class AcDate(types.Date):
def get_col_spec(self):
return "DATETIME"
class AcText(types.Text):
def get_col_spec(self):
return "MEMO"
class AcString(types.String):
def get_col_spec(self):
return "TEXT" + (self.length and ("(%d)" % self.length) or "")
class AcUnicode(types.Unicode):
def get_col_spec(self):
return "TEXT" + (self.length and ("(%d)" % self.length) or "")
def bind_processor(self, dialect):
return None
def result_processor(self, dialect, coltype):
return None
class AcChar(types.CHAR):
def get_col_spec(self):
return "TEXT" + (self.length and ("(%d)" % self.length) or "")
class AcBinary(types.LargeBinary):
def get_col_spec(self):
return "BINARY"
class AcBoolean(types.Boolean):
def get_col_spec(self):
return "YESNO"
class AcTimeStamp(types.TIMESTAMP):
def get_col_spec(self):
return "TIMESTAMP"
class AccessExecutionContext(default.DefaultExecutionContext):
def _has_implicit_sequence(self, column):
if column.primary_key and column.autoincrement:
if isinstance(column.type, types.Integer) and \
not column.foreign_keys:
if column.default is None or \
(isinstance(column.default, schema.Sequence) and \
column.default.optional):
return True
return False
def post_exec(self):
"""If we inserted into a row with a COUNTER column, fetch the ID"""
if self.compiled.isinsert:
tbl = self.compiled.statement.table
if not hasattr(tbl, 'has_sequence'):
tbl.has_sequence = None
for column in tbl.c:
if getattr(column, 'sequence', False) or \
self._has_implicit_sequence(column):
tbl.has_sequence = column
break
if bool(tbl.has_sequence):
# TBD: for some reason _last_inserted_ids doesn't exist here
# (but it does at corresponding point in mssql???)
#if not len(self._last_inserted_ids) or
# self._last_inserted_ids[0] is None:
self.cursor.execute("SELECT @@identity AS lastrowid")
row = self.cursor.fetchone()
self._last_inserted_ids = [int(row[0])]
#+ self._last_inserted_ids[1:]
# print "LAST ROW ID", self._last_inserted_ids
super(AccessExecutionContext, self).post_exec()
const, daoEngine = None, None
class AccessDialect(default.DefaultDialect):
colspecs = {
types.Unicode : AcUnicode,
types.Integer : AcInteger,
types.SmallInteger: AcSmallInteger,
types.Numeric : AcNumeric,
types.Float : AcFloat,
types.DateTime : AcDateTime,
types.Date : AcDate,
types.String : AcString,
types.LargeBinary : AcBinary,
types.Boolean : AcBoolean,
types.Text : AcText,
types.CHAR: AcChar,
types.TIMESTAMP: AcTimeStamp,
}
name = 'access'
supports_sane_rowcount = False
supports_sane_multi_rowcount = False
ported_sqla_06 = False
def type_descriptor(self, typeobj):
newobj = types.adapt_type(typeobj, self.colspecs)
return newobj
def __init__(self, **params):
super(AccessDialect, self).__init__(**params)
self.text_as_varchar = False
self._dtbs = None
@classmethod
def dbapi(cls):
import win32com.client, pythoncom
global const, daoEngine
if const is None:
const = win32com.client.constants
for suffix in (".36", ".35", ".30"):
try:
daoEngine = win32com.client.\
gencache.\
EnsureDispatch("DAO.DBEngine" + suffix)
break
except pythoncom.com_error:
pass
else:
raise exc.InvalidRequestError(
"Can't find a DB engine. Check "
"http://support.microsoft.com/kb/239114 for details.")
import pyodbc as module
return module
def create_connect_args(self, url):
opts = url.translate_connect_args()
connectors = ["Driver={Microsoft Access Driver (*.mdb)}"]
connectors.append("Dbq=%s" % opts["database"])
user = opts.get("username", None)
if user:
connectors.append("UID=%s" % user)
connectors.append("PWD=%s" % opts.get("password", ""))
return [[";".join(connectors)], {}]
def last_inserted_ids(self):
return self.context.last_inserted_ids
def do_execute(self, cursor, statement, params, context=None):
if params == {}:
params = ()
super(AccessDialect, self).\
do_execute(cursor, statement, params, **kwargs)
def _execute(self, c, statement, parameters):
try:
if parameters == {}:
parameters = ()
c.execute(statement, parameters)
self.context.rowcount = c.rowcount
except Exception, e:
raise exc.DBAPIError.instance(statement, parameters, e)
def has_table(self, connection, tablename, schema=None):
# This approach seems to be more reliable that using DAO
try:
connection.execute('select top 1 * from [%s]' % tablename)
return True
except Exception, e:
return False
def reflecttable(self, connection, table, include_columns):
# This is defined in the function, as it relies on win32com constants,
# that aren't imported until dbapi method is called
if not hasattr(self, 'ischema_names'):
self.ischema_names = {
const.dbByte: AcBinary,
const.dbInteger: AcInteger,
const.dbLong: AcInteger,
const.dbSingle: AcFloat,
const.dbDouble: AcFloat,
const.dbDate: AcDateTime,
const.dbLongBinary: AcBinary,
const.dbMemo: AcText,
const.dbBoolean: AcBoolean,
const.dbText: AcUnicode, # All Access strings are
# unicode
const.dbCurrency: AcNumeric,
}
# A fresh DAO connection is opened for each reflection
# This is necessary, so we get the latest updates
dtbs = daoEngine.OpenDatabase(connection.engine.url.database)
try:
for tbl in dtbs.TableDefs:
if tbl.Name.lower() == table.name.lower():
break
else:
raise exc.NoSuchTableError(table.name)
for col in tbl.Fields:
coltype = self.ischema_names[col.Type]
if col.Type == const.dbText:
coltype = coltype(col.Size)
colargs = \
{
'nullable': not(col.Required or
col.Attributes & const.dbAutoIncrField),
}
default = col.DefaultValue
if col.Attributes & const.dbAutoIncrField:
colargs['default'] = schema.Sequence(col.Name + '_seq')
elif default:
if col.Type == const.dbBoolean:
default = default == 'Yes' and '1' or '0'
colargs['server_default'] = \
schema.DefaultClause(sql.text(default))
table.append_column(
schema.Column(col.Name, coltype, **colargs))
# TBD: check constraints
# Find primary key columns first
for idx in tbl.Indexes:
if idx.Primary:
for col in idx.Fields:
thecol = table.c[col.Name]
table.primary_key.add(thecol)
if isinstance(thecol.type, AcInteger) and \
not (thecol.default and
isinstance(
thecol.default.arg,
schema.Sequence
)):
thecol.autoincrement = False
# Then add other indexes
for idx in tbl.Indexes:
if not idx.Primary:
if len(idx.Fields) == 1:
col = table.c[idx.Fields[0].Name]
if not col.primary_key:
col.index = True
col.unique = idx.Unique
else:
pass # TBD: multi-column indexes
for fk in dtbs.Relations:
if fk.ForeignTable != table.name:
continue
scols = [c.ForeignName for c in fk.Fields]
rcols = ['%s.%s' % (fk.Table, c.Name) for c in fk.Fields]
table.append_constraint(
schema.ForeignKeyConstraint(scols, rcols,\
link_to_name=True))
finally:
dtbs.Close()
@reflection.cache
def get_table_names(self, connection, schema=None, **kw):
# A fresh DAO connection is opened for each reflection
# This is necessary, so we get the latest updates
dtbs = daoEngine.OpenDatabase(connection.engine.url.database)
names = [t.Name for t in dtbs.TableDefs
if t.Name[:4] != "MSys" and t.Name[:4] != "~TMP"]
dtbs.Close()
return names
class AccessCompiler(compiler.SQLCompiler):
extract_map = compiler.SQLCompiler.extract_map.copy()
extract_map.update ({
'month': 'm',
'day': 'd',
'year': 'yyyy',
'second': 's',
'hour': 'h',
'doy': 'y',
'minute': 'n',
'quarter': 'q',
'dow': 'w',
'week': 'ww'
})
def visit_select_precolumns(self, select):
"""Access puts TOP, it's version of LIMIT here """
s = select.distinct and "DISTINCT " or ""
if select.limit:
s += "TOP %s " % (select.limit)
if select.offset:
raise exc.InvalidRequestError(
'Access does not support LIMIT with an offset')
return s
def limit_clause(self, select):
"""Limit in access is after the select keyword"""
return ""
def binary_operator_string(self, binary):
"""Access uses "mod" instead of "%" """
return binary.operator == '%' and 'mod' or binary.operator
function_rewrites = {'current_date': 'now',
'current_timestamp': 'now',
'length': 'len',
}
def visit_function(self, func):
"""Access function names differ from the ANSI SQL names;
rewrite common ones"""
func.name = self.function_rewrites.get(func.name, func.name)
return super(AccessCompiler, self).visit_function(func)
def for_update_clause(self, select):
"""FOR UPDATE is not supported by Access; silently ignore"""
return ''
# Strip schema
def visit_table(self, table, asfrom=False, **kwargs):
if asfrom:
return self.preparer.quote(table.name, table.quote)
else:
return ""
def visit_join(self, join, asfrom=False, **kwargs):
return (self.process(join.left, asfrom=True) + \
(join.isouter and " LEFT OUTER JOIN " or " INNER JOIN ") + \
self.process(join.right, asfrom=True) + " ON " + \
self.process(join.onclause))
def visit_extract(self, extract, **kw):
field = self.extract_map.get(extract.field, extract.field)
return 'DATEPART("%s", %s)' % \
(field, self.process(extract.expr, **kw))
class AccessDDLCompiler(compiler.DDLCompiler):
def get_column_specification(self, column, **kwargs):
colspec = self.preparer.format_column(column) + " " + \
column.type.dialect_impl(self.dialect).get_col_spec()
# install a sequence if we have an implicit IDENTITY column
if (not getattr(column.table, 'has_sequence', False)) and \
column.primary_key and \
column.autoincrement and \
isinstance(column.type, types.Integer) and \
not column.foreign_keys:
if column.default is None or \
(isinstance(column.default, schema.Sequence) and
column.default.optional):
column.sequence = schema.Sequence(column.name + '_seq')
if not column.nullable:
colspec += " NOT NULL"
if hasattr(column, 'sequence'):
column.table.has_sequence = column
colspec = self.preparer.format_column(column) + " counter"
else:
default = self.get_column_default_string(column)
if default is not None:
colspec += " DEFAULT " + default
return colspec
def visit_drop_index(self, drop):
index = drop.element
self.append("\nDROP INDEX [%s].[%s]" % \
(index.table.name,
self._index_identifier(index.name)))
class AccessIdentifierPreparer(compiler.IdentifierPreparer):
reserved_words = compiler.RESERVED_WORDS.copy()
reserved_words.update(['value', 'text'])
def __init__(self, dialect):
super(AccessIdentifierPreparer, self).\
__init__(dialect, initial_quote='[', final_quote=']')
dialect = AccessDialect
dialect.poolclass = pool.SingletonThreadPool
dialect.statement_compiler = AccessCompiler
dialect.ddlcompiler = AccessDDLCompiler
dialect.preparer = AccessIdentifierPreparer
dialect.execution_ctx_cls = AccessExecutionContext
-31
View File
@@ -1,31 +0,0 @@
from sqlalchemy import *
from sqlalchemy import sql
from sqlalchemy.databases import access
from sqlalchemy.testing import *
class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
__dialect__ = access.dialect()
def test_extract(self):
t = sql.table('t', sql.column('col1'))
mapping = {
'month': 'm',
'day': 'd',
'year': 'yyyy',
'second': 's',
'hour': 'h',
'doy': 'y',
'minute': 'n',
'quarter': 'q',
'dow': 'w',
'week': 'ww'
}
for field, subst in mapping.items():
self.assert_compile(
select([extract(field, t.c.col1)]),
'SELECT DATEPART("%s", t.col1) AS anon_1 FROM t' % subst)
+1 -3
View File
@@ -193,7 +193,6 @@ class DefaultRequirements(SuiteRequirements):
"""Target database must support savepoints."""
return skip_if([
"access",
"sqlite",
"sybase",
("mysql", "<", (5, 0, 3)),
@@ -215,7 +214,7 @@ class DefaultRequirements(SuiteRequirements):
@property
def update_nowait(self):
"""Target database must support SELECT...FOR UPDATE NOWAIT"""
return skip_if(["access", "firebird", "mssql", "mysql", "sqlite", "sybase"],
return skip_if(["firebird", "mssql", "mysql", "sqlite", "sybase"],
"no FOR UPDATE NOWAIT support"
)
@@ -259,7 +258,6 @@ class DefaultRequirements(SuiteRequirements):
"""Target database must support two-phase transactions."""
return skip_if([
no_support('access', 'two-phase xact not supported by database'),
no_support('firebird', 'no SA implementation'),
no_support('maxdb', 'two-phase xact not supported by database'),
no_support('mssql', 'two-phase xact not supported by drivers'),