Skip to content

Commit

Permalink
Add support for PK and FK reflection via ACE DAO
Browse files Browse the repository at this point in the history
  • Loading branch information
gordthompson committed Aug 4, 2021
1 parent 8c24549 commit d15c0fd
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 56 deletions.
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ then you can use the older 32-bit ``Microsoft Access Driver (*.mdb)`` that ships
Co-requisites
-------------

This dialect requires SQLAlchemy and pyodbc. They are both specified as requirements so ``pip`` will install
them if they are not already in place. To install, just::
This dialect requires SQLAlchemy, pyodbc, and pywin32. They are specified as requirements so ``pip``
will install them if they are not already in place. To install, just::

pip install sqlalchemy-access

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
},
packages=find_packages(include=["sqlalchemy_access"]),
include_package_data=True,
install_requires=["SQLAlchemy", "pyodbc>=4.0.27"],
install_requires=["SQLAlchemy", "pyodbc>=4.0.27", "pywin32"],
zip_safe=False,
entry_points={
"sqlalchemy.dialects": [
Expand Down
2 changes: 1 addition & 1 deletion sqlalchemy_access/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

import pyodbc

__version__ = "1.0.9b1.dev0"
__version__ = "1.1.0"

pyodbc.pooling = False # required for Access databases with ODBC linked tables
_registry.register(
Expand Down
100 changes: 58 additions & 42 deletions sqlalchemy_access/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@
"""
import warnings

from sqlalchemy import types, exc, pool
import pyodbc
import pywintypes
from sqlalchemy import types, exc, pool, util
from sqlalchemy.sql import compiler
from sqlalchemy.engine import default, reflection

import pyodbc
import win32com.client


# AutoNumber
Expand Down Expand Up @@ -727,6 +728,7 @@ def _decode_sketchy_utf16(self, raw_bytes):
pass
return s

@reflection.cache
def get_columns(self, connection, table_name, schema=None, **kw):
pyodbc_cnxn = connection.engine.raw_connection()
# work around bug in Access ODBC driver
Expand Down Expand Up @@ -764,51 +766,65 @@ def get_primary_keys(self, connection, table_name, schema=None, **kw):
self, connection, table_name, schema=schema, **kw
)

def _get_dao_string(self, crsr):
if crsr.connection.getinfo(pyodbc.SQL_DRIVER_NAME) == "odbcjt32.dll":
return "DAO.DBEngine.36"
else:
return "DAO.DBEngine.120"

@reflection.cache
def get_pk_constraint(self, connection, table_name, schema=None, **kw):
pyodbc_crsr = connection.engine.raw_connection().cursor()
try:
result = pyodbc_crsr.primaryKeys(table_name)
except pyodbc.InterfaceError as ie:
if ie.args[0] == "IM001":
# ('IM001', '[IM001] [Microsoft][ODBC Driver Manager] Driver does not support this function (0) (SQLPrimaryKeys)')
warnings.warn(
(
'The Access ODBC driver does not support the ODBC "SQLPrimaryKeys" function. '
"get_pk_constraint() is returning an empty list."
),
exc.SAWarning,
stacklevel=3,
)
return []
else:
raise
except:
raise
return [row[3] for row in result]
db_path = pyodbc_crsr.tables(table=table_name).fetchval()
if db_path:
db_engine = win32com.client.Dispatch(
self._get_dao_string(pyodbc_crsr)
)
db = db_engine.OpenDatabase(db_path)
tbd = db.TableDefs(table_name)
for idx in tbd.Indexes:
if idx.Primary:
return {
"constrained_columns": [
fld.Name for fld in idx.Fields
],
"name": idx.Name,
}
else:
util.raise_(
exc.NoSuchTableError("Table '%s' not found." % table_name),
)

@reflection.cache
def get_foreign_keys(self, connection, table_name, schema=None, **kw):
pyodbc_crsr = connection.engine.raw_connection().cursor()
try:
result = pyodbc_crsr.foreignKeys(table_name)
except pyodbc.InterfaceError as ie:
if ie.args[0] == "IM001":
# ('IM001', '[IM001] [Microsoft][ODBC Driver Manager] Driver does not support this function (0) (SQLForeignKeys)')
warnings.warn(
(
'The Access ODBC driver does not support the ODBC "SQLForeignKeys" function. '
"get_foreign_keys() is returning an empty list."
),
exc.SAWarning,
stacklevel=3,
)
return []
else:
raise
except:
raise
# this will tell us if Access ODBC ever starts supporting SQLForeignKeys
raise NotImplementedError()
db_path = pyodbc_crsr.tables(table=table_name).fetchval()
if db_path:
db_engine = win32com.client.Dispatch(
self._get_dao_string(pyodbc_crsr)
)
db = db_engine.OpenDatabase(db_path)
fk_list = []
for rel in db.Relations:
if rel.ForeignTable.casefold() == table_name.casefold():
fk_dict = {
"constrained_columns": [],
"referred_schema": None,
"referred_table": rel.Table,
"referred_columns": [],
"name": rel.Name,
}
for fld in rel.Fields:
fk_dict["constrained_columns"].append(fld.ForeignName)
fk_dict["referred_columns"].append(fld.Name)
fk_list.append(fk_dict)
return fk_list
else:
util.raise_(
exc.NoSuchTableError("Table '%s' not found." % table_name),
)

@reflection.cache
def get_indexes(self, connection, table_name, schema=None, **kw):
pyodbc_crsr = connection.engine.raw_connection().cursor()
indexes = {}
Expand Down
14 changes: 7 additions & 7 deletions sqlalchemy_access/requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ def datetime_microseconds(self):
def floats_to_four_decimals(self):
return exclusions.closed()

# TODO: remove this when SQLA released with
# https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/2990
@property
def foreign_key_constraint_reflection(self):
return exclusions.closed()
def implicitly_named_constraints(self):
return exclusions.open()

@property
def nullable_booleans(self):
Expand All @@ -44,8 +46,8 @@ def precision_generic_float_type(self):
return exclusions.closed()

@property
def primary_key_constraint_reflection(self):
return exclusions.closed()
def reflects_pk_names(self):
return exclusions.open()

@property
def sql_expression_limit_offset(self):
Expand Down Expand Up @@ -77,9 +79,7 @@ def timestamp_microseconds(self):

@property
def unicode_ddl(self):
# Access ODBC does not support `SQLForeignKeys` so test teardown code
# cannot determine the correct order in which to drop the tables.
# And even if it did, Access won't let you drop a child table unless
# Access won't let you drop a child table unless
# you drop the FK constraint first. Not worth the grief.
return exclusions.closed()

Expand Down
4 changes: 2 additions & 2 deletions test/test_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def test_not_equals_operator(self, connection):
tbl.insert(),
[{"id": 1}],
)
result = connection.execute(tbl.select(tbl.c.id != 1)).fetchall()
result = connection.execute(tbl.select().where(tbl.c.id != 1)).fetchall()
eq_(len(result), 0)
result = connection.execute(tbl.select(tbl.c.id != 2)).fetchall()
result = connection.execute(tbl.select().where(tbl.c.id != 2)).fetchall()
eq_(len(result), 1)
2 changes: 1 addition & 1 deletion test/test_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test_special_type(self):
class ComponentReflectionTest(_ComponentReflectionTest):
@testing.skip("access")
def test_get_noncol_index(self):
# Driver does not support this function (0) (SQLPrimaryKeys)
# Access creates extra indexes that this test does not expect
return

@testing.skip("access")
Expand Down

0 comments on commit d15c0fd

Please sign in to comment.