diff --git a/COPYING b/COPYING.txt similarity index 100% rename from COPYING rename to COPYING.txt diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..c8a5f6c --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include COPYING.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..c64eef9 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ + +# SQLBase7-SA + +SQLBase7-SA is a SQLAlchemy driver/dialect for the Centura SQLBase +database, specifically version 7.5.1. + +## About the Project + +From what I can tell, SQLBase is still an actively-developed database, +but it is no longer owned by Centura (see +[here](http://en.wikipedia.org/wiki/Gupta_Technologies)). Also, the +current version (according to [this +page](http://www.unify.com/Products/Data_Management/SQLBase/), as of +25 Apr 2010) is 11.5, so I have no idea how useful this project will +be for versions of SQLBase more recent than 7.5.1. + +This project exists only for the sake of providing read-only access to +legacy data, specifically that used by the +[CAM32](http://www.camcommerce.com/products/CAM32.aspx) Point of Sale +software. It's possible that it could allow writing data, etc., but I +personally won't be adding any such features unless/until the need +arises. + +I don't expect there to be much of anyone using SQLBase 7.5.1 at this +point (besides perhaps other CAM32 users), but if you do happen to +need additional functionality from this project or just have questions +or comments, feel free to drop me a line at lance@edbob.org. + +## Downloads + +The code is released under the [GNU General Public +License](http://www.gnu.org/licenses/gpl.html), version 3. + +It is available at [PyPI](http://pypi.python.org/pypi/SQLBase7-SA), so +the easiest way to get the package is with the command: + + # pip install SQLBase7-SA + +Again, this project is extremely specific to my needs, so I'm only +building eggs for Python 2.5 and 2.6 at this point. If you happen to +need something else then please contact me. + +Copyright © 2010 Lance Edgar diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..9c7b914 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[egg_info] +tag_build = .dev diff --git a/setup.py b/setup.py index c2bd74d..75395a7 100644 --- a/setup.py +++ b/setup.py @@ -1,53 +1,86 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -################################################################################ -# -# SQLBase7-SA -- SQLAlchemy driver/dialect for Centura SQLBase v7 -# Copyright © 2010 Lance Edgar -# -# This file is part of SQLBase7-SA. -# -# SQLBase7-SA is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# SQLBase7-SA is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SQLBase7-SA. If not, see . -# -################################################################################ - - -from setuptools import setup, find_packages - - -import os -execfile(os.path.join(os.path.dirname(__file__), 'sqlbase7_sa', '_version.py')) - - -setup( - name = 'SQLBase7-SA', - version = __version__, - description = 'SQLAlchemy driver/dialect for Centura SQLBase v7', - author = 'Lance Edgar', - author_email = 'lance@edbob.org', - - packages = find_packages(), - - install_requires = [ - 'SQLAlchemy>=0.6.0', - ], - - entry_points = { - 'sqlalchemy.dialects' : [ - 'sqlbase7 = sqlbase7_sa:base.dialect', - ], - }, - - test_suite = 'sqlbase7_sa.tests', - ) +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# SQLBase7-SA -- SQLAlchemy driver/dialect for Centura SQLBase v7 +# Copyright © 2010 Lance Edgar +# +# This file is part of SQLBase7-SA. +# +# SQLBase7-SA is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SQLBase7-SA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SQLBase7-SA. If not, see . +# +################################################################################ + + +from setuptools import setup, find_packages + + +import os +execfile(os.path.join(os.path.dirname(__file__), 'sqlbase7_sa', '_version.py')) + + +setup( + name = 'SQLBase7-SA', + version = __version__, + author = 'Lance Edgar', + author_email = 'lance@edbob.org', + url = "https://forgejo.wuttaproject.org/rattail/sqlbase7-sa", + license = "GNU GPL v3", + description = 'SQLAlchemy dialect for Centura SQLBase v7', + long_description = """ +SQLBase7-SA - SQLAlchemy dialect for Centura SQLBase v7 +------------------------------------------------------- + +This package provides a (possibly rudimentary) implementation +of a SQLAlchemy dialect for the Centura SQLBase database +engine. It is only intended (and known) to work with a very +specific version of this database, that version being 7.5.1. +""", + + classifiers = [ + 'Development Status :: 4 - Beta', + 'Environment :: Plugins', + 'Environment :: Win32 (MS Windows)', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Natural Language :: English', + 'Operating System :: Microsoft :: Windows', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Topic :: Database', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + + packages = find_packages(), + + install_requires = [ + 'SQLAlchemy>0.5.2', + ], + + entry_points = { + + # SQLAlchemy 0.5 + 'sqlalchemy.databases' : [ + 'sqlbase7 = sqlbase7_sa.sqlbase7_sa05:SQLBase7Dialect_SA05', + ], + + # SQLAlchemy 0.6 + 'sqlalchemy.dialects' : [ + 'sqlbase7 = sqlbase7_sa.sqlbase7_sa06:SQLBase7Dialect_SA06_pyodbc', + ], + }, + + test_suite = 'sqlbase7_sa.tests', + ) diff --git a/sqlbase7_sa/__init__.py b/sqlbase7_sa/__init__.py index 96991e6..a7c7be0 100644 --- a/sqlbase7_sa/__init__.py +++ b/sqlbase7_sa/__init__.py @@ -23,11 +23,4 @@ ################################################################################ -from sqlbase7_sa import base -from sqlbase7_sa import pyodbc - from sqlbase7_sa._version import __version__ - - -# default dialect -base.dialect = pyodbc.dialect diff --git a/sqlbase7_sa/_version.py b/sqlbase7_sa/_version.py index 5f74cfc..5bb4149 100644 --- a/sqlbase7_sa/_version.py +++ b/sqlbase7_sa/_version.py @@ -23,4 +23,4 @@ ################################################################################ -__version__ = '0.1a1' +__version__ = '0.1b5' diff --git a/sqlbase7_sa/pyodbc.py b/sqlbase7_sa/pyodbc.py deleted file mode 100644 index b56d374..0000000 --- a/sqlbase7_sa/pyodbc.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -################################################################################ -# -# SQLBase7-SA -- SQLAlchemy driver/dialect for Centura SQLBase v7 -# Copyright © 2010 Lance Edgar -# -# This file is part of SQLBase7-SA. -# -# SQLBase7-SA is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# SQLBase7-SA is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SQLBase7-SA. If not, see . -# -################################################################################ - - -from sqlalchemy.connectors.pyodbc import PyODBCConnector - -from sqlbase7_sa.base import SQLBase7Dialect - - -class SQLBase7_pyodbc(PyODBCConnector, SQLBase7Dialect): - - def create_connect_args(self, url): - connection_string = ';'.join(( - "DRIVER={Centura SQLBase 3.5 32-bit Driver -NT & Win95}", - "SERVER=%s" % url.host, - "DATABASE=%s" % url.database, - "UID=%s" % url.username, - "PWD=%s" % url.password, - )) - return [connection_string], {} - - -dialect = SQLBase7_pyodbc diff --git a/sqlbase7_sa/base.py b/sqlbase7_sa/sqlbase7.py similarity index 51% rename from sqlbase7_sa/base.py rename to sqlbase7_sa/sqlbase7.py index be7dfd1..690395c 100644 --- a/sqlbase7_sa/base.py +++ b/sqlbase7_sa/sqlbase7.py @@ -25,8 +25,16 @@ from sqlalchemy.engine.default import DefaultDialect from sqlalchemy import types, and_ -from sqlalchemy.sql.compiler import SQLCompiler from sqlalchemy.sql.expression import Join +from sqlalchemy.sql import visitors, operators, ClauseElement + + +import sqlalchemy +from pkg_resources import parse_version +if parse_version(sqlalchemy.__version__) <= parse_version('0.5.99'): + from sqlalchemy.sql.compiler import DefaultCompiler as CompilerBase +else: + from sqlalchemy.sql.compiler import SQLCompiler as CompilerBase class LimitClauseNotSupported(Exception): @@ -39,7 +47,7 @@ class LimitClauseNotSupported(Exception): return "Centura SQLBase 7.5.1 doesn't support a LIMIT clause for the SELECT statement (received: limit = %u, offset = %u)" % (self.limit, self.offset) -class SQLBase7Compiler(SQLCompiler): +class SQLBase7Compiler(CompilerBase): # Most of the code below was copied from the Oracle dialect. Thanks to Michael Bayer # for pointing that out. Oh, and for writing SQLAlchemy; that was pretty cool. @@ -49,7 +57,12 @@ class SQLBase7Compiler(SQLCompiler): return self.process(join.left, **kwargs) + ", " + self.process(join.right, **kwargs) def visit_select(self, select, **kwargs): - froms = select._get_display_froms() + if self.stack and 'from' in self.stack[-1]: + existingfroms = self.stack[-1]['from'] + else: + existingfroms = None + + froms = select._get_display_froms(existingfroms) whereclause = self._get_join_whereclause(froms) if whereclause is not None: select = select.where(whereclause) @@ -58,13 +71,22 @@ class SQLBase7Compiler(SQLCompiler): raise LimitClauseNotSupported(select._limit, select._offset) kwargs['iswrapper'] = getattr(select, '_is_wrapper', False) - return SQLCompiler.visit_select(self, select, **kwargs) + return super(SQLBase7Compiler, self).visit_select(select, **kwargs) def _get_join_whereclause(self, froms): clauses = [] def visit_join(join): - clauses.append(join.onclause) + if join.isouter: + def visit_binary(binary): + if binary.operator == operators.eq: + if binary.left.table is join.right: + binary.left = _OuterJoinColumn(binary.left) + elif binary.right.table is join.right: + binary.right = _OuterJoinColumn(binary.right) + clauses.append(visitors.cloned_traverse(join.onclause, {}, {'binary':visit_binary})) + else: + clauses.append(join.onclause) for j in join.left, join.right: if isinstance(j, Join): visit_join(j) @@ -77,23 +99,26 @@ class SQLBase7Compiler(SQLCompiler): return and_(*clauses) return None - def visit_ilike_op(self, binary, **kw): - escape = binary.modifiers.get("escape", None) - return '@lower(%s) LIKE @lower(%s)' % ( - self.process(binary.left, **kw), - self.process(binary.right, **kw)) \ - + (escape and ' ESCAPE \'%s\'' % escape or '') + def visit_outer_join_column(self, vc): + return self.process(vc.column) + "(+)" + + +class _OuterJoinColumn(ClauseElement): + __visit_name__ = 'outer_join_column' + + def __init__(self, column): + self.column = column + - class SQLBase7Dialect(DefaultDialect): - + name = 'sqlbase7' - max_identifier_length = 18 - statement_compiler = SQLBase7Compiler - type_map = { + max_identifier_length = 18 + + _type_map = { 'CHAR' : types.CHAR, 'DATE' : types.DATE, 'DECIMAL' : types.DECIMAL, @@ -104,59 +129,29 @@ class SQLBase7Dialect(DefaultDialect): 'VARCHAR' : types.VARCHAR, } - def _check_unicode_returns(self, connection): - return False + def create_connect_args(self, url): + connection_string = ';'.join(( + "DRIVER={Centura SQLBase 3.5 32-bit Driver -NT & Win95}", + "SERVER=%s" % url.host, + "DATABASE=%s" % url.database, + "UID=%s" % url.username, + "PWD=%s" % url.password, + )) + return [connection_string], {} - def get_table_names(self, connection, schema=None, **kw): - if schema is None: - schema = '' - else: - schema = '%s.' % schema - - cursor = connection.connection.cursor() - table_names = [row.NAME for row in cursor.execute( - "SELECT NAME FROM %sSYSTABLES WHERE REMARKS IS NOT NULL" % schema - )] - cursor.close() - return table_names - - def get_columns(self, connection, table_name, schema=None, **kw): - if schema is None: - schema = '' - else: - schema = '%s.' % schema - - cursor = connection.connection.cursor() - columns = [] - - for row in cursor.execute("SELECT NAME,COLTYPE,NULLS FROM %sSYSCOLUMNS WHERE TBNAME = '%s'" % (schema, table_name)): - - columns.append({ - 'name' : row.NAME, - 'type' : self.type_map[row.COLTYPE], - 'nullable' : row.NULLS == 'Y', - 'default' : None, - 'autoincrement' : False, - }) - - cursor.close() - return columns - - def get_primary_keys(self, connection, table_name, schema=None, **kw): - if schema is None: - schema = '' - else: - schema = '%s.' % schema - - cursor = connection.connection.cursor() - primary_keys = [row.COLNAME for row in cursor.execute( - "SELECT COLNAME FROM %sSYSPKCONSTRAINTS WHERE NAME = '%s' ORDER BY PKCOLSEQNUM" % (schema, table_name) - )] - cursor.close() - return primary_keys - - def get_foreign_keys(self, connection, table_name, schema=None, **kw): - return [] - - def get_indexes(self, connection, table_name, schema=None, **kw): - return [] + def get_default_schema_name(self, connection): + return 'SYSADM' + + def do_execute(self, cursor, statement, parameters, context=None): + # For some (perhaps good?) reason, the SQLBase ODBC driver doesn't like + # parameters if they're of Unicode or Long type. I'd hoped at first that + # the "supports_unicode_binds" attribute would take care of the Unicode + # problem but it didn't seem to. And now the Long parameters seem to + # throw the same error, so... + parameters = list(parameters) + for i, parameter in enumerate(parameters): + if isinstance(parameter, unicode): + parameters[i] = str(parameter) + elif isinstance(parameter, long): + parameters[i] = int(parameter) + super(SQLBase7Dialect, self).do_execute(cursor, statement, tuple(parameters), context) diff --git a/sqlbase7_sa/sqlbase7_sa05.py b/sqlbase7_sa/sqlbase7_sa05.py new file mode 100644 index 0000000..83a5b79 --- /dev/null +++ b/sqlbase7_sa/sqlbase7_sa05.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# SQLBase7-SA -- SQLAlchemy driver/dialect for Centura SQLBase v7 +# Copyright © 2010 Lance Edgar +# +# This file is part of SQLBase7-SA. +# +# SQLBase7-SA is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SQLBase7-SA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SQLBase7-SA. If not, see . +# +################################################################################ + + +from sqlalchemy import types, Column, PrimaryKeyConstraint +from sqlalchemy.sql.compiler import OPERATORS +from sqlalchemy.sql import operators as sql_operators + +from sqlbase7_sa.sqlbase7 import SQLBase7Compiler, SQLBase7Dialect + + +class SQLBase7Compiler_SA05(SQLBase7Compiler): + + operators = SQLBase7Compiler.operators.copy() + operators.update({ + sql_operators.ilike_op: lambda x, y, escape=None: "@lower(%s) LIKE @lower(%s)" % (x, y) + (escape and ' ESCAPE \'%s\'' % escape or ''), + }) + + +class SQLBase7Dialect_SA05(SQLBase7Dialect): + + statement_compiler = SQLBase7Compiler_SA05 + + @classmethod + def dbapi(cls): + import pyodbc + return pyodbc + + def table_names(self, connection, schema): + cursor = connection.connection.cursor() + table_names = [row.NAME for row in cursor.execute( + "SELECT NAME FROM %s.SYSTABLES WHERE REMARKS IS NOT NULL" % schema + )] + cursor.close() + return table_names + + def reflecttable(self, connection, table, include_columns=None): + if table.schema is None: + table.schema = connection.default_schema_name() + + sql = "SELECT NAME,COLTYPE,NULLS FROM %s.SYSCOLUMNS WHERE TBNAME = '%s'" % (table.schema, table.name) + if include_columns: + sql += " AND NAME NOT IN (%s)" % ','.join(include_columns) + cursor = connection.connection.cursor() + for row in cursor.execute(sql): + table.append_column(Column(row.NAME, self._type_map[row.COLTYPE])) + cursor.close() + + cursor = connection.connection.cursor() + key_columns = [row.COLNAME for row in cursor.execute( + "SELECT COLNAME FROM %s.SYSPKCONSTRAINTS WHERE NAME = '%s' ORDER BY PKCOLSEQNUM" % (table.schema, table.name) + )] + if key_columns: + table.append_constraint(PrimaryKeyConstraint(*key_columns)) + cursor.close() diff --git a/sqlbase7_sa/sqlbase7_sa06.py b/sqlbase7_sa/sqlbase7_sa06.py new file mode 100644 index 0000000..0e9ee12 --- /dev/null +++ b/sqlbase7_sa/sqlbase7_sa06.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# SQLBase7-SA -- SQLAlchemy driver/dialect for Centura SQLBase v7 +# Copyright © 2010 Lance Edgar +# +# This file is part of SQLBase7-SA. +# +# SQLBase7-SA is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SQLBase7-SA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SQLBase7-SA. If not, see . +# +################################################################################ + + +from sqlbase7_sa.sqlbase7 import SQLBase7Compiler, SQLBase7Dialect + +from sqlalchemy.connectors.pyodbc import PyODBCConnector + + +class SQLBase7Compiler_SA06(SQLBase7Compiler): + + def visit_ilike_op(self, binary, **kw): + escape = binary.modifiers.get("escape", None) + return '@lower(%s) LIKE @lower(%s)' % ( + self.process(binary.left, **kw), + self.process(binary.right, **kw)) \ + + (escape and ' ESCAPE \'%s\'' % escape or '') + + +class SQLBase7Dialect_SA06(SQLBase7Dialect): + + statement_compiler = SQLBase7Compiler_SA06 + + def _get_default_schema_name(self, connection): + return 'SYSADM' + + def _check_unicode_returns(self, connection): + return False + + def get_table_names(self, connection, schema=None, **kw): + cursor = connection.connection.cursor() + table_names = [row.NAME for row in cursor.execute( + "SELECT NAME FROM %s.SYSTABLES WHERE REMARKS IS NOT NULL" % schema + )] + cursor.close() + return table_names + + def get_columns(self, connection, table_name, schema=None, **kw): + if schema is None: + schema = self.get_default_schema_name(connection) + cursor = connection.connection.cursor() + columns = [] + + for row in cursor.execute("SELECT NAME,COLTYPE,NULLS FROM %s.SYSCOLUMNS WHERE TBNAME = '%s'" % (schema, table_name)): + + columns.append({ + 'name' : row.NAME, + 'type' : self._type_map[row.COLTYPE], + 'nullable' : row.NULLS == 'Y', + 'default' : None, + 'autoincrement' : False, + }) + + cursor.close() + return columns + + def get_primary_keys(self, connection, table_name, schema=None, **kw): + if schema is None: + schema = self.get_default_schema_name(connection) + cursor = connection.connection.cursor() + primary_keys = [row.COLNAME for row in cursor.execute( + "SELECT COLNAME FROM %s.SYSPKCONSTRAINTS WHERE NAME = '%s' ORDER BY PKCOLSEQNUM" % (schema, table_name) + )] + cursor.close() + return primary_keys + + def get_foreign_keys(self, connection, table_name, schema=None, **kw): + return [] + + def get_indexes(self, connection, table_name, schema=None, **kw): + return [] + + +class SQLBase7Dialect_SA06_pyodbc(SQLBase7Dialect_SA06, PyODBCConnector): + pass + + +dialect = SQLBase7Dialect_SA06_pyodbc diff --git a/sqlbase7_sa/tests/__init__.py b/sqlbase7_sa/tests/__init__.py index be9e204..420a860 100644 --- a/sqlbase7_sa/tests/__init__.py +++ b/sqlbase7_sa/tests/__init__.py @@ -51,6 +51,5 @@ class ConnectionTestCase(TestCase): class ReflectionTestCase(TestCase): def runTest(self): - metadata = MetaData(bind=self.engine) - metadata.reflect(schema='SYSADM') + metadata = MetaData(bind=self.engine, reflect=True) self.assert_(metadata.tables)